@tea-agent/loop-agent 0.44.0-next.8 → 0.44.0-next.9
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 +97 -0
- package/dist/application/evaluation/budget.js +21 -0
- package/dist/application/evaluation/corpus-hash.js +10 -15
- package/dist/application/evaluation/corpus.js +2 -1
- package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
- package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
- package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
- package/dist/application/evaluation/frontend-pair-registration.js +143 -0
- package/dist/application/evaluation/frontend-paired-summary.js +150 -0
- package/dist/application/evaluation/frontend-run-observation.js +144 -0
- package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
- package/dist/application/evaluation/types.js +45 -12
- package/dist/application/task-lifecycle/observe.js +26 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +11 -0
- package/dist/cli/program.js +4 -0
- package/dist/commands/task-advance.js +3 -5
- package/dist/commands/task-source-prepare.js +13 -1
- package/dist/executors/dag-pi/tools/design-terminal-tools.js +17 -7
- package/dist/executors/dag-pi/tools/review-terminal-tools.js +17 -7
- package/dist/executors/dag-pi-executor.js +2048 -125
- package/dist/executors/pi-executor.js +6 -1
- package/dist/executors/pi-sdk-executor.js +76 -11
- package/dist/executors/shell-executor.js +88 -12
- package/dist/infrastructure/console/operation-store.js +2 -0
- package/dist/infrastructure/evaluation/corpus-store.js +37 -12
- package/dist/shared/operator/capabilities.js +10 -5
- package/dist/task/config-types.js +22 -9
- package/dist/task/contract/project.js +13 -0
- package/dist/task/contract/schema.js +2 -8
- package/dist/task/source-prepare/parse-intent.js +124 -5
- package/dist/task/source-prepare/prepare.js +6 -1
- package/dist/task/source-prepare/semantic-intake.js +17 -2
- package/dist/task/source-prepare/source-execution.js +65 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +21 -8
- package/dist/task/source-prepare/source-provider-budget.js +290 -0
- package/dist/worker/console/operator-actions.js +0 -1
- package/dist/worker/console/operator-user-error.js +1 -1
- package/dist/worker/console/prd-intake-bridge.js +5 -15
- package/dist/workflows/dag/budget-enforcement.js +31 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-contract-facts.js +11 -8
- package/dist/workflows/dag/frontend-design-policy.js +6 -6
- package/dist/workflows/dag/frontend-durable-tools.js +90 -70
- package/dist/workflows/dag/frontend-implementation-contract.js +146 -59
- package/dist/workflows/dag/frontend-plan-canary.js +66 -16
- package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
- package/dist/workflows/dag/frontend-plan-progress.js +92 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +11 -10
- package/dist/workflows/dag/frontend-recovery-run.js +51 -46
- package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
- package/dist/workflows/dag/frontend-review-context.js +29 -1
- package/dist/workflows/dag/frontend-review-scopes.js +92 -2
- package/dist/workflows/dag/frontend-risk.js +8 -3
- package/dist/workflows/dag/frontend-root-observation.js +261 -0
- package/dist/workflows/dag/frontend-session-budget.js +52 -39
- package/dist/workflows/dag/frontend-session-context.js +10 -0
- package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -6
- package/dist/workflows/dag/frontend-verification-trace.js +6 -0
- package/dist/workflows/dag/frontend-writer-admission.js +2 -1
- package/dist/workflows/dag/init-hybrid.js +108 -37
- package/dist/workflows/dag/node-execution.js +11 -11
- package/dist/workflows/dag/rerun-feedback.js +35 -4
- package/dist/workflows/dag/rerun-task.js +62 -67
- package/dist/workflows/dag/runner.js +58 -12
- package/dist/workflows/dag/types.js +15 -8
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/templates/backend-test-dag.json +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +31 -11
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +8 -11
- package/skills/frontend-design-review/references/review-checklist.md +3 -4
- package/skills/frontend-review/SKILL.md +5 -1
- package/skills/loop-agent/references/command-reference.md +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -23,6 +23,103 @@
|
|
|
23
23
|
|
|
24
24
|
- Console 操作文案改为面向操作者的说明并给出下一步:等待超时不再伪装成「需要核对」,改为提示操作可能仍在后台运行;「源内容已变更」「前端拆分状态被并发修改」等原本直接展示英文原文的错误,改为说明原因与具体处理步骤;恢复页按钮与提示去掉 reconcile / supersede / orphan / Human Gate 等术语。
|
|
25
25
|
|
|
26
|
+
- Operator Chat 新增对话驱动的真实浏览器自动化:右侧「浏览器」面板同步共享状态,登录态与下载隔离在仓库外;模型可操作同一浏览器完成导航、搜索、结构化快照、元素操作与截图,不读写仓库文件,也不暴露页面脚本执行。
|
|
27
|
+
- 模型驱动的共享浏览器默认改为可见外部窗口,并保留右侧预览;地址、搜索与元素操作不再默认生成大截图,搜索逐字填写并提交真实表单,启动时降低自动化指纹;无法嵌入的页面提供外部打开与聚焦入口,同一持久配置可完成站点安全验证。
|
|
28
|
+
- 修复观测页「执行历史」把仍在重试的节点误报为「最终失败」的问题:只要节点最后一个已记录的尝试失败,就会显示「N 次 · 最终失败」,即使该节点仍在运行、重试已经排队,会让人误判运行已结束。现在节点处于运行/已启动/暂停状态时改为「N 次 · 最近一次失败,仍在重试」,真正到达终态才显示「最终失败」;修复轮次的多轮标签同样处理。
|
|
29
|
+
|
|
30
|
+
- 修复重试额度提升到 5 次后遗留的过期说明:只读 Pi 节点的默认重试次数已在 2026-09-05 由 3 次提升到 5 次,但运行时文档、模板示例与 npm 包内置的 `backend-test-dag.json` 仍写着 3 次,导致目标项目看到的仍是旧额度。现已同步为 5 次,并把模板漂移检查扩展到重试策略字段,避免同类过期再次漏过。
|
|
31
|
+
|
|
32
|
+
- 修复 Console「更新程序」被一条卡住的旧操作永久堵死的问题:停在「等待核对」的操作记录已被启动对账跳过、自身无法再推进,却又被当成「仍在运行」,导致之后每次更新都提示「另一个 Console 操作正在进行」。现将其明确归为已结束、不再占用操作位,并在恢复页保留可见;同时补全「已安装成功但接棒失败」记录的自动收敛,不再永久停留。
|
|
33
|
+
|
|
34
|
+
- Console 操作文案改为面向操作者的说明并给出下一步:等待超时不再伪装成「需要核对」,改为提示操作可能仍在后台运行;「源内容已变更」「前端拆分状态被并发修改」等原本直接展示英文原文的错误,改为说明原因与具体处理步骤;恢复页按钮与提示去掉 reconcile / supersede / orphan / Human Gate 等术语。
|
|
35
|
+
|
|
36
|
+
- 前端配对观测支持核对六类浏览器报告的登记、终态资源和检查结果,拒绝矛盾或缺失证据;浏览器通过与完整产品验收分开记录,环境异常保留未知。
|
|
37
|
+
|
|
38
|
+
- 实验语料登记新增六类前端配对格式,核对需求、初始文件与检查器实物摘要;复用现有实验计划生成器,保留通用语料原有约束。
|
|
39
|
+
|
|
40
|
+
- 前端配对验收可读取任务已登记的网关快照,按实验格归属汇总供应商调用;拒绝缺失或摘要不符的证据,明确区分观测记录与尚未证明的全任务统计。
|
|
41
|
+
|
|
42
|
+
- 前端配对任务可冻结网关布局,启动前核对实验计划、全部实验格、合同和登记顺序;网关引用先登记的计划摘要,避免摘要相互依赖。
|
|
43
|
+
|
|
44
|
+
- 前端实验日志可按预登记拓扑核对跨网关请求图,只汇总供应商边并追溯实验格;断链、串网关与重复ID拒绝,未解析的失败转发保持未知成本。
|
|
45
|
+
|
|
46
|
+
- 前端实验支持按固定摘要核对单网关合同、逐请求日志和总账,拒绝缺行/重复/身份或用量冲突;未知用量继续区别于保守预留,不将单网关记录冒充全任务证据。
|
|
47
|
+
|
|
48
|
+
- 修复实验计量网关多实例同时打开同一账本可超额的问题:增加独占writer锁,正常关闭和启动失败释放,旧锁不自动夺取或重置预算。
|
|
49
|
+
|
|
50
|
+
- 实验网关记录请求转发关联和上游类型,区分多层日志与供应商侧调用尝试;内部关联头不发送给供应商,共享总额度经过跨实验格HTTP回归验证。
|
|
51
|
+
|
|
52
|
+
- 归档前端实验计量网关,补充实验格绑定、请求正文摘要、选定显式参数和响应模型记录;日志不保存提示词或凭据,重启时拒绝实验身份漂移。
|
|
53
|
+
|
|
54
|
+
- 修正前端评测的任务成本口径:预算预留数和DAG阶段耗时单独保留,缺少全任务实采时不再当成实际请求数或总耗时;缺来源用量时不把局部token当成总量。
|
|
55
|
+
|
|
56
|
+
- 前端配对统计补充首次和最终成功率差异的保守置信区间,保留缺失样本与小样本不确定性,不自动据此晋升候选。
|
|
57
|
+
|
|
58
|
+
- 前端配对验收可核对预登记的来源、预算、初始树及全任务模型收据;快照漂移拒绝,缺少采样参数或供应商实际模型版本证据时继续保留未知。
|
|
59
|
+
|
|
60
|
+
- 修复前端计划把现有只读测试误判为写入越界的问题;计划预检和正式策略门禁均允许安全读取已有测试,新增或修改文件仍严格受写入范围约束。
|
|
61
|
+
|
|
62
|
+
- 前端评审按合同核验行为测试报告覆盖,缺失整组报告、仅执行其他文件、失败或跳过用例时阻止进入评审;完整原始报告与分片继续按摘要复核。
|
|
63
|
+
|
|
64
|
+
- 修正旧计数器浏览器证据把任务成功率误计为通过的问题:浏览器成功仅保留局部事实,完整质量门禁缺失时交付结论保持未知,已证实的行为失败仍保留。
|
|
65
|
+
|
|
66
|
+
- Counter 独立浏览器报告可接入任务统计,核对登记的检查器、终态产物和完整检查项;自测、串运行与矛盾通过标记被拒绝,执行错误保留未知。
|
|
67
|
+
|
|
68
|
+
- 前端任务统计可读取冻结运行和完整根账本,保留失败消耗;缺少来源或独立行为验收时保留未知,避免将 DAG 完成状态直接计为正确交付。
|
|
69
|
+
|
|
70
|
+
- 显式前端配对评测可在运行前核验协议摘要、已登记样本和实际控制器指纹,并将绑定冻结进运行事实;恢复时重新核验,版本不符在分配运行目录前拒绝。
|
|
71
|
+
|
|
72
|
+
- 前端配对评测新增预登记身份核验,重验候选文件、计划内容和登记时间,仅接受明确登记的版本差异,共同模型/输入/预算/验收身份变化继续拒绝。
|
|
73
|
+
|
|
74
|
+
- 前端评测新增基于已登记样本的任务级描述统计,区分首次成功与最终完成,保留失败成本和未知观测;统计本身不构成验收或晋升结论。
|
|
75
|
+
|
|
76
|
+
- 修复 Console 放弃或替代孤立运行时的命令参数错误;运行器收到退出信号时保留监听器直到状态保存结束,避免 Pi 的退出依赖提前终止进程。
|
|
77
|
+
|
|
78
|
+
- 前端 SDK 收到明确预算耗尽错误时主动结束自动重试等待,处理重试对象延后创建的取消时序;普通限流仍可重试,退出未确认仍阻断后续执行。
|
|
79
|
+
|
|
80
|
+
- 前端风险分级与流程路由统一识别明确的排除条款,避免“无需新增依赖或构建系统”误升级;有例外条件或正向需求时仍保留风险信号。
|
|
81
|
+
|
|
82
|
+
- 修复前端契约补充分组信息后,规划阶段重复计算同一需求、错误阻断执行的问题;按需求 ID 读取最新记录并保留完整历史。
|
|
83
|
+
|
|
84
|
+
- 来源成本证据绑定冻结合同版本,区分当前与历史批次,拒绝同版本内容冲突和版本归属漂移。
|
|
85
|
+
|
|
86
|
+
- 来源成本验收核对批次记录、请求时间与任务归属;缺批次编号或记录损坏时保留未知,防止旧记录混入完整证据。
|
|
87
|
+
|
|
88
|
+
- 前端来源准备记录独立执行编号及前后合同版本,请求可追溯到具体重试批次,失败记录继续保留。
|
|
89
|
+
|
|
90
|
+
- 前端配对验收新增全任务成本检查,避免 Plan 局部节省掩盖来源和后续执行成本上涨;用量未知不再获得整体通过。
|
|
91
|
+
|
|
92
|
+
- 前端 DAG 将已确认的来源处理 token 纳入累计预算;验收核对冻结收据,证据丢失或变化时保留未知状态,避免漏算成本。
|
|
93
|
+
|
|
94
|
+
- 任务请求预留记录所属观测会话,来源用量可按会话核对;缺失、冲突或无法归属的用量继续标为未知,避免漏算为零。
|
|
95
|
+
|
|
96
|
+
- 前端语义整理接入共享任务请求额度,预算停止后不再换模型重试,并为用量记录保存输入文档指纹。
|
|
97
|
+
|
|
98
|
+
- 前端 DAG 冻结并继承任务请求账本,来源与后续 DAG 共用预留额度;账本缺失、身份漂移或计数回退拒绝运行,未知来源 token 保留缺失标记。
|
|
99
|
+
|
|
100
|
+
- 来源整理与复核默认使用任务持久请求账本,先保存预留再调用模型;重启不清零,损坏账本、额度漂移及未绑定旧用量会阻断。来源账本与 DAG 的累计继承仍待完成。
|
|
101
|
+
|
|
102
|
+
- 来源整理与复核适配器支持传递共享请求预算预留回调,供统一预算账本接入;默认任务的来源费用与 DAG 账本整合仍在进行中。
|
|
103
|
+
|
|
104
|
+
- 修复前端验收漏算 continuation 父运行成本、同一模型列表掩盖实际模型变化、回归场景仅凭声明通过的问题;恢复预算账本拒绝用量倒退、额度漂移和缺失事实丢失。
|
|
105
|
+
|
|
106
|
+
- 来源整理与独立复核收到预算耗尽后立即停止模型回退,失败信息按实际尝试数记录。
|
|
107
|
+
- 前端 canary 支持从独立评测目录读取绑定运行指纹的验收记录和配对文件;核对真实 Plan 模型、设计评审终态和逐项验收结果,缺证据、错配或行为漏检继续阻断整体通过。
|
|
108
|
+
|
|
109
|
+
- 明确的模型调用预算耗尽错误保留为预算停止,不再被网络错误覆盖并触发节点重试或模型回退。
|
|
110
|
+
|
|
111
|
+
- 前端 Scout 同一次节点执行中重试时复用已成功提交事实的分支,仅补失败或缺事实分支;模型变更不复用,已消耗 token 不重复累计。明确设计与测试入口的调查分工,减少重复查看依赖内部实现。
|
|
112
|
+
|
|
113
|
+
- 来源保真复核仅在项目配置的模型档位间回退,不再自动尝试全局目录中的其他模型。
|
|
114
|
+
|
|
115
|
+
- 修复结构化 draft 创建前端任务时漏建来源账本、导致 Contract 节点在模型调用前失败的问题;统一使用现有来源保真校验。
|
|
116
|
+
|
|
117
|
+
- 前端评审获得合同引用的未修改测试源码与实际测试执行记录,按 hash 复核来源及完整性;data/Mock 分批不再重复扣除固定提示词预算,共享领域仍完整处理。
|
|
118
|
+
|
|
119
|
+
- 前端 DAG 恢复按问题归属从 Contract、Scout、Plan 或实现节点重跑,清除失效的下游批准;父 Plan 快照绑定来源、账本和当前评审,避免复用过期问题。
|
|
120
|
+
- 前端评审支持受冻结需求约束的修复断言;无效断言保留阻断问题。Plan 无进展时停止追加模型请求,成功终态不被迟到错误推翻。
|
|
121
|
+
- 前端测试执行前核对 Vitest 报告能力和 npm 测试脚本(含前后置钩子);保留计划证据等级与接口边界,明确测试文件通过不等于需求行为已充分验证。
|
|
122
|
+
- 前端合同保留必须遵守的能力条款,交付文件进入写入范围;交付义务不再豁免真实实现位置的归属检查。
|
|
26
123
|
- 修复后端测试 DAG 模板遗漏 `upstream-unavailable` 重试类别的问题:内置模板的 Markdown 计划节点缺少该类别,与运行时生成器不一致,导致照模板配置的项目少一类重试;现已补齐,并把模板漂移检查扩展到重试策略字段,避免同类不一致再次漏过。
|
|
27
124
|
|
|
28
125
|
- Operator Chat 右侧辅助栏改为三个表面:仓库、会话、工作台。点上下文环进入「会话 / 上下文」,文件和终端不再与会话检查挤在同一条标签上;工作台用内侧图标轨切换终端、浏览器和数据库。
|
|
@@ -208,3 +208,24 @@ export function formatBudgetReportMarkdown(ledger) {
|
|
|
208
208
|
}
|
|
209
209
|
return `${lines.join("\n")}\n`;
|
|
210
210
|
}
|
|
211
|
+
export const frontendObservedSessionIdentitySchema = z.object({
|
|
212
|
+
sourceExecutionId: z.string().uuid().optional(),
|
|
213
|
+
sessionId: z.string().min(1), phase: z.string().min(1), executionKind: z.enum(["model", "runtime"]), dispatchReason: z.enum(["initial", "correction"]),
|
|
214
|
+
scopeIds: z.array(z.string()), taskId: z.string().nullable(), runId: z.string().nullable(), nodeId: z.string().nullable(), attempt: z.number().int().nullable(),
|
|
215
|
+
sourceDigest: z.string().nullable(), contractDigest: z.string().nullable(),
|
|
216
|
+
}).strict();
|
|
217
|
+
/** Durable task request authority and the source-bound snapshot carried by a DAG. */
|
|
218
|
+
export const sourceProviderBudgetSchema = z.object({
|
|
219
|
+
schemaVersion: z.literal(1), taskId: z.string().min(1), budgetId: z.string().uuid(),
|
|
220
|
+
maxProviderRequests: z.number().int().positive(), reservedRequests: z.number().int().nonnegative(),
|
|
221
|
+
requestSessions: z.record(z.object({ identity: frontendObservedSessionIdentitySchema, reservedRequests: z.number().int().positive() }).strict()).optional(),
|
|
222
|
+
}).strict().refine(value => value.reservedRequests <= value.maxProviderRequests && Object.entries(value.requestSessions ?? {}).every(([key, session]) => key === `${session.identity.runId ?? "task"}:${session.identity.sessionId}` && session.identity.executionKind === "model" && session.identity.taskId === value.taskId) && Object.values(value.requestSessions ?? {}).reduce((sum, session) => sum + session.reservedRequests, 0) <= value.reservedRequests);
|
|
223
|
+
export const sourceProviderBudgetBindingSchema = z.object({
|
|
224
|
+
ledger: sourceProviderBudgetSchema,
|
|
225
|
+
contract: z.object({ revision: z.number().int().nonnegative(), canonicalHash: z.string().regex(/^[a-f0-9]{64}$/) }).strict().optional(),
|
|
226
|
+
sourceBindingSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
227
|
+
evidence: z.object({ executions: z.array(z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/), content: z.string() }).strict()).optional(), receipts: z.array(z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/), content: z.string() }).strict()), diagnostics: z.array(z.string()) }).strict().optional(),
|
|
228
|
+
}).strict();
|
|
229
|
+
export function sourceBudgetContractIdentity(binding) {
|
|
230
|
+
return binding ? { revision: binding.revision, canonicalHash: binding.canonicalHash } : undefined;
|
|
231
|
+
}
|
|
@@ -9,24 +9,19 @@ function normalizeTask(task) {
|
|
|
9
9
|
featureId: task.featureId ?? null,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Canonical payload for corpusHash.
|
|
14
|
-
* Excludes corpusId, createdAt, description, corpusHash, and free-text notes.
|
|
15
|
-
*/
|
|
12
|
+
/** Excludes display metadata, but binds every frontend input and oracle ref. */
|
|
16
13
|
export function buildCanonicalCorpusPayload(manifest) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
const files = (refs) => refs.map(ref => ({ path: ref.path, sha256: ref.sha256 })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
15
|
+
const tasks = (manifest.schemaVersion === 2
|
|
16
|
+
? manifest.tasks.map(task => ({ ...normalizeTask(task), category: task.category,
|
|
17
|
+
requirements: files(task.requirements), initialFiles: files(task.initialFiles), oracleFiles: files(task.oracleFiles) }))
|
|
18
|
+
: manifest.tasks.map(normalizeTask))
|
|
19
|
+
.sort((a, b) => a.taskRef < b.taskRef ? -1 : a.taskRef > b.taskRef ? 1 : 0);
|
|
20
|
+
return { schemaVersion: manifest.schemaVersion,
|
|
21
|
+
...(manifest.schemaVersion === 2 ? { purpose: manifest.purpose } : {}), tasks };
|
|
24
22
|
}
|
|
25
23
|
export function computeCorpusHash(manifest) {
|
|
26
|
-
const
|
|
27
|
-
const digest = createHash("sha256")
|
|
28
|
-
.update(stableStringify(payload))
|
|
29
|
-
.digest("hex");
|
|
24
|
+
const digest = createHash("sha256").update(stableStringify(buildCanonicalCorpusPayload(manifest))).digest("hex");
|
|
30
25
|
return formatContentSha(digest);
|
|
31
26
|
}
|
|
32
27
|
export function assertCorpusHashMatches(manifest) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { loadCorpusManifestFromPath, listCorpusIds, materializeCorpusManifest, readCorpusManifest, registerCorpusManifest, summarizeCorpus, } from "../../infrastructure/evaluation/corpus-store.js";
|
|
1
|
+
import { loadCorpusManifestFromPath, assertCorpusArtifacts, listCorpusIds, materializeCorpusManifest, readCorpusManifest, registerCorpusManifest, summarizeCorpus, } from "../../infrastructure/evaluation/corpus-store.js";
|
|
2
2
|
export async function validateCorpus(input) {
|
|
3
3
|
const raw = await loadCorpusManifestFromPath(input.repoRoot, input.manifestPath);
|
|
4
4
|
const manifest = await materializeCorpusManifest(raw);
|
|
5
|
+
await assertCorpusArtifacts(input.repoRoot, manifest);
|
|
5
6
|
return { manifest, summary: summarizeCorpus(manifest) };
|
|
6
7
|
}
|
|
7
8
|
export async function registerCorpus(input) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
7
|
+
const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
8
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
9
|
+
const refSchema = z.object({ path: z.string().min(1), sha256: digest }).strict();
|
|
10
|
+
const files = ["index.html", "src/app.js", "test/counter.test.js", "acceptance-prd.md"];
|
|
11
|
+
const requiredChecks = ["initial", "idle", "limit", "reset", "loading", "empty", "real-integration"];
|
|
12
|
+
const additionalChecks = ["noPageErrors", "preservesQuery", "loadedState", "disabledWhileLoading", "replacesOldItems", "errorState", "errorClearsOldItems"];
|
|
13
|
+
const checksSchema = (keys) => z.object(Object.fromEntries(keys.map(key => [key, z.boolean()]))).strict();
|
|
14
|
+
/** Adapter only for the frozen counter corpus and independent browser script. */
|
|
15
|
+
export async function readFrontendBrowserAcceptance(input) {
|
|
16
|
+
const observed = [];
|
|
17
|
+
const read = async (root, name, expected) => {
|
|
18
|
+
const target = path.resolve(root, name);
|
|
19
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: target, label: "frontend browser acceptance" });
|
|
20
|
+
if (expected && sha(bytes) !== expected)
|
|
21
|
+
throw Error(`browser evidence hash mismatch: ${name}`);
|
|
22
|
+
observed.push({ root, target, bytes });
|
|
23
|
+
return bytes;
|
|
24
|
+
};
|
|
25
|
+
const readRef = (value) => { const ref = refSchema.parse(value); return read(input.repoRoot, ref.path, ref.sha256); };
|
|
26
|
+
const contract = z.object({ schemaVersion: z.literal(1), kind: z.literal("counter-browser-acceptance-v1"), checker: refSchema, oracle: refSchema }).strict().parse(JSON.parse((await readRef(input.contract)).toString("utf8")));
|
|
27
|
+
await readRef(contract.checker);
|
|
28
|
+
await readRef(contract.oracle);
|
|
29
|
+
const raw = JSON.parse((await readRef(input.report)).toString("utf8"));
|
|
30
|
+
const header = z.object({ schemaVersion: z.literal(1), evidenceKind: z.literal("actual-task-artifact-browser"), runId: z.string().min(1), runStatus: z.enum(["finished", "failed", "partial_failed"]), stateSha256: digest, checkpointSha256: digest, checkerSha256: digest, frozenOracleSha256: digest,
|
|
31
|
+
sourceHashes: z.object(Object.fromEntries(files.map(file => [file, digest]))).strict(), pass: z.boolean() }).passthrough().parse(raw);
|
|
32
|
+
if (header.checkerSha256 !== contract.checker.sha256 || header.frozenOracleSha256 !== contract.oracle.sha256)
|
|
33
|
+
throw Error("browser checker not registered");
|
|
34
|
+
const state = JSON.parse((await read(input.runDir, "state.json", header.stateSha256)).toString("utf8"));
|
|
35
|
+
if (typeof state.cwd !== "string" || await realpath(state.cwd) !== await realpath(input.repoRoot))
|
|
36
|
+
throw Error("browser workspace mismatch");
|
|
37
|
+
if (state.runId !== header.runId || state.status !== header.runStatus || !state.finishedAt)
|
|
38
|
+
throw Error("browser run identity mismatch");
|
|
39
|
+
const checkpoint = z.object({ gitHeadSha: z.string().regex(/^[a-f0-9]{40,64}$/), changedPaths: z.array(z.object({ path: z.string(), contentSha256: digest.optional() }).passthrough()) }).passthrough().parse(JSON.parse((await read(input.runDir, ".runtime/workspace-checkpoint-terminal.json", header.checkpointSha256)).toString("utf8")));
|
|
40
|
+
for (const name of files) {
|
|
41
|
+
const changed = checkpoint.changedPaths.filter(row => row.path === name);
|
|
42
|
+
if (changed.length > 1)
|
|
43
|
+
throw Error("ambiguous browser checkpoint path");
|
|
44
|
+
const expected = changed.length ? changed[0].contentSha256 : sha(execFileSync("git", ["show", `${checkpoint.gitHeadSha}:${name}`], { cwd: input.repoRoot }));
|
|
45
|
+
if (header.sourceHashes[name] !== expected)
|
|
46
|
+
throw Error(`browser artifact not bound to terminal checkpoint: ${name}`);
|
|
47
|
+
}
|
|
48
|
+
let passed;
|
|
49
|
+
const diagnostics = ["counter-browser-only-product-gates-pending"];
|
|
50
|
+
if (typeof raw.error === "string") {
|
|
51
|
+
if (raw.result !== undefined || raw.additional !== undefined)
|
|
52
|
+
throw Error("browser execution error conflicts with completed checks");
|
|
53
|
+
if (header.pass)
|
|
54
|
+
throw Error("browser error cannot pass");
|
|
55
|
+
passed = null;
|
|
56
|
+
diagnostics.push("browser-execution-error");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const result = z.object({ checks: checksSchema(requiredChecks), failedChecks: z.array(z.string()) }).passthrough().parse(raw.result);
|
|
60
|
+
const additional = checksSchema(additionalChecks).parse(raw.additional);
|
|
61
|
+
const errors = z.array(z.string()).parse(raw.errors);
|
|
62
|
+
const failed = Object.entries(result.checks).filter(([, value]) => !value).map(([key]) => key).sort();
|
|
63
|
+
if (JSON.stringify(failed) !== JSON.stringify([...result.failedChecks].sort()))
|
|
64
|
+
throw Error("browser failed-check list mismatch");
|
|
65
|
+
passed = failed.length === 0 && Object.values(additional).every(Boolean) && errors.length === 0;
|
|
66
|
+
if (header.pass !== passed)
|
|
67
|
+
throw Error("browser summary contradicts checks");
|
|
68
|
+
if (passed && state.status !== "finished")
|
|
69
|
+
throw Error("failed run cannot become successful browser delivery");
|
|
70
|
+
}
|
|
71
|
+
for (const entry of observed) {
|
|
72
|
+
const latest = await readFileBoundarySafe({ boundaryRoot: entry.root, targetPath: entry.target, label: "browser evidence recheck" });
|
|
73
|
+
if (!latest.equals(entry.bytes))
|
|
74
|
+
throw Error("browser evidence changed during collection");
|
|
75
|
+
}
|
|
76
|
+
return { passed, diagnostics, runId: header.runId, reportSha256: input.report.sha256, contractSha256: input.contract.sha256 };
|
|
77
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
7
|
+
import { dagEvaluationBindingSchema } from "../../workflows/dag/types.js";
|
|
8
|
+
import { verifyControllerIdentityForResume } from "../../workflows/dag/controller-identity.js";
|
|
9
|
+
import { assertFrontendPairExecution } from "./frontend-pair-registration.js";
|
|
10
|
+
import { corpusManifestSchema } from "./types.js";
|
|
11
|
+
import { assertCorpusHashMatches } from "./corpus-hash.js";
|
|
12
|
+
const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
13
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
14
|
+
const ref = z.object({ path: z.string().min(1), sha256: digest }).strict();
|
|
15
|
+
const contractSchema = z.object({ schemaVersion: z.literal(1), kind: z.literal("frontend-corpus-browser-acceptance-v1"), corpus: ref, checker: ref, oracle: ref, coverage: ref }).strict();
|
|
16
|
+
const observationSchema = z.object({
|
|
17
|
+
schemaVersion: z.literal(1), evidenceKind: z.literal("unbound-corpus-browser-observation"), runId: z.null(), caseId: z.string(),
|
|
18
|
+
browser: z.string().nullable(), pass: z.boolean(), checks: z.record(z.boolean()), failedChecks: z.array(z.string()),
|
|
19
|
+
pageErrors: z.array(z.string()), resourceErrors: z.array(z.string()), servedFiles: z.record(digest), checkerSha256: digest,
|
|
20
|
+
infrastructureError: z.string().optional(),
|
|
21
|
+
}).passthrough();
|
|
22
|
+
const reportSchema = z.object({
|
|
23
|
+
schemaVersion: z.literal(1), evidenceKind: z.literal("actual-corpus-task-browser"), caseId: z.string(), runId: z.string().min(1),
|
|
24
|
+
runStatus: z.enum(["finished", "failed", "partial_failed"]), taskRef: z.string(), cellId: z.string(), artifactHashes: z.record(digest),
|
|
25
|
+
stateSha256: digest, runSha256: digest, checkpointSha256: digest, registration: ref, contractSha256: digest, corpusSha256: digest,
|
|
26
|
+
checkerSha256: digest, oracleSha256: digest, coverageSha256: digest, pass: z.boolean(), allChecksPresent: z.boolean(), observation: observationSchema,
|
|
27
|
+
}).strict();
|
|
28
|
+
const webAsset = (name) => name === "index.html" || ["src/", "data/", "public/", "assets/"].some(prefix => name.startsWith(prefix));
|
|
29
|
+
function safeName(name) {
|
|
30
|
+
if (!name || name.includes("\\") || name.includes(":") || name.includes("\0") || name.startsWith("/") || name.split("/").some(part => !part || part === "." || part === ".."))
|
|
31
|
+
throw Error("invalid corpus browser artifact path");
|
|
32
|
+
return name;
|
|
33
|
+
}
|
|
34
|
+
/** Validates browser facts for a registered run. Success here does not prove
|
|
35
|
+
* source fidelity, writer boundaries, Node-test quality, or model execution. */
|
|
36
|
+
export async function readFrontendCorpusBrowserAcceptance(input) {
|
|
37
|
+
const observed = [];
|
|
38
|
+
const read = async (root, name, expected) => {
|
|
39
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.resolve(root, name), label: "corpus browser evidence" });
|
|
40
|
+
if (expected !== undefined && sha(bytes) !== expected)
|
|
41
|
+
throw Error(`corpus browser evidence hash mismatch: ${name}`);
|
|
42
|
+
observed.push({ root, name, bytes });
|
|
43
|
+
return bytes;
|
|
44
|
+
};
|
|
45
|
+
const readRef = (value) => { const parsed = ref.parse(value); return read(input.repoRoot, parsed.path, parsed.sha256); };
|
|
46
|
+
const report = reportSchema.parse(JSON.parse((await readRef(input.report)).toString("utf8")));
|
|
47
|
+
const contract = contractSchema.parse(JSON.parse((await readRef(input.contract)).toString("utf8")));
|
|
48
|
+
if (report.contractSha256 !== input.contract.sha256 || report.corpusSha256 !== contract.corpus.sha256 ||
|
|
49
|
+
report.checkerSha256 !== contract.checker.sha256 || report.oracleSha256 !== contract.oracle.sha256 ||
|
|
50
|
+
report.coverageSha256 !== contract.coverage.sha256 || report.observation.checkerSha256 !== contract.oracle.sha256)
|
|
51
|
+
throw Error("corpus browser checker identity mismatch");
|
|
52
|
+
await readRef(contract.checker);
|
|
53
|
+
await readRef(contract.oracle);
|
|
54
|
+
const corpus = corpusManifestSchema.parse(JSON.parse((await readRef(contract.corpus)).toString("utf8")));
|
|
55
|
+
if (corpus.schemaVersion !== 2)
|
|
56
|
+
throw Error("corpus browser requires frontend corpus");
|
|
57
|
+
assertCorpusHashMatches(corpus);
|
|
58
|
+
for (const task of corpus.tasks)
|
|
59
|
+
for (const value of [...task.requirements, ...task.initialFiles, ...task.oracleFiles])
|
|
60
|
+
await readRef(value);
|
|
61
|
+
const coverage = z.object({ schemaVersion: z.literal(1), cases: z.record(z.record(z.array(z.string().min(1)).min(1))) }).passthrough()
|
|
62
|
+
.parse(JSON.parse((await readRef(contract.coverage)).toString("utf8")));
|
|
63
|
+
const state = JSON.parse((await read(input.runDir, "state.json", report.stateSha256)).toString("utf8"));
|
|
64
|
+
const spec = JSON.parse((await read(input.runDir, "run.json", report.runSha256)).toString("utf8"));
|
|
65
|
+
if (state.evidenceKind?.includes("selftest"))
|
|
66
|
+
throw Error("selftest cannot be corpus run evidence");
|
|
67
|
+
if (typeof state.cwd !== "string" || await realpath(state.cwd) !== await realpath(input.repoRoot))
|
|
68
|
+
throw Error("corpus browser workspace mismatch");
|
|
69
|
+
if (state.runId !== report.runId || state.status !== report.runStatus || !Number.isFinite(Date.parse(state.startedAt)) ||
|
|
70
|
+
!state.finishedAt || !Number.isFinite(Date.parse(state.finishedAt)) || Date.parse(state.finishedAt) < Date.parse(state.startedAt))
|
|
71
|
+
throw Error("corpus browser terminal identity mismatch");
|
|
72
|
+
const binding = dagEvaluationBindingSchema.parse(state.evaluation);
|
|
73
|
+
if (!binding.frontendPair || JSON.stringify(binding) !== JSON.stringify(spec.evaluation) || binding.taskRef !== report.taskRef ||
|
|
74
|
+
binding.frontendPair.cellId !== report.cellId || binding.frontendPair.registration.path !== report.registration.path ||
|
|
75
|
+
binding.frontendPair.registration.sha256 !== report.registration.sha256)
|
|
76
|
+
throw Error("corpus browser frozen evaluation mismatch");
|
|
77
|
+
const controller = JSON.parse((await read(input.runDir, "controller-identity.json")).toString("utf8"));
|
|
78
|
+
if (state.controllerIdentityRef?.path !== "controller-identity.json" || controller.status !== "pinned" || !controller.identity)
|
|
79
|
+
throw Error("corpus browser controller not pinned");
|
|
80
|
+
const pinned = await verifyControllerIdentityForResume({ runDir: input.runDir, state, currentIdentity: controller.identity });
|
|
81
|
+
const registration = await assertFrontendPairExecution({ repoRoot: input.repoRoot, binding, controllerFingerprint: pinned.identity.packageFingerprint.value, startedAt: state.startedAt });
|
|
82
|
+
if (registration.shared.acceptance !== input.contract.sha256)
|
|
83
|
+
throw Error("corpus browser acceptance contract not registered");
|
|
84
|
+
await readRef(report.registration);
|
|
85
|
+
const tasks = corpus.tasks.filter(task => task.taskRef === binding.taskRef);
|
|
86
|
+
if (tasks.length !== 1)
|
|
87
|
+
throw Error("corpus browser task missing");
|
|
88
|
+
const task = tasks[0];
|
|
89
|
+
if (task.category !== report.caseId || task.category !== report.observation.caseId || task.split !== binding.split || !task.seeds.includes(binding.seed))
|
|
90
|
+
throw Error("corpus browser case mismatch");
|
|
91
|
+
for (const key of ["checker", "oracle", "coverage"])
|
|
92
|
+
if (!task.oracleFiles.some(value => value.path === contract[key].path && value.sha256 === contract[key].sha256))
|
|
93
|
+
throw Error("corpus browser checker not owned by task");
|
|
94
|
+
const caseCoverage = coverage.cases[task.category];
|
|
95
|
+
if (!caseCoverage || !Object.keys(caseCoverage).length)
|
|
96
|
+
throw Error("corpus browser coverage missing");
|
|
97
|
+
const required = new Set([...Object.values(caseCoverage).flat(), "no-page-errors", "bound-resources"]);
|
|
98
|
+
const observation = report.observation;
|
|
99
|
+
if (Object.keys(observation.checks).some(name => !required.has(name) && name !== "interaction-completed"))
|
|
100
|
+
throw Error("unknown corpus browser check");
|
|
101
|
+
if (observation.checks["interaction-completed"] === true)
|
|
102
|
+
throw Error("invalid corpus interaction diagnostic");
|
|
103
|
+
// The producer records infrastructure errors before case interactions. It
|
|
104
|
+
// cannot relabel completed business checks as an environment-only failure.
|
|
105
|
+
if (observation.infrastructureError !== undefined && Object.keys(observation.checks).some(name => name !== "bound-resources"))
|
|
106
|
+
throw Error("corpus browser infrastructure phase contradicts checks");
|
|
107
|
+
const allPresent = [...required].every(name => Object.hasOwn(observation.checks, name));
|
|
108
|
+
if (allPresent !== report.allChecksPresent)
|
|
109
|
+
throw Error("corpus browser check coverage mismatch");
|
|
110
|
+
const failed = Object.entries(observation.checks).filter(([, passed]) => !passed).map(([name]) => name).sort();
|
|
111
|
+
if (JSON.stringify(failed) !== JSON.stringify([...observation.failedChecks].sort()))
|
|
112
|
+
throw Error("corpus browser failed-check list mismatch");
|
|
113
|
+
for (const [name, errors] of [["no-page-errors", observation.pageErrors], ["bound-resources", observation.resourceErrors]])
|
|
114
|
+
if (Object.hasOwn(observation.checks, name) && observation.checks[name] !== (errors.length === 0))
|
|
115
|
+
throw Error("corpus browser error flags mismatch");
|
|
116
|
+
const observedPass = observation.infrastructureError === undefined && Object.keys(observation.checks).length > 0 && failed.length === 0;
|
|
117
|
+
if (observation.pass !== observedPass || report.pass !== (state.status === "finished" && observedPass && allPresent))
|
|
118
|
+
throw Error("corpus browser summary contradicts checks");
|
|
119
|
+
const checkpoint = z.object({ schemaVersion: z.literal(1), gitHeadSha: z.string().regex(/^[a-f0-9]{40,64}$/),
|
|
120
|
+
changedPaths: z.array(z.object({ path: z.string(), state: z.string(), contentSha256: digest.optional(), mode: z.string().optional() }).passthrough()) }).passthrough()
|
|
121
|
+
.parse(JSON.parse((await read(input.runDir, ".runtime/workspace-checkpoint-terminal.json", report.checkpointSha256)).toString("utf8")));
|
|
122
|
+
const changes = new Map();
|
|
123
|
+
for (const row of checkpoint.changedPaths) {
|
|
124
|
+
safeName(row.path);
|
|
125
|
+
if (changes.has(row.path))
|
|
126
|
+
throw Error("duplicate corpus checkpoint path");
|
|
127
|
+
changes.set(row.path, row);
|
|
128
|
+
}
|
|
129
|
+
const entries = new Map();
|
|
130
|
+
const tree = execFileSync("git", ["ls-tree", "-rz", "--full-tree", checkpoint.gitHeadSha], { cwd: input.repoRoot, maxBuffer: 16 * 1024 * 1024 }).toString("utf8");
|
|
131
|
+
for (const item of tree.split("\0").filter(Boolean)) {
|
|
132
|
+
const tab = item.indexOf("\t"), name = item.slice(tab + 1), [mode, type] = item.slice(0, tab).split(" ");
|
|
133
|
+
if (webAsset(name))
|
|
134
|
+
entries.set(safeName(name), { mode, type: type });
|
|
135
|
+
}
|
|
136
|
+
for (const [name, row] of changes)
|
|
137
|
+
if (webAsset(name)) {
|
|
138
|
+
if (row.state === "deleted")
|
|
139
|
+
entries.delete(name);
|
|
140
|
+
else
|
|
141
|
+
entries.set(name, { mode: row.mode, type: "blob" });
|
|
142
|
+
}
|
|
143
|
+
if (!entries.has("index.html") || !entries.has("src/app.js") || JSON.stringify([...entries.keys()].sort()) !== JSON.stringify(Object.keys(report.artifactHashes).sort()))
|
|
144
|
+
throw Error("corpus browser terminal resource set mismatch");
|
|
145
|
+
for (const [name, entry] of entries) {
|
|
146
|
+
if (entry.type !== "blob" || entry.mode && !["100644", "100755"].includes(entry.mode))
|
|
147
|
+
throw Error("unsupported corpus browser resource mode");
|
|
148
|
+
const changed = changes.get(name);
|
|
149
|
+
if (changed && !changed.contentSha256)
|
|
150
|
+
throw Error("corpus browser checkpoint digest missing");
|
|
151
|
+
const expected = changed ? changed.contentSha256 : sha(execFileSync("git", ["show", `${checkpoint.gitHeadSha}:${name}`], { cwd: input.repoRoot, maxBuffer: 16 * 1024 * 1024 }));
|
|
152
|
+
if (report.artifactHashes[name] !== expected)
|
|
153
|
+
throw Error("corpus browser terminal resource hash mismatch");
|
|
154
|
+
}
|
|
155
|
+
for (const [name, value] of Object.entries(observation.servedFiles))
|
|
156
|
+
if (!entries.has(safeName(name)) || report.artifactHashes[name] !== value)
|
|
157
|
+
throw Error("corpus browser served unbound resource");
|
|
158
|
+
if (report.pass && (!observation.servedFiles["index.html"] || !observation.servedFiles["src/app.js"]))
|
|
159
|
+
throw Error("corpus browser entry request missing");
|
|
160
|
+
for (const snapshot of observed) {
|
|
161
|
+
const latest = await readFileBoundarySafe({ boundaryRoot: snapshot.root, targetPath: path.resolve(snapshot.root, snapshot.name), label: "corpus browser recheck" });
|
|
162
|
+
if (!latest.equals(snapshot.bytes))
|
|
163
|
+
throw Error("corpus browser evidence changed during collection");
|
|
164
|
+
}
|
|
165
|
+
const diagnostics = ["corpus-browser-only-product-gates-pending"];
|
|
166
|
+
let browserPassed = failed.length || observation.pageErrors.length || observation.resourceErrors.length ? false : allPresent ? true : null;
|
|
167
|
+
if (observation.infrastructureError !== undefined) {
|
|
168
|
+
browserPassed = observation.resourceErrors.length ? false : null;
|
|
169
|
+
diagnostics.push("corpus-browser-infrastructure-error");
|
|
170
|
+
}
|
|
171
|
+
else if (!allPresent)
|
|
172
|
+
diagnostics.push("corpus-browser-checks-incomplete");
|
|
173
|
+
return { browserPassed, diagnostics, runId: report.runId, caseId: report.caseId, taskRef: report.taskRef, cellId: report.cellId,
|
|
174
|
+
corpusHash: corpus.corpusHash, reportSha256: input.report.sha256, contractSha256: input.contract.sha256, artifactHashes: report.artifactHashes };
|
|
175
|
+
}
|