@ai-agent-forge/agent-forge 0.88.5 → 0.88.6
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 +28 -0
- package/bundled-plugins/agent-loop-policy/agent-forge.json +1 -1
- package/bundled-plugins/agent-loop-policy/plugin.json +1 -1
- package/bundled-plugins/alignment/agent-forge.json +19 -0
- package/bundled-plugins/alignment/dist/baseline.d.ts +12 -0
- package/bundled-plugins/alignment/dist/baseline.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/baseline.js +68 -0
- package/bundled-plugins/alignment/dist/baseline.js.map +1 -0
- package/bundled-plugins/alignment/dist/engine.d.ts +102 -0
- package/bundled-plugins/alignment/dist/engine.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/engine.js +654 -0
- package/bundled-plugins/alignment/dist/engine.js.map +1 -0
- package/bundled-plugins/alignment/dist/entry.d.ts +14 -0
- package/bundled-plugins/alignment/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/entry.js +23 -0
- package/bundled-plugins/alignment/dist/entry.js.map +1 -0
- package/bundled-plugins/alignment/dist/export.d.ts +15 -0
- package/bundled-plugins/alignment/dist/export.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/export.js +122 -0
- package/bundled-plugins/alignment/dist/export.js.map +1 -0
- package/bundled-plugins/alignment/dist/http-server.d.ts +33 -0
- package/bundled-plugins/alignment/dist/http-server.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/http-server.js +119 -0
- package/bundled-plugins/alignment/dist/http-server.js.map +1 -0
- package/bundled-plugins/alignment/dist/index.d.ts +24 -0
- package/bundled-plugins/alignment/dist/index.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/index.js +719 -0
- package/bundled-plugins/alignment/dist/index.js.map +1 -0
- package/bundled-plugins/alignment/dist/page.d.ts +15 -0
- package/bundled-plugins/alignment/dist/page.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/page.js +832 -0
- package/bundled-plugins/alignment/dist/page.js.map +1 -0
- package/bundled-plugins/alignment/dist/render.d.ts +40 -0
- package/bundled-plugins/alignment/dist/render.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/render.js +194 -0
- package/bundled-plugins/alignment/dist/render.js.map +1 -0
- package/bundled-plugins/alignment/dist/types.d.ts +217 -0
- package/bundled-plugins/alignment/dist/types.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/types.js +30 -0
- package/bundled-plugins/alignment/dist/types.js.map +1 -0
- package/bundled-plugins/alignment/plugin.json +12 -0
- package/bundled-plugins/browser-playwright/MANUAL.md +60 -0
- package/bundled-plugins/browser-playwright/agent-forge.json +25 -0
- package/bundled-plugins/browser-playwright/dist/capability.d.ts +25 -0
- package/bundled-plugins/browser-playwright/dist/capability.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/capability.js +612 -0
- package/bundled-plugins/browser-playwright/dist/capability.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/entry.d.ts +13 -0
- package/bundled-plugins/browser-playwright/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/entry.js +14 -0
- package/bundled-plugins/browser-playwright/dist/entry.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/index.d.ts +7 -0
- package/bundled-plugins/browser-playwright/dist/index.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/index.js +5 -0
- package/bundled-plugins/browser-playwright/dist/index.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/operations.d.ts +69 -0
- package/bundled-plugins/browser-playwright/dist/operations.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/operations.js +149 -0
- package/bundled-plugins/browser-playwright/dist/operations.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.d.ts +275 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.js +953 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/setup.d.ts +88 -0
- package/bundled-plugins/browser-playwright/dist/setup.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/setup.js +252 -0
- package/bundled-plugins/browser-playwright/dist/setup.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/types.d.ts +270 -0
- package/bundled-plugins/browser-playwright/dist/types.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/types.js +2 -0
- package/bundled-plugins/browser-playwright/dist/types.js.map +1 -0
- package/bundled-plugins/browser-playwright/plugin.json +20 -0
- package/bundled-plugins/coding-tools/agent-forge.json +1 -1
- package/bundled-plugins/coding-tools/dist/bash.d.ts +2 -7
- package/bundled-plugins/coding-tools/dist/bash.d.ts.map +1 -1
- package/bundled-plugins/coding-tools/dist/bash.js +2 -34
- package/bundled-plugins/coding-tools/dist/bash.js.map +1 -1
- package/bundled-plugins/coding-tools/plugin.json +1 -1
- package/bundled-plugins/compaction-policy/agent-forge.json +1 -1
- package/bundled-plugins/compaction-policy/plugin.json +1 -1
- package/bundled-plugins/mcp-http/dist/mcp-http.d.ts.map +1 -1
- package/bundled-plugins/mcp-http/dist/mcp-http.js +84 -31
- package/bundled-plugins/mcp-http/dist/mcp-http.js.map +1 -1
- package/bundled-plugins/overflow-policy/agent-forge.json +1 -1
- package/bundled-plugins/overflow-policy/plugin.json +1 -1
- package/bundled-plugins/provider-failover/agent-forge.json +1 -1
- package/bundled-plugins/provider-failover/plugin.json +1 -1
- package/bundled-plugins/retry-policy/agent-forge.json +1 -1
- package/bundled-plugins/retry-policy/plugin.json +1 -1
- package/dist/bundle/.build-manifest.json +60 -58
- package/dist/bundle/chunks/{anthropic-messages-3FJHRAJL.js → anthropic-messages-NL4Y6WPB.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-GCL47IDS.js → azure-openai-responses-CPXC42HO.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/chunk-5LRQFZFE.js +2 -0
- package/dist/bundle/chunks/{chunk-HTEVUY7F.js → chunk-7HSJPQKN.js} +1 -1
- package/dist/bundle/chunks/{chunk-AC2FIS4G.js → chunk-EDO7Q6XR.js} +1 -1
- package/dist/bundle/chunks/{chunk-755VNWIF.js → chunk-ISLHFRER.js} +6 -6
- package/dist/bundle/chunks/{chunk-HV64ASMT.js → chunk-K3S6GTL7.js} +1 -1
- package/dist/bundle/chunks/{chunk-HEL6TAWE.js → chunk-POLDAUYA.js} +37 -36
- package/dist/bundle/chunks/{chunk-V3ACW7VD.js → chunk-WJSKB3FC.js} +1 -1
- package/dist/bundle/chunks/{chunk-32YDZI4J.js → chunk-ZS5JEI76.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-6BMXWA44.js → google-generative-ai-UMZ53EFA.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-6H7NLRTK.js → google-vertex-ZNR5NS6H.js} +1 -1
- package/dist/bundle/chunks/image-resize-worker.js +1 -1
- package/dist/bundle/chunks/{mistral-conversations-IXUHSFSJ.js → mistral-conversations-IT66WUNN.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-YHUAEVCM.js → openai-codex-responses-FOVCVG6I.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-ULQCFC72.js → openai-completions-FLQ7CBOL.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-NLLAJDPS.js → openai-responses-ABGYHS2T.js} +1 -1
- package/dist/bundle/chunks/{pi-messages-EJTI2EJ3.js → pi-messages-6K5W6LUN.js} +1 -1
- package/dist/bundle/chunks/{virtual-modules-TQWEGNNZ.js → virtual-modules-2QGILFV7.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/client.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/client/remote-session.d.ts.map +1 -1
- package/dist/client/remote-session.js +65 -6
- package/dist/client/remote-session.js.map +1 -1
- package/dist/core/agent-session-store.d.ts.map +1 -1
- package/dist/core/agent-session-store.js +20 -3
- package/dist/core/agent-session-store.js.map +1 -1
- package/dist/core/extensions/agent-composition.d.ts.map +1 -1
- package/dist/core/extensions/agent-composition.js +20 -1
- package/dist/core/extensions/agent-composition.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.js +1 -0
- package/dist/core/i18n/locales/zh-cn/core-errors.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js +10 -3
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js.map +1 -1
- package/dist/core/keybindings.d.ts +35 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +34 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +62 -15
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +11 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +2 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/capability-mount.d.ts.map +1 -1
- package/dist/core/tools/capability-mount.js +4 -21
- package/dist/core/tools/capability-mount.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -2
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +2 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +25 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/changelog-pager.d.ts +53 -0
- package/dist/modes/interactive/components/changelog-pager.d.ts.map +1 -0
- package/dist/modes/interactive/components/changelog-pager.js +239 -0
- package/dist/modes/interactive/components/changelog-pager.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.d.ts +41 -11
- package/dist/modes/interactive/components/tool-run-group.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.js +126 -14
- package/dist/modes/interactive/components/tool-run-group.js.map +1 -1
- package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-commands.js +23 -13
- package/dist/modes/interactive/interactive-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-events.d.ts +14 -4
- package/dist/modes/interactive/interactive-events.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-events.js +94 -12
- package/dist/modes/interactive/interactive-events.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +6 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +21 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/plugins/ecosystem/node-local-package-source.d.ts +2 -1
- package/dist/plugins/ecosystem/node-local-package-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-local-package-source.js +110 -4
- package/dist/plugins/ecosystem/node-local-package-source.js.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.js +106 -39
- package/dist/plugins/ecosystem/node-package-install-host.js.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js +59 -2
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js.map +1 -1
- package/dist/suites/builtin-suites.d.ts.map +1 -1
- package/dist/suites/builtin-suites.js +2 -2
- package/dist/suites/builtin-suites.js.map +1 -1
- package/dist/utils/changelog.d.ts +27 -0
- package/dist/utils/changelog.d.ts.map +1 -1
- package/dist/utils/changelog.js +110 -0
- package/dist/utils/changelog.js.map +1 -1
- package/docs/keybindings.md +14 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/extensions/with-deps/plugin.json +1 -1
- package/package.json +1 -1
- package/dist/bundle/chunks/chunk-A3JYRWB6.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.88.6] - 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Remote session 默认自动续租:attached 会话按租约 TTL/3 周期调用 `lease_renew`(无配置开关),连续 3 次失败按 `session_removed` 同路径转入 unbound;`SessionLease.renew()`/`leaseExpiresAt` API 见 packages/client。测试 `test/client/remote-session-lease-renew.test.ts`。
|
|
10
|
+
- bundled 分发通道纳入 browser-playwright(2026-10-08 用户裁决:浏览器操作开箱可用,从 0 安装即可 mount):构建拷 plugin.json / agent-forge.json / MANUAL.md / dist/** 四件,宿主 bundled 发现根与装配豁免集合同步登记;`activation: on-demand`,仅 mountable 卡片进系统提示,`capability_mount` 按需挂载。测试 `test/bundled-browser-playwright.test.ts`,设计 docs/design/插件独立分发与启动装配设计.md §4.9。
|
|
11
|
+
- bundled 分发通道纳入 alignment(同日用户裁决:需求对齐是编程助手开箱能力,从 0 安装即可用):构建拷 plugin.json / agent-forge.json / dist/** 三件,`always` 激活三工具进默认 full-control 工具面(默认工具面 8→13)。测试 `test/bundled-browser-playwright.test.ts`,设计 docs/design/插件独立分发与启动装配设计.md §4.9。
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- 工具运行折叠组展示升级(D-083 2026-10-08 增补,设计见 docs/design/终端输出模式设计.md §3):marker 图标 `⏺` 改 `●`、思考摘要 `✻` 改 `※`(原字符跨终端字体覆盖差,无字形终端显示豆腐块);`Ctrl+O` 切换工具输出展开不再向转录写入「工具输出:已展开/已折叠」状态行;marker 行并入组级墙钟耗时——运行中为盲文 spinner + 工具名 + 走动秒数(如 `⠙ bash 3.2s`),完成后定格 `● 3 个工具 5.1s`(会话恢复的历史组无耗时);活动工具执行中在 marker 行下实时渲染其输出尾部 8 行,最新完成工具的尾部预览保持显示至下一工具开始或组封口(quiet 态唯一过程文本例外);相邻回合的工具组中间无任何用户可见内容时合并为一组(计数累加、耗时连续累计)。
|
|
16
|
+
- 启动"新特性"面板改渲染紧凑摘要:空版本段整体跳过(v0.88.5 那种光杆版本头不再出现),每条只保留首句,每版本与版本数有上限,超出部分指向 `/changelog`。设计见 docs/design/版本新特性面板与Changelog分页.md。
|
|
17
|
+
- `/changelog` 从全量灌入 chat 改为 overlay 预览器:内容按块懒渲染(只渲染可视窗口,不再一次性加载全部历史),`↑`/`↓`/`PgUp`/`PgDn`/`Home`/`End`/滚轮翻页滚动,`Ctrl+F` 搜索(`Enter` 跳转下一个命中),`q`/`Esc` 关闭且不在滚动缓冲留下任何内容。设计见 docs/design/版本新特性面板与Changelog分页.md。
|
|
18
|
+
- 全局"按需加载"治理(2026-10-08 审计轮,设计见 docs/design/版本新特性面板与Changelog分页.md):`/resume` 扫描每会话搜索文本设 64K 字符预算(超预算尾部不参与会话搜索,扫描内存与 fuzzy 匹配成本有界);`/agents` 详情回读只从文件头部读 maxBytes 字节(超大 transcript 不再整文件进内存)。
|
|
19
|
+
- 提示词冲突审计轮(docs/design/提示词设计宪法.md §6 变更记录,2026-10-07):① Work rules 仲裁句补完成标准——根修完全超出任务范围时 "delivering that decision-ready trade-off completes the task when no in-scope fix exists",消除与首条 Work rule "never with a plan" 的收尾冲突;② coding 人格 Cautious 的 "take the safer path" 改为 "gather evidence and verify before changing — doubt is never resolved by making the fix smaller"(safer 锚定到验证而非尺寸,封死浅修偏置经 "safer" 换皮回归);③ coding 人格 Docs as code 改为 "follow the repository's documentation conventions; …"(原文在无文档约定的外部仓库与 "add nothing unsolicited" 对撞);④ 主模板 Agent Forge 文档段 "Always read the relevant .md files completely" 补 "When working on Agent Forge topics" 作用域(防 "Always" 逃逸出段落标题)。人格规格同步 方案系统设计.md §6.2;assistant 人格与 Work rules 的结构性对立(A4)登记为宪法 §7 已知限制,未改文本。角色种子 reviewer 的同步修复见 plugins/subagent-delegate CHANGELOG。
|
|
20
|
+
- Work rules 最小改动措辞收紧为"增量上限"而非"深度上限"(docs/design/提示词设计宪法.md §3 原则 5 仲裁,2026-10-07):原句 "Make the smallest change that fully satisfies the task; add nothing unsolicited — no extra features, refactors, warnings, or disclaimers" 与 coding 人格的 first-principles 根因指令并排且无仲裁,模型把 "smallest" 读作修复深度上限、"no refactors" 读作重构禁令、"no warnings/disclaimers" 封死取舍上报,bug 修复倾向症状级小补丁。重写后禁令清单显式限定 `beyond the task itself`,并新增仲裁句 `"Smallest" bounds what you add, never how deeply you fix` 与上报义务(根修超出任务范围或风险时 surface the trade-off and your recommendation,禁止 silently downgrading to a symptom patch)。根因指令保持单源在 coding 人格(一次原则不动);取材确认参考基准(gpt-6-astra、claude-code-opus-5.5、claude-code-2.1.172)无现成仲裁措辞,自拟。文本断言钉在 `test/system-prompt.test.ts`。
|
|
21
|
+
- capability_mount/capability_unmount 失败(未知插件、无归属工具、unmount 未挂载)改为抛异常、由宿主统一标 isError,不再返回内嵌错误文本的正常结果(模型此前把失败当成功继续走);错误消息文本不变。测试 `test/capability-mount.test.ts`,设计 docs/design/插件按需挂载与能力手册设计.md §5.2。
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `operation.onCancel()` 注册的处理器现在在所有取消路径必然收到一次通知(携带 reason);此前 revoke 排空先移除底层监听器导致回调永不执行。契约钉死:通知到达时操作 facade 已 revoked,handler 只能做不依赖 facade 的清理。测试 `test/agent-composition.test.ts`。
|
|
26
|
+
- 插件激活改为"先持久提交、成功后再停旧 runtime":此前提交失败时旧 runtime 已被提前释放,账面 active 指向已死实例(只能靠调用方回滚或重启自愈);现提交失败时旧 runtime 与账面一致可重试,停旧失败也不影响已提交的新版本存活。同 key 的 activate 按实例级队列串行化(公共 API 并发调用者此前可产生账面 active 而运行时已死的静默不一致;跨进程一致性仍由持久层 per-key 唯一 active 校验兜底)。旧 runtime dispose 失败残留的句柄在 rollback 重注册时与新句柄组合清理(此前被直接覆盖、原始实例永久泄漏)。测试 `test/plugins/ecosystem/node-package-install-host.test.ts`。
|
|
27
|
+
- `plugin install <目录>` 采集遵循 package.json `files` 白名单(对齐 npm pack:目录条目含子树、README*/LICENSE*/package.json 隐式保留、glob 条目明确报错指引改用 .tgz),恒排除 `node_modules`/`.git`——混入 `tsconfig.json` 致安装店 jiti 装载崩溃的缺陷闭合;无 `files` 字段的包保持全采集。package.json 读取/解析失败(权限、IO、畸形 JSON)现在使安装失败并保留原始错误,不再静默降级为全量采集。测试 `test/plugins/ecosystem/node-local-package-source.test.ts`。
|
|
28
|
+
- memory 插件未启用时提交 `/promote` 现在给出本地错误提示(需要 memory 插件命令面),不再把命令原文当作普通消息静默发给模型;memory 启用时行为不变。
|
|
29
|
+
- bootstrap npm 安装事务在 activate 失败时先回滚旧 active 版本(对齐 CLI 更新事务),修复同 key 旧版本 runtime 被提前释放后无法恢复、需重启才能自愈的问题。测试 `test/plugins/ecosystem/node-plugin-bootstrap-rollback.test.ts`。
|
|
30
|
+
- bash 输出组件加留存上限(尾部 50KB,与执行器滚动缓冲同预算同口径):长输出命令运行期 UI 内存不再无界累积、消除每 chunk 全量 join 的 O(n²) 重聚合,命令结束后不再常驻;显示语义不变(预览/展开与截断提示照旧,完整输出仍走 fullOutputPath)。
|
|
31
|
+
- memory: inference service bin is now a self-contained esbuild bundle — npm-form store copies could not resolve host-supplied specifiers (`@agent-forge/protocol` is not on npm), so the service died at module resolution and semantic recall degraded. Details in `plugins/memory/CHANGELOG.md`. (Fix shipped in v0.88.5; backfilled because the v0.88.5 host section was released empty and the What's-New panel reads only this file.)
|
|
32
|
+
|
|
5
33
|
## [0.88.5] - 2026-10-07
|
|
6
34
|
|
|
7
35
|
## [0.88.4] - 2026-10-07
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"packageFormatVersion": 1,
|
|
3
|
+
"id": "agent-forge.plugin.alignment",
|
|
4
|
+
"pluginVersion": "0.88.6",
|
|
5
|
+
"apiVersion": "1",
|
|
6
|
+
"minHostVersion": "0.84.4",
|
|
7
|
+
"platform": {
|
|
8
|
+
"os": [
|
|
9
|
+
"windows",
|
|
10
|
+
"macos",
|
|
11
|
+
"linux"
|
|
12
|
+
],
|
|
13
|
+
"runtime": [
|
|
14
|
+
{
|
|
15
|
+
"name": "node"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Final-baseline persistence (设计 §5 round flow 4, §13.2 收口): when the
|
|
3
|
+
* design step confirms, the engine writes a read-only human-readable snapshot
|
|
4
|
+
* to `<workspace>/alignments/<id>.md`. Workspace product-subdirectory writes
|
|
5
|
+
* follow the image-generation `generated-images/` precedent (flag "wx" so a
|
|
6
|
+
* name collision fails instead of overwriting). The id whitelist in the
|
|
7
|
+
* engine makes the filename traversal-proof.
|
|
8
|
+
*/
|
|
9
|
+
import type { AlignmentRecord } from "./types.ts";
|
|
10
|
+
export declare function baselineFilePath(workspaceRoot: string, alignmentId: string): string;
|
|
11
|
+
export declare function writeBaselineFile(record: AlignmentRecord, workspaceRoot: string): Promise<string>;
|
|
12
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAelD,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2CvG","sourcesContent":["/**\n * Final-baseline persistence (设计 §5 round flow 4, §13.2 收口): when the\n * design step confirms, the engine writes a read-only human-readable snapshot\n * to `<workspace>/alignments/<id>.md`. Workspace product-subdirectory writes\n * follow the image-generation `generated-images/` precedent (flag \"wx\" so a\n * name collision fails instead of overwriting). The id whitelist in the\n * engine makes the filename traversal-proof.\n */\n\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AlignmentRecord } from \"./types.ts\";\n\nconst KIND_CN: Record<string, string> = {\n\tfact: \"取证事实\",\n\tassumption: \"假设\",\n\tdecision: \"决策\",\n\tmissing: \"待补充\",\n};\n\nconst STEP_CN: Record<string, string> = {\n\tobjective: \"目标\",\n\tdetail: \"细节\",\n\tdesign: \"设计\",\n};\n\nexport function baselineFilePath(workspaceRoot: string, alignmentId: string): string {\n\treturn join(workspaceRoot, \"alignments\", `${alignmentId}.md`);\n}\n\nexport async function writeBaselineFile(record: AlignmentRecord, workspaceRoot: string): Promise<string> {\n\tconst frontmatter = [\n\t\t\"---\",\n\t\t`id: ${record.id}`,\n\t\t`title: ${JSON.stringify(record.title)}`,\n\t\trecord.goalId ? `goal: ${record.goalId}` : null,\n\t\t`status: ${record.status}`,\n\t\t`revision: ${record.revision}`,\n\t\t`rounds: ${record.roundCount}`,\n\t\t\"schema: alignment-baseline/1\",\n\t\t\"---\",\n\t]\n\t\t.filter((line): line is string => line !== null)\n\t\t.join(\"\\n\");\n\n\tconst sections = record.steps\n\t\t.map((step) => {\n\t\t\tconst header = `## ${STEP_CN[step.step] ?? step.step}(${step.blocks.length} 块,第 ${step.baselineConfirmedAtRound ?? \"-\"} 轮确认)`;\n\t\t\tconst items = step.blocks\n\t\t\t\t.map((block) => {\n\t\t\t\t\tconst lines = [\n\t\t\t\t\t\t`- [${KIND_CN[block.kind] ?? block.kind}] ${block.id}: ${block.content.replace(/\\n/g, \" \")}`,\n\t\t\t\t\t];\n\t\t\t\t\tif (block.rationale) lines.push(` - 理由:${block.rationale.replace(/\\n/g, \" \")}`);\n\t\t\t\t\treturn lines.join(\"\\n\");\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t\treturn `${header}\\n\\n${items}`;\n\t\t})\n\t\t.join(\"\\n\\n\");\n\n\tconst body = `${frontmatter}\n\n# 对齐基线:${record.title}\n\n> 只读快照(机器真值在会话 entries 的 alignment.state);实施中改动任一决策,须回对齐插件重新确认该块。\n\n${sections}\n`;\n\tconst path = baselineFilePath(workspaceRoot, record.id);\n\tawait mkdir(join(workspaceRoot, \"alignments\"), { recursive: true });\n\tawait writeFile(path, body, { encoding: \"utf8\", flag: \"wx\" });\n\treturn path;\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Final-baseline persistence (设计 §5 round flow 4, §13.2 收口): when the
|
|
3
|
+
* design step confirms, the engine writes a read-only human-readable snapshot
|
|
4
|
+
* to `<workspace>/alignments/<id>.md`. Workspace product-subdirectory writes
|
|
5
|
+
* follow the image-generation `generated-images/` precedent (flag "wx" so a
|
|
6
|
+
* name collision fails instead of overwriting). The id whitelist in the
|
|
7
|
+
* engine makes the filename traversal-proof.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
const KIND_CN = {
|
|
12
|
+
fact: "取证事实",
|
|
13
|
+
assumption: "假设",
|
|
14
|
+
decision: "决策",
|
|
15
|
+
missing: "待补充",
|
|
16
|
+
};
|
|
17
|
+
const STEP_CN = {
|
|
18
|
+
objective: "目标",
|
|
19
|
+
detail: "细节",
|
|
20
|
+
design: "设计",
|
|
21
|
+
};
|
|
22
|
+
export function baselineFilePath(workspaceRoot, alignmentId) {
|
|
23
|
+
return join(workspaceRoot, "alignments", `${alignmentId}.md`);
|
|
24
|
+
}
|
|
25
|
+
export async function writeBaselineFile(record, workspaceRoot) {
|
|
26
|
+
const frontmatter = [
|
|
27
|
+
"---",
|
|
28
|
+
`id: ${record.id}`,
|
|
29
|
+
`title: ${JSON.stringify(record.title)}`,
|
|
30
|
+
record.goalId ? `goal: ${record.goalId}` : null,
|
|
31
|
+
`status: ${record.status}`,
|
|
32
|
+
`revision: ${record.revision}`,
|
|
33
|
+
`rounds: ${record.roundCount}`,
|
|
34
|
+
"schema: alignment-baseline/1",
|
|
35
|
+
"---",
|
|
36
|
+
]
|
|
37
|
+
.filter((line) => line !== null)
|
|
38
|
+
.join("\n");
|
|
39
|
+
const sections = record.steps
|
|
40
|
+
.map((step) => {
|
|
41
|
+
const header = `## ${STEP_CN[step.step] ?? step.step}(${step.blocks.length} 块,第 ${step.baselineConfirmedAtRound ?? "-"} 轮确认)`;
|
|
42
|
+
const items = step.blocks
|
|
43
|
+
.map((block) => {
|
|
44
|
+
const lines = [
|
|
45
|
+
`- [${KIND_CN[block.kind] ?? block.kind}] ${block.id}: ${block.content.replace(/\n/g, " ")}`,
|
|
46
|
+
];
|
|
47
|
+
if (block.rationale)
|
|
48
|
+
lines.push(` - 理由:${block.rationale.replace(/\n/g, " ")}`);
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
})
|
|
51
|
+
.join("\n");
|
|
52
|
+
return `${header}\n\n${items}`;
|
|
53
|
+
})
|
|
54
|
+
.join("\n\n");
|
|
55
|
+
const body = `${frontmatter}
|
|
56
|
+
|
|
57
|
+
# 对齐基线:${record.title}
|
|
58
|
+
|
|
59
|
+
> 只读快照(机器真值在会话 entries 的 alignment.state);实施中改动任一决策,须回对齐插件重新确认该块。
|
|
60
|
+
|
|
61
|
+
${sections}
|
|
62
|
+
`;
|
|
63
|
+
const path = baselineFilePath(workspaceRoot, record.id);
|
|
64
|
+
await mkdir(join(workspaceRoot, "alignments"), { recursive: true });
|
|
65
|
+
await writeFile(path, body, { encoding: "utf8", flag: "wx" });
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Final-baseline persistence (设计 §5 round flow 4, §13.2 收口): when the\n * design step confirms, the engine writes a read-only human-readable snapshot\n * to `<workspace>/alignments/<id>.md`. Workspace product-subdirectory writes\n * follow the image-generation `generated-images/` precedent (flag \"wx\" so a\n * name collision fails instead of overwriting). The id whitelist in the\n * engine makes the filename traversal-proof.\n */\n\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AlignmentRecord } from \"./types.ts\";\n\nconst KIND_CN: Record<string, string> = {\n\tfact: \"取证事实\",\n\tassumption: \"假设\",\n\tdecision: \"决策\",\n\tmissing: \"待补充\",\n};\n\nconst STEP_CN: Record<string, string> = {\n\tobjective: \"目标\",\n\tdetail: \"细节\",\n\tdesign: \"设计\",\n};\n\nexport function baselineFilePath(workspaceRoot: string, alignmentId: string): string {\n\treturn join(workspaceRoot, \"alignments\", `${alignmentId}.md`);\n}\n\nexport async function writeBaselineFile(record: AlignmentRecord, workspaceRoot: string): Promise<string> {\n\tconst frontmatter = [\n\t\t\"---\",\n\t\t`id: ${record.id}`,\n\t\t`title: ${JSON.stringify(record.title)}`,\n\t\trecord.goalId ? `goal: ${record.goalId}` : null,\n\t\t`status: ${record.status}`,\n\t\t`revision: ${record.revision}`,\n\t\t`rounds: ${record.roundCount}`,\n\t\t\"schema: alignment-baseline/1\",\n\t\t\"---\",\n\t]\n\t\t.filter((line): line is string => line !== null)\n\t\t.join(\"\\n\");\n\n\tconst sections = record.steps\n\t\t.map((step) => {\n\t\t\tconst header = `## ${STEP_CN[step.step] ?? step.step}(${step.blocks.length} 块,第 ${step.baselineConfirmedAtRound ?? \"-\"} 轮确认)`;\n\t\t\tconst items = step.blocks\n\t\t\t\t.map((block) => {\n\t\t\t\t\tconst lines = [\n\t\t\t\t\t\t`- [${KIND_CN[block.kind] ?? block.kind}] ${block.id}: ${block.content.replace(/\\n/g, \" \")}`,\n\t\t\t\t\t];\n\t\t\t\t\tif (block.rationale) lines.push(` - 理由:${block.rationale.replace(/\\n/g, \" \")}`);\n\t\t\t\t\treturn lines.join(\"\\n\");\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t\treturn `${header}\\n\\n${items}`;\n\t\t})\n\t\t.join(\"\\n\\n\");\n\n\tconst body = `${frontmatter}\n\n# 对齐基线:${record.title}\n\n> 只读快照(机器真值在会话 entries 的 alignment.state);实施中改动任一决策,须回对齐插件重新确认该块。\n\n${sections}\n`;\n\tconst path = baselineFilePath(workspaceRoot, record.id);\n\tawait mkdir(join(workspaceRoot, \"alignments\"), { recursive: true });\n\tawait writeFile(path, body, { encoding: \"utf8\", flag: \"wx\" });\n\treturn path;\n}\n"]}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure alignment state machine (设计 §5) — no host API access, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* Every function takes JSON-safe state and returns the next state plus the
|
|
5
|
+
* audit transitions to persist; index.ts owns persistence (entries + events)
|
|
6
|
+
* and http-server.ts owns transport. Deterministic by construction: same
|
|
7
|
+
* inputs, same outputs, no clocks, no randomness.
|
|
8
|
+
*/
|
|
9
|
+
import { type AlignmentBlockKind, type AlignmentRecord, type AlignmentRoundPost, type AlignmentRoundResult, type AlignmentState, type AlignmentStepId, type AlignmentStepState, type AlignmentTransitionEntry, type GateSnapshot } from "./types.ts";
|
|
10
|
+
export interface EngineRun {
|
|
11
|
+
state: AlignmentState;
|
|
12
|
+
transitions: AlignmentTransitionEntry[];
|
|
13
|
+
}
|
|
14
|
+
/** Model-facing block input (alignment_set); comment ownership is checked against it. */
|
|
15
|
+
export interface BlockInput {
|
|
16
|
+
id: string;
|
|
17
|
+
kind: AlignmentBlockKind;
|
|
18
|
+
content: string;
|
|
19
|
+
rationale?: string;
|
|
20
|
+
impact?: string[];
|
|
21
|
+
/** New assistant comments for this block (author is forced to assistant). */
|
|
22
|
+
comments?: {
|
|
23
|
+
id: string;
|
|
24
|
+
note: string;
|
|
25
|
+
quote?: string;
|
|
26
|
+
}[];
|
|
27
|
+
/** Resolutions the model attaches to existing comments. */
|
|
28
|
+
commentResolutions?: {
|
|
29
|
+
commentId: string;
|
|
30
|
+
kind: "answered" | "dismissed" | "incorporated";
|
|
31
|
+
text?: string;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
export interface AlignmentSetInput {
|
|
35
|
+
id: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
goalId?: string;
|
|
38
|
+
revision?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Explicit confirmation that this write may rewrite blocks the user already
|
|
41
|
+
* approved (总则 3): without it, a fingerprint change on an approved block
|
|
42
|
+
* is rejected with `approved-blocks-conflict` instead of silently resetting
|
|
43
|
+
* it to pending.
|
|
44
|
+
*/
|
|
45
|
+
reviseApproved?: boolean;
|
|
46
|
+
blocks: BlockInput[];
|
|
47
|
+
}
|
|
48
|
+
export interface AdvanceInput {
|
|
49
|
+
alignmentId: string;
|
|
50
|
+
action: "inspect" | "retreat" | "cancel" | "rollback";
|
|
51
|
+
step?: AlignmentStepId;
|
|
52
|
+
note?: string;
|
|
53
|
+
/** Rollback target: the active step's snapshot revision anchor; latest snapshot when omitted. */
|
|
54
|
+
versionId?: number;
|
|
55
|
+
}
|
|
56
|
+
export interface RoundPostOutcome {
|
|
57
|
+
record: AlignmentRecord;
|
|
58
|
+
transitions: AlignmentTransitionEntry[];
|
|
59
|
+
result: AlignmentRoundResult;
|
|
60
|
+
}
|
|
61
|
+
export interface AdvanceOutcome {
|
|
62
|
+
state: AlignmentState;
|
|
63
|
+
transitions: AlignmentTransitionEntry[];
|
|
64
|
+
}
|
|
65
|
+
export declare function validAlignmentId(id: string): boolean;
|
|
66
|
+
/** Fingerprint of the fields whose change must re-ask for approval (总则 3). */
|
|
67
|
+
export declare function blockFingerprint(kind: AlignmentBlockKind, content: string, rationale: string | undefined, impact: string[] | undefined): string;
|
|
68
|
+
export declare function findRecord(state: AlignmentState, id: string): AlignmentRecord | undefined;
|
|
69
|
+
export declare function activeStep(record: AlignmentRecord): AlignmentStepState;
|
|
70
|
+
/**
|
|
71
|
+
* Gate facts (总则 2): the three server-checkable conditions. A missing block
|
|
72
|
+
* without a stored user answer counts as `missing-empty`; once answered it is
|
|
73
|
+
* just an unapproved block waiting for the model to restructure it.
|
|
74
|
+
*/
|
|
75
|
+
export declare function gate(record: AlignmentRecord): GateSnapshot;
|
|
76
|
+
export declare function applyAlignmentSet(state: AlignmentState, input: AlignmentSetInput): EngineRun;
|
|
77
|
+
export declare function applyRoundPost(record0: AlignmentRecord, post: AlignmentRoundPost): RoundPostOutcome;
|
|
78
|
+
/** Injection succeeded — the delivered transition closes the round (terminal audit D1). */
|
|
79
|
+
export declare function markDelivered(record: AlignmentRecord, round: number): AlignmentTransitionEntry[];
|
|
80
|
+
export declare function applyAlignmentAdvance(state: AlignmentState, input: AdvanceInput): AdvanceOutcome;
|
|
81
|
+
export interface VersionRollbackInput {
|
|
82
|
+
alignmentId: string;
|
|
83
|
+
/** Target snapshot revision from `record.versions` (must be the active step's). */
|
|
84
|
+
targetRevision: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Step-scoped version rollback (走查 2026-10-03): drop the current unconfirmed
|
|
88
|
+
* version (not archived), promote the target snapshot to live, engine-bump the
|
|
89
|
+
* revision (D4). Comments survive append-only — restored blocks carry the union
|
|
90
|
+
* of live and archived comments by comment id, live status winning (a
|
|
91
|
+
* resolution is irreversible). Only the active (unlocked) step accepts it;
|
|
92
|
+
* locked steps go through retreat. Other steps are never touched.
|
|
93
|
+
*/
|
|
94
|
+
export declare function applyVersionRollback(state: AlignmentState, input: VersionRollbackInput): EngineRun;
|
|
95
|
+
/** Compact final baseline listing (§5 round flow 4): one line per block, answers first. */
|
|
96
|
+
export declare function finalBaselineLines(record: AlignmentRecord): {
|
|
97
|
+
step: AlignmentStepId;
|
|
98
|
+
kind: AlignmentBlockKind;
|
|
99
|
+
id: string;
|
|
100
|
+
line: string;
|
|
101
|
+
}[] | undefined;
|
|
102
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure alignment state machine (设计 §5) — no host API access, no I/O.\n *\n * Every function takes JSON-safe state and returns the next state plus the\n * audit transitions to persist; index.ts owns persistence (entries + events)\n * and http-server.ts owns transport. Deterministic by construction: same\n * inputs, same outputs, no clocks, no randomness.\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n\tALIGNMENT_LIMITS,\n\ttype AlignmentBlock,\n\ttype AlignmentBlockKind,\n\ttype AlignmentComment,\n\tAlignmentError,\n\ttype AlignmentRecord,\n\ttype AlignmentRoundPost,\n\ttype AlignmentRoundResult,\n\ttype AlignmentState,\n\ttype AlignmentStepId,\n\ttype AlignmentStepState,\n\ttype AlignmentTransitionEntry,\n\ttype AlignmentVersionSnapshot,\n\ttype GateSnapshot,\n\ttype PendingItem,\n\tSTEP_ORDER,\n} from \"./types.ts\";\n\nexport interface EngineRun {\n\tstate: AlignmentState;\n\ttransitions: AlignmentTransitionEntry[];\n}\n\n/** Model-facing block input (alignment_set); comment ownership is checked against it. */\nexport interface BlockInput {\n\tid: string;\n\tkind: AlignmentBlockKind;\n\tcontent: string;\n\trationale?: string;\n\timpact?: string[];\n\t/** New assistant comments for this block (author is forced to assistant). */\n\tcomments?: { id: string; note: string; quote?: string }[];\n\t/** Resolutions the model attaches to existing comments. */\n\tcommentResolutions?: {\n\t\tcommentId: string;\n\t\tkind: \"answered\" | \"dismissed\" | \"incorporated\";\n\t\ttext?: string;\n\t}[];\n}\n\nexport interface AlignmentSetInput {\n\tid: string;\n\ttitle?: string;\n\tgoalId?: string;\n\trevision?: number;\n\t/**\n\t * Explicit confirmation that this write may rewrite blocks the user already\n\t * approved (总则 3): without it, a fingerprint change on an approved block\n\t * is rejected with `approved-blocks-conflict` instead of silently resetting\n\t * it to pending.\n\t */\n\treviseApproved?: boolean;\n\tblocks: BlockInput[];\n}\n\nexport interface AdvanceInput {\n\talignmentId: string;\n\taction: \"inspect\" | \"retreat\" | \"cancel\" | \"rollback\";\n\tstep?: AlignmentStepId;\n\tnote?: string;\n\t/** Rollback target: the active step's snapshot revision anchor; latest snapshot when omitted. */\n\tversionId?: number;\n}\n\nexport interface RoundPostOutcome {\n\trecord: AlignmentRecord;\n\ttransitions: AlignmentTransitionEntry[];\n\tresult: AlignmentRoundResult;\n}\n\nexport interface AdvanceOutcome {\n\tstate: AlignmentState;\n\ttransitions: AlignmentTransitionEntry[];\n}\n\n// ---------------------------------------------------------------------------\n// Hashing & validation\n// ---------------------------------------------------------------------------\n\nconst ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nexport function validAlignmentId(id: string): boolean {\n\t// 入参可能来自未校验的工具输入(空对象 → undefined):test() 会把实参\n\t// 字符串化为 \"undefined\" 而\"通过\"模式匹配,必须先做类型守卫。\n\treturn typeof id === \"string\" && ID_PATTERN.test(id) && id.length <= 128;\n}\n\n/** Fingerprint of the fields whose change must re-ask for approval (总则 3). */\nexport function blockFingerprint(\n\tkind: AlignmentBlockKind,\n\tcontent: string,\n\trationale: string | undefined,\n\timpact: string[] | undefined,\n): string {\n\tconst canonical = JSON.stringify([kind, content, rationale ?? null, impact ?? null]);\n\treturn createHash(\"sha256\").update(canonical, \"utf8\").digest(\"hex\");\n}\n\nfunction nonEmptyString(value: unknown, name: string, max: number): string {\n\tif (typeof value !== \"string\" || value.trim() === \"\") {\n\t\tthrow new AlignmentError(\"invalid-input\", `${name} must be a non-empty string`);\n\t}\n\tif (value.length > max) {\n\t\tthrow new AlignmentError(\"invalid-input\", `${name} exceeds ${max} characters`);\n\t}\n\treturn value;\n}\n\nfunction validateBlockInput(input: BlockInput): void {\n\tif (typeof input.id !== \"string\" || input.id.trim() === \"\" || input.id.length > 128) {\n\t\tthrow new AlignmentError(\n\t\t\t\"invalid-block\",\n\t\t\t`block id must be a non-empty string (≤128 chars): ${String(input.id)}`,\n\t\t);\n\t}\n\tif (![\"fact\", \"assumption\", \"decision\", \"missing\", \"visual\"].includes(input.kind)) {\n\t\tthrow new AlignmentError(\"invalid-block\", `invalid block kind: ${String(input.kind)}`);\n\t}\n\tnonEmptyString(input.content, `block ${input.id} content`, ALIGNMENT_LIMITS.contentMaxLength);\n\tif (input.rationale !== undefined && input.rationale.length > ALIGNMENT_LIMITS.rationaleMaxLength) {\n\t\tthrow new AlignmentError(\n\t\t\t\"invalid-block\",\n\t\t\t`block ${input.id} rationale exceeds ${ALIGNMENT_LIMITS.rationaleMaxLength} characters`,\n\t\t);\n\t}\n\tif (input.impact?.some((item) => typeof item !== \"string\" || item.trim() === \"\")) {\n\t\tthrow new AlignmentError(\"invalid-block\", `block ${input.id} impact must be a non-empty string array`);\n\t}\n\tif ((input.comments?.length ?? 0) > ALIGNMENT_LIMITS.notesPerBlockMax) {\n\t\tthrow new AlignmentError(\n\t\t\t\"invalid-block\",\n\t\t\t`block ${input.id} carries more than ${ALIGNMENT_LIMITS.notesPerBlockMax} comments`,\n\t\t);\n\t}\n\tfor (const comment of input.comments ?? []) {\n\t\tif (typeof comment.id !== \"string\" || comment.id.trim() === \"\") {\n\t\t\tthrow new AlignmentError(\"invalid-comment\", `comment id on block ${input.id} must be a non-empty string`);\n\t\t}\n\t\tnonEmptyString(comment.note, `comment ${comment.id} note`, ALIGNMENT_LIMITS.noteMaxLength);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// State helpers\n// ---------------------------------------------------------------------------\n\nexport function findRecord(state: AlignmentState, id: string): AlignmentRecord | undefined {\n\treturn state.alignments.find((record) => record.id === id);\n}\n\nexport function activeStep(record: AlignmentRecord): AlignmentStepState {\n\tconst found = record.steps.find((candidate) => candidate.step === record.activeStep);\n\tif (!found) throw new AlignmentError(\"invalid-input\", `active step missing: ${record.activeStep}`);\n\treturn found;\n}\n\n/**\n * Gate facts (总则 2): the three server-checkable conditions. A missing block\n * without a stored user answer counts as `missing-empty`; once answered it is\n * just an unapproved block waiting for the model to restructure it.\n */\nexport function gate(record: AlignmentRecord): GateSnapshot {\n\tconst step = activeStep(record);\n\tconst pending: PendingItem[] = [];\n\tlet unapproved = 0;\n\tfor (const block of step.blocks) {\n\t\tif (block.kind === \"missing\" && (block.draftAnswer ?? \"\").trim() === \"\") {\n\t\t\tpending.push({ blockId: block.id, reason: \"missing-empty\" });\n\t\t\tunapproved += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (block.status !== \"approved\") {\n\t\t\tpending.push({ blockId: block.id, reason: \"unapproved\" });\n\t\t\tunapproved += 1;\n\t\t}\n\t}\n\tlet openComments = 0;\n\tfor (const block of step.blocks) {\n\t\tfor (const comment of block.comments) {\n\t\t\tif (comment.status !== \"open\") continue;\n\t\t\topenComments += 1;\n\t\t\tif (comment.author === \"assistant\") {\n\t\t\t\tpending.push({ blockId: block.id, reason: \"assistant-question-open\" });\n\t\t\t}\n\t\t}\n\t}\n\tconst pendingAdditions = record.pendingFeedback?.additions.length ?? 0;\n\treturn {\n\t\tunapproved,\n\t\topenComments,\n\t\tpendingAdditions,\n\t\tallGreen: unapproved === 0 && openComments === 0 && pendingAdditions === 0,\n\t\tpending,\n\t};\n}\n\nfunction nextStep(step: AlignmentStepId): AlignmentStepId | undefined {\n\tconst index = STEP_ORDER.indexOf(step);\n\treturn index >= 0 && index < STEP_ORDER.length - 1 ? STEP_ORDER[index + 1] : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// alignment_set: create / update (总则 3 hash inheritance, 总则 5 comment merge)\n// ---------------------------------------------------------------------------\n\nfunction materializeBlock(\n\tinput: BlockInput,\n\tprevious: AlignmentBlock | undefined,\n\tcurrentRound: number,\n): AlignmentBlock {\n\tconst fingerprint = blockFingerprint(input.kind, input.content, input.rationale, input.impact);\n\tconst unchanged = previous !== undefined && previous.contentHash === fingerprint;\n\tconst block: AlignmentBlock = {\n\t\tid: input.id,\n\t\tkind: input.kind,\n\t\tcontent: input.content,\n\t\trationale: input.rationale,\n\t\timpact: input.impact,\n\t\t// Hash-identical resent blocks keep their approval (防确认疲劳); changed\n\t\t// content resets to pending and flags the round it changed in.\n\t\tstatus: unchanged ? previous.status : \"pending\",\n\t\tcontentHash: fingerprint,\n\t\t...(unchanged ? {} : { modifiedInRound: currentRound + 1 }),\n\t\t...(previous?.draftAnswer !== undefined ? { draftAnswer: previous.draftAnswer } : {}),\n\t\tcomments: [],\n\t};\n\tblock.comments = mergeComments(previous?.comments ?? [], input, currentRound);\n\treturn block;\n}\n\nfunction mergeComments(existing: AlignmentComment[], input: BlockInput, round: number): AlignmentComment[] {\n\tconst merged: AlignmentComment[] = existing.map((comment) => ({ ...comment }));\n\tconst resolutions = new Map((input.commentResolutions ?? []).map((item) => [item.commentId, item]));\n\t// Existing comments are append-only (总则 5): user text survives resends\n\t// verbatim, open assistant questions cannot silently disappear; only an\n\t// explicit resolution closes them.\n\tfor (const comment of merged) {\n\t\tconst resolution = resolutions.get(comment.id);\n\t\tif (!resolution) continue;\n\t\tresolutions.delete(comment.id);\n\t\tif (comment.status === \"resolved\") {\n\t\t\tthrow new AlignmentError(\"comment-ownership\", `comment ${comment.id} is already resolved`);\n\t\t}\n\t\tcomment.status = \"resolved\";\n\t\tcomment.resolution = { kind: resolution.kind, text: resolution.text, round };\n\t}\n\tif (resolutions.size > 0) {\n\t\tconst ids = [...resolutions.keys()].join(\", \");\n\t\tthrow new AlignmentError(\"comment-ownership\", `resolutions reference unknown comments: ${ids}`);\n\t}\n\t// New comments are assistant-only and deduped by note so a resent open\n\t// question never duplicates itself.\n\tfor (const fresh of input.comments ?? []) {\n\t\tif (merged.some((comment) => comment.author === \"assistant\" && comment.note === fresh.note)) continue;\n\t\tmerged.push({\n\t\t\tid: fresh.id,\n\t\t\tauthor: \"assistant\",\n\t\t\t...(fresh.quote !== undefined ? { quote: fresh.quote } : {}),\n\t\t\tnote: fresh.note,\n\t\t\tstatus: \"open\",\n\t\t\tcreatedAtRound: round,\n\t\t});\n\t}\n\treturn merged;\n}\n\nexport function applyAlignmentSet(state: AlignmentState, input: AlignmentSetInput): EngineRun {\n\tif (!validAlignmentId(input.id)) {\n\t\tthrow new AlignmentError(\n\t\t\t\"invalid-id\",\n\t\t\t`alignment id must match [A-Za-z0-9][A-Za-z0-9._-]* (no leading dot): ${input.id}`,\n\t\t);\n\t}\n\tif (!Array.isArray(input.blocks) || input.blocks.length === 0) {\n\t\tthrow new AlignmentError(\"invalid-input\", \"blocks must be a non-empty array\");\n\t}\n\tif (input.blocks.length > ALIGNMENT_LIMITS.blocksPerStepMax) {\n\t\tthrow new AlignmentError(\"invalid-input\", `a step may carry at most ${ALIGNMENT_LIMITS.blocksPerStepMax} blocks`);\n\t}\n\tconst seen = new Set<string>();\n\tfor (const block of input.blocks) {\n\t\tvalidateBlockInput(block);\n\t\tif (seen.has(block.id)) {\n\t\t\tthrow new AlignmentError(\"invalid-block\", `duplicate block id within the step: ${block.id}`);\n\t\t}\n\t\tseen.add(block.id);\n\t}\n\n\tconst existing = findRecord(state, input.id);\n\tif (!existing) {\n\t\t// Create: created-as-aligning (no draft state) and single-active (总则 9).\n\t\tconst active = state.alignments.find((record) => record.status === \"aligning\" || record.status === \"finalizing\");\n\t\tif (active) {\n\t\t\tthrow new AlignmentError(\n\t\t\t\t\"active-alignment-exists\",\n\t\t\t\t`an alignment is already in progress: ${active.id}; complete or cancel it first`,\n\t\t\t);\n\t\t}\n\t\tconst record: AlignmentRecord = {\n\t\t\tid: input.id,\n\t\t\ttitle: input.title ?? input.id,\n\t\t\t...(input.goalId !== undefined ? { goalId: input.goalId } : {}),\n\t\t\tstatus: \"aligning\",\n\t\t\trevision: 0,\n\t\t\tactiveStep: \"objective\",\n\t\t\tsteps: [\n\t\t\t\t{\n\t\t\t\t\tstep: \"objective\",\n\t\t\t\t\tstatus: \"active\",\n\t\t\t\t\tblocks: input.blocks.map((block) => materializeBlock(block, undefined, 0)),\n\t\t\t\t},\n\t\t\t\t{ step: \"detail\", status: \"pending\", blocks: [] },\n\t\t\t\t{ step: \"design\", status: \"pending\", blocks: [] },\n\t\t\t],\n\t\t\troundCount: 0,\n\t\t};\n\t\treturn { state: { alignments: [...state.alignments, record] }, transitions: [] };\n\t}\n\n\tif (existing.status !== \"aligning\") {\n\t\tthrow new AlignmentError(\"terminal-state\", `alignment is terminal and cannot be updated: ${input.id}`);\n\t}\n\tconst revision = input.revision ?? 0;\n\tif (revision <= existing.revision) {\n\t\tthrow new AlignmentError(\n\t\t\t\"stale-revision\",\n\t\t\t`stale alignment revision: ${input.id} expects > ${existing.revision}, got ${revision}`,\n\t\t);\n\t}\n\tconst step = activeStep(existing);\n\tif (step.status !== \"active\" && step.status !== \"reset\") {\n\t\tthrow new AlignmentError(\n\t\t\t\"step-not-writable\",\n\t\t\t`step ${existing.activeStep} is ${step.status}; only the active step accepts blocks`,\n\t\t);\n\t}\n\t// Deleting a block that still carries open comments is rejected (总则 3):\n\t// deletion must not become a way to dodge unresolved objections.\n\tfor (const previous of step.blocks) {\n\t\tif (input.blocks.some((block) => block.id === previous.id)) continue;\n\t\tif (previous.comments.some((comment) => comment.status === \"open\")) {\n\t\t\tthrow new AlignmentError(\n\t\t\t\t\"block-has-open-comments\",\n\t\t\t\t`block ${previous.id} carries open comments; resolve them before deleting it (edit the content instead)`,\n\t\t\t);\n\t\t}\n\t}\n\t// 已批准块写入防护(v3.7,2026-10-08 dogfood 根因):全量重写会按内容指纹\n\t// 把已确认块静默重置 pending——未显式确认时结构化拒绝,列出将被重置的块。\n\tif (!input.reviseApproved) {\n\t\tconst conflicts = input.blocks\n\t\t\t.filter((blockInput) => {\n\t\t\t\tconst previous = step.blocks.find((candidate) => candidate.id === blockInput.id);\n\t\t\t\treturn (\n\t\t\t\t\tprevious?.status === \"approved\" &&\n\t\t\t\t\tprevious.contentHash !==\n\t\t\t\t\t\tblockFingerprint(blockInput.kind, blockInput.content, blockInput.rationale, blockInput.impact)\n\t\t\t\t);\n\t\t\t})\n\t\t\t.map((blockInput) => blockInput.id);\n\t\tif (conflicts.length > 0) {\n\t\t\tthrow new AlignmentError(\n\t\t\t\t\"approved-blocks-conflict\",\n\t\t\t\t`this write would reset approved blocks to pending: ${conflicts.join(\", \")}; resubmit with reviseApproved: true, or keep the approved block content unchanged`,\n\t\t\t);\n\t\t}\n\t}\n\tconst blocks = input.blocks.map((blockInput) => {\n\t\tconst previous = step.blocks.find((candidate) => candidate.id === blockInput.id);\n\t\treturn materializeBlock(blockInput, previous, existing.roundCount);\n\t});\n\t// Version snapshot (走查 2026-10-03): archive the outgoing step block set\n\t// before it is replaced. Append-only, bounded rolling; the page views\n\t// historical versions read-only — only the latest accepts interaction.\n\t// An empty outgoing set has nothing to roll back to — skip it, otherwise\n\t// the version bar fills with chips that render a blank view.\n\tconst archive = JSON.stringify(step.blocks);\n\tconst snapshot: AlignmentVersionSnapshot = {\n\t\trevision: existing.revision,\n\t\tround: existing.roundCount,\n\t\tstep: existing.activeStep,\n\t\tblocks: archive.length <= ALIGNMENT_LIMITS.versionsSnapshotMaxBytes ? structuredClone(step.blocks) : [],\n\t\t...(archive.length > ALIGNMENT_LIMITS.versionsSnapshotMaxBytes ? { truncated: true as const } : {}),\n\t};\n\tconst priorVersions = (\n\t\tstep.blocks.length === 0 ? (existing.versions ?? []) : [...(existing.versions ?? []), snapshot]\n\t).slice(-ALIGNMENT_LIMITS.versionsMax);\n\tconst updated: AlignmentRecord = {\n\t\t...existing,\n\t\ttitle: input.title ?? existing.title,\n\t\tgoalId: input.goalId ?? existing.goalId,\n\t\trevision,\n\t\tsteps: existing.steps.map((candidate) =>\n\t\t\tcandidate.step === existing.activeStep ? { ...candidate, blocks } : candidate,\n\t\t),\n\t\tpendingFeedback: undefined,\n\t\tversions: priorVersions,\n\t};\n\treturn {\n\t\tstate: { alignments: state.alignments.map((record) => (record.id === updated.id ? updated : record)) },\n\t\ttransitions: [],\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// POST /round: the user feedback round (§5 round flow, v3.2 audits)\n// ---------------------------------------------------------------------------\n\nexport function applyRoundPost(record0: AlignmentRecord, post: AlignmentRoundPost): RoundPostOutcome {\n\tif (record0.status !== \"aligning\" && record0.status !== \"finalizing\") {\n\t\tthrow new AlignmentError(\"terminal-state\", `alignment is terminal: ${record0.id}`);\n\t}\n\t// Anti-staleness gate (terminal audit D6): the page must confirm against\n\t// the exact state it rendered.\n\tif (post.baseRevision !== record0.revision) {\n\t\tthrow new AlignmentError(\n\t\t\t\"stale-state\",\n\t\t\t`page snapshot is stale (base ${post.baseRevision}, current ${record0.revision}); reload and resubmit`,\n\t\t);\n\t}\n\tconst hasNewFeedback = post.comments.length > 0 || post.additions.length > 0 || post.answers.length > 0;\n\tif (post.stepConfirmed && hasNewFeedback) {\n\t\tthrow new AlignmentError(\n\t\t\t\"confirmed-with-feedback\",\n\t\t\t\"step confirmation cannot carry new comments/additions/answers; submit the feedback first\",\n\t\t);\n\t}\n\t// 终审阶段唯一合法提交是终审确认(走查 2026-10-03)。此时所有步已锁定,\n\t// 反馈字段没有落点——尤其 approvals 镜像会把已锁定的 design 块整体重置为\n\t// pending,再被无门禁的终审分支带进 completed。显式拒绝,不做静默重置。\n\tif (\n\t\trecord0.status === \"finalizing\" &&\n\t\t(hasNewFeedback || post.commentReplies.length > 0 || post.approvals.length > 0 || !post.stepConfirmed)\n\t) {\n\t\tthrow new AlignmentError(\n\t\t\t\"invalid-input\",\n\t\t\t\"finalizing accepts only the final confirm (stepConfirmed without feedback fields)\",\n\t\t);\n\t}\n\n\tconst record: AlignmentRecord = structuredClone(record0);\n\tconst step = record.steps.find((candidate) => candidate.step === record.activeStep);\n\tif (!step) throw new AlignmentError(\"invalid-input\", \"active step missing\");\n\tconst blockById = new Map(step.blocks.map((block) => [block.id, block]));\n\tconst round = record.roundCount + 1;\n\n\t// New user comments: the quote must match the block's current content.\n\tfor (const comment of post.comments) {\n\t\tconst block = blockById.get(comment.blockId);\n\t\tif (!block) {\n\t\t\tthrow new AlignmentError(\"invalid-input\", `comment targets unknown block: ${comment.blockId}`);\n\t\t}\n\t\tconst note = nonEmptyString(comment.note, \"comment note\", ALIGNMENT_LIMITS.noteMaxLength);\n\t\tif (typeof comment.quote !== \"string\" || !block.content.includes(comment.quote)) {\n\t\t\tthrow new AlignmentError(\n\t\t\t\t\"comment-quote-mismatch\",\n\t\t\t\t`comment quote does not match block ${comment.blockId} content`,\n\t\t\t);\n\t\t}\n\t\tblock.comments.push({\n\t\t\tid: `c${round}-${block.comments.length + 1}`,\n\t\t\tauthor: \"user\",\n\t\t\tquote: comment.quote,\n\t\t\tnote,\n\t\t\t...(comment.escalate === true ? { escalate: true } : {}),\n\t\t\tstatus: \"open\",\n\t\t\tcreatedAtRound: round,\n\t\t});\n\t}\n\n\t// Replies to assistant comments (answer/dismiss are mutually exclusive).\n\tfor (const reply of post.commentReplies) {\n\t\tconst block = blockById.get(reply.blockId);\n\t\tconst comment = block?.comments.find((candidate) => candidate.id === reply.commentId);\n\t\tif (!comment || comment.author !== \"assistant\") {\n\t\t\tthrow new AlignmentError(\n\t\t\t\t\"invalid-input\",\n\t\t\t\t`reply targets unknown or non-assistant comment: ${reply.commentId}`,\n\t\t\t);\n\t\t}\n\t\tif (comment.status !== \"open\") continue; // idempotent resubmission\n\t\tif ((reply.answer !== undefined) === (reply.dismiss === true)) {\n\t\t\tthrow new AlignmentError(\n\t\t\t\t\"invalid-input\",\n\t\t\t\t`reply must carry exactly one of answer or dismiss: ${reply.commentId}`,\n\t\t\t);\n\t\t}\n\t\tcomment.status = \"resolved\";\n\t\tcomment.resolution = reply.dismiss\n\t\t\t? { kind: \"dismissed\", text: \"user marked unimportant\", round }\n\t\t\t: {\n\t\t\t\t\tkind: \"answered\",\n\t\t\t\t\ttext: nonEmptyString(reply.answer ?? \"\", \"reply answer\", ALIGNMENT_LIMITS.answerMaxLength),\n\t\t\t\t\tround,\n\t\t\t\t};\n\t}\n\n\t// Fill-in answers for missing blocks: stored verbatim as the user's words;\n\t// the model restructures them into the block on its next alignment_set.\n\tfor (const answer of post.answers) {\n\t\tconst block = blockById.get(answer.blockId);\n\t\tif (!block || block.kind !== \"missing\") {\n\t\t\tthrow new AlignmentError(\"invalid-input\", `answer targets unknown or non-missing block: ${answer.blockId}`);\n\t\t}\n\t\tif ((block.draftAnswer ?? \"\").trim() !== \"\") continue; // idempotent resubmission\n\t\tblock.draftAnswer = nonEmptyString(answer.value, \"answer\", ALIGNMENT_LIMITS.answerMaxLength);\n\t}\n\n\t// Approvals mirror the page's full approved set for the active step.\n\t// Skipped in finalizing (走查 2026-10-03): the step is locked and the\n\t// final confirm carries approvals=[] — mirroring would reopen its blocks.\n\tif (record0.status !== \"finalizing\") {\n\t\tconst approvalSet = new Set(post.approvals);\n\t\tfor (const id of approvalSet) {\n\t\t\tif (!blockById.has(id)) {\n\t\t\t\tthrow new AlignmentError(\"invalid-input\", `approval references unknown block: ${id}`);\n\t\t\t}\n\t\t}\n\t\tfor (const block of step.blocks) {\n\t\t\tblock.status = approvalSet.has(block.id) ? \"approved\" : \"pending\";\n\t\t}\n\t}\n\n\t// Additions queue as pendingFeedback — the server-side truth of gate\n\t// condition three (terminal audit D7); cleared by the next alignment_set.\n\tconst additions = [...(record.pendingFeedback?.additions ?? [])];\n\tfor (const addition of post.additions) {\n\t\tadditions.push(nonEmptyString(addition, \"addition\", ALIGNMENT_LIMITS.additionMaxLength));\n\t}\n\trecord.pendingFeedback = additions.length > 0 ? { round, additions } : undefined;\n\n\tconst transitions: AlignmentTransitionEntry[] = [\n\t\t{\n\t\t\talignmentId: record.id,\n\t\t\tstep: record.activeStep,\n\t\t\tevent: \"round.submitted\",\n\t\t\tround,\n\t\t\treason: {\n\t\t\t\tcode: \"round-submitted\",\n\t\t\t\tdetail: `approvals=${post.approvals.length} comments=${post.comments.length} replies=${post.commentReplies.length} answers=${post.answers.length} additions=${post.additions.length}`,\n\t\t\t},\n\t\t\trevision: record.revision,\n\t\t},\n\t];\n\trecord.roundCount = round;\n\n\tlet stepConfirmedNow = false;\n\tif (post.stepConfirmed && record0.status === \"finalizing\") {\n\t\t// 终审总览确认(走查 2026-10-03): the user reviewed the full plan and\n\t\t// confirmed it — the only path to `completed` + baseline write.\n\t\trecord.status = \"completed\";\n\t\trecord.revision += 1;\n\t\tfor (const locked of record.steps) locked.baselineConfirmedAtRound ??= record.roundCount;\n\t\ttransitions.push({\n\t\t\talignmentId: record.id,\n\t\t\tevent: \"alignment.completed\",\n\t\t\treason: { code: \"final-review-approved\", detail: \"final review confirmed the complete plan\" },\n\t\t\trevision: record.revision,\n\t\t});\n\t\tconst result: AlignmentRoundResult = {\n\t\t\talignmentId: record.id,\n\t\t\tround: record.roundCount,\n\t\t\tstep: record.activeStep,\n\t\t\trevision: record.revision,\n\t\t\tapprovals: post.approvals,\n\t\t\tcomments: post.comments,\n\t\t\tcommentReplies: post.commentReplies,\n\t\t\tanswers: post.answers,\n\t\t\tadditions: post.additions,\n\t\t\tstepConfirmed: true,\n\t\t};\n\t\treturn { record, transitions, result };\n\t}\n\tif (post.stepConfirmed) {\n\t\tconst snapshot = gate(record);\n\t\tif (!snapshot.allGreen) {\n\t\t\tthrow new AlignmentError(\n\t\t\t\t\"gate-not-satisfied\",\n\t\t\t\t`gate not satisfied: ${snapshot.unapproved} unapproved blocks, ${snapshot.openComments} open comments, ${snapshot.pendingAdditions} pending additions`,\n\t\t\t);\n\t\t}\n\t\tstep.status = \"locked\";\n\t\tstep.baselineConfirmedAtRound = round;\n\t\trecord.revision += 1;\n\t\ttransitions.push({\n\t\t\talignmentId: record.id,\n\t\t\tstep: record.activeStep,\n\t\t\tevent: \"step.confirmed\",\n\t\t\tround,\n\t\t\treason: { code: \"step-baseline-locked\", detail: `${step.blocks.length} blocks frozen` },\n\t\t\trevision: record.revision,\n\t\t});\n\t\tconst upcoming = nextStep(record.activeStep);\n\t\tif (upcoming) {\n\t\t\trecord.activeStep = upcoming;\n\t\t\tconst upcomingStep = record.steps.find((candidate) => candidate.step === upcoming);\n\t\t\tif (upcomingStep) upcomingStep.status = \"active\";\n\t\t\trecord.revision += 1;\n\t\t} else {\n\t\t\t// Design locked (方案确认): enter the final-review overview instead\n\t\t\t// of completing (走查 2026-10-03) — the baseline is written only\n\t\t\t// after the user confirms the full plan on the 终审 tab.\n\t\t\trecord.status = \"finalizing\";\n\t\t\trecord.revision += 1;\n\t\t\ttransitions.push({\n\t\t\t\talignmentId: record.id,\n\t\t\t\tevent: \"design.locked\",\n\t\t\t\tround,\n\t\t\t\treason: {\n\t\t\t\t\tcode: \"scheme-locked-awaiting-final-review\",\n\t\t\t\t\tdetail: \"all three baselines locked; awaiting final review\",\n\t\t\t\t},\n\t\t\t\trevision: record.revision,\n\t\t\t});\n\t\t}\n\t\tstepConfirmedNow = true;\n\t}\n\n\tconst result: AlignmentRoundResult = {\n\t\talignmentId: record.id,\n\t\tround,\n\t\tstep: record0.activeStep,\n\t\trevision: record.revision,\n\t\tapprovals: post.approvals,\n\t\tcomments: post.comments,\n\t\tcommentReplies: post.commentReplies,\n\t\tanswers: post.answers,\n\t\tadditions: post.additions,\n\t\tstepConfirmed: stepConfirmedNow,\n\t};\n\treturn { record, transitions, result };\n}\n\n/** Injection succeeded — the delivered transition closes the round (terminal audit D1). */\nexport function markDelivered(record: AlignmentRecord, round: number): AlignmentTransitionEntry[] {\n\treturn [\n\t\t{\n\t\t\talignmentId: record.id,\n\t\t\tevent: \"round.delivered\",\n\t\t\tround,\n\t\t\treason: { code: \"round-delivered\", detail: \"feedback injected into the session\" },\n\t\t\trevision: record.revision,\n\t\t},\n\t];\n}\n\n// ---------------------------------------------------------------------------\n// alignment_advance: inspect / retreat / cancel (总则 6)\n// ---------------------------------------------------------------------------\n\nexport function applyAlignmentAdvance(state: AlignmentState, input: AdvanceInput): AdvanceOutcome {\n\tconst record = findRecord(state, input.alignmentId);\n\tif (!record) {\n\t\tthrow new AlignmentError(\"not-found\", `unknown alignment: ${input.alignmentId}`);\n\t}\n\tif (input.action === \"inspect\") return { state, transitions: [] };\n\tif (input.action === \"rollback\") {\n\t\t// Rollback is routed through applyVersionRollback (设计 §6); refusing\n\t\t// here keeps the advance branch from ever treating it as a retreat.\n\t\tthrow new AlignmentError(\"invalid-input\", \"rollback routes through applyVersionRollback, not advance\");\n\t}\n\tif (record.status === \"completed\" || record.status === \"cancelled\") {\n\t\tthrow new AlignmentError(\"terminal-state\", `alignment is terminal: ${record.id}`);\n\t}\n\n\tif (input.action === \"cancel\") {\n\t\tconst cancelled: AlignmentRecord = { ...record, status: \"cancelled\" };\n\t\treturn {\n\t\t\tstate: { alignments: state.alignments.map((item) => (item.id === record.id ? cancelled : item)) },\n\t\t\ttransitions: [\n\t\t\t\t{\n\t\t\t\t\talignmentId: record.id,\n\t\t\t\t\tevent: \"alignment.cancelled\",\n\t\t\t\t\treason: { code: \"alignment-cancelled\" },\n\t\t\t\t\trevision: record.revision,\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\t// retreat: comments survive the reset append-only (terminal audit D5); the\n\t// escalate comments that triggered it are resolved here with the note.\n\tconst note = nonEmptyString(input.note ?? \"\", \"retreat note\", ALIGNMENT_LIMITS.noteMaxLength);\n\tconst target = input.step;\n\tif (!target || STEP_ORDER.indexOf(target) >= STEP_ORDER.indexOf(record.activeStep)) {\n\t\tthrow new AlignmentError(\n\t\t\t\"retreat-target-invalid\",\n\t\t\t`retreat target must be an earlier step than ${record.activeStep}`,\n\t\t);\n\t}\n\tconst affected: string[] = [];\n\tconst retreated: AlignmentRecord = { ...record, steps: record.steps.map((step) => ({ ...step })) };\n\tfor (const step of retreated.steps) {\n\t\tif (STEP_ORDER.indexOf(step.step) < STEP_ORDER.indexOf(target)) continue;\n\t\taffected.push(step.step);\n\t\tstep.status = \"reset\";\n\t\tstep.baselineConfirmedAtRound = undefined;\n\t\tfor (const block of step.blocks) {\n\t\t\tblock.status = \"pending\";\n\t\t\tfor (const comment of block.comments) {\n\t\t\t\tif (comment.status === \"open\" && comment.author === \"user\" && comment.escalate === true) {\n\t\t\t\t\tcomment.status = \"resolved\";\n\t\t\t\t\tcomment.resolution = { kind: \"incorporated\", text: note, round: record.roundCount };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tretreated.activeStep = target;\n\t// Finalizing → retreated: the final-review state is gone with the\n\t// downstream resets; the task is writable at the target step again.\n\tif (retreated.status === \"finalizing\") retreated.status = \"aligning\";\n\tretreated.revision += 1;\n\treturn {\n\t\tstate: { alignments: state.alignments.map((item) => (item.id === record.id ? retreated : item)) },\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\talignmentId: record.id,\n\t\t\t\tevent: \"step.retreated\",\n\t\t\t\treason: {\n\t\t\t\t\tcode: \"step-retreated\",\n\t\t\t\t\tdetail: `target=${target}; reset=${affected.join(\",\")}; note=${note}`,\n\t\t\t\t},\n\t\t\t\trevision: retreated.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\nexport interface VersionRollbackInput {\n\talignmentId: string;\n\t/** Target snapshot revision from `record.versions` (must be the active step's). */\n\ttargetRevision: number;\n}\n\n/**\n * Step-scoped version rollback (走查 2026-10-03): drop the current unconfirmed\n * version (not archived), promote the target snapshot to live, engine-bump the\n * revision (D4). Comments survive append-only — restored blocks carry the union\n * of live and archived comments by comment id, live status winning (a\n * resolution is irreversible). Only the active (unlocked) step accepts it;\n * locked steps go through retreat. Other steps are never touched.\n */\nexport function applyVersionRollback(state: AlignmentState, input: VersionRollbackInput): EngineRun {\n\tconst record = findRecord(state, input.alignmentId);\n\tif (!record) {\n\t\tthrow new AlignmentError(\"not-found\", `unknown alignment: ${input.alignmentId}`);\n\t}\n\tif (record.status !== \"aligning\") {\n\t\tthrow new AlignmentError(\"terminal-state\", `version rollback needs an aligning task: ${record.id}`);\n\t}\n\tconst step = activeStep(record);\n\tif (step.status !== \"active\" && step.status !== \"reset\") {\n\t\tthrow new AlignmentError(\n\t\t\t\"step-not-writable\",\n\t\t\t`step ${record.activeStep} is ${step.status}; locked steps go through retreat`,\n\t\t);\n\t}\n\tconst versions = [...(record.versions ?? [])];\n\tconst index = versions.findIndex(\n\t\t(snapshot) => snapshot.revision === input.targetRevision && snapshot.step === record.activeStep,\n\t);\n\tif (index < 0) {\n\t\tthrow new AlignmentError(\n\t\t\t\"invalid-input\",\n\t\t\t`no snapshot of step ${record.activeStep} at revision ${input.targetRevision}`,\n\t\t);\n\t}\n\tconst snapshot = versions[index];\n\tif (snapshot.truncated) {\n\t\tthrow new AlignmentError(\n\t\t\t\"invalid-input\",\n\t\t\t`snapshot r${input.targetRevision} exceeded the size budget and has no blocks`,\n\t\t);\n\t}\n\tconst liveById = new Map(step.blocks.map((block) => [block.id, block]));\n\tconst restored = snapshot.blocks.map((archived) => {\n\t\tconst live = liveById.get(archived.id);\n\t\tif (!live) return structuredClone(archived);\n\t\tconst merged = new Map(archived.comments.map((comment) => [comment.id, comment]));\n\t\tfor (const comment of live.comments) merged.set(comment.id, comment); // live status wins\n\t\treturn { ...structuredClone(archived), comments: [...merged.values()] };\n\t});\n\t// The current version is dropped, the target is promoted: remove it from\n\t// the archive rather than re-archiving the live set.\n\tversions.splice(index, 1);\n\tconst rolled: AlignmentRecord = {\n\t\t...record,\n\t\trevision: record.revision + 1,\n\t\tsteps: record.steps.map((candidate) =>\n\t\t\tcandidate.step === record.activeStep\n\t\t\t\t? { ...candidate, blocks: restored, status: candidate.status === \"locked\" ? \"reset\" : candidate.status }\n\t\t\t\t: candidate,\n\t\t),\n\t\tversions,\n\t};\n\treturn {\n\t\tstate: { alignments: state.alignments.map((item) => (item.id === rolled.id ? rolled : item)) },\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\talignmentId: rolled.id,\n\t\t\t\tstep: rolled.activeStep,\n\t\t\t\tevent: \"version.rolled-back\",\n\t\t\t\treason: { code: \"version-rolled-back\", detail: `target=r${input.targetRevision}` },\n\t\t\t\trevision: rolled.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\n/** Compact final baseline listing (§5 round flow 4): one line per block, answers first. */\nexport function finalBaselineLines(\n\trecord: AlignmentRecord,\n): { step: AlignmentStepId; kind: AlignmentBlockKind; id: string; line: string }[] | undefined {\n\tif (record.status !== \"completed\") return undefined;\n\tconst lines: { step: AlignmentStepId; kind: AlignmentBlockKind; id: string; line: string }[] = [];\n\tfor (const step of record.steps) {\n\t\tfor (const block of step.blocks) {\n\t\t\tconst confirmed = block.content.split(\"\\n\").find((line) => line.includes(\"→ 已确认\"));\n\t\t\tconst text = (confirmed ? confirmed.replace(/^\\s*→\\s*/, \"\") : block.content.split(\"\\n\")[0]).slice(0, 120);\n\t\t\tlines.push({ step: step.step, kind: block.kind, id: block.id, line: text });\n\t\t}\n\t}\n\treturn lines;\n}\n"]}
|