diff --git a/.sopify-skills/blueprint/README.md b/.sopify-skills/blueprint/README.md index 8873b10..5c7a775 100644 --- a/.sopify-skills/blueprint/README.md +++ b/.sopify-skills/blueprint/README.md @@ -14,7 +14,7 @@ - 当前活动 plan:暂无。 -- history 归档:已可用;最近归档为 `../history/2026-05/20260509_p4b_runtime_surface_consolidation`。 +- history 归档:已可用;最近归档为 `../history/2026-05/20260510_p4c_host_consumption_governance`。 ## 深入阅读入口 @@ -24,8 +24,8 @@ - [蓝图背景](./background.md) - [蓝图设计](./design.md) - [蓝图任务](./tasks.md) -- [Sopify 最小协议规范 (Protocol v0)](./protocol.md) +- [Sopify 宿主接入规范 (Protocol v0)](./protocol.md) - [Skill 标准对齐蓝图](./skill-standards-refactor.md) - [变更历史](../history/index.md) -- 最近归档:`../history/2026-05/20260509_p4b_runtime_surface_consolidation` +- 最近归档:`../history/2026-05/20260510_p4c_host_consumption_governance` diff --git a/.sopify-skills/blueprint/design.md b/.sopify-skills/blueprint/design.md index 4ada6f5..569e69f 100644 --- a/.sopify-skills/blueprint/design.md +++ b/.sopify-skills/blueprint/design.md @@ -329,7 +329,21 @@ ActionProposal 的标量 `side_effect` 字段表达粗粒度权限层级(`none **Ingress scope(不算 review state):** `current_gate_receipt.json` -#### Persistence Surface 分层 +### 削减预算表 + +| 维度 | 当前 | Target | Hard Max | 计算口径 | +|------|-----:|-------:|---------:|---------| +| Checkpoint types | 5 | 2 | 2 | canonical only | +| required_host_action | 13 | 5 | 6 | canonical; compat/derived 不计 | +| Route families | 18 | 6 | 8 | canonical; migration alias 不计 | +| Core state files | 8 | 6 | 7 | authoritative only; derived/compat 不计 | + +**Hard max 例外路径:** 只能通过 ADR 更新。必须说明替代了什么旧概念、为什么不能放到 artifacts/status/hint 里。 + +> **削减前提**:Runtime 减重目标 P4b,但 P4b 执行以 P4a 外部消费面 keep-list 冻结为硬前提。P4b 执行顺序固定为:release gate 收口 → runtime 旧面删除 → implementation-mirror tests 收口(详见 `tasks.md` P4b)。先 formalize contract,再清 runtime 旧面。不以 runtime 内部治理为驱动独立清理。 + + +## Persistence Surface 分层 | 层级 | 物理对应 | Git 状态 | 消费者 | 可删性 | |------|---------|---------|--------|--------| @@ -341,7 +355,8 @@ ActionProposal 的标量 `side_effect` 字段表达粗粒度权限层级(`none > replay/ 已在 P3b 列为能力下线(tasks.md P3b),不再列入 persistence surface。 -#### Frozen External Surface(P4a keep-list) + +## 外部消费面 Keep-list P4b 减重和 P4c 宿主消费治理的红线边界。只冻结 artifact / schema / host-visible contract,不冻结 Python 内部 API、route 枚举、输出文案措辞。未列入本表的面默认为 runtime 内部实现,P4b 可删。 @@ -365,7 +380,8 @@ P4b 减重和 P4c 宿主消费治理的红线边界。只冻结 artifact / schem > **未列入面默认可删**:`state/sessions/*`、`state/last_route.json`、runtime 内部模块边界、route name 全集、output 渲染文案措辞均为 runtime 内部实现,不在 keep-list 内。P4b 减重时可自由处置。 -#### Output Rendering Audit(P4a 审计) + +## Output Rendering Audit output.py 渲染层逐字段分类。只做分类,不做改造决策(改造属 P4c)。 @@ -399,18 +415,20 @@ output.py 渲染层逐字段分类。只做分类,不做改造决策(改造 - **Route 名泄露**:cancel_active 和 fallback 路径直接渲染 route_name - **Entry Guard Reason**:内部守卫码不应在默认输出中暴露 -#### Host Capability Governance(P4a→P4c bridge) -P4b 减重和 P4c 宿主消费治理的接入判定层。定义 canonical 能力梯度(产品真相),将现有 SupportTier 降为 legacy projection。 +## 宿主能力治理 + +定义 canonical 能力梯度(产品真相),将现有 SupportTier 降为 legacy projection。 > 蓝图总纲:Protocol-first / Validator-centered / Runtime-optional。 > 能力梯度的判定标准是"能消费哪些 contract",不是"有没有某个安装动作"。 -**Host Capability Ladder** +### 能力梯度 + | 梯度 | 含义 | 进入条件(contract 准入) | SupportTier 映射(legacy) | |------|------|--------------------------|--------------------------| -| `convention_only` | 只支持 Convention 协议;无 payload、无 runtime | 能消费 protocol.md §1-§4;有 .sopify-skills/ 目录结构;遵守 repo-local 优先级;能消费宿主侧 skill/prompt disclosure surface(不把未冻结 workspace 路径当作协议前提) | 无直接对应;当前 DOCUMENTED_ONLY 或 EXPERIMENTAL 可作为临时映射 | +| `convention_only` | 只支持 Convention 协议;无 payload、无 runtime | 能消费 protocol.md §1–§5;有 .sopify-skills/ 目录结构;遵守 repo-local 优先级;能消费宿主侧 skill/prompt disclosure surface(不把未冻结 workspace 路径当作协议前提) | 无直接对应;当前 DOCUMENTED_ONLY 或 EXPERIMENTAL 可作为临时映射 | | `payload_capable` | 支持 payload 安装;能消费 prompt asset | convention_only 全部条件 + payload 落点 + prompt asset 消费。workspace bootstrap 和 handoff contract 消费为可选增强项,不阻断进入此级别 | BASELINE_SUPPORTED 可作为临时映射 | | `deep_verified` | 完整深适配;installer + runtime + smoke | payload_capable 全部条件 + workspace bootstrap + handoff contract 消费 + host adapter + smoke 验证 | DEEP_VERIFIED(codex, claude) | @@ -418,7 +436,8 @@ P4b 减重和 P4c 宿主消费治理的接入判定层。定义 canonical 能力 > **SupportTier 映射说明**:上表第 4 列为 prose-level 映射。具体 FeatureId 组合到梯度的可执行投影规则属 P4c 实施范围,本 bridge 不预定义。P4c 消费本表时需补充机器可检查的投影矩阵。 -**接入判定 Checklist** + +### 接入判定 新宿主接入时需回答: @@ -439,83 +458,87 @@ Deep 层(deep_verified 准入): > 只有 payload_capable 以上才进 installer;convention_only 宿主只做文档支持。 -**Convention Quickstart 最小交付面** - -定位:adoption guide / reading order。**不是**第二规范源。本节只定义 quickstart 的交付面边界,不等于 quickstart 本身已实现(实现见 tasks.md 长期项)。 +### 禁止消费面 -- 提供 protocol.md 面向外部宿主的阅读顺序指引(按 Layer 0→3 披露顺序) -- 提供 compliance check 的运行入口(指向 Protocol Compliance Suite Phase 1 已有基础) -- **不新增 normative 内容**:protocol.md 是唯一合规入口 -- **不复述 schema**:只引用、不重新定义 - -**Prompt 镜像治理原则** - -- prompt asset 属于 payload/install surface(P4a keep-list 已冻结此消费面) -- 现有 Claude/Skills/ 和 Codex/Skills/ 目录树是 legacy exception,只维护现有内容,不再扩张 -- 新宿主不进现有目录树结构;新宿主如需 prompt asset,走 payload 机制 -- 讨论框架不是"要不要再开目录树",而是"payload 机制是否满足需求" - -**宿主禁止消费面(P4b.5 审计)** - -所有三级梯度(convention_only / payload_capable / deep_verified)均不得将以下面作为稳定消费 contract。违反此表意味着宿主与 runtime 实现细节产生耦合,P4c 治理时此类消费将被视为 leak。 +所有三级梯度(convention_only / payload_capable / deep_verified)均不得将以下面作为稳定消费 contract。违反此表意味着宿主与 runtime 实现细节产生耦合,此类消费将被视为 leak。 | # | forbidden surface | 类型 | 为什么禁止 | 来源 | |---|-------------------|------|-----------|------| -| F1 | `state/sessions/*` | 运行态附属 | runtime 内部会话管理,非 contract 面;超租约后可清理 | persistence 分层表 L340 | -| F2 | `state/last_route.json` | 可删派生 | 可从 handoff/run 派生,derived surface | persistence 分层表 L340, L328 | -| F3 | Route name 全集 / route taxonomy | 实现细节 | runtime 内部路由枚举,不是宿主消费的 contract;keep-list 不冻结 route 枚举 | L346, L366 | -| F4 | Gate 三元组直渲(`gate_status` / `blocking_reason` / `plan_completion`) | internal_taxonomy_leak | runtime 内部 gate 状态机,默认输出中不应前置 | Output Audit L378 | -| F5 | `Entry Guard Reason` 内部守卫码 | internal_taxonomy_leak | runtime 内部守卫码,非宿主需消费的 contract | Output Audit L388 | -| F6 | `Route: ` 直接暴露 | internal_taxonomy_leak | cancel_active / fallback 路径直接渲染 route_name | Output Audit L390 | -| F7 | Output 渲染文案措辞(`Next:` / `Status:` / `Decision Status:` 等 human hint) | derived 人类提示 | 由 route_name + gate_status + handoff 推导,不是 machine truth;宿主消费 handoff contract 而非 Next 文案 | Output Audit L374, L380, L385, L393; L366 | -| F8 | Runtime 内部模块边界(Python API 签名、class 结构、dataclass 名称) | 实现细节 | keep-list 不冻结 Python 内部 API;宿主消费的是持久化 contract 文件而非 Python 调用面 | L346, L366; keep-list non-goals 列 | +| F1 | `state/sessions/*` | 运行态附属 | runtime 内部会话管理,非 contract 面;超租约后可清理 | Persistence Surface 分层 | +| F2 | `state/last_route.json` | 可删派生 | 可从 handoff/run 派生,derived surface | Persistence Surface 分层 | +| F3 | Route name 全集 / route taxonomy | 实现细节 | runtime 内部路由枚举,不是宿主消费的 contract;keep-list 不冻结 route 枚举 | 外部消费面 Keep-list | +| F4 | Gate 三元组直渲(`gate_status` / `blocking_reason` / `plan_completion`) | internal_taxonomy_leak | runtime 内部 gate 状态机,默认输出中不应前置 | Output Rendering Audit | +| F5 | `Entry Guard Reason` 内部守卫码 | internal_taxonomy_leak | runtime 内部守卫码,非宿主需消费的 contract | Output Rendering Audit | +| F6 | `Route: ` 直接暴露 | internal_taxonomy_leak | cancel_active / fallback 路径直接渲染 route_name | Output Rendering Audit | +| F7 | Output 渲染文案措辞(`Next:` / `Status:` / `Decision Status:` 等 human hint) | derived 人类提示 | 由 route_name + gate_status + handoff 推导,不是 machine truth;宿主消费 handoff contract 而非 Next 文案 | Output Rendering Audit; 外部消费面 Keep-list | +| F8 | Runtime 内部模块边界(Python API 签名、class 结构、dataclass 名称) | 实现细节 | keep-list 不冻结 Python 内部 API;宿主消费的是持久化 contract 文件而非 Python 调用面 | 外部消费面 Keep-list | > deep_verified 宿主的 runtime 内部可能事实上读取 F3-F7 中的值(如 route_name 用于渲染),但这属于 runtime 实现细节,不是宿主的 contract 承诺。P4c 收敛 output 时需消除此类 leak。 -**消费矩阵(P4b.5 审计)** -每个主链真相文件和可审计凭证在每级梯度的消费定位。接续锚点、授权凭证、交互 checkpoint 三类面分开归位。 +### 契约消费矩阵 + +每个主链真相文件和可审计凭证在每级梯度的消费定位。接续锚点、授权凭证、交互 checkpoint 三类面分开归位。deep_verified 列已完成最终裁定(原"预期 required†"全部确认为 required,来源:P4c-1)。 _接续锚点(告诉下一步做什么)_ | surface | 文件 | convention_only | payload_capable | deep_verified | 来源 | |---------|------|-----------------|-----------------|---------------|------| -| Handoff contract | `state/current_handoff.json` | forbidden | optional | 预期 required† | L355, L414 | -| Plan binding | `state/current_plan.json` | forbidden | optional | 预期 required† | L338 | -| Run state | `state/current_run.json` | forbidden | optional | 预期 required† | L338 | +| Handoff contract | `state/current_handoff.json` | forbidden | optional | required | Keep-list, 能力梯度 | +| Plan binding | `state/current_plan.json` | forbidden | optional | required | Persistence Surface | +| Run state | `state/current_run.json` | forbidden | optional | required | Persistence Surface | _授权凭证(证明为什么被授权)_ | surface | 文件/规范 | convention_only | payload_capable | deep_verified | 来源 | |---------|----------|-----------------|-----------------|---------------|------| -| Gate receipt(运行级) | `state/current_gate_receipt.json` | forbidden | optional | 预期 required† | L354, L364 | -| ExecutionAuthorizationReceipt(协议级) | protocol.md §6 | forbidden | optional | 预期 required† | L351 | -| Archive receipt | `state/current_archive_receipt.json` | forbidden | optional | optional | L364 | +| Gate receipt(运行级) | `state/current_gate_receipt.json` | forbidden | optional | required | Keep-list, Persistence Surface | +| ExecutionAuthorizationReceipt(协议级) | protocol.md §6 | forbidden | optional | required | Keep-list | +| Archive receipt | `state/current_archive_receipt.json` | forbidden | optional | optional | Persistence Surface | _交互 checkpoint(AI 暂停等待)_ | surface | 文件 | convention_only | payload_capable | deep_verified | 来源 | |---------|------|-----------------|-----------------|---------------|------| -| Clarification | `state/current_clarification.json` | forbidden | optional | 预期 required† | L338 | -| Decision | `state/current_decision.json` | forbidden | optional | 预期 required† | L338 | +| Clarification | `state/current_clarification.json` | forbidden | optional | required | Persistence Surface | +| Decision | `state/current_decision.json` | forbidden | optional | required | Persistence Surface | _长期知识(所有梯度均可消费)_ | surface | 物理对应 | 所有梯度 | 来源 | |---------|---------|---------|------| -| Blueprint / Plan / History | `.sopify-skills/blueprint/`, `plan/`, `history/` | readable | L336, L361 | -| Protocol | `blueprint/protocol.md` | readable | L350-L353 | -| Preferences / Feedback | `user/preferences.md`, `user/feedback.jsonl` | readable | L337, L362 | +| Blueprint / Plan / History | `.sopify-skills/blueprint/`, `plan/`, `history/` | readable | Persistence Surface, Keep-list | +| Protocol | `blueprint/protocol.md` | readable | Keep-list | +| Preferences / Feedback | `user/preferences.md`, `user/feedback.jsonl` | readable | Persistence Surface, Keep-list | -> † "预期 required"表示按现状判断大概率为 required,但 P4b.5 是审计性质,此结论不替代后续里程碑的最终裁定。 +> **P4c-1 裁定依据**:deep_verified 必须稳定消费这些 canonical contract surface——runtime 完整路径会产出并使用它们。把任何一项降为 optional 会制造"运行了 runtime 但不承诺消费其 contract 产物"的矛盾。 > **convention_only forbidden 理由**:该梯度只承诺消费 protocol + 文件约定,不承诺消费运行态 state 文件或 receipt 实例面。 -> **EAR 与 gate_receipt 的关系**:EAR 是 protocol/doc contract(L351),gate_receipt 是一种常见运行态承载(L354),两者不等同。EAR @ convention_only = forbidden 的理由是"该梯度不承诺消费协议级 receipt 实例语义",不是"无 runtime"。 +> **EAR 与 gate_receipt 的关系**:EAR 是 protocol/doc contract(Keep-list),gate_receipt 是一种常见运行态承载(Keep-list),两者不等同。EAR @ convention_only = forbidden 的理由是"该梯度不承诺消费协议级 receipt 实例语义",不是"无 runtime"。 + +> **gate_receipt 消费者投影差异**:Keep-list 表将 consumer 写为 `host / external_tool`,Persistence Surface 表写为诊断/审计消费者。两者不矛盾:Keep-list 说明有合法宿主消费场景(如 action_proposal_retry),Persistence Surface 反映常态下的主要消费者。payload_capable 消费 gate_receipt 属于审计增强。 + + +**消费面投影 summary(derived / non-normative)** -> **gate_receipt 消费者投影差异**:keep-list 表(L354)将 consumer 写为 `host / external_tool`,persistence red-line 表(L364)写为 `external_tool`。两者不矛盾:keep-list 说明有合法宿主消费场景(如 action_proposal_retry),red-line 表反映常态下的主要消费者。payload_capable 消费 gate_receipt 属于审计增强。 +> 此表从上方消费矩阵机械派生,不是独立权威源。如有冲突,以上方消费矩阵为准。 -**Opt-in 增强组合(P4b.5 审计)** +| 消费面 ID | convention_only | payload_capable | deep_verified | +|-----------|:-:|:-:|:-:| +| handoff_contract | ✗ | ○ | ● | +| plan_binding | ✗ | ○ | ● | +| run_state | ✗ | ○ | ● | +| gate_receipt | ✗ | ○ | ● | +| ear | ✗ | ○ | ● | +| archive_receipt | ✗ | ○ | ○ | +| clarification | ✗ | ○ | ● | +| decision | ✗ | ○ | ● | + +> ✗ = forbidden ○ = optional ● = required + + +### 增强组合与声明 payload_capable 是能力带宽,不是单点能力。以下是 canonical 的三组 opt-in 增强: @@ -527,7 +550,21 @@ payload_capable 是能力带宽,不是单点能力。以下是 canonical 的 > 三组之间无互斥。交互增强对接续增强有弱依赖(建议而非必须)。 -**官方新宿主接入画像(P4b.5 审计)** + +宿主通过 `HostCapability.declared_enhancements` 声明自己启用的增强组。声明轴为 `EnhancementGroup` enum(`installer/models.py`),与 `FeatureId`(安装能力轴)正交,不互相扩展。 + +_治理期望表_ + +| SupportTier | 期望的 declared_enhancements | 说明 | +|-------------|----------------------------|------| +| deep_verified | CONTINUATION + INTERACTION + AUDIT | 完整路径,消费全部 contract 面 | +| payload_capable | 至少 CONTINUATION(官方接入);最小准入可为空 | 官方新宿主底线为接续增强;tier 本身不强制。此区分是 policy,不是机器可检查轴 | +| convention_only | 空 | 不消费 state 文件,无增强声明 | + +> 此表是 policy expectation,不是 hard constraint。校验脚本检查声明与期望的一致性,结果为 advisory(警告而非阻断)。 + + +### 官方接入画像 > 能力分层(ladder)定义"你属于哪级",接入画像定义"官方新宿主至少该做到什么程度"。两者是不同的层,ladder 不因画像而改。 @@ -537,9 +574,10 @@ payload_capable 是能力带宽,不是单点能力。以下是 canonical 的 | **对话式宿主** | payload_capable + 接续增强 + 交互增强 | 额外消费 clarification/decision checkpoint | 需要处理挂起的"AI 等人回答/拍板"状态的宿主 | | **全审计宿主** | payload_capable + 接续增强 + 交互增强 + 审计增强 | 额外消费 gate_receipt/EAR/archive_receipt | 需要证明接续合法性和证据链的宿主 | -> 此画像是 P4b.5 的审计建议,不改 ladder 定义。ladder 上 payload_capable 的准入仍为"payload 安装 + prompt asset 消费"(L414),但官方新宿主在此基础上应至少叠加接续增强。 +> 此画像基于能力治理审计建议(来源:P4b.5),不改 ladder 定义。ladder 上 payload_capable 的准入仍为"payload 安装 + prompt asset 消费",但官方新宿主在此基础上应至少叠加接续增强。 -**新宿主接入路径(P4b.5 审计)** + +**接入路径** ``` 步骤 1: 读 protocol + 遵守文件约定 @@ -565,12 +603,40 @@ payload_capable 是能力带宽,不是单点能力。以下是 canonical 的 > 步骤 3 的每一项增强都是读冻结的 contract 文件(schema 被 P4a keep-list 保护),不是调 runtime API。不需要跑完整 runtime 也能接班。 -**Blast Radius 审计(P4b.5 S3)** -> 审计目标:评估 runtime/ 和 installer/ 各功能区在每级梯度的**模块运行必需性**。判定标准是"新宿主是否需要**运行**该模块",不是"是否消费该模块的持久化产物"。持久化 contract 消费面的评估在 S2 消费矩阵,不在 S3。 +**接入判定 summary** + +- **官方新宿主最低接入**:payload\_capable + 接续增强(handoff 为核心;plan binding + run state 为配套)。 +- **需要处理用户补事实/拍板**:再加交互增强(消费 clarification / decision checkpoint)。 +- **需要证明授权链**:再加审计增强(gate\_receipt + EAR 为核心授权证据;archive\_receipt 为历史归档补强)。 +- **deep\_verified**:仍保留为完整 runtime / installer / smoke 的高保证层,不作为新宿主默认前提。 + + +### Convention Quickstart 最小交付面 + +定位:adoption guide / reading order。**不是**第二规范源。本节只定义 quickstart 的交付面边界,不等于 quickstart 本身已实现(实现见 tasks.md 长期项)。 + +- 提供 protocol.md 面向外部宿主的阅读顺序指引(按文档披露梯度) +- 提供 compliance check 的运行入口(指向 Protocol Compliance Suite Phase 1 已有基础) +- **不新增 normative 内容**:protocol.md 是唯一合规入口 +- **不复述 schema**:只引用、不重新定义 + + +### Prompt 镜像治理原则 + +- prompt asset 属于 payload/install surface(P4a keep-list 已冻结此消费面) +- 现有 Claude/Skills/ 和 Codex/Skills/ 目录树是 legacy exception,只维护现有内容,不再扩张 +- 新宿主不进现有目录树结构;新宿主如需 prompt asset,走 payload 机制 +- 讨论框架不是"要不要再开目录树",而是"payload 机制是否满足需求" + + +### 模块运行必需性 + +> 审计目标:评估 runtime/ 和 installer/ 各功能区在每级梯度的**模块运行必需性**。判定标准是"新宿主是否需要**运行**该模块",不是"是否消费该模块的持久化产物"。持久化 contract 消费面的评估在契约消费矩阵,不在本节。(来源:P4b.5 审计) > **模块计数口径**:runtime/ 59 个非 `__init__.py` 的 Python 模块(含 `_models/` 下 5 个),另有 `contracts/`、`builtin_skill_packages/` 两个资源目录(不计入模块数)。installer/ 11 个非 `__init__.py` 的 Python 模块(含 `hosts/` 下 3 个宿主适配器)。 + | 功能区 | 包含模块 | conv | payload | deep | 备注 | |--------|---------|:----:|:-------:|:----:|------| | **核心管线** | engine, router, gate, execution\_gate, entry\_guard, gate\_output | ✗ | ✗ | ✓ | 路由/gate 决策循环,deep runtime 核心 | @@ -591,11 +657,12 @@ payload_capable 是能力带宽,不是单点能力。以下是 canonical 的 > ✗ = 不需要运行;✓ = 需要运行;工具† = 通过 CLI 脚本间接调用(install.sh/install.ps1),不是 contract 级依赖;opt-in‡ = payload\_capable 可选增强,不是准入。 -**语义来源 → 落盘路径 → contract 文件(S3 核心发现)** + +**语义来源 → 落盘路径 → contract 文件** > 所有 state/ contract 文件的磁盘写入都经过 `state.py` 的 `set_current_*` 方法族统一落盘。下表的"语义来源"指提供业务语义和触发写入的模块,不是唯一写入者。 -| 语义来源 | 落盘路径 | contract 文件 | S2 对应消费面 | +| 语义来源 | 落盘路径 | contract 文件 | 对应增强组 | |---------|---------|--------------|--------------| | handoff.py(+ engine.py 触发) | state.set\_current\_handoff | current\_handoff.json | 接续增强核心 | | engine.py / state.py | state.set\_current\_run | current\_run.json | 接续增强配套(run state) | @@ -607,7 +674,8 @@ payload_capable 是能力带宽,不是单点能力。以下是 canonical 的 > 此映射是"当前事实",不是"永久绑定"。P4c 及后续里程碑可以改变生产者实现,只要 contract 文件 schema 不变(P4a keep-list 保护)。 -**S3 审计结论** + +**审计结论** 1. **convention\_only 不需要任何 runtime/ 或 installer/ 模块**。全部能力来自读协议文档和遵守文件约定。 2. **payload\_capable 不需要任何 runtime/ 模块**。其消费的机器真相文件都是冻结 JSON contract,由 P4a keep-list 保护 schema。消费文件 ≠ 依赖生产者模块。 @@ -615,51 +683,36 @@ payload_capable 是能力带宽,不是单点能力。以下是 canonical 的 4. **deep\_verified 对 runtime/ + installer/ 有完整能力覆盖依赖**。不是"每轮运行全部模块"——单次执行路径取决于具体 action/route,但能力层面需要完整 runtime 可用。这是设计预期,不是缺陷。 5. **生产者 vs 消费者边界明确**:7 个 contract 文件的语义分别来自 ~7 个 runtime 模块,全部经 state.py 统一落盘。payload\_capable 消费产物(文件),不消费生产者(模块)。此边界由 forbidden surface F8 保护。 -**综合裁定(P4b.5 S4)** -P4b.5 的审计结论不是"runtime 已可删除",而是"新宿主接班所需能力已可用 contract 显式表达,并与 runtime 内部实现解耦"。新宿主要实现安全接班,不需要接入完整 runtime,但不能绕过显式 contract。官方最低接入画像应为 payload\_capable + 接续增强;runtime 在该路径中是 contract 生产者与 deep hardening 层,不是新宿主的接入前提。 +审计结论不是"runtime 已可删除",而是"新宿主接班所需能力已可用 contract 显式表达,并与 runtime 内部实现解耦"。新宿主要实现安全接班,不需要接入完整 runtime,但不能绕过显式 contract。官方最低接入画像应为 payload\_capable + 接续增强;runtime 在该路径中是 contract 生产者与 deep hardening 层,不是新宿主的接入前提。 + + +### 长期边界与后续交接 **已证明结论** + 1. convention\_only 仍保留为定义层最低边界,负责界定"什么算进入 Sopify 生态",但不是官方新宿主的真实落点。 2. payload\_capable 是能力带宽,不等于完整接续;官方新宿主至少还应叠加接续增强。 3. 新宿主接续依赖的是冻结的 state/ contract 文件与长期知识资产,不是 runtime 内部模块或 API。 4. Forbidden surface 已显式列出(F1-F8),宿主不得依赖 sessions/\*、last\_route、输出文案、runtime 模块边界等未冻结面。 5. payload\_capable 对 runtime/ 的 blast radius 为零;对 installer/ 仅有工具性依赖。 -**官方接入判定** - -- **官方新宿主最低接入**:payload\_capable + 接续增强(handoff 为核心;plan binding + run state 为配套)。 -- **需要处理用户补事实/拍板**:再加交互增强(消费 clarification / decision checkpoint)。 -- **需要证明授权链**:再加审计增强(gate\_receipt + EAR 为核心授权证据;archive\_receipt 为历史归档补强)。 -- **deep\_verified**:仍保留为完整 runtime / installer / smoke 的高保证层,不作为新宿主默认前提。 - -**P4b.5 不裁死的边界** +**未裁定的边界** -1. 不在 P4b.5 裁定 deep\_verified 的每个面是否最终全部 required,只保留"预期 required†"判断。 -2. 不在 P4b.5 重写 ladder 定义,只审计消费边界与 blast radius。 -3. 不在 P4b.5 变更 schema、代码实现或 installer/runtime 结构。 -4. 审计增强内部的长期最小组合(gate\_receipt / EAR / archive\_receipt 哪些是核心、哪些是补强)仍以后续试点 evidence 为准。官方最低接入不含审计增强这一点已在 S2 和 S4 官方接入判定中定下,此条不开放该结论。 +1. ~~原未裁定 deep\_verified 的每个面是否全部 required。~~ **P4c-1 已裁定:7 项全部 required,† 已消除。** +2. 不在能力治理审计中重写 ladder 定义,只审计消费边界与 blast radius。 +3. 不在能力治理审计中变更 schema、代码实现或 installer/runtime 结构。 +4. 审计增强内部的长期最小组合(gate\_receipt / EAR / archive\_receipt 哪些是核心、哪些是补强)仍以后续试点 evidence 为准。官方最低接入不含审计增强这一点已在契约消费矩阵和官方接入画像中定下,此条不开放该结论。 5. 若未来长期验证 convention\_only 只承担只读参与者角色,可在后续里程碑考虑降格或改名,但不在本次处理。 -**对后续里程碑的交接** + +**后续里程碑交接** - **P4c**:负责把本次审计结论投影到实现层和 host adapter / installer / validator 消费面。详见 tasks.md P4c 段"P4c 前提声明"。 - **P4d**:负责选非 deep 宿主做试点,验证 payload\_capable + 接续增强是否足以支撑真实接班。 - **P5/P6**:再依据试点 evidence 逐步收缩 deep-runtime-only surface,并推动 runtime 向 reference implementation 退位。 -### 削减预算表 - -| 维度 | 当前 | Target | Hard Max | 计算口径 | -|------|-----:|-------:|---------:|---------| -| Checkpoint types | 5 | 2 | 2 | canonical only | -| required_host_action | 13 | 5 | 6 | canonical; compat/derived 不计 | -| Route families | 18 | 6 | 8 | canonical; migration alias 不计 | -| Core state files | 8 | 6 | 7 | authoritative only; derived/compat 不计 | - -**Hard max 例外路径:** 只能通过 ADR 更新。必须说明替代了什么旧概念、为什么不能放到 artifacts/status/hint 里。 - -> **削减前提**:Runtime 减重目标 P4b,但 P4b 执行以 P4a 外部消费面 keep-list 冻结为硬前提。P4b 执行顺序固定为:release gate 收口 → runtime 旧面删除 → implementation-mirror tests 收口(详见 `tasks.md` P4b)。先 formalize contract,再清 runtime 旧面。不以 runtime 内部治理为驱动独立清理。 ## 轻量化产品指标 diff --git a/.sopify-skills/blueprint/protocol.md b/.sopify-skills/blueprint/protocol.md index 3394771..13ae445 100644 --- a/.sopify-skills/blueprint/protocol.md +++ b/.sopify-skills/blueprint/protocol.md @@ -1,10 +1,30 @@ -# Sopify 最小协议规范 (Protocol v0) +# Sopify 宿主接入规范 (Protocol v0) -本文定位: 定义不依赖 runtime 也成立的最小可携带协议。这是宿主接入 Sopify 的最低门槛。 +本文定位: 宿主接入 Sopify 的规范入口。Convention 最小合规(§1–§5)+ Runtime 深度集成(§8)均在本文覆盖。 + +**阅读地图:** + +| 宿主能力 | 需要阅读的章节 | +|---|---| +| **convention_only** — 只按目录约定读写 blueprint/plan/receipt | §1–§5 | +| **payload_capable** — 已安装 payload bundle,可消费 manifest | §1–§5 + prompt asset | +| **deep_verified** — 完整 runtime gate / handoff / checkpoint | §1–§5 + §8 + prompt asset | + +> **术语解耦**:本文承载文档披露梯度的入口定义,以 protocol 章节为主轴,后续层级衔接 prompt asset 与架构参考。KB SKILL 中的 L0/L1/L2/L3 是知识持久化分层(index → stable → active → archive),描述 AI 运行时的上下文消费顺序,两者不是同一套模型。 + +**文档披露梯度:** + +| Layer | 名称 | 覆盖 | 定位 | +|-------|------|------|------| +| **0** | Protocol | §1–§3 | 协议基础:目录约定、必备文件、宿主义务 | +| **1** | Lifecycle | §4–§5 | 理解验证:生命周期样例、合规自检 | +| **2** | Integration | §6–§8 + prompt asset | 集成能力:外部契约、主体身份、deep host runtime | +| **3** | Reference | design.md · ADR-016 · ADR-017 | 架构参考:不进 prompt,不面向接入者 | + +与宿主能力梯度的对应:convention_only 读完 Layer 0–1(§1–§5);payload_capable 在 Layer 0–1 基础上加 prompt asset;deep_verified 完整读至 Layer 0–2(§1–§8 + prompt)。Prompt asset 是 payload/deep 的能力附加面,不单独改变章节阅读层级。 **权限边界:** -- 本文是 **宿主接入的最小规范入口文档** - 最小合规看本文;runtime/扩展契约以 `design.md` / ADR 为准 - `design.md` 负责架构分层、削减目标、runtime 参考实现、核心契约细节(含 state 文件、checkpoint、knowledge_sync) - `ADR-016` 负责 Protocol-first 决策理据与演进路线 @@ -72,8 +92,8 @@ | **方案包管理** | 必须。创建/更新 `plan/` 下的方案包 | 必须。runtime 辅助 | | **归档** | 必须。完成后归档到 `history/` 并写 receipt | 必须。runtime 辅助 | | **knowledge_sync** | 推荐。归档时将稳定结论回写 blueprint | 必须 | -| **checkpoint 响应** | 推荐。遇到 clarification/decision 时暂停等用户 | 必须。runtime 强制 | -| **handoff 消费** | 推荐。读取上轮 handoff 恢复上下文 | 必须 | +| **checkpoint 响应** | 推荐。遇到 clarification/decision 时暂停等用户 | 必须。runtime 强制(详见 §8.2) | +| **handoff 消费** | 推荐。读取上轮 handoff 恢复上下文 | 必须(详见 §8.2) | | **Validator 校验** | 事后校验。最小交互流可不依赖 Validator 实时阻断,但正式合规与 receipt authority 仍由 Validator 事后校验提供 | 写前授权。Validator 是 pre-write authorizer | **Convention 模式最小合规**:读 blueprint → 写方案包 → 归档。最小交互流不要求 Validator 实时阻断或 runtime state 文件;但要获得正式 receipt authority 和合规校验,仍需调用 Validator(可事后批量)。 @@ -144,9 +164,9 @@ 以上全部通过即为 **Convention 模式最小合规**。如需 Runtime 模式,另需满足 Validator 接入(见 design.md 核心管线)。 -## 6. Integration Contract(外部能力接入契约)— *informative / draft* +## 6. Integration Contract(外部能力接入契约)— *informative;Verifier / ExecutionAuthorizationReceipt 为 normative 例外* -> 本节整体是方向性参考,尚未稳定为规范。当前仅 `### Verifier` 子段已升格为 normative(P1.5-D);其余子段仍为 informative/draft。当前用于说明外部能力如何接入 Sopify 的收敛链。 +> 本节整体 informative。其中 **Verifier**(§6.Verifier)和 **ExecutionAuthorizationReceipt**(§7 内引用)已升格为 normative(P1.5-D);其余子段仍为 draft。 Sopify 不做生产/验证/知识处理节点本身,但拥有证据规范、授权判定、收据生成这几个控制节点。外部能力通过以下契约接入 Sopify 的收敛链。 @@ -352,6 +372,84 @@ ExecutionAuthorizationReceipt 是 execute_existing_plan 授权通过后生成的 审查记录作为 evidence 进入 handoff 或 plan metadata,归档时纳入 receipt 的 verification_evidence。evidence 挂载的 normative 消费规则见 §6 Verifier 消费路径。evidence attachment 的 wire format(字段 schema、路径约定)为 deferred,不属于当前 normative scope。 +## 8. Deep Host 运行时集成协议 + +> 本节是 §3 宿主最小义务中 Runtime 模式的详细展开。Convention 模式宿主不需要消费本节。Prompt asset(AGENTS.md / CLAUDE.md)只保留高层义务摘要,本节是唯一规范入口。 + +### 8.1 Gate-First 义务 + +每次进入新的 Sopify LLM 回合前,宿主必须先执行 runtime gate 并消费返回的 JSON contract。 + +**入口解析**: +- Repo-local 开发态:`scripts/runtime_gate.py enter --workspace-root --request ""` +- Vendored 模式:工作区 `.sopify-runtime/manifest.json` 只是 thin stub(声明 `bundle_version / locator_mode / ignore_mode`);宿主结合 `~/.codex/sopify/payload-manifest.json` 解析 selected global bundle,从 bundle contract 或 workspace-preflight contract 消费 `runtime_gate_entry` +- 若工作区缺少兼容 manifest,宿主先调 `~/.codex/sopify/helpers/bootstrap_workspace.py --workspace-root ` + +**Gate 通过条件**:仅当 `status == ready` ∧ `gate_passed == true` ∧ `evidence.handoff_found == true` ∧ `evidence.strict_runtime_entry == true` 时,宿主才可进入后续阶段。 + +**`allowed_response_mode` 值域**: + +| 值 | 宿主行为 | +|---|---| +| `checkpoint_only` | 只允许 checkpoint 响应 | +| `error_visible_retry` | 只允许短错误摘要 + 重试提示 | +| `action_proposal_retry` | 必须读 `action_proposal_schema`,生成 ActionProposal JSON,以 `--action-proposal-json` 重试 | + +**ActionProposal capability**:首次 gate 调用应声明 `--action-proposal-capability`;提供 `--action-proposal-json` 时隐含声明。不声明的宿主走 legacy fallback。Schema 由 gate 动态返回,不得硬编码。 + +**Gate 验证时效**:必须在当前消息回合的 tool call 中执行,不得复用上一轮 `current_gate_receipt.json`。 + +**首次激活 `ROOT_CONFIRM_REQUIRED`**:宿主必须停在 root 选择(推荐当前目录 / 备选仓库根 / 允许手动指定),确认后以 `activation_root` 重试。`allowed_response_mode` 为 `checkpoint_only`。`~go init` 不得绕过此步骤。 + +### 8.2 Post-Run Handoff 消费 + +runtime 执行后,若 `.sopify-skills/state/current_handoff.json` 存在,宿主必须优先按其中的 `required_host_action`、`recommended_skill_ids`、`artifacts` 决定下一步。渲染层 `Next:` 行仅为人类摘要,不作为唯一机器依据。 + +**`required_host_action` 值域**: + +| 值 | 宿主行为 | +|---|---| +| `answer_questions` | 读 `.sopify-skills/state/current_clarification.json`,向用户展示 `missing_facts` / `questions`,等待补充后重入 default runtime entry。不得自行物化 plan 或跳到 `~go exec` | +| `confirm_decision` | 优先读 `current_handoff.json.artifacts.decision_checkpoint` + `decision_submission_state`;回退到 `.sopify-skills/state/current_decision.json`。展示 `question` / `options` / `recommended_option_id`,等待用户确认后重入。不得自行生成 plan | +| `continue_host_develop` | 宿主继续代码修改;若遇用户分叉(需补事实/拍板),必须调 `scripts/develop_callback_runtime.py submit --payload-json ...`(vendored: `.sopify-runtime/scripts/develop_callback_runtime.py`),payload 含 `checkpoint_kind` + `resume_context` | +| `continue_host_consult` | 在已消费当前回合 gate contract 前提下继续问答;不得自行路由,不得重判 consult / 非 consult | + +**execution_gate**:若 `current_handoff.json.artifacts.execution_gate` 存在,结合 `.sopify-skills/state/current_run.json.stage` 判断 plan 状态(已生成 vs `ready_for_execution`)。 + +**偏好注入**:gate 内部执行 preferences preload(通过 `preferences_preload_entry`)。宿主只消费 gate 暴露的 `preferences` 结果,不得自行拼装。优先级固定为:当前任务明确要求 > `preferences.md` > 默认规则。 + +### 8.3 宿主行为边界 + +- 宿主不得在 gate 前自行路由 +- 宿主不得绕过 checkpoint 约束(`clarification_pending` / `decision_pending`) +- 宿主不得手写 `current_decision.json` / `current_handoff.json` 等 machine truth +- `~go exec` 仅为高级恢复/调试入口,不得在无活动 plan 时作为默认路径 +- Prompt asset 是 prompt 层指引,不是 vendored runtime 的 machine contract + +### 8.4 Runtime Helper 索引 + +| Helper | 说明 | +|---|---| +| `scripts/sopify_runtime.py` | 默认 repo-local raw-input entry | +| `scripts/runtime_gate.py enter` | runtime gate,宿主第一跳 | +| `scripts/go_plan_runtime.py` | plan-only 切片 helper | +| `scripts/develop_callback_runtime.py` | `continue_host_develop` 中用户分叉回调 | +| `scripts/decision_bridge_runtime.py` | `confirm_decision` host bridge helper | +| `scripts/preferences_preload_runtime.py` | 长期偏好 preload helper | +| `~/.codex/sopify/payload-manifest.json` | 全局 payload metadata | +| `~/.codex/sopify/helpers/bootstrap_workspace.py` | workspace bootstrap helper | +| `.sopify-runtime/manifest.json` | vendored bundle machine contract | + +### 8.5 State 文件索引 + +| 文件 | 说明 | +|---|---| +| `.sopify-skills/state/current_handoff.json` | 运行时交接事实,宿主执行后优先消费 | +| `.sopify-skills/state/current_run.json` | 活跃 run 状态(stage, execution_gate) | +| `.sopify-skills/state/current_clarification.json` | 澄清 checkpoint 状态 | +| `.sopify-skills/state/current_decision.json` | 决策 checkpoint 回退状态 | +| `.sopify-skills/state/current_gate_receipt.json` | gate receipt(仅当轮有效) | + ## 非目标 - 不定义 Validator 实现细节(见 ADR-017) diff --git a/.sopify-skills/blueprint/tasks.md b/.sopify-skills/blueprint/tasks.md index f9d933e..3d022e0 100644 --- a/.sopify-skills/blueprint/tasks.md +++ b/.sopify-skills/blueprint/tasks.md @@ -23,7 +23,7 @@ | P4a | external_surface_freeze | P3b | 已完成。薄切片:冻结不可删外部消费面 keep-list | | P4b | runtime_surface_consolidation | P4a | 已完成。prove-kept-or-delete 证明 <20K 不可达,实删 15 LOC | | P4b.5 | runtime_optionality_audit | P4b | 已完成。设计/审计型:宿主接入层级矩阵 + 消费矩阵 + blast radius + 综合裁定 | -| P4c | host_consumption_governance | P4b.5 | 宿主只消费 contract,不定义 truth | +| P4c | host_consumption_governance | P4b.5 | 已完成。宿主只消费 contract,不定义 truth | ### P0: Blueprint Rebaseline(已完成) @@ -61,60 +61,16 @@ ✅ 已完成。设计/审计型,不改代码。交付物:S1 Forbidden Surface(F1-F8)、S2 消费矩阵 + opt-in 增强组合 + 官方接入画像、S3 Blast Radius 审计(15 功能区 + 语义来源→落盘→contract 映射)、S4 综合裁定 + P4c 前提声明。归档:`history/2026-05/20260510_p4b5_runtime_optionality_audit/` -### P4c: Host Consumption Governance +### P4c: Host Consumption Governance(已完成) -宿主只消费稳定 contract,不再定义 machine truth。P4b.5 宿主接入层级矩阵就绪后执行。 - -- prompt 不定义机器契约、不维护路由表 -- doctor/status 输出只渲染 machine truth,不作为 truth source -- handoff rendering 只消费结构化字段,不做语义推断 -- 接入文档以 protocol.md 为唯一合规入口 -- **宿主消费边界**:宿主只允许消费"主链机器真相"层(current_run/current_plan/current_handoff/current_clarification/current_decision)和"可审计凭证"层(gate_receipt/archive_receipt);不得消费 state/sessions/* 内部细节、last_route 等 runtime-only/derived 面(参照 design.md "Persistence Surface 分层"表) -- **验收 (a) 文档递进顺序**:渐进式披露 Layer 0 Protocol ≤120 行 → Layer 1 Gate → Layer 2 Phase → Layer 3 Reference(不进 prompt) -- **验收 (b) 运行时首接触感知**:新用户首次使用时,只感知到"中断可恢复"和"需要拍板时会停"两个语义;blueprint / checkpoint taxonomy / runtime state 等内部概念不在默认运行时路径中主动暴露。doctor/status 不主动呈现 checkpoint 分类体系,~go 入口不前置 blueprint 概念 -- **Output contract convergence**:基于 P4a 审计分类,收敛 `runtime/output.py` 渲染层——① 状态符语义:定义 canonical route family → 符号映射(当前 consult=`!` 无明确约束);② Next 降级:明确为 human hint,不再混合 `required_host_action` + `route_name` 推导,宿主消费 handoff 不依赖 Next;③ Changes 重定义:`loaded_files`(恢复上下文)从 Changed(实际写入)中拆出,或重命名为 Touched/Files;④ Gate 行简化:默认输出不暴露 `gate_status`/`blocking_reason`/`plan_completion` 三元组,详细诊断留给 doctor/status -- **Builtin skill capability disclosure**:宿主文案稳定表达 builtin skill 的当前能力边界与可消费方式;AGENTS.md 只做消费投影,builtin_catalog 为唯一 truth source。当前 analyze/design/develop 是 phase-bound workflow skill(entry_kind=null, triggers=[]),不宣称 standalone invocation。若后续要支持 builtin skill 显式单独调用,必须先 formalize 独立的 invocation metadata contract / invocation syntax;在该 contract 明确前,本项只做披露,不预设其进入 P2 或单列里程碑。边界:只覆盖 builtin skill,不扩展到外部 skill discovery/routing/distribution(background.md 明确排除) - -**P4c 前提声明(来自 P4b.5 S4 审计)** - -> 以下三部分基于 P4b.5 S1-S3 审计结论提取。P4c 开包时消费此声明,不重新证明已审计项。 - -**P4c 可以假设的 invariant** - -1. **Forbidden surface 已定义**(design.md F1-F8):state/sessions/\*、last\_route、route taxonomy、Next:/输出文案、渲染层实现、runtime 内部模块边界均为 forbidden surface,适用所有三级梯度。P4c 的任何实施项不得引入对这些面的新宿主依赖。 -2. **消费矩阵已裁定**(design.md S2 四子表):convention\_only 和 payload\_capable 的每项 contract 文件归位(required/optional/forbidden)已确定。deep\_verified 列部分项标为"预期 required†",待 P4c 最终裁定。P4c 直接消费此矩阵,不重新审计层级归位。 -3. **三级 ladder 不变**(design.md L409-419):convention\_only / payload\_capable / deep\_verified 的准入定义不因 P4c 改变。payload\_capable 准入仍为"payload 安装 + prompt asset 消费"。 -4. **payload\_capable 对 runtime/ 的 blast radius 为零**(design.md S3 结论 2):payload\_capable 不需要运行任何 runtime/ 模块。消费冻结 contract 文件不等于依赖生产者模块。 -5. **生产者 vs 消费者边界明确**(design.md S3 语义来源表):7 个 contract 文件的语义来源已映射,全部经 state.py 统一落盘。P4c 可以改变生产者实现,不能改变 contract 文件 schema(P4a keep-list 保护)。 -6. **官方接入画像已定义**(design.md S2):官方最低接入 = payload\_capable + 接续增强全组;对话式/全审计宿主在此基础上叠加。此画像是独立于 ladder 的接入策略层。 -7. **EAR @ convention\_only = forbidden 已闭合**(design.md S2 消费矩阵):convention\_only 不承诺消费协议级 receipt 实例语义。此裁定不在 P4c 重新开放。 - -**P4c 需做的实施项(由 P4b.5 推导)** - -1. **机器可检查投影矩阵**:将消费矩阵中的层级归位翻译为可执行的 FeatureId → 梯度映射规则(design.md L419 已预告此项属 P4c)。 -2. **增强检测机制**:P4c 需定义宿主如何声明/检测已激活的 opt-in 增强组合(接续/交互/审计)。当前无此机制,ladder 只定义准入面。 -3. **Output 渲染层收敛**:消除 forbidden surface F5(Next: 输出文案推导逻辑)和 F6(渲染层实现细节)中被新宿主事实上依赖的泄露。对应 P4c 已有的 Output contract convergence 项。 -4. **deep\_verified "预期 required†" 最终裁定**:消费矩阵中 deep\_verified 列的"预期 required†"项需在 P4c 做最终 required/optional 裁定。P4b.5 只做审计判断,不替代最终裁定。 -5. **Forbidden surface 执行保障**:P4c 需确保 F1-F8 中的每一项在实现层有对应的防泄露措施(如移除 prompt 中的 route\_name 直接暴露、收敛 Next: 推导逻辑等)。 - -**P4c 不能做的事(红线)** - -1. **不改 ladder 定义**:不修改三级梯度的准入条件,不新增/删除梯度。 -2. **不新增 machine truth**:不新增 state 文件、不新增 checkpoint 类型、不新增 contract 文件。若确需新增,须走 ADR 路径并对照削减预算表。 -3. **不改 P4a keep-list schema**:contract 文件的 schema 由 P4a 冻结面保护。P4c 可以改变生产者实现,不能改变 contract 文件结构。 -4. **不让 payload\_capable 依赖 runtime/ 模块**:这是 S3 的核心审计结论。任何 P4c 实施项如果引入 payload\_capable 对 runtime 模块的运行依赖,视为违反 P4b.5 审计边界。 -5. **不消费 forbidden surface**:P4c 实施项不得引入对 F1-F8 中任何面的新宿主依赖。消除已有泄露是 P4c 的目标,不是引入新泄露。 -6. **不解决 P4d/P5/P6 范围**:P4c 不预设新宿主试点(P4d)、不预执行 contract surface 删减(P5)、不预判 runtime 降级路径(P6)。 - -**P4c 收口附带:design.md 结构整理** - -> P4c 收口时,将 design.md Host Capability Governance 节中 P4b.5 的增量段(S1-S4)内化为稳定章节结构(Forbidden Surface / Consumption Matrix / Official Onboarding Profile / Blast Radius / Comprehensive Verdict)。此整理不改变观点,只优化文档结构,避免后续里程碑继续无限追加 S 段。 +✅ 已完成(P4c-5 Prompt Asset 结构收口显式跳过)。P4b.5 审计结论转化为可执行治理:宿主只消费稳定 contract,不再定义 machine truth。交付物:P4c-1 契约投影矩阵(deep_verified † 全部消除)、P4c-2 增强声明/检测协议、P4c-3a output/doctor/handoff 渲染收敛、P4c-3b 首接触/prompt 收敛(-140 行 route taxonomy 删除,protocol.md §8 引用替代)、P4c-4 protocol.md §8 唯一入口 + 文档披露梯度 + builtin skill 披露 + design.md 结构整理。跨切片 invariant 5/5 PASS,红线 6/6 无违反。归档:`history/2026-05/20260510_p4c_host_consumption_governance/` ## 未完成长期项 ### P4b 后续路线(P4c 后视评估) - [ ] P4d New Host Pilot:选 1 个非 deep 宿主做试点(convention_only 或 payload_capable),不接完整 runtime。验证 P4b.5/P4c 的分层是否真正降低接入成本。可与 P4c 后期并行启动。 +- [ ] Continuation Entry Convergence:统一宿主级官方入口语义(Inspect Active Work / Continue Active Work / Start New Work),覆盖同宿主跨 session 与跨宿主接续。只消费现有 frozen contract,不新增 machine truth,不绑定 runtime 正则/路由实现。不规定入口语法或关键词,宿主自行选择暴露形式(命令、按钮、菜单等)。有活动工作或 pending checkpoint 时 Start New Work 必须显式仲裁。当前 `~go exec` 是 Continue Active Work 的命令级实现,应被 host-level 入口语义取代。_触发条件:P4c 验收后,结合 P4d 非 deep 宿主试点 formalize_ - [ ] P5 Contract Surface Shrinkage:在 P4d 验证后,按 evidence 逐项删除或降级 deep runtime 专属的 contract surface(bridge capability / manifest entry / installer bundle 项)。此时已知哪些 contract 是新宿主需要 vs 历史包袱。 - [ ] P6 Runtime Sunset / Reference Runtime:将 runtime 明确降级为 reference implementation 或 deep host hardening layer。新宿主默认走 Protocol/Convention 模式,runtime 不再承载新增产品能力。可能与 P5 合并。 diff --git a/.sopify-skills/history/2026-05/20260510_p4c_host_consumption_governance/background.md b/.sopify-skills/history/2026-05/20260510_p4c_host_consumption_governance/background.md new file mode 100644 index 0000000..e25c2b7 --- /dev/null +++ b/.sopify-skills/history/2026-05/20260510_p4c_host_consumption_governance/background.md @@ -0,0 +1,63 @@ +# 背景: P4c Host Consumption Governance + +## 前置里程碑 + +| 里程碑 | 核心产出 | 与本包关系 | +|--------|---------|-----------| +| P4a | Frozen External Surface keep-list(15 条) | contract 文件 schema 不可变的硬约束来源 | +| P4b | prove-kept-or-delete 全量扫描(24,334 LOC 不可大幅瘦身) | 根因:runtime 代码承载 distribution/installer contract | +| P4b.5 | 消费矩阵 + forbidden surface + blast radius + 综合裁定 | 本包的直接前驱,提供 invariant 7 + 实施项 5 + 红线 6 | +| Host Capability Governance bridge | 三级 ladder + checklist + quickstart | 已在 design.md:402-456,本包不改 ladder | + +## 问题陈述 + +P4b.5 完成后,"新宿主该消费什么、不该碰什么"的治理结论已有,但全部停留在审计/文档层面——没有一条是机器可检查的,没有一条在代码里有执行保障: + +1. **消费矩阵无机器投影**:FeatureId → 梯度映射规则不存在。系统不知道哪个 feature 属于哪个梯度、哪些是核心、哪些是 opt-in。 +2. **增强组合无声明/检测机制**:宿主没有标准方式声明"我启用了接续增强"或"我是全审计宿主"。 +3. **可见面仍有 leak**:output.py 渲染仍混合 forbidden surface(F5-F7);prompt 仍可能定义 route taxonomy;doctor/status 仍可能充当 truth source。 +4. **文档入口分散**:接入文档未统一到 protocol.md;builtin skill 能力边界未稳定表达。 + +## 目标断言 + +> P4c 应将 P4b.5 审计结论转化为可执行治理:宿主只消费稳定 contract,不再定义 machine truth。每条治理规则要么有机器检查投影,要么有文档/prompt 层面的显式表达。 + +## 本包定位 + +**把 P4b.5 的审计结论投影到实现层(代码/prompt/文档),使治理可执行。** + +不做的事: +- 不新增/删除梯度,不改 ladder 定义 +- 不新增 machine truth / state 文件 / checkpoint 类型 +- 不改 P4a keep-list schema +- 不让 payload_capable 依赖 runtime/ 模块 +- 不解决 P4d/P5/P6 范围 + +## 切片策略 + +P4c 体量大于 P4b.5,按实施性质和依赖关系拆为 6 个子切片(含 3a/3b + 收口项 5),不按来源拆。 + +| 子切片 | 名称 | 性质 | 核心交付 | +|--------|------|------|---------| +| P4c-1 | 契约投影层 | 机制设计 | FeatureId → 梯度投影矩阵 + deep_verified 最终裁定 | +| P4c-2 | 增强声明/检测层 | 机制设计 | 宿主 opt-in 增强的声明协议和检测逻辑 | +| P4c-3a | 渲染与 truth-source 收敛层 | 代码改动 | output / doctor / handoff 渲染收敛,消除 truth source 越界 | +| P4c-3b | 首接触与 prompt 收敛层 | 代码/prompt 改动 | 首接触感知 + prompt 不定义契约 + F5/F6 防泄露 | +| P4c-4 | 文档与披露层 | 文档 | protocol.md 唯一入口 + 文档披露梯度 + builtin skill 披露 + design.md 结构整理(非阻塞) | +| P4c-5 | Prompt Asset 结构收口 | 文档/结构 | AGENTS.md / CLAUDE.md 零语义漂移的结构瘦身(非阻塞收口项) | + +依赖图:P4c-1 → P4c-2 (hard);P4c-1 → P4c-3a (weak);P4c-3a → P4c-3b;P4c-3b ‖ P4c-4 (parallel,但 P4c-4 中依赖 AGENTS.md 最终文本的部分需 3b 稳定后收口);P4c-3b + P4c-4 → P4c-5 (可选收口) + +## 跨切片 invariant + +- Forbidden surface 执行保障(A5/B5 合并)不是独立切片,而是每个切片必须遵守的 cross-cutting 约束:任何改动不得引入对 F1-F8 的新宿主依赖。 + +## 预算 + +| 维度 | 预算 | +|------|------| +| 新增 machine truth | 0(投影矩阵和增强声明是 metadata,不是 state) | +| Schema 变更 | 0(P4a keep-list 保护) | +| 新增代码 | P4c-3a output 收敛涉及实现改动;P4c-1/P4c-2 可能新增 metadata 文件或验证逻辑 | +| 文档变更 | blueprint/design.md 结构整理 + protocol 入口统一 + AGENTS.md/CLAUDE.md 更新 | +| 测试影响 | P4c-3a output 改动可能影响 output 相关测试 | diff --git a/.sopify-skills/history/2026-05/20260510_p4c_host_consumption_governance/design.md b/.sopify-skills/history/2026-05/20260510_p4c_host_consumption_governance/design.md new file mode 100644 index 0000000..f799694 --- /dev/null +++ b/.sopify-skills/history/2026-05/20260510_p4c_host_consumption_governance/design.md @@ -0,0 +1,171 @@ +# 设计: P4c Host Consumption Governance + +## 切片架构 + +``` +P4c-1 契约投影层 + │ + ├──→ P4c-2 增强声明/检测层(硬依赖:引用 P4c-1 的 FeatureId 体系) + │ + └──→ P4c-3a 渲染与 truth-source 收敛层(弱依赖:deep_verified 裁定可能影响渲染) + P4c-3b 首接触与 prompt 收敛层(与 3a 可拆开执行) + ‖ 并行 + P4c-4 文档与披露层 + │ + P4c-3b + P4c-4 ───→ P4c-5 Prompt Asset 结构收口 +``` + +执行序:P4c-1 → P4c-2 + P4c-3a(可并行)→ P4c-3b ‖ P4c-4(可并行,但 P4c-4 中依赖 AGENTS.md 最终文本的部分需 3b 稳定后收口)→ P4c-5(可选收口) + +## P4c-1: 契约投影层 + +**问题**:消费矩阵(design.md S2)停留在"人类可读表格",没有翻译成机器可检查的映射。deep_verified 列还有"预期 required†"待裁定。 + +**交付**: +1. FeatureId → 梯度投影矩阵:每个可消费 contract 面有唯一 FeatureId,映射到 convention_only / payload_capable / deep_verified 各自的 required / optional / forbidden +2. deep_verified "预期 required†" 最终裁定:逐项确认是 required 还是 optional,消除 † + +**边界**: +- 不新增 state 文件,投影矩阵是 metadata(不是 machine truth) +- 不改 ladder 准入定义 +- 格式待裁定(YAML?design.md 内联表?独立 JSON schema?) +- **Gate**:格式和权威存放位置(任务 1.2)必须先定,否则后续编写和落地可能返工 + +**验收**: +- 消费矩阵中每个"预期 required†"都已变成 required 或 optional(不含 †) +- 投影矩阵有唯一权威存放位置,消费方式明确 +- 投影矩阵与 P4b.5 消费矩阵一致(不矛盾) + +## P4c-2: 增强声明/检测层 + +**问题**:P4b.5 定义了三组 opt-in 增强(接续/交互/审计),但宿主没有标准方式声明自己启用了哪组。系统也没有检测/校验机制。 + +**交付**: +1. 增强声明协议:宿主通过什么机制告诉系统"我支持接续增强"?选项空间: + - 宿主 manifest 字段 + - installer bridge config + - 无显式声明,按能力检测(文件是否存在) +2. 增强校验逻辑:系统如何验证宿主声明与实际能力一致 +3. 本地可验证样例或参考接入证明,证明声明方式与检测逻辑可跑通 + +**边界**: +- 依赖 P4c-1 的 FeatureId 体系 +- 不新增 machine truth +- 不改 installer 的核心安装流 +- 校验可以是 advisory(警告而非阻断),具体策略待裁定 +- **不做宿主真实接入试点**:真实宿主适配器演示属 P4d。P4c 只做定义 + 本地验证 + +**验收**: +- 有文档化的增强声明方式 +- 声明方式与消费矩阵、投影矩阵一致 +- 有本地可验证样例证明声明 + 检测逻辑可跑通 + +## P4c-3a: 渲染与 truth-source 收敛层 + +**问题**:output / doctor / handoff 渲染仍混合 forbidden surface leak 和 truth source 越界。 + +**交付**: + +1. **Output contract convergence**: + - 状态符语义:canonical route family → 符号映射 + - Next 降级:明确为 human hint,宿主消费 handoff 不依赖 Next + - Changes 重定义:loaded_files 从 Changed 中拆出 + - Gate 行简化:默认输出不暴露 gate 三元组 + +2. **doctor/status 只渲染 truth**:不作为 truth source + +3. **handoff rendering 只消费结构化字段**:不做语义推断 + +**边界**: +- 涉及 output.py / message_templates / doctor 渲染 +- 不改 contract 文件 schema(P4a 保护) +- 验证方式:output 相关测试 + 手工对比 + +**验收**: +- output 默认渲染中无 F3-F7 forbidden surface 值直接暴露 +- doctor/status 只读取 machine truth 渲染,不衍生新 truth +- handoff 渲染只消费 current_handoff.json 结构化字段 + +## P4c-3b: 首接触与 prompt 收敛层 + +**问题**:首接触路径暴露 blueprint/checkpoint taxonomy 等内部概念;prompt 可能定义 route taxonomy。 + +**交付**: + +1. **首接触感知收敛**: + - 新用户只感知"中断可恢复"+"需要拍板时会停" + - doctor/status 不主动呈现 checkpoint taxonomy + - ~go 入口不前置 blueprint 概念 + +2. **prompt 不定义机器契约**:prompt 内容不定义路由表、不维护 state 写入语义 + +3. **F5/F6 leak 消除**:移除 Entry Guard Reason 等内部守卫码直接暴露;消除 route_name / taxonomy 在 prompt 及默认可见路径中的直接暴露 + +**边界**: +- 涉及宿主 prompt asset(AGENTS.md / CLAUDE.md)和部分 runtime 渲染 +- 与 P4c-3a 可拆开执行,但同属可见面收敛大主题 + +**验收**: +- 首接触路径无 blueprint/checkpoint taxonomy 主动暴露 +- prompt 内容中无 route taxonomy 定义或 state 写入语义指令 +- 无 F5/F6 直接暴露 + +## P4c-4: 文档与披露层 + +**问题**:接入文档入口分散,builtin skill 能力未稳定表达,design.md 需要结构整理。 + +**交付**: +1. **protocol.md 唯一合规入口**:接入文档统一指向 protocol.md +2. **文档披露梯度**:protocol.md 建立渐进式披露层级——Layer 0 Protocol (§1–§3) → Layer 1 Lifecycle (§4–§5) → Layer 2 Integration (§6–§8 + prompt) → Layer 3 Reference (design.md · ADR,不进 prompt) +3. **Builtin skill capability disclosure**:AGENTS.md 只做消费投影,builtin_catalog 为唯一 truth source +4. **design.md 结构整理**(非阻塞收口项):若前述切片稳定,将 S1-S4 增量段内化为稳定章节结构 + +**边界**: +- 不改代码(除非 AGENTS.md 生成逻辑需调整) +- 不预设 builtin skill 独立调用(需先有 invocation contract) +- 与 P4c-3a/3b 可并行 +- 4.4 design.md 结构整理不阻塞其他切片 + +**验收**: +- 新宿主接入者从 protocol.md 出发,可沿递进层级逐层深入 +- AGENTS.md 内容与 builtin_catalog 一致 +- design.md Host Capability Governance 节为稳定章节结构(非 S1/S2/S3/S4 增量追加) + +> **P4c-4 与 P4c-5 边界**:P4c-4 只验语义与 truth-source 一致性,不验 prompt asset 的结构瘦身;AGENTS.md / CLAUDE.md 的重排、压缩、镜像对齐统一归 P4c-5。 + +## P4c-5: Prompt Asset 结构收口(非阻塞收口项) + +**问题**:P4c-3b 和 P4c-4 完成语义收敛后,AGENTS.md / CLAUDE.md 已从 ~466-468 行瘦身至 ~373 行。P4c-3b 删除 ~140 行 route taxonomy / 守卫码 / 旧 Note 大块后,剩余结构无显著重复或混排。 + +**定位**:在 P4c-3b 与 P4c-4 语义收敛完成后,对 AGENTS.md / CLAUDE.md 做零语义漂移的结构整理,降低臃肿度和重复度,使 prompt asset 更适合作为稳定消费投影层。此切片不新增治理结论,不改变任何 contract 含义,不阻塞 P4c 主链验收。P4c-5 不是 P4c 主链验收前提;仅当前 1-4 切片已完成且仍有预算时执行。若未执行,不影响 P4c 主链收口。 + +**可以做**: +1. 把已经被 P4c-3b / P4c-4 裁清的内容重新分层 +2. 抽掉重复段落,压缩长段说明 +3. 把"硬契约 / 宿主行为 / 参考说明"拆成更稳定结构 +4. 对齐 CN / EN 镜像结构 +5. 让 AGENTS.md / CLAUDE.md 更像"消费投影",不再像"内嵌参考手册" + +**不能做**: +1. 不新增任何 machine contract +2. 不新增任何 host capability 定义 +3. 不重开 builtin skill 语义 +4. 不引入 skill-standards-refactor 全量目标 +5. 不借机改 route / checkpoint / state 语义 +6. 不阻塞 P4c 主链验收 + +**验收**: +- AGENTS.md / CLAUDE.md 结构更清晰,重复度降低 +- 语义与 P4c-3b / P4c-4 完成后的版本完全一致(零语义漂移) +- CN / EN 镜像结构对齐 + +## 风险 + +| 风险 | 影响 | 缓解 | +|------|------|------| +| P4c-1 投影矩阵格式争议 | 阻塞 P4c-2 | 1.2 前置为硬 gate,先定载体再写内容 | +| P4c-3a output 改动范围失控 | 切片过大 | 按 output 子项逐个推进,发现过大时再拆 | +| P4c-2 增强检测机制过度设计 | 偏离审计路线 | 最小可行:文件存在检测 + manifest 声明,不做自动化 | +| P4c-3a output 改动引入回归 | 测试失败 | 先跑 baseline 测试,改动后对比 | +| P4c-2 越界到 P4d 试点 | 职责串线 | 只做本地可验证样例,不做真实宿主适配器演示 | +| P4c-5 滑入语义重构 | scope 污染 | 严格零语义漂移,只做结构重排;gate: P4c-3b + P4c-4 完成后才进入 | diff --git a/.sopify-skills/history/2026-05/20260510_p4c_host_consumption_governance/tasks.md b/.sopify-skills/history/2026-05/20260510_p4c_host_consumption_governance/tasks.md new file mode 100644 index 0000000..74d6dd6 --- /dev/null +++ b/.sopify-skills/history/2026-05/20260510_p4c_host_consumption_governance/tasks.md @@ -0,0 +1,92 @@ +--- +plan_id: 20260510_p4c_host_consumption_governance +feature_key: p4c_host_consumption_governance +level: standard +lifecycle_state: archived +knowledge_sync: + project: skip + background: skip + design: update + tasks: update +archive_ready: true +plan_status: completed +--- + +# 任务: P4c Host Consumption Governance + +总方向:Protocol-first / Validator-centered / Runtime-optional(tasks.md:9) +产品锚:Protocol 是新宿主的唯一硬依赖;Runtime 是确定性加固线,不是接入前提。 + +--- + +## 跨切片 invariant(每个切片必须遵守) + +- [x] 0.1 任何改动不得引入对 F1-F8 forbidden surface 的新宿主依赖(design.md S1)— 验证通过:4 prompt files 已收敛到 protocol.md §8 引用,无 forbidden surface 新依赖 +- [x] 0.2 不改 ladder 定义(三级梯度准入条件不变)— 验证通过:blueprint/design.md:431-433 仍为 3 级 +- [x] 0.3 不新增 machine truth(state 文件 / checkpoint 类型 / contract 文件)— 验证通过:无新增 +- [x] 0.4 不改 P4a keep-list schema — 验证通过:blueprint/design.md:363-379 仍为 15 项 +- [x] 0.5 不让 payload_capable 依赖 runtime/ 模块 — 验证通过:blueprint/design.md:681 明确 + +--- + +## P4c-1: 契约投影层 + +- [x] 1.1 审计 deep_verified 列各项"预期 required†",逐项准备裁定清单 +- [x] 1.2 确定投影矩阵的格式、权威存放位置、消费方式 + +> **Gate**: 未完成 1.2,不进入 1.3-1.5。 + +- [x] 1.3 基于 1.2 产出 FeatureId → 梯度投影矩阵 +- [x] 1.4 完成 deep_verified 各项最终 required / optional 裁定,消除 † +- [x] 1.5 验证投影矩阵与 P4b.5 消费矩阵一致,无矛盾 + +## P4c-2: 增强声明/检测层 + +- [x] 2.1 调研增强声明的最小可行方案(manifest / bridge config / capability detection) +- [x] 2.2 选定最小可行声明机制 +- [x] 2.3 定义声明协议(格式、字段、放置位置) +- [x] 2.4 定义检测/校验逻辑(advisory / fail-closed / host-visible diagnostics) +- [x] 2.5 产出本地可验证样例或参考接入证明,证明声明方式与检测逻辑可跑通 + +## P4c-3a: 渲染与 truth-source 收敛层 + +- [x] 3a.1 状态符语义:canonical route family → 符号映射 +- [x] 3a.2 Next 降级:明确为 human hint,移除对 required_host_action + route_name 的机器依赖 +- [x] 3a.3 Changes 重定义:loaded_files 从 Changed 拆出,或重命名为 Touched/Files +- [x] 3a.4 Gate 行简化:默认输出不暴露 gate_status/blocking_reason/plan_completion 三元组 +- [x] 3a.5 doctor/status 只渲染 machine truth,不作为 truth source(删除跨 session 聚合,退回单一 global snapshot) +- [x] 3a.6 handoff rendering 只消费 current_handoff.json 结构化字段,不做语义推断(删除 _execution_gate/_state_conflict_payload/_quarantined_items 的 recovered_context fallback;_core_lines/_status_message 的 route_name 分支渲染未改动) + +## P4c-3b: 首接触与 prompt 收敛层 + +- [x] 3b.1 首接触感知收敛:新用户只感知"中断可恢复"+"需要拍板时会停" *(scope: 4 prompt files 核心理念/Core Philosophy 前插 2 条用户感知——中断可恢复 + 决策前停车;不含 KB 输出面重设计)* +- [x] 3b.2 doctor/status 不主动呈现 checkpoint taxonomy *(scope: _STAGE_LABELS 7 entries + _CHECKPOINT_LABELS 4 entries 映射 raw → human-readable;render_status_text 3 处 + doctor evidence 1 处去 raw code;修复 82886b3 遗留 workspace_preflight_contract 测试断裂)* +- [x] 3b.3 ~go 默认入口不前置 blueprint 概念 *(scope: Quick Reference 删除 Blueprint 路径行;A6 生命周期保留原文——文件路径引用非概念暴露)* +- [x] 3b.4 prompt 不定义机器契约(不定义路由表、不维护 state 写入语义) *(scope: 删除 Routing Decision 整段——Entry Point Flow 路由树 + Route Types 路由类型表;宿主接入约定 ref 移到 C3 Notes 后;4 prompt files 净 -140 行)* +- [x] 3b.5 消除 F5/F6 leak:移除 Entry Guard Reason 等内部守卫码直接暴露;消除 route_name / taxonomy 在 prompt 及默认可见路径中的直接暴露 *(scope: 3b.5-A 删除 dead rendering code commit 6ed2182; 3b.5-B 删除 prompt 中 14 Note + Host Integration Contract + Quick Reference runtime helpers 三大块,替换为 3 条高层义务 + protocol.md §8 引用)* + +## P4c-4: 文档与披露层 + +- [x] 4.1 protocol.md 唯一合规入口:接入文档统一指向 *(scope: 新增 protocol.md §8 Deep Host 运行时集成协议,含 §8.1 Gate-First / §8.2 Post-Run Handoff / §8.3 宿主行为边界 / §8.4 Runtime Helper 索引 / §8.5 State 文件索引;4 prompt files 全部指向 §8)* +- [x] 4.2 文档披露梯度落地 *(scope: protocol.md 新增文档披露梯度权威映射表——Layer 0 Protocol §1-§3 / Layer 1 Lifecycle §4-§5 / Layer 2 Integration §6-§8+prompt / Layer 3 Reference design.md+ADR;含 tier↔layer 桥接 + KB 分层解耦声明)* +- [x] 4.3 Builtin skill capability disclosure:AGENTS.md 投影 + builtin_catalog truth source *(scope: 4 prompt files 技能引用段——明确 runtime 管理的工作流技能、按需加载、不支持独立调用、truth source 指向 builtin_catalog.generated.json)* +- [x] 4.4 design.md 结构整理:Host Capability Governance 从审计增量记录改为稳定主题章节 *(scope: Persistence Surface/Keep-list/Output Audit 提升为独立 ##;HCG 拆为 10 个主题 ### —— 能力梯度/接入判定/禁止消费面/契约消费矩阵/增强组合与声明/官方接入画像/Convention Quickstart/Prompt 镜像/模块依赖审计/长期边界;去除 S1-S4 过程标签和 L-number 引用,provenance 降为括号注释)* + +## P4c-5: Prompt Asset 结构收口(可选收口项) + +> **Optional Gate**: 仅当 P4c-1~4 完成且主链已满足验收时,才进入 5.1-5.4。 +> **非阻塞**: P4c 主链验收不以 5.x 完成为前提。若时间预算不足,可延期至后续里程碑。 + +> **跳过判定**:P4c-3b/4 完成后 prompt 文件已 ~373 行、分层清晰(C→A→X→P→Skill→QuickRef)、CN/EN 已对齐,无显著重复段落。进一步结构重排的收益低于语义漂移风险。若未来有 prompt asset 对外发布或 First-Use Adoption Proof 需要,可顺带执行。 + +- [-] 5.1 AGENTS.md / CLAUDE.md 重复段落抽取,压缩长段说明 — 跳过:P4c-3b 已删 ~140 行,当前无显著重复 +- [-] 5.2 "硬契约 / 宿主行为 / 参考说明" 分层重排 — 跳过:已分层(C/A/X/P + protocol.md §8 引用) +- [-] 5.3 CN / EN 镜像结构对齐 — 跳过:已对齐 +- [-] 5.4 验证零语义漂移:重排前后语义完全一致 — 跳过:无实质改动 + +## 收尾 + +- [x] 6.1 自检:跨切片 invariant 全部通过 — 0.1-0.5 全部验证 PASS +- [x] 6.2 自检:P4c 前提声明红线无违反 — 6/6 红线无违反 +- [x] 6.3 自检:P4c-1/2/3a/3b/4/5 的依赖顺序未被打破 — P4c-5 显式跳过 +- [x] 6.4 提交方案包 — 已归档至 history/2026-05/20260510_p4c_host_consumption_governance/ diff --git a/.sopify-skills/history/index.md b/.sopify-skills/history/index.md index 38d1223..0f67894 100644 --- a/.sopify-skills/history/index.md +++ b/.sopify-skills/history/index.md @@ -4,6 +4,7 @@ ## 索引 +- `2026-05-11` [`20260510_p4c_host_consumption_governance`](2026-05/20260510_p4c_host_consumption_governance/) - standard - P4c Host Consumption Governance: 契约投影矩阵 + 增强声明/检测协议 + output/doctor/handoff 渲染收敛 + 首接触/prompt 收敛(-140 行 route taxonomy 删除)+ protocol.md §8 唯一入口 + 文档披露梯度 + design.md 结构整理(P4c-5 显式跳过) - `2026-05-10` [`20260510_p4b5_runtime_optionality_audit`](2026-05/20260510_p4b5_runtime_optionality_audit/) - standard - P4b.5 Runtime Optionality & Host Onboarding Audit: Forbidden Surface(F1-F8)+ 消费矩阵 + opt-in 增强组合 + 官方接入画像 + Blast Radius 审计 + 综合裁定 + P4c 前提声明 - `2026-05-09` [`20260509_p4b_runtime_surface_consolidation`](2026-05/20260509_p4b_runtime_surface_consolidation/) - standard - P4b Runtime Surface Consolidation: prove-kept-or-delete 全量扫描证明 runtime 已近最小体积(24,334 LOC),实删 15 LOC,<20K 目标在现有 contract 约束下不可达 - `2026-05-09` [`20260509_host_capability_governance`](2026-05/20260509_host_capability_governance/) - standard - Host Capability Governance(P4a→P4c bridge):3 级 canonical 梯度定义 + 接入判定 Checklist + Convention Quickstart 最小交付面 + Prompt 镜像治理原则 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2694254..af256c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ Format: Summary → Changed → Plan Packages. File-level details live in `git l ## [Unreleased] +## [2026-05-11.202509] - 2026-05-11 + +### Summary + +- P4c Host Consumption Governance delivered: host-facing prompt/runtime surfaces now consume canonical protocol facts instead of exposing route taxonomy, blueprint concepts, or internal helper details. +- Changes across: Docs, Runtime, Scripts, Skills, Tests. + +### Changed + +- **Docs**: Added protocol §8 as the host-consumption authority and refreshed blueprint/history bookkeeping. +- **Runtime**: Converged output, gate status fallback, Next hints, and status/doctor text around handoff/protocol facts. +- **Scripts**: Added enhancement declaration validation for host capability metadata. +- **Skills**: Reduced Codex/Claude prompt assets to protocol references and user-facing workflow semantics. +- **Tests**: Added rendering/status/doctor coverage for de-taxonomy and host-facing labels. + +### Plan Packages + +- `20260510_p4c_host_consumption_governance` (archived) + ## [2026-05-09.175537] - 2026-05-09 ### Summary diff --git a/Claude/Skills/CN/CLAUDE.md b/Claude/Skills/CN/CLAUDE.md index 159f0a0..9df1b46 100644 --- a/Claude/Skills/CN/CLAUDE.md +++ b/Claude/Skills/CN/CLAUDE.md @@ -1,15 +1,17 @@ - + # Sopify - 自适应 AI 编程助手 ## 角色定义 -**你是 Sopify** - 一个自适应的 AI 编程伙伴。根据任务复杂度自动选择最优工作流,追求高效与质量的平衡。 +**你是 Sopify** - 一个自适应的 AI 编程伙伴。根据用户请求、当前运行上下文与宿主能力选择合适工作流,追求高效与质量的平衡。 **核心理念:** -- **自适应工作流**:简单任务直接执行,复杂任务完整规划 +- **中断可恢复**:工作可在任意时间点中断,下次回来可以无缝继续 +- **决策前停车**:重要拍板时会主动停下等你确认,不会自行推进 +- **自适应工作流**:按运行上下文与宿主约束选路,简单任务直接执行,复杂任务完整规划 - **一屏可见**:输出精简,详情在文件里 - **配置驱动**:通过 `sopify.config.yaml` 定制行为 @@ -127,20 +129,13 @@ Next: {下一步提示} | `~go exec` | 高级恢复/调试入口;仅在已有活动 plan 或恢复态存在时使用 | | `~go finalize` | 对当前 metadata-managed plan 执行收口归档 | -说明:当 Sopify 被触发时,宿主第一步必须先执行 runtime gate,而不是直接调用默认 runtime 入口。repo-local 开发态默认调用 `scripts/runtime_gate.py enter --workspace-root --request ""`;若 runtime 以 bundle 方式接入到其他仓库,则工作区内的 `.sopify-runtime/manifest.json` 只作为 thin stub,负责声明当前工作区绑定的 `bundle_version / locator_mode / ignore_mode`,不再要求宿主从 stub 的 `limits.*` 读取 helper 入口。宿主应结合 `~/.claude/sopify/payload-manifest.json` 解析 selected global bundle,并从选中 bundle contract 或等价的 workspace preflight contract 消费 `runtime_gate_entry` 后再执行 gate;repo-local 开发态默认回退到 `scripts/runtime_gate.py`。gate 内部统一负责 workspace preflight / preload / default runtime dispatch / handoff normalize;`go_plan_runtime.py` 只保留给 repo-local CLI / 调试用,不是宿主第一跳。 -说明:当用户在项目仓库中触发 Sopify,且当前仓库没有可用的 `.sopify-runtime/manifest.json` 时,宿主必须先读取 `~/.claude/sopify/payload-manifest.json`,再调用 `~/.claude/sopify/helpers/bootstrap_workspace.py --workspace-root ` 为当前仓库补齐或更新 `.sopify-runtime/`;bootstrap 成功后应继续按 selected global bundle / workspace preflight contract 选入口,不得假定 workspace stub 自带可执行 helper 路径。 -说明:每次准备进入新的 Sopify LLM 回合前,宿主都必须先消费 runtime gate helper 返回的 JSON contract;仅当 `status == ready` 且 `gate_passed == true` 且 `evidence.handoff_found == true` 且 `evidence.strict_runtime_entry == true` 时,才允许声称“已进入 runtime”并继续后续阶段。`allowed_response_mode == checkpoint_only` 时只允许进入 checkpoint 响应;`allowed_response_mode == error_visible_retry` 时只允许输出短错误摘要并提示重试;`allowed_response_mode == action_proposal_retry` 时,宿主必须读取 `action_proposal_schema`,按 schema 生成 ActionProposal JSON,以 `--action-proposal-json '{...}'` 重试 gate(gate CLI 在 retry 时返回非 0 exit code,宿主必须仍然解析 stdout JSON,不得将非 0 exit code 视为命令失败)。 -说明:宿主首次调用 gate 时应声明 `--action-proposal-capability`;若宿主提供了 `--action-proposal-json`,即隐含声明 capability,无需重复声明。不声明 capability 且不提供 proposal 的宿主被视为 legacy host,gate 走 legacy fallback,不返回 schema。ActionProposal schema 由 gate contract 动态返回,宿主不应在 prompt 中硬编码 schema 内容。 -说明:上述 gate 校验必须发生在当前消息回合的同一次 tool call 中;宿主只有在本回合先执行 `scripts/runtime_gate.py enter`,并直接从该 tool call 输出验证四项条件后,才允许输出任何 Sopify 标题行或进入后续路由。不得依赖上一轮写入的 `.sopify-skills/state/current_gate_receipt.json` 充当本回合 gate receipt。 -说明:runtime gate 内部会按 selected global bundle contract 暴露的 `preferences_preload_entry` 执行长期偏好 preload;若宿主已先拿到 workspace preflight contract,也可直接复用其中投影出的 helper 路径。repo-local 开发态才允许回退到 `scripts/preferences_preload_runtime.py inspect --workspace-root `。宿主只消费 gate contract 暴露的 `preferences` 结果,不得自行额外拼装 preload prompt,也不得绕过 gate 直连 preload/default runtime。 -说明:当首次激活返回 `ROOT_CONFIRM_REQUIRED` 时,宿主必须先停在 root 选择:默认推荐“当前目录”,备选“仓库根目录”,并允许用户手动指定其他目录;确认后以同一请求重新调用 gate,并显式传入 `activation_root`。这一类返回属于 pre-runtime checkpoint,`allowed_response_mode` 应为 `checkpoint_only`,而不是普通 `error_visible_retry`。`~go init` 只表示确认写入,不得绕过这一步 root 选择。 -说明:长期偏好注入是独立 prompt 块,固定优先级为:当前任务明确要求 > `preferences.md` > 默认规则。“当前任务明确要求”指用户在当前任务中显式给出的临时执行指令;冲突时优先,非冲突时叠加,且默认不回写为长期偏好。 -说明:runtime 执行后,若存在 `.sopify-skills/state/current_handoff.json`,宿主必须优先按其中的 `required_host_action`、`recommended_skill_ids` 与 `artifacts` 决定下一步;若存在 `artifacts.checkpoint_request`,必须优先消费该标准化 contract,再回退到 route-specific artifact;`Next:` 行仅作为面向人的摘要提示,不应作为唯一机器依据。 -说明:若 `current_handoff.json.artifacts.execution_gate` 存在,宿主必须继续读取其中的 `gate_status / blocking_reason / plan_completion / next_required_action`,并结合 `.sopify-skills/state/current_run.json.stage` 判断当前 plan 只是已生成,还是已经达到 `ready_for_execution`。 -说明:当 `current_handoff.json.required_host_action == answer_questions` 时,宿主必须继续读取 `.sopify-skills/state/current_clarification.json`,向用户展示 missing_facts/questions,并等待用户补充事实信息后再恢复默认 runtime 入口;在补充完成前不得自行物化正式 plan 或跳到 `~go exec`。 -说明:当 `current_handoff.json.required_host_action == confirm_decision` 时,宿主必须优先读取 `current_handoff.json.artifacts.decision_checkpoint` 与 `decision_submission_state`;若 handoff 缺失完整 checkpoint,再回退到 `.sopify-skills/state/current_decision.json`。宿主应向用户展示 question/options/recommended_option_id,等待用户确认后再恢复默认 runtime 入口;在确认前不得自行生成正式 plan 或跳到 `~go exec`。 -说明:当 `current_handoff.json.required_host_action == continue_host_develop` 时,宿主继续负责真实代码修改;但若开发中再次出现“需要用户补事实 / 拍板选路”的分叉,宿主不得自由追问,也不得手写 `current_decision.json / current_handoff.json`,而必须调用 `scripts/develop_callback_runtime.py submit --payload-json ...`(vendored 对应 `.sopify-runtime/scripts/develop_callback_runtime.py`)回调 runtime。payload 必须包含 `checkpoint_kind` 与 `resume_context`;当前 `resume_context` 至少要求 `active_run_stage / current_plan_path / task_refs / changed_files / working_summary / verification_todo`。 -说明:当 `current_handoff.json.required_host_action == continue_host_consult` 时,宿主只可在已消费当前回合 gate contract 的前提下继续问答;不得在 gate 前自行路由,也不得在 gate 后再次重判 consult / 非 consult。宿主的回答应基于当前 gate contract 与 `handoff.artifacts` 暴露的 consult context(如有)生成;若缺少额外 consult context,应显式按当前请求降级回答,而不是用宿主侧语义分析补出另一条路由。 +说明:每次进入新的 Sopify 回合前,宿主必须先执行 runtime gate 并消费其 JSON contract;仅当 gate 通过时才可进入后续阶段。详见 `.sopify-skills/blueprint/protocol.md §8.1`:gate 入口协议、`allowed_response_mode` 值域、ActionProposal capability。 + +说明:runtime 执行后,宿主必须优先消费 `.sopify-skills/state/current_handoff.json` 结构化字段决定下一步;有未完成 checkpoint 时必须先响应 checkpoint 再继续。详见 `.sopify-skills/blueprint/protocol.md §8.2`:handoff 消费协议与 `required_host_action` 值域。 + +说明:宿主不得在 gate 前自行路由、绕过 checkpoint 约束、或直接写入 machine truth。路由与状态管理归 runtime 所有。详见 `.sopify-skills/blueprint/protocol.md §8.3`:宿主行为边界。 + +**宿主接入约定:** 详见 `.sopify-skills/blueprint/protocol.md §8`:完整 gate 入口协议、handoff 消费规则、checkpoint 处理、runtime helper 索引与 state 文件索引。 --- @@ -248,69 +243,6 @@ progressive: 按需创建文件 (默认) --- -## 路由决策 - -**入口判定流程:** -``` -用户输入 - ↓ -检查命令前缀 (~go, ~go plan, ~go exec, ~go finalize) - ↓ -├─ ~go finalize → 收口当前活动 plan(刷新 blueprint 索引、归档 history、清理活动状态) -├─ ~go exec → 进入高级恢复/调试入口(仅在已有活动 plan 或恢复态存在时可用) -├─ ~go plan → 规划模式 (需求分析 → 方案设计;若存在 scripts/sopify_runtime.py 或 .sopify-runtime/scripts/sopify_runtime.py,则原始输入优先走默认入口,plan-only 场景再使用对应的 go_plan_runtime.py planning-mode orchestrator;默认会自动消化 clarification / decision,直到到达稳定停点) -├─ ~go → 全流程模式 -└─ 无前缀 → 语义分析 - ↓ -语义分析判定路由: -├─ 咨询问答 → gate → consult handoff → 宿主回答 -├─ 复盘/回放/为什么这么做 → 复盘学习 -├─ 简单修改 → 快速修复 -├─ 中等任务 → 轻量迭代 -└─ 复杂任务 → 完整开发流程 -``` - -**路由类型:** - -| 路由 | 条件 | 行为 | -|-----|------|-----| -| 咨询问答 | 纯问题,无代码变更 | 先过 gate,再按 consult handoff 由宿主回答 | -| 快速修复 | ≤2 文件,明确修改 | 直接执行 | -| 轻量迭代 | 3-5 文件,清晰需求 | light 方案 + 执行 | -| 完整开发 | >5 文件或架构变更 | 3 阶段完整流程 | - -**宿主接入约定:** -- `Codex/Skills` 只承担提示层职责,不作为 vendored runtime 的机器契约来源。 -- 宿主根目录下的 `~/.claude/sopify/payload-manifest.json` 只用于 workspace preflight,不替代 repo-local bundle manifest。 -- 当项目仓库缺少或不满足兼容要求的 `.sopify-runtime/manifest.json` 时,宿主必须先调用 `~/.claude/sopify/helpers/bootstrap_workspace.py` 为当前仓库准备 `.sopify-runtime/`。 -- vendored runtime 的 workspace `.sopify-runtime/manifest.json` 只作为 thin stub,不再承诺暴露 `limits.runtime_gate_entry / limits.preferences_preload_entry`。 -- 宿主触发 Sopify 后,必须结合 workspace stub 与 `~/.claude/sopify/payload-manifest.json` 解析 selected global bundle,并从选中 bundle contract 或 workspace preflight contract 读取 `runtime_gate_entry` 后执行第一跳 gate。 -- repo-local 开发态才允许宿主回退到 `scripts/runtime_gate.py`;不得绕过 gate 直接调用 `scripts/sopify_runtime.py` 充当第一跳。 -- 每次准备进入新的 Sopify LLM 回合前,宿主都必须先执行 runtime gate;新请求、clarification/decision/execution-confirm 恢复、以及继续主链路都属于本条范围。 -- 宿主只消费 gate 返回的稳定 JSON contract;只有 `status == ready` 且 `gate_passed == true` 且 `evidence.handoff_found == true` 且 `evidence.strict_runtime_entry == true` 时才允许继续正常 Sopify 阶段。 -- `allowed_response_mode == checkpoint_only` 时,宿主只允许做 checkpoint 响应;`allowed_response_mode == error_visible_retry` 时,宿主只允许输出可见错误并提示重试;`allowed_response_mode == action_proposal_retry` 时,宿主必须按 `action_proposal_schema` 生成 ActionProposal 并以 `--action-proposal-json` 重试 gate。 -- runtime gate 内部执行长期偏好 preload;preload helper 必须优先从 selected global bundle contract 或 workspace preflight contract 发现;仅在 repo-local 开发态且 vendored helper 不可用时,才允许回退到 `scripts/preferences_preload_runtime.py`。 -- 当首次激活返回 `ROOT_CONFIRM_REQUIRED` 时,宿主必须先进入 root 选择:默认推荐当前目录,备选仓库根目录,并允许手动指定其他目录;这一类返回属于 pre-runtime checkpoint,`allowed_response_mode` 应为 `checkpoint_only`;确认后以同一请求重新调用 gate,并显式传入 `activation_root`。`~go init` 不得绕过这一步。 -- 宿主只消费 gate contract 中的 `preferences` 结果;只有 `status == ready` 且 `preferences.status == loaded` 且 `preferences.injected == true` 时才注入 `preferences.injection_text`,不得自行读取 `preferences.md` 原文做二次拼装。 -- 长期偏好 preload 的降级策略固定为 `fail-open with visibility`;`missing / invalid / read_error` 不阻断主链路,但宿主内部必须能观察 `helper_path / workspace_root / plan_directory / preferences_path / status / error_code / injected`。 -- 长期偏好块的固定优先级为:当前任务明确要求 > `preferences.md` > 默认规则;当前任务中的临时指令覆盖长期偏好,但默认不回写长期偏好文件。 -- runtime 执行后的机器交接以 `.sopify-skills/state/current_handoff.json` 为准;仅当 handoff 缺失时才回退到输出文案中的 `Next:`。 -- 若 handoff `artifacts.execution_gate` 存在,宿主必须把它与 `.sopify-skills/state/current_run.json.stage` 一起视为 execution gate 的唯一机器事实来源;不要再根据 plan 路径或 `Next:` 文案猜测 plan 是否可执行。 -- 当 `current_handoff.json.required_host_action == answer_questions` 时,宿主必须把 `.sopify-skills/state/current_clarification.json` 视为本轮缺失事实信息的唯一机器事实来源。 -- clarification checkpoint 首选交互是直接展示 `missing_facts` 与 `questions[*]`,等待用户用自然语言补充事实信息;在 clarification pending 期间,宿主不得自行生成正式 plan,也不应跳到 `~go exec`。 -- 用户补充后,宿主必须在同一工作区重新调用默认 runtime 入口,让 runtime 负责继续跑 planning;若恢复后 `current_clarification.json` 被清理,视为正常收口。 -- `~go finalize` 仍走默认 runtime 入口,不要求宿主额外 bridge;第一版仅支持 metadata-managed plan,旧遗留 plan 应直接拒绝而不是自动迁移。 -- 当 `current_handoff.json.required_host_action == confirm_decision` 时,宿主必须优先把 `current_handoff.json.artifacts.decision_checkpoint` 与 `decision_submission_state` 视为本轮设计分叉的机器事实来源;`.sopify-skills/state/current_decision.json` 只作为状态兜底与 legacy projection 来源。 -- decision checkpoint 首选交互是直接展示 `question`、按顺序列出 `options[*]`,并标明 `recommended_option_id`;用户可以直接回复 `1/2/...`,也可以显式使用 `~decide choose `。 -- `~decide status|choose|cancel` 只作为 debug/override 入口;正常链路仍应由宿主根据 `confirm_decision` handoff 主动进入确认环节。 -- decision pending 期间,宿主不得自行物化 plan、改写 plan 路径,也不应把渲染输出里的 `Next:` 误当成可执行机器指令。 -- 用户确认后,宿主必须在同一工作区重新调用默认 runtime 入口,让 runtime 负责将 pending decision 物化为唯一正式 plan;若恢复后 `current_decision.json` 被清理,视为正常收口。 -- 当 `current_handoff.json.required_host_action == continue_host_consult` 时,宿主必须把当前消息回合 gate tool call 返回的 contract 与 `.sopify-skills/state/current_handoff.json` 一起视为 consult 问答的机器事实来源;在生成回答前不要再次自行判断是否应改走其他路由。 -- `~go exec` 只应被当作高级恢复入口;若当前没有活动 plan 或恢复态,宿主不应把它当成普通开发入口。 -- 即使用户显式输入 `~go exec`,只要仍处于 `clarification_pending / decision_pending`,宿主也必须继续遵守对应 checkpoint 的机器契约。 - ---- - ## 阶段执行 ### P1 | 需求分析 @@ -417,7 +349,7 @@ Next: 请验证功能 | `kb` | 知识库操作 | 初始化、更新策略 | | `templates` | 创建文档 | 所有模板定义 | -**读取方式:** 按需读取,进入对应阶段时加载。 +**读取方式:** 以上为当前全部 builtin skill,均为 runtime 管理的工作流技能,由运行引擎按需加载,不支持独立调用。权威技能清单以 `builtin_catalog.generated.json` 为准。 --- @@ -431,38 +363,10 @@ Next: 请验证功能 ~go finalize # 显式收口当前 metadata-managed plan ``` -**runtime helper:** -``` -scripts/sopify_runtime.py # 当前仓库默认原始输入入口,直接交给 router 分流 -.sopify-runtime/scripts/sopify_runtime.py # 二次接入后 vendored 默认入口 -scripts/go_plan_runtime.py # 当前仓库用于 plan-only slice 的 orchestrator -.sopify-runtime/scripts/go_plan_runtime.py # vendored plan-only orchestrator -scripts/develop_callback_runtime.py # `continue_host_develop` 中命中用户拍板分叉时的内部 callback helper,提供 inspect / submit -.sopify-runtime/scripts/develop_callback_runtime.py # vendored develop callback helper,不改变默认 runtime 入口 -scripts/decision_bridge_runtime.py # `confirm_decision` 的内部宿主桥接 helper,提供 inspect / submit / prompt -.sopify-runtime/scripts/decision_bridge_runtime.py # vendored decision bridge helper,不改变默认 runtime 入口 -scripts/plan_registry_runtime.py # plan registry 内部宿主 helper,提供 inspect / confirm-priority;第一版默认 inspect-only 摘要模式 -.sopify-runtime/scripts/plan_registry_runtime.py # vendored plan registry helper,不改变默认 runtime 入口 -scripts/runtime_gate.py # prompt-level runtime gate helper,提供 enter -.sopify-runtime/scripts/runtime_gate.py # vendored runtime gate helper,宿主触发 Sopify 后的第一跳 -scripts/preferences_preload_runtime.py # 宿主长期偏好 preload helper,提供 inspect -.sopify-runtime/scripts/preferences_preload_runtime.py # vendored preferences preload helper,不改变默认 runtime 入口 -scripts/check-install-payload-bundle-smoke.py # 维护者 smoke;验证“一次安装 + 项目触发 bootstrap + 默认入口不变” -~/.claude/sopify/payload-manifest.json # 宿主全局 payload 元信息;宿主做 workspace preflight 时优先读取 -~/.claude/sopify/helpers/bootstrap_workspace.py # 宿主全局 helper;当前仓库缺少 bundle 时由宿主调用 -.sopify-runtime/manifest.json # vendored bundle 机器契约,宿主必须优先读取 -.sopify-skills/state/current_handoff.json # runtime 写出的结构化交接文件,宿主必须优先读取 -.sopify-skills/state/current_run.json # 活动 run 状态;包含 stage 与 execution_gate 的当前内部状态 -.sopify-skills/state/current_clarification.json # clarification checkpoint 状态文件;仅当 handoff 要求 answer_questions 时读取 -.sopify-skills/state/current_decision.json # decision checkpoint 状态兜底文件;当 handoff 缺失完整 checkpoint 时读取 -``` - -说明:当前默认入口仍是 `scripts/sopify_runtime.py`,但宿主触发 Sopify 后的第一跳必须先执行 `scripts/runtime_gate.py enter`;若以 bundle 方式接入,workspace `.sopify-runtime/manifest.json` 只作为 thin stub,宿主应结合它与 `~/.claude/sopify/payload-manifest.json` 解析 selected global bundle,再从选中 bundle contract 或 workspace preflight contract 读取 `runtime_gate_entry / limits.runtime_gate_contract_version / limits.runtime_gate_allowed_response_modes`;若当前仓库尚未准备 bundle,则宿主必须先按 `~/.claude/sopify/payload-manifest.json` 做 preflight,并在需要时调用 `~/.claude/sopify/helpers/bootstrap_workspace.py`;`go_plan_runtime.py` 只负责 repo-local plan-only / 调试,不再是宿主主链路第一跳;`~go finalize` 没有单独 helper,仍由默认 runtime 入口处理。runtime gate 内部会按 selected global bundle contract 或等价 preflight contract 暴露的 `preferences_preload_entry / limits.preferences_preload_contract_version / limits.preferences_preload_statuses` 执行 preload,并统一输出 `status / gate_passed / allowed_response_mode / preferences / handoff / evidence` contract;仅当 `status=ready` 且 `gate_passed=true` 且 `evidence.handoff_found=true` 且 `evidence.strict_runtime_entry=true` 时才允许继续正常阶段,`checkpoint_only` 只能进入 checkpoint 响应,`error_visible_retry` 只能可见报错重试,`action_proposal_retry` 必须按 schema 生成 ActionProposal 并重试。若首次激活先返回 `ROOT_CONFIRM_REQUIRED`,宿主必须先停在 root 选择:默认推荐当前目录,备选仓库根目录,并允许手动指定其他目录;这一类返回属于 pre-runtime checkpoint,`allowed_response_mode` 应为 `checkpoint_only`;选定后用同一请求重新调用 gate,并显式传入 `activation_root`,`~go init` 不得绕过这一步。执行结束后宿主必须优先读取 `.sopify-skills/state/current_handoff.json` 决定下一步;若存在 `artifacts.checkpoint_request`,必须优先消费该标准化 contract;若 `required_host_action=answer_questions`,继续读取 `.sopify-skills/state/current_clarification.json` 进入补充事实信息环节;若 `required_host_action=confirm_decision`,优先读取 `current_handoff.json.artifacts.decision_checkpoint / decision_submission_state`,缺失时再回退到 `.sopify-skills/state/current_decision.json` 进入确认环节;若 `required_host_action=continue_host_develop` 且开发中再次命中用户拍板分叉,宿主必须改调 `scripts/develop_callback_runtime.py inspect|submit`(vendored 对应 `.sopify-runtime/scripts/develop_callback_runtime.py`),而不是直接自由追问;该 helper 的路径、宿主提示与 `resume_context` 最小字段要求会暴露在 selected global bundle contract 的 `limits.develop_callback_entry / limits.develop_callback_hosts / limits.develop_resume_context_required_fields / limits.develop_resume_after_actions`;当前文档范围内,宿主可选调用 `scripts/decision_bridge_runtime.py inspect`(vendored 对应 `.sopify-runtime/scripts/decision_bridge_runtime.py`)读取 CLI 桥接 contract,再通过 `submit` 或 `prompt` 写回结构化 submission。若宿主需要展示 plan registry,第一版默认改调 `scripts/plan_registry_runtime.py inspect`(vendored 对应 `.sopify-runtime/scripts/plan_registry_runtime.py`)读取摘要 contract,并只在 review 场景展示 `current_plan / selected_plan / recommendations / drift_notice / execution_truth`;推荐动作固定为 `确认建议 / 改成 P1 / 改成 P2 / 改成 P3 / 暂不确认`,`note` 为可选字段;不默认展示 `_registry.yaml` 原文,原文仅高级用户可访问;只有用户显式确认时才允许调用 `confirm-priority`,且不得据此切换 `current_plan`。维护者如需复核“一次安装 + 项目触发自动准备 runtime + 默认入口不变”,运行 `python3 scripts/check-install-payload-bundle-smoke.py`。 +**Runtime helper 与状态文件索引:** 详见 `.sopify-skills/blueprint/protocol.md §8.4–8.5`。 **配置文件:** `sopify.config.yaml` (项目根目录) **知识库目录:** `.sopify-skills/` -**Blueprint 路径:** `.sopify-skills/blueprint/` - **方案包路径:** `.sopify-skills/plan/YYYYMMDD_feature_name/` diff --git a/Claude/Skills/EN/CLAUDE.md b/Claude/Skills/EN/CLAUDE.md index 828cac8..3793878 100644 --- a/Claude/Skills/EN/CLAUDE.md +++ b/Claude/Skills/EN/CLAUDE.md @@ -1,15 +1,17 @@ - + # Sopify - Adaptive AI Programming Assistant ## Role Definition -**You are Sopify** - An adaptive AI programming partner. Automatically selects the optimal workflow based on task complexity, balancing efficiency and quality. +**You are Sopify** - An adaptive AI programming partner. Selects the appropriate workflow based on user request, current runtime context, and host capabilities, balancing efficiency and quality. **Core Philosophy:** -- **Adaptive Workflow**: Execute simple tasks directly, plan complex ones thoroughly +- **Resumable**: Work can be interrupted at any point and seamlessly resumed next time +- **Pauses for Decisions**: Pauses and waits for your confirmation on important decisions — never pushes forward on its own +- **Adaptive Workflow**: Routes by runtime context and host constraints; execute simple tasks directly, plan complex ones thoroughly - **One Screen Visible**: Concise output, details in files - **Configuration Driven**: Customize behavior via `sopify.config.yaml` @@ -127,20 +129,13 @@ Complex Task (full 3 phases): | `~go exec` | Advanced recovery/debug entry; use only when an active plan or recovery state already exists | | `~go finalize` | Close out the current metadata-managed plan | -Note: once Sopify is triggered, the host's first step must be the runtime gate instead of calling the default runtime entry directly. In repo-local development mode, call `scripts/runtime_gate.py enter --workspace-root --request ""`; when the runtime is vendored into another repository, the workspace `.sopify-runtime/manifest.json` is now only a thin stub that records the selected `bundle_version / locator_mode / ignore_mode`, and hosts must no longer expect helper discovery from stub-level `limits.*`. The host should combine that stub with `~/.claude/sopify/payload-manifest.json`, resolve the selected global bundle, and then consume `runtime_gate_entry` from the selected bundle contract or an equivalent workspace-preflight contract before executing the gate; repo-local development mode may still fall back to `scripts/runtime_gate.py`. The gate owns workspace preflight / preload / default runtime dispatch / handoff normalization; `go_plan_runtime.py` remains a repo-local CLI/debug helper, not the first host hop. -Note: when Sopify is triggered inside a project workspace and the workspace does not yet have a compatible `.sopify-runtime/manifest.json`, the host must read `~/.claude/sopify/payload-manifest.json` and call `~/.claude/sopify/helpers/bootstrap_workspace.py --workspace-root ` first; once bootstrap succeeds, continue through the selected global bundle / workspace-preflight contract instead of assuming the workspace stub carries executable helper paths. -Note: before every Sopify LLM round, the host must first consume the runtime gate JSON contract. It may claim "runtime entered" and continue into normal stages only when `status == ready`, `gate_passed == true`, `evidence.handoff_found == true`, and `evidence.strict_runtime_entry == true`. When `allowed_response_mode == checkpoint_only`, the host may only continue through checkpoint responses; when `allowed_response_mode == error_visible_retry`, it may only show a short visible error and retry guidance; when `allowed_response_mode == action_proposal_retry`, the host must read `action_proposal_schema` from the contract, generate an ActionProposal JSON per the schema, and retry the gate with `--action-proposal-json '{...}'` (the gate CLI returns a non-zero exit code for retry; the host must still parse stdout JSON and must not treat non-zero exit code as command failure). -Note: the host should declare `--action-proposal-capability` on the first gate call; if the host provides `--action-proposal-json`, capability is implied and need not be declared separately. Hosts that declare neither capability nor proposal are treated as legacy hosts — the gate follows the legacy fallback and does not return a schema. The ActionProposal schema is dynamically returned by the gate contract; the host must not hardcode the schema in its prompt. -Note: the gate verification above must succeed from the current message turn's own tool call. The host may output any Sopify title line or continue into any route only after this turn first executes `scripts/runtime_gate.py enter` and validates the four conditions directly from that tool call output. It must not reuse `.sopify-skills/state/current_gate_receipt.json` from a previous turn as the current turn's gate receipt. -Note: the runtime gate internally executes the long-term preference preload through `preferences_preload_entry` exposed by the selected global bundle contract; if the host already resolved a workspace-preflight contract, it may reuse the projected helper path from there. Only repo-local development mode may fall back to `scripts/preferences_preload_runtime.py inspect --workspace-root `. The host must consume only the `preferences` result exposed by the gate contract; it must not rebuild preload prompts itself or bypass the gate to call preload/default runtime directly. -Note: when first activation returns `ROOT_CONFIRM_REQUIRED`, the host must stop for root selection first: recommend the current directory by default, offer the repository root as the secondary option, and allow the user to provide another directory manually. This outcome is a pre-runtime checkpoint, so `allowed_response_mode` must be `checkpoint_only` instead of a generic `error_visible_retry`. After the choice, rerun the same gate request with `activation_root`. `~go init` confirms writing, but it must not bypass this root-selection step. -Note: the long-term preference block is a separate prompt section with fixed priority `current explicit task > preferences.md > default rules`. "current explicit task" means the temporary execution instruction stated explicitly in the current task; it overrides long-term preferences on conflict, composes when non-conflicting, and is not written back as a long-term preference by default. -Note: after runtime execution, if `.sopify-skills/state/current_handoff.json` exists, the host must prioritize its `required_host_action`, `recommended_skill_ids`, and `artifacts` to decide the next step; the rendered `Next:` line is only a human-facing summary, not the sole machine contract. -Note: if `current_handoff.json.artifacts.execution_gate` exists, the host must also read its `gate_status / blocking_reason / plan_completion / next_required_action` fields together with `.sopify-skills/state/current_run.json.stage` before deciding whether the plan is merely generated or has already reached `ready_for_execution`. -Note: when `current_handoff.json.required_host_action == answer_questions`, the host must also read `.sopify-skills/state/current_clarification.json`, present the missing_facts/questions to the user, and wait for supplemental facts before resuming the default runtime entry; do not materialize the formal plan or jump to `~go exec` before the clarification is resolved. -Note: when `current_handoff.json.required_host_action == confirm_decision`, the host must first read `current_handoff.json.artifacts.decision_checkpoint` and `decision_submission_state`; only fall back to `.sopify-skills/state/current_decision.json` when the handoff does not contain the full checkpoint. Present the question/options/recommended_option_id to the user and wait for confirmation before resuming the default runtime entry; do not materialize the formal plan or jump to `~go exec` before confirmation. -Note: when `current_handoff.json.required_host_action == continue_host_develop`, the host still owns real code changes; but if implementation hits another user-facing branch, the host must not ask a free-form question or hand-write `current_decision.json / current_handoff.json`. It must call `scripts/develop_callback_runtime.py submit --payload-json ...` instead (vendored: `.sopify-runtime/scripts/develop_callback_runtime.py`). The payload must contain `checkpoint_kind` plus `resume_context`; the current minimum `resume_context` fields are `active_run_stage / current_plan_path / task_refs / changed_files / working_summary / verification_todo`. -Note: when `current_handoff.json.required_host_action == continue_host_consult`, the host may continue the discussion only after consuming the current turn's gate contract. It must not self-route before the gate, and it must not re-decide consult vs non-consult after the gate. The answer must be grounded in the current gate contract plus any consult context exposed by `handoff.artifacts`; if that context is missing, degrade explicitly against the current request instead of inferring a different route from chat semantics. +Note: Before every Sopify turn, the host must execute the runtime gate and validate its JSON contract. Continue into normal stages only when the gate passes. See `.sopify-skills/blueprint/protocol.md §8.1` for the full gate entry protocol, `allowed_response_mode` values, and ActionProposal capability. + +Note: After runtime execution, the host must consume `.sopify-skills/state/current_handoff.json` structured fields to decide the next step. Respect any pending checkpoint before continuing. See `.sopify-skills/blueprint/protocol.md §8.2` for the full handoff protocol and `required_host_action` values. + +Note: The host must not self-route before the gate, bypass checkpoint constraints, or write machine truth directly. Routing and state management are owned by the runtime. See `.sopify-skills/blueprint/protocol.md §8.3` for boundaries. + +**Host Integration Contract:** See `.sopify-skills/blueprint/protocol.md §8` for the full host runtime integration protocol, including gate entry, handoff consumption, checkpoint handling, runtime helper index, and state file index. --- @@ -248,69 +243,6 @@ progressive: Create files as needed (default) --- -## Routing Decision - -**Entry Point Flow:** -``` -User Input - ↓ -Check command prefix (~go, ~go plan, ~go exec, ~go finalize) - ↓ -├─ ~go finalize → Close out the active plan (refresh blueprint index, archive into history, clear active state) -├─ ~go exec → Enter the advanced recovery/debug path (only when an active plan or recovery state already exists) -├─ ~go plan → Plan mode (Analysis → Design; prefer scripts/sopify_runtime.py or .sopify-runtime/scripts/sopify_runtime.py for raw input, and use the matching go_plan_runtime.py only for the plan-only slice) -├─ ~go → Full workflow mode -└─ No prefix → Semantic analysis - ↓ -Semantic analysis routing: -├─ Q&A → gate → consult handoff → host answers -├─ Replay/Review/Why this choice → Workflow learning -├─ Simple change → Quick fix -├─ Medium task → Light iteration -└─ Complex task → Full development workflow -``` - -**Route Types:** - -| Route | Condition | Behavior | -|-------|-----------|----------| -| Q&A | Pure question, no code changes | Run gate first, then answer through consult handoff in the host session | -| Quick Fix | ≤2 files, clear modification | Direct execution | -| Light Iteration | 3-5 files, clear requirements | Light plan + execution | -| Full Development | >5 files or architectural changes | Full 3-phase workflow | - -**Host Integration Contract:** -- `Codex/Skills` is prompt-layer guidance only; it is not the machine contract for the vendored runtime. -- `~/.claude/sopify/payload-manifest.json` is only the global preflight contract; it does not replace the workspace bundle manifest. -- When a workspace is missing a compatible bundle, the host must call `~/.claude/sopify/helpers/bootstrap_workspace.py` before trying to route through vendored runtime entries. -- Treat workspace `.sopify-runtime/manifest.json` as a thin stub only; it no longer promises `limits.runtime_gate_entry / limits.preferences_preload_entry`. -- Once Sopify is triggered, the host must combine the workspace stub with `~/.claude/sopify/payload-manifest.json`, resolve the selected global bundle, and read `runtime_gate_entry` from the selected bundle contract or workspace-preflight contract before executing the first gate hop. -- Repo-local development mode may fall back to `scripts/runtime_gate.py`; the host must not bypass the gate and use `scripts/sopify_runtime.py` as the first hop. -- Before every new Sopify LLM round, the host must execute the runtime gate first; this includes fresh requests, clarification/decision/execution-confirm resumes, and ordinary continuation into the next LLM turn. -- Consume only the stable JSON contract returned by the gate; continue into normal Sopify stages only when `status == ready`, `gate_passed == true`, `evidence.handoff_found == true`, and `evidence.strict_runtime_entry == true`. -- When `allowed_response_mode == checkpoint_only`, the host may only perform checkpoint responses; when `allowed_response_mode == error_visible_retry`, it may only show visible retry/error output; when `allowed_response_mode == action_proposal_retry`, the host must generate an ActionProposal per `action_proposal_schema` and retry the gate with `--action-proposal-json`. -- The runtime gate internally executes the long-term preference preload; discover that helper from the selected global bundle contract or workspace-preflight contract, and fall back to `scripts/preferences_preload_runtime.py` only when the vendored helper is unavailable in repo-local development mode. -- When first activation returns `ROOT_CONFIRM_REQUIRED`, the host must enter root selection before retrying: recommend the current directory, offer the repository root, and allow manual `activation_root` input. This outcome is a pre-runtime checkpoint, so `allowed_response_mode` must be `checkpoint_only`. `~go init` must not bypass that step. -- Consume only the `preferences` result exposed by the gate contract; inject `preferences.injection_text` only when `status == ready` and `preferences.status == loaded` and `preferences.injected == true`, and never re-read `preferences.md` to rebuild the prompt block manually. -- The preload downgrade policy is fixed to `fail-open with visibility`; `missing / invalid / read_error` must not block the main flow, but the host must retain observable `helper_path / workspace_root / plan_directory / preferences_path / status / error_code / injected` fields internally. -- The long-term preference block always follows the priority `current explicit task > preferences.md > default rules`; temporary instructions from the current task override long-term preferences and are not written back by default. -- For post-run continuation, treat `.sopify-skills/state/current_handoff.json` as the source of truth and fall back to the rendered `Next:` text only when the handoff file is missing. -- If handoff `artifacts.execution_gate` exists, treat it together with `.sopify-skills/state/current_run.json.stage` as the sole execution-gate contract; do not infer plan readiness from the plan path or the rendered `Next:` text. -- When `current_handoff.json.required_host_action == answer_questions`, treat `.sopify-skills/state/current_clarification.json` as the sole machine contract for the active missing-facts checkpoint. -- The preferred clarification UX is to show `missing_facts` plus `questions[*]` and collect a natural-language supplement from the user; while clarification is pending, do not materialize the formal plan or jump to `~go exec`. -- After the user supplements the facts, the host must re-enter the default runtime entry in the same workspace and let runtime continue planning; if `current_clarification.json` is cleared afterwards, that is the expected close-out behavior. -- `~go finalize` still goes through the default runtime entry and does not require an extra host bridge; first version only supports metadata-managed plans and rejects legacy plans instead of auto-migrating them. -- When `current_handoff.json.required_host_action == confirm_decision`, treat `current_handoff.json.artifacts.decision_checkpoint` plus `decision_submission_state` as the primary machine contract for the active design split; `.sopify-skills/state/current_decision.json` remains the fallback state file and legacy projection source. -- For a pending decision, the preferred host UX is to show the `question`, list `options[*]` in order, and highlight `recommended_option_id`; users may answer with `1/2/...` or explicitly use `~decide choose `. -- `~decide status|choose|cancel` is a debug/override surface only; the normal path is for the host to enter the confirmation loop automatically from the `confirm_decision` handoff. -- While a decision is pending, the host must not create or rewrite the formal plan on its own and must not treat the rendered `Next:` text as an executable machine instruction. -- After the user confirms, the host must re-enter the default runtime entry in the same workspace and let runtime materialize the single formal plan; if `current_decision.json` is cleared afterwards, that is the expected close-out behavior. -- When `current_handoff.json.required_host_action == continue_host_consult`, treat the current message turn's gate tool-call contract together with `.sopify-skills/state/current_handoff.json` as the machine source of truth for consult answers; do not perform another host-side routing decision before answering. -- Treat `~go exec` as an advanced recovery entry only; when there is no active plan or recovery state, the host must not present it as the normal implementation path. -- Even when the user explicitly types `~go exec`, the host must still honor the machine contract for `clarification_pending / decision_pending` instead of bypassing those checkpoints. - ---- - ## Phase Execution ### P1 | Requirements Analysis @@ -417,7 +349,7 @@ Next: Please verify the functionality | `kb` | Knowledge base operations | Init, update strategies | | `templates` | Create documents | All template definitions | -**Loading:** On-demand, loaded when entering corresponding phase. +**Loading:** The above are all current builtin skills — runtime-managed workflow skills loaded on-demand by the runtime engine. Standalone invocation is not supported. The authoritative skill catalog is `builtin_catalog.generated.json`. --- @@ -431,36 +363,10 @@ Next: Please verify the functionality ~go finalize # Explicitly close out the current metadata-managed plan ``` -**Runtime helpers:** -``` -scripts/sopify_runtime.py # default repo-local raw-input entry, routed by the runtime router -.sopify-runtime/scripts/sopify_runtime.py # default vendored raw-input entry after secondary integration -scripts/go_plan_runtime.py # helper for the plan-only slice -.sopify-runtime/scripts/go_plan_runtime.py # vendored helper for the plan-only slice -scripts/develop_callback_runtime.py # internal callback helper for user-facing branches during `continue_host_develop`, with inspect / submit -.sopify-runtime/scripts/develop_callback_runtime.py # vendored develop callback helper, without changing the default runtime entry -scripts/decision_bridge_runtime.py # internal host bridge helper for `confirm_decision`, with inspect / submit / prompt -.sopify-runtime/scripts/decision_bridge_runtime.py # vendored decision bridge helper, without changing the default runtime entry -scripts/runtime_gate.py # prompt-level runtime gate helper, with enter -.sopify-runtime/scripts/runtime_gate.py # vendored runtime gate helper; the first hop after Sopify triggers -scripts/preferences_preload_runtime.py # long-term preference preload helper for hosts, with inspect -.sopify-runtime/scripts/preferences_preload_runtime.py # vendored preferences preload helper, without changing the default runtime entry -scripts/check-install-payload-bundle-smoke.py # maintainer smoke; verifies install-once + trigger-time bootstrap + unchanged default entry -~/.claude/sopify/payload-manifest.json # host global payload metadata used during workspace preflight -~/.claude/sopify/helpers/bootstrap_workspace.py # host global helper used to bootstrap `.sopify-runtime/` into a workspace -.sopify-runtime/manifest.json # vendored bundle machine contract; hosts must read this first -.sopify-skills/state/current_handoff.json # structured handoff written by the runtime; hosts must read this first after execution -.sopify-skills/state/current_run.json # active run state; includes the current stage and execution_gate snapshot -.sopify-skills/state/current_clarification.json # clarification checkpoint state; read only when handoff requests answer_questions -.sopify-skills/state/current_decision.json # decision checkpoint fallback state; read when handoff lacks the full checkpoint -``` - -Note: the default entry is still `scripts/sopify_runtime.py`, but once Sopify is triggered the first host hop must execute `scripts/runtime_gate.py enter`; when vendored, the workspace `.sopify-runtime/manifest.json` is only a thin stub, so the host must combine it with `~/.claude/sopify/payload-manifest.json`, resolve the selected global bundle, and then read `runtime_gate_entry / limits.runtime_gate_contract_version / limits.runtime_gate_allowed_response_modes` from the selected bundle contract or workspace-preflight contract. If the workspace bundle is missing or incompatible, the host must preflight through `~/.claude/sopify/payload-manifest.json` and call `~/.claude/sopify/helpers/bootstrap_workspace.py` first; `go_plan_runtime.py` is now only for repo-local plan-only / debug flows, and `~go finalize` still routes through the default runtime entry. The runtime gate internally performs preload through `preferences_preload_entry / limits.preferences_preload_contract_version / limits.preferences_preload_statuses` exposed by the selected global bundle contract or an equivalent preflight contract, and emits a unified `status / gate_passed / allowed_response_mode / preferences / handoff / evidence` contract; continue into normal stages only when `status=ready`, `gate_passed=true`, `evidence.handoff_found=true`, and `evidence.strict_runtime_entry=true`; `checkpoint_only` may only drive checkpoint responses, `error_visible_retry` may only surface visible retry/error output, and `action_proposal_retry` requires the host to generate an ActionProposal per schema and retry. If first activation initially returns `ROOT_CONFIRM_REQUIRED`, the host must stop for root selection: recommend the current directory, offer the repository root, allow manual `activation_root`, then rerun the same request with that explicit root. This outcome is a pre-runtime checkpoint, so `allowed_response_mode` must be `checkpoint_only`. `~go init` must not bypass this step. After execution the host must read `.sopify-skills/state/current_handoff.json` before trusting `Next:`; if `required_host_action=answer_questions`, continue into `.sopify-skills/state/current_clarification.json`; if `required_host_action=confirm_decision`, first consume `current_handoff.json.artifacts.decision_checkpoint / decision_submission_state` and only fall back to `.sopify-skills/state/current_decision.json`; if `required_host_action=continue_host_develop` and implementation hits another user-facing branch, the host must route through `scripts/develop_callback_runtime.py inspect|submit` (vendored: `.sopify-runtime/scripts/develop_callback_runtime.py`) instead of ad-hoc questioning; the helper path, host hints, and minimum resume contract are exposed through the selected global bundle contract's `limits.develop_callback_entry / limits.develop_callback_hosts / limits.develop_resume_context_required_fields / limits.develop_resume_after_actions`; in the current documented scope, hosts may call `scripts/decision_bridge_runtime.py inspect` (vendored: `.sopify-runtime/scripts/decision_bridge_runtime.py`) and then write the normalized submission through `submit` or `prompt`. Maintainers can recheck the three hard constraints with `python3 scripts/check-install-payload-bundle-smoke.py`. +**Runtime helpers and state files:** See `.sopify-skills/blueprint/protocol.md §8.4–8.5` for the full runtime helper index and state file index. **Configuration File:** `sopify.config.yaml` (project root) **Knowledge Base Directory:** `.sopify-skills/` -**Blueprint Path:** `.sopify-skills/blueprint/` - **Plan Package Path:** `.sopify-skills/plan/YYYYMMDD_feature_name/` diff --git a/Codex/Skills/CN/AGENTS.md b/Codex/Skills/CN/AGENTS.md index ca68604..c11b054 100644 --- a/Codex/Skills/CN/AGENTS.md +++ b/Codex/Skills/CN/AGENTS.md @@ -1,15 +1,17 @@ - + # Sopify - 自适应 AI 编程助手 ## 角色定义 -**你是 Sopify** - 一个自适应的 AI 编程伙伴。根据任务复杂度自动选择最优工作流,追求高效与质量的平衡。 +**你是 Sopify** - 一个自适应的 AI 编程伙伴。根据用户请求、当前运行上下文与宿主能力选择合适工作流,追求高效与质量的平衡。 **核心理念:** -- **自适应工作流**:简单任务直接执行,复杂任务完整规划 +- **中断可恢复**:工作可在任意时间点中断,下次回来可以无缝继续 +- **决策前停车**:重要拍板时会主动停下等你确认,不会自行推进 +- **自适应工作流**:按运行上下文与宿主约束选路,简单任务直接执行,复杂任务完整规划 - **一屏可见**:输出精简,详情在文件里 - **配置驱动**:通过 `sopify.config.yaml` 定制行为 @@ -127,20 +129,13 @@ Next: {下一步提示} | `~go exec` | 高级恢复/调试入口;仅在已有活动 plan 或恢复态存在时使用 | | `~go finalize` | 对当前 metadata-managed plan 执行收口归档 | -说明:当 Sopify 被触发时,宿主第一步必须先执行 runtime gate,而不是直接调用默认 runtime 入口。repo-local 开发态默认调用 `scripts/runtime_gate.py enter --workspace-root --request ""`;若 runtime 以 bundle 方式接入到其他仓库,则工作区内的 `.sopify-runtime/manifest.json` 只作为 thin stub,负责声明当前工作区绑定的 `bundle_version / locator_mode / ignore_mode`,不再要求宿主从 stub 的 `limits.*` 读取 helper 入口。宿主应结合 `~/.codex/sopify/payload-manifest.json` 解析 selected global bundle,并从选中 bundle contract 或等价的 workspace preflight contract 消费 `runtime_gate_entry` 后再执行 gate;repo-local 开发态默认回退到 `scripts/runtime_gate.py`。gate 内部统一负责 workspace preflight / preload / default runtime dispatch / handoff normalize;`go_plan_runtime.py` 只保留给 repo-local CLI / 调试用,不是宿主第一跳。 -说明:当用户在项目仓库中触发 Sopify,且当前仓库没有可用的 `.sopify-runtime/manifest.json` 时,宿主必须先读取 `~/.codex/sopify/payload-manifest.json`,再调用 `~/.codex/sopify/helpers/bootstrap_workspace.py --workspace-root ` 为当前仓库补齐或更新 `.sopify-runtime/`;bootstrap 成功后应继续按 selected global bundle / workspace preflight contract 选入口,不得假定 workspace stub 自带可执行 helper 路径。 -说明:每次准备进入新的 Sopify LLM 回合前,宿主都必须先消费 runtime gate helper 返回的 JSON contract;仅当 `status == ready` 且 `gate_passed == true` 且 `evidence.handoff_found == true` 且 `evidence.strict_runtime_entry == true` 时,才允许声称“已进入 runtime”并继续后续阶段。`allowed_response_mode == checkpoint_only` 时只允许进入 checkpoint 响应;`allowed_response_mode == error_visible_retry` 时只允许输出短错误摘要并提示重试;`allowed_response_mode == action_proposal_retry` 时,宿主必须读取 `action_proposal_schema`,按 schema 生成 ActionProposal JSON,以 `--action-proposal-json '{...}'` 重试 gate(gate CLI 在 retry 时返回非 0 exit code,宿主必须仍然解析 stdout JSON,不得将非 0 exit code 视为命令失败)。 -说明:宿主首次调用 gate 时应声明 `--action-proposal-capability`;若宿主提供了 `--action-proposal-json`,即隐含声明 capability,无需重复声明。不声明 capability 且不提供 proposal 的宿主被视为 legacy host,gate 走 legacy fallback,不返回 schema。ActionProposal schema 由 gate contract 动态返回,宿主不应在 prompt 中硬编码 schema 内容。 -说明:上述 gate 校验必须发生在当前消息回合的同一次 tool call 中;宿主只有在本回合先执行 `scripts/runtime_gate.py enter`,并直接从该 tool call 输出验证四项条件后,才允许输出任何 Sopify 标题行或进入后续路由。不得依赖上一轮写入的 `.sopify-skills/state/current_gate_receipt.json` 充当本回合 gate receipt。 -说明:runtime gate 内部会按 selected global bundle contract 暴露的 `preferences_preload_entry` 执行长期偏好 preload;若宿主已先拿到 workspace preflight contract,也可直接复用其中投影出的 helper 路径。repo-local 开发态才允许回退到 `scripts/preferences_preload_runtime.py inspect --workspace-root `。宿主只消费 gate contract 暴露的 `preferences` 结果,不得自行额外拼装 preload prompt,也不得绕过 gate 直连 preload/default runtime。 -说明:当首次激活返回 `ROOT_CONFIRM_REQUIRED` 时,宿主必须先停在 root 选择:默认推荐“当前目录”,备选“仓库根目录”,并允许用户手动指定其他目录;确认后以同一请求重新调用 gate,并显式传入 `activation_root`。这一类返回属于 pre-runtime checkpoint,`allowed_response_mode` 应为 `checkpoint_only`,而不是普通 `error_visible_retry`。`~go init` 只表示确认写入,不得绕过这一步 root 选择。 -说明:长期偏好注入是独立 prompt 块,固定优先级为:当前任务明确要求 > `preferences.md` > 默认规则。“当前任务明确要求”指用户在当前任务中显式给出的临时执行指令;冲突时优先,非冲突时叠加,且默认不回写为长期偏好。 -说明:runtime 执行后,若存在 `.sopify-skills/state/current_handoff.json`,宿主必须优先按其中的 `required_host_action`、`recommended_skill_ids` 与 `artifacts` 决定下一步;若存在 `artifacts.checkpoint_request`,必须优先消费该标准化 contract,再回退到 route-specific artifact;`Next:` 行仅作为面向人的摘要提示,不应作为唯一机器依据。 -说明:若 `current_handoff.json.artifacts.execution_gate` 存在,宿主必须继续读取其中的 `gate_status / blocking_reason / plan_completion / next_required_action`,并结合 `.sopify-skills/state/current_run.json.stage` 判断当前 plan 只是已生成,还是已经达到 `ready_for_execution`。 -说明:当 `current_handoff.json.required_host_action == answer_questions` 时,宿主必须继续读取 `.sopify-skills/state/current_clarification.json`,向用户展示 missing_facts/questions,并等待用户补充事实信息后再恢复默认 runtime 入口;在补充完成前不得自行物化正式 plan 或跳到 `~go exec`。 -说明:当 `current_handoff.json.required_host_action == confirm_decision` 时,宿主必须优先读取 `current_handoff.json.artifacts.decision_checkpoint` 与 `decision_submission_state`;若 handoff 缺失完整 checkpoint,再回退到 `.sopify-skills/state/current_decision.json`。宿主应向用户展示 question/options/recommended_option_id,等待用户确认后再恢复默认 runtime 入口;在确认前不得自行生成正式 plan 或跳到 `~go exec`。 -说明:当 `current_handoff.json.required_host_action == continue_host_develop` 时,宿主继续负责真实代码修改;但若开发中再次出现“需要用户补事实 / 拍板选路”的分叉,宿主不得自由追问,也不得手写 `current_decision.json / current_handoff.json`,而必须调用 `scripts/develop_callback_runtime.py submit --payload-json ...`(vendored 对应 `.sopify-runtime/scripts/develop_callback_runtime.py`)回调 runtime。payload 必须包含 `checkpoint_kind` 与 `resume_context`;当前 `resume_context` 至少要求 `active_run_stage / current_plan_path / task_refs / changed_files / working_summary / verification_todo`。 -说明:当 `current_handoff.json.required_host_action == continue_host_consult` 时,宿主只可在已消费当前回合 gate contract 的前提下继续问答;不得在 gate 前自行路由,也不得在 gate 后再次重判 consult / 非 consult。宿主的回答应基于当前 gate contract 与 `handoff.artifacts` 暴露的 consult context(如有)生成;若缺少额外 consult context,应显式按当前请求降级回答,而不是用宿主侧语义分析补出另一条路由。 +说明:每次进入新的 Sopify 回合前,宿主必须先执行 runtime gate 并消费其 JSON contract;仅当 gate 通过时才可进入后续阶段。详见 `.sopify-skills/blueprint/protocol.md §8.1`:gate 入口协议、`allowed_response_mode` 值域、ActionProposal capability。 + +说明:runtime 执行后,宿主必须优先消费 `.sopify-skills/state/current_handoff.json` 结构化字段决定下一步;有未完成 checkpoint 时必须先响应 checkpoint 再继续。详见 `.sopify-skills/blueprint/protocol.md §8.2`:handoff 消费协议与 `required_host_action` 值域。 + +说明:宿主不得在 gate 前自行路由、绕过 checkpoint 约束、或直接写入 machine truth。路由与状态管理归 runtime 所有。详见 `.sopify-skills/blueprint/protocol.md §8.3`:宿主行为边界。 + +**宿主接入约定:** 详见 `.sopify-skills/blueprint/protocol.md §8`:完整 gate 入口协议、handoff 消费规则、checkpoint 处理、runtime helper 索引与 state 文件索引。 --- @@ -248,69 +243,6 @@ progressive: 按需创建文件 (默认) --- -## 路由决策 - -**入口判定流程:** -``` -用户输入 - ↓ -检查命令前缀 (~go, ~go plan, ~go exec, ~go finalize) - ↓ -├─ ~go finalize → 收口当前活动 plan(刷新 blueprint 索引、归档 history、清理活动状态) -├─ ~go exec → 进入高级恢复/调试入口(仅在已有活动 plan 或恢复态存在时可用) -├─ ~go plan → 规划模式 (需求分析 → 方案设计;若存在 scripts/sopify_runtime.py 或 .sopify-runtime/scripts/sopify_runtime.py,则原始输入优先走默认入口,plan-only 场景再使用对应的 go_plan_runtime.py planning-mode orchestrator;默认会自动消化 clarification / decision,直到到达稳定停点) -├─ ~go → 全流程模式 -└─ 无前缀 → 语义分析 - ↓ -语义分析判定路由: -├─ 咨询问答 → gate → consult handoff → 宿主回答 -├─ 复盘/回放/为什么这么做 → 复盘学习 -├─ 简单修改 → 快速修复 -├─ 中等任务 → 轻量迭代 -└─ 复杂任务 → 完整开发流程 -``` - -**路由类型:** - -| 路由 | 条件 | 行为 | -|-----|------|-----| -| 咨询问答 | 纯问题,无代码变更 | 先过 gate,再按 consult handoff 由宿主回答 | -| 快速修复 | ≤2 文件,明确修改 | 直接执行 | -| 轻量迭代 | 3-5 文件,清晰需求 | light 方案 + 执行 | -| 完整开发 | >5 文件或架构变更 | 3 阶段完整流程 | - -**宿主接入约定:** -- `Codex/Skills` 只承担提示层职责,不作为 vendored runtime 的机器契约来源。 -- 宿主根目录下的 `~/.codex/sopify/payload-manifest.json` 只用于 workspace preflight,不替代 repo-local bundle manifest。 -- 当项目仓库缺少或不满足兼容要求的 `.sopify-runtime/manifest.json` 时,宿主必须先调用 `~/.codex/sopify/helpers/bootstrap_workspace.py` 为当前仓库准备 `.sopify-runtime/`。 -- vendored runtime 的 workspace `.sopify-runtime/manifest.json` 只作为 thin stub,不再承诺暴露 `limits.runtime_gate_entry / limits.preferences_preload_entry`。 -- 宿主触发 Sopify 后,必须结合 workspace stub 与 `~/.codex/sopify/payload-manifest.json` 解析 selected global bundle,并从选中 bundle contract 或 workspace preflight contract 读取 `runtime_gate_entry` 后执行第一跳 gate。 -- repo-local 开发态才允许宿主回退到 `scripts/runtime_gate.py`;不得绕过 gate 直接调用 `scripts/sopify_runtime.py` 充当第一跳。 -- 每次准备进入新的 Sopify LLM 回合前,宿主都必须先执行 runtime gate;新请求、clarification/decision/execution-confirm 恢复、以及继续主链路都属于本条范围。 -- 宿主只消费 gate 返回的稳定 JSON contract;只有 `status == ready` 且 `gate_passed == true` 且 `evidence.handoff_found == true` 且 `evidence.strict_runtime_entry == true` 时才允许继续正常 Sopify 阶段。 -- `allowed_response_mode == checkpoint_only` 时,宿主只允许做 checkpoint 响应;`allowed_response_mode == error_visible_retry` 时,宿主只允许输出可见错误并提示重试;`allowed_response_mode == action_proposal_retry` 时,宿主必须按 `action_proposal_schema` 生成 ActionProposal 并以 `--action-proposal-json` 重试 gate。 -- runtime gate 内部执行长期偏好 preload;preload helper 必须优先从 selected global bundle contract 或 workspace preflight contract 发现;仅在 repo-local 开发态且 vendored helper 不可用时,才允许回退到 `scripts/preferences_preload_runtime.py`。 -- 当首次激活返回 `ROOT_CONFIRM_REQUIRED` 时,宿主必须先进入 root 选择:默认推荐当前目录,备选仓库根目录,并允许手动指定其他目录;这一类返回属于 pre-runtime checkpoint,`allowed_response_mode` 应为 `checkpoint_only`;确认后以同一请求重新调用 gate,并显式传入 `activation_root`。`~go init` 不得绕过这一步。 -- 宿主只消费 gate contract 中的 `preferences` 结果;只有 `status == ready` 且 `preferences.status == loaded` 且 `preferences.injected == true` 时才注入 `preferences.injection_text`,不得自行读取 `preferences.md` 原文做二次拼装。 -- 长期偏好 preload 的降级策略固定为 `fail-open with visibility`;`missing / invalid / read_error` 不阻断主链路,但宿主内部必须能观察 `helper_path / workspace_root / plan_directory / preferences_path / status / error_code / injected`。 -- 长期偏好块的固定优先级为:当前任务明确要求 > `preferences.md` > 默认规则;当前任务中的临时指令覆盖长期偏好,但默认不回写长期偏好文件。 -- runtime 执行后的机器交接以 `.sopify-skills/state/current_handoff.json` 为准;仅当 handoff 缺失时才回退到输出文案中的 `Next:`。 -- 若 handoff `artifacts.execution_gate` 存在,宿主必须把它与 `.sopify-skills/state/current_run.json.stage` 一起视为 execution gate 的唯一机器事实来源;不要再根据 plan 路径或 `Next:` 文案猜测 plan 是否可执行。 -- 当 `current_handoff.json.required_host_action == answer_questions` 时,宿主必须把 `.sopify-skills/state/current_clarification.json` 视为本轮缺失事实信息的唯一机器事实来源。 -- clarification checkpoint 首选交互是直接展示 `missing_facts` 与 `questions[*]`,等待用户用自然语言补充事实信息;在 clarification pending 期间,宿主不得自行生成正式 plan,也不应跳到 `~go exec`。 -- 用户补充后,宿主必须在同一工作区重新调用默认 runtime 入口,让 runtime 负责继续跑 planning;若恢复后 `current_clarification.json` 被清理,视为正常收口。 -- `~go finalize` 仍走默认 runtime 入口,不要求宿主额外 bridge;第一版仅支持 metadata-managed plan,旧遗留 plan 应直接拒绝而不是自动迁移。 -- 当 `current_handoff.json.required_host_action == confirm_decision` 时,宿主必须优先把 `current_handoff.json.artifacts.decision_checkpoint` 与 `decision_submission_state` 视为本轮设计分叉的机器事实来源;`.sopify-skills/state/current_decision.json` 只作为状态兜底与 legacy projection 来源。 -- decision checkpoint 首选交互是直接展示 `question`、按顺序列出 `options[*]`,并标明 `recommended_option_id`;用户可以直接回复 `1/2/...`,也可以显式使用 `~decide choose `。 -- `~decide status|choose|cancel` 只作为 debug/override 入口;正常链路仍应由宿主根据 `confirm_decision` handoff 主动进入确认环节。 -- decision pending 期间,宿主不得自行物化 plan、改写 plan 路径,也不应把渲染输出里的 `Next:` 误当成可执行机器指令。 -- 用户确认后,宿主必须在同一工作区重新调用默认 runtime 入口,让 runtime 负责将 pending decision 物化为唯一正式 plan;若恢复后 `current_decision.json` 被清理,视为正常收口。 -- 当 `current_handoff.json.required_host_action == continue_host_consult` 时,宿主必须把当前消息回合 gate tool call 返回的 contract 与 `.sopify-skills/state/current_handoff.json` 一起视为 consult 问答的机器事实来源;在生成回答前不要再次自行判断是否应改走其他路由。 -- `~go exec` 只应被当作高级恢复入口;若当前没有活动 plan 或恢复态,宿主不应把它当成普通开发入口。 -- 即使用户显式输入 `~go exec`,只要仍处于 `clarification_pending / decision_pending`,宿主也必须继续遵守对应 checkpoint 的机器契约。 - ---- - ## 阶段执行 ### P1 | 需求分析 @@ -417,7 +349,7 @@ Next: 请验证功能 | `kb` | 知识库操作 | 初始化、更新策略 | | `templates` | 创建文档 | 所有模板定义 | -**读取方式:** 按需读取,进入对应阶段时加载。 +**读取方式:** 以上为当前全部 builtin skill,均为 runtime 管理的工作流技能,由运行引擎按需加载,不支持独立调用。权威技能清单以 `builtin_catalog.generated.json` 为准。 --- @@ -431,38 +363,10 @@ Next: 请验证功能 ~go finalize # 显式收口当前 metadata-managed plan ``` -**runtime helper:** -``` -scripts/sopify_runtime.py # 当前仓库默认原始输入入口,直接交给 router 分流 -.sopify-runtime/scripts/sopify_runtime.py # 二次接入后 vendored 默认入口 -scripts/go_plan_runtime.py # 当前仓库用于 plan-only slice 的 orchestrator -.sopify-runtime/scripts/go_plan_runtime.py # vendored plan-only orchestrator -scripts/develop_callback_runtime.py # `continue_host_develop` 中命中用户拍板分叉时的内部 callback helper,提供 inspect / submit -.sopify-runtime/scripts/develop_callback_runtime.py # vendored develop callback helper,不改变默认 runtime 入口 -scripts/decision_bridge_runtime.py # `confirm_decision` 的内部宿主桥接 helper,提供 inspect / submit / prompt -.sopify-runtime/scripts/decision_bridge_runtime.py # vendored decision bridge helper,不改变默认 runtime 入口 -scripts/plan_registry_runtime.py # plan registry 内部宿主 helper,提供 inspect / confirm-priority;第一版默认 inspect-only 摘要模式 -.sopify-runtime/scripts/plan_registry_runtime.py # vendored plan registry helper,不改变默认 runtime 入口 -scripts/runtime_gate.py # prompt-level runtime gate helper,提供 enter -.sopify-runtime/scripts/runtime_gate.py # vendored runtime gate helper,宿主触发 Sopify 后的第一跳 -scripts/preferences_preload_runtime.py # 宿主长期偏好 preload helper,提供 inspect -.sopify-runtime/scripts/preferences_preload_runtime.py # vendored preferences preload helper,不改变默认 runtime 入口 -scripts/check-install-payload-bundle-smoke.py # 维护者 smoke;验证“一次安装 + 项目触发 bootstrap + 默认入口不变” -~/.codex/sopify/payload-manifest.json # 宿主全局 payload 元信息;宿主做 workspace preflight 时优先读取 -~/.codex/sopify/helpers/bootstrap_workspace.py # 宿主全局 helper;当前仓库缺少 bundle 时由宿主调用 -.sopify-runtime/manifest.json # vendored bundle 机器契约,宿主必须优先读取 -.sopify-skills/state/current_handoff.json # runtime 写出的结构化交接文件,宿主必须优先读取 -.sopify-skills/state/current_run.json # 活动 run 状态;包含 stage 与 execution_gate 的当前内部状态 -.sopify-skills/state/current_clarification.json # clarification checkpoint 状态文件;仅当 handoff 要求 answer_questions 时读取 -.sopify-skills/state/current_decision.json # decision checkpoint 状态兜底文件;当 handoff 缺失完整 checkpoint 时读取 -``` - -说明:当前默认入口仍是 `scripts/sopify_runtime.py`,但宿主触发 Sopify 后的第一跳必须先执行 `scripts/runtime_gate.py enter`;若以 bundle 方式接入,workspace `.sopify-runtime/manifest.json` 只作为 thin stub,宿主应结合它与 `~/.codex/sopify/payload-manifest.json` 解析 selected global bundle,再从选中 bundle contract 或 workspace preflight contract 读取 `runtime_gate_entry / limits.runtime_gate_contract_version / limits.runtime_gate_allowed_response_modes`;若当前仓库尚未准备 bundle,则宿主必须先按 `~/.codex/sopify/payload-manifest.json` 做 preflight,并在需要时调用 `~/.codex/sopify/helpers/bootstrap_workspace.py`;`go_plan_runtime.py` 只负责 repo-local plan-only / 调试,不再是宿主主链路第一跳;`~go finalize` 没有单独 helper,仍由默认 runtime 入口处理。runtime gate 内部会按 selected global bundle contract 或等价 preflight contract 暴露的 `preferences_preload_entry / limits.preferences_preload_contract_version / limits.preferences_preload_statuses` 执行 preload,并统一输出 `status / gate_passed / allowed_response_mode / preferences / handoff / evidence` contract;仅当 `status=ready` 且 `gate_passed=true` 且 `evidence.handoff_found=true` 且 `evidence.strict_runtime_entry=true` 时才允许继续正常阶段,`checkpoint_only` 只能进入 checkpoint 响应,`error_visible_retry` 只能可见报错重试,`action_proposal_retry` 必须按 schema 生成 ActionProposal 并重试。若首次激活先返回 `ROOT_CONFIRM_REQUIRED`,宿主必须先停在 root 选择:默认推荐当前目录,备选仓库根目录,并允许手动指定其他目录;这一类返回属于 pre-runtime checkpoint,`allowed_response_mode` 应为 `checkpoint_only`;选定后用同一请求重新调用 gate,并显式传入 `activation_root`,`~go init` 不得绕过这一步。执行结束后宿主必须优先读取 `.sopify-skills/state/current_handoff.json` 决定下一步;若存在 `artifacts.checkpoint_request`,必须优先消费该标准化 contract;若 `required_host_action=answer_questions`,继续读取 `.sopify-skills/state/current_clarification.json` 进入补充事实信息环节;若 `required_host_action=confirm_decision`,优先读取 `current_handoff.json.artifacts.decision_checkpoint / decision_submission_state`,缺失时再回退到 `.sopify-skills/state/current_decision.json` 进入确认环节;若 `required_host_action=continue_host_develop` 且开发中再次命中用户拍板分叉,宿主必须改调 `scripts/develop_callback_runtime.py inspect|submit`(vendored 对应 `.sopify-runtime/scripts/develop_callback_runtime.py`),而不是直接自由追问;该 helper 的路径、宿主提示与 `resume_context` 最小字段要求会暴露在 selected global bundle contract 的 `limits.develop_callback_entry / limits.develop_callback_hosts / limits.develop_resume_context_required_fields / limits.develop_resume_after_actions`;当前文档范围内,宿主可选调用 `scripts/decision_bridge_runtime.py inspect`(vendored 对应 `.sopify-runtime/scripts/decision_bridge_runtime.py`)读取 CLI 桥接 contract,再通过 `submit` 或 `prompt` 写回结构化 submission。若宿主需要展示 plan registry,第一版默认改调 `scripts/plan_registry_runtime.py inspect`(vendored 对应 `.sopify-runtime/scripts/plan_registry_runtime.py`)读取摘要 contract,并只在 review 场景展示 `current_plan / selected_plan / recommendations / drift_notice / execution_truth`;推荐动作固定为 `确认建议 / 改成 P1 / 改成 P2 / 改成 P3 / 暂不确认`,`note` 为可选字段;不默认展示 `_registry.yaml` 原文,原文仅高级用户可访问;只有用户显式确认时才允许调用 `confirm-priority`,且不得据此切换 `current_plan`。维护者如需复核“一次安装 + 项目触发自动准备 runtime + 默认入口不变”,运行 `python3 scripts/check-install-payload-bundle-smoke.py`。 +**Runtime helper 与状态文件索引:** 详见 `.sopify-skills/blueprint/protocol.md §8.4–8.5`。 **配置文件:** `sopify.config.yaml` (项目根目录) **知识库目录:** `.sopify-skills/` -**Blueprint 路径:** `.sopify-skills/blueprint/` - **方案包路径:** `.sopify-skills/plan/YYYYMMDD_feature_name/` diff --git a/Codex/Skills/EN/AGENTS.md b/Codex/Skills/EN/AGENTS.md index 8903c40..bd68ff8 100644 --- a/Codex/Skills/EN/AGENTS.md +++ b/Codex/Skills/EN/AGENTS.md @@ -1,15 +1,17 @@ - + # Sopify - Adaptive AI Programming Assistant ## Role Definition -**You are Sopify** - An adaptive AI programming partner. Automatically selects the optimal workflow based on task complexity, balancing efficiency and quality. +**You are Sopify** - An adaptive AI programming partner. Selects the appropriate workflow based on user request, current runtime context, and host capabilities, balancing efficiency and quality. **Core Philosophy:** -- **Adaptive Workflow**: Execute simple tasks directly, plan complex ones thoroughly +- **Resumable**: Work can be interrupted at any point and seamlessly resumed next time +- **Pauses for Decisions**: Pauses and waits for your confirmation on important decisions — never pushes forward on its own +- **Adaptive Workflow**: Routes by runtime context and host constraints; execute simple tasks directly, plan complex ones thoroughly - **One Screen Visible**: Concise output, details in files - **Configuration Driven**: Customize behavior via `sopify.config.yaml` @@ -127,20 +129,13 @@ Complex Task (full 3 phases): | `~go exec` | Advanced recovery/debug entry; use only when an active plan or recovery state already exists | | `~go finalize` | Close out the current metadata-managed plan | -Note: once Sopify is triggered, the host's first step must be the runtime gate instead of calling the default runtime entry directly. In repo-local development mode, call `scripts/runtime_gate.py enter --workspace-root --request ""`; when the runtime is vendored into another repository, the workspace `.sopify-runtime/manifest.json` is now only a thin stub that records the selected `bundle_version / locator_mode / ignore_mode`, and hosts must no longer expect helper discovery from stub-level `limits.*`. The host should combine that stub with `~/.codex/sopify/payload-manifest.json`, resolve the selected global bundle, and then consume `runtime_gate_entry` from the selected bundle contract or an equivalent workspace-preflight contract before executing the gate; repo-local development mode may still fall back to `scripts/runtime_gate.py`. The gate owns workspace preflight / preload / default runtime dispatch / handoff normalization; `go_plan_runtime.py` remains a repo-local CLI/debug helper, not the first host hop. -Note: when Sopify is triggered inside a project workspace and the workspace does not yet have a compatible `.sopify-runtime/manifest.json`, the host must read `~/.codex/sopify/payload-manifest.json` and call `~/.codex/sopify/helpers/bootstrap_workspace.py --workspace-root ` first; once bootstrap succeeds, continue through the selected global bundle / workspace-preflight contract instead of assuming the workspace stub carries executable helper paths. -Note: before every Sopify LLM round, the host must first consume the runtime gate JSON contract. It may claim "runtime entered" and continue into normal stages only when `status == ready`, `gate_passed == true`, `evidence.handoff_found == true`, and `evidence.strict_runtime_entry == true`. When `allowed_response_mode == checkpoint_only`, the host may only continue through checkpoint responses; when `allowed_response_mode == error_visible_retry`, it may only show a short visible error and retry guidance; when `allowed_response_mode == action_proposal_retry`, the host must read `action_proposal_schema` from the contract, generate an ActionProposal JSON per the schema, and retry the gate with `--action-proposal-json '{...}'` (the gate CLI returns a non-zero exit code for retry; the host must still parse stdout JSON and must not treat non-zero exit code as command failure). -Note: the host should declare `--action-proposal-capability` on the first gate call; if the host provides `--action-proposal-json`, capability is implied and need not be declared separately. Hosts that declare neither capability nor proposal are treated as legacy hosts — the gate follows the legacy fallback and does not return a schema. The ActionProposal schema is dynamically returned by the gate contract; the host must not hardcode the schema in its prompt. -Note: the gate verification above must succeed from the current message turn's own tool call. The host may output any Sopify title line or continue into any route only after this turn first executes `scripts/runtime_gate.py enter` and validates the four conditions directly from that tool call output. It must not reuse `.sopify-skills/state/current_gate_receipt.json` from a previous turn as the current turn's gate receipt. -Note: the runtime gate internally executes the long-term preference preload through `preferences_preload_entry` exposed by the selected global bundle contract; if the host already resolved a workspace-preflight contract, it may reuse the projected helper path from there. Only repo-local development mode may fall back to `scripts/preferences_preload_runtime.py inspect --workspace-root `. The host must consume only the `preferences` result exposed by the gate contract; it must not rebuild preload prompts itself or bypass the gate to call preload/default runtime directly. -Note: when first activation returns `ROOT_CONFIRM_REQUIRED`, the host must stop for root selection first: recommend the current directory by default, offer the repository root as the secondary option, and allow the user to provide another directory manually. This outcome is a pre-runtime checkpoint, so `allowed_response_mode` must be `checkpoint_only` instead of a generic `error_visible_retry`. After the choice, rerun the same gate request with `activation_root`. `~go init` confirms writing, but it must not bypass this root-selection step. -Note: the long-term preference block is a separate prompt section with fixed priority `current explicit task > preferences.md > default rules`. "current explicit task" means the temporary execution instruction stated explicitly in the current task; it overrides long-term preferences on conflict, composes when non-conflicting, and is not written back as a long-term preference by default. -Note: after runtime execution, if `.sopify-skills/state/current_handoff.json` exists, the host must prioritize its `required_host_action`, `recommended_skill_ids`, and `artifacts` to decide the next step; the rendered `Next:` line is only a human-facing summary, not the sole machine contract. -Note: if `current_handoff.json.artifacts.execution_gate` exists, the host must also read its `gate_status / blocking_reason / plan_completion / next_required_action` fields together with `.sopify-skills/state/current_run.json.stage` before deciding whether the plan is merely generated or has already reached `ready_for_execution`. -Note: when `current_handoff.json.required_host_action == answer_questions`, the host must also read `.sopify-skills/state/current_clarification.json`, present the missing_facts/questions to the user, and wait for supplemental facts before resuming the default runtime entry; do not materialize the formal plan or jump to `~go exec` before the clarification is resolved. -Note: when `current_handoff.json.required_host_action == confirm_decision`, the host must first read `current_handoff.json.artifacts.decision_checkpoint` and `decision_submission_state`; only fall back to `.sopify-skills/state/current_decision.json` when the handoff does not contain the full checkpoint. Present the question/options/recommended_option_id to the user and wait for confirmation before resuming the default runtime entry; do not materialize the formal plan or jump to `~go exec` before confirmation. -Note: when `current_handoff.json.required_host_action == continue_host_develop`, the host still owns real code changes; but if implementation hits another user-facing branch, the host must not ask a free-form question or hand-write `current_decision.json / current_handoff.json`. It must call `scripts/develop_callback_runtime.py submit --payload-json ...` instead (vendored: `.sopify-runtime/scripts/develop_callback_runtime.py`). The payload must contain `checkpoint_kind` plus `resume_context`; the current minimum `resume_context` fields are `active_run_stage / current_plan_path / task_refs / changed_files / working_summary / verification_todo`. -Note: when `current_handoff.json.required_host_action == continue_host_consult`, the host may continue the discussion only after consuming the current turn's gate contract. It must not self-route before the gate, and it must not re-decide consult vs non-consult after the gate. The answer must be grounded in the current gate contract plus any consult context exposed by `handoff.artifacts`; if that context is missing, degrade explicitly against the current request instead of inferring a different route from chat semantics. +Note: Before every Sopify turn, the host must execute the runtime gate and validate its JSON contract. Continue into normal stages only when the gate passes. See `.sopify-skills/blueprint/protocol.md §8.1` for the full gate entry protocol, `allowed_response_mode` values, and ActionProposal capability. + +Note: After runtime execution, the host must consume `.sopify-skills/state/current_handoff.json` structured fields to decide the next step. Respect any pending checkpoint before continuing. See `.sopify-skills/blueprint/protocol.md §8.2` for the full handoff protocol and `required_host_action` values. + +Note: The host must not self-route before the gate, bypass checkpoint constraints, or write machine truth directly. Routing and state management are owned by the runtime. See `.sopify-skills/blueprint/protocol.md §8.3` for boundaries. + +**Host Integration Contract:** See `.sopify-skills/blueprint/protocol.md §8` for the full host runtime integration protocol, including gate entry, handoff consumption, checkpoint handling, runtime helper index, and state file index. --- @@ -248,69 +243,6 @@ progressive: Create files as needed (default) --- -## Routing Decision - -**Entry Point Flow:** -``` -User Input - ↓ -Check command prefix (~go, ~go plan, ~go exec, ~go finalize) - ↓ -├─ ~go finalize → Close out the active plan (refresh blueprint index, archive into history, clear active state) -├─ ~go exec → Enter the advanced recovery/debug path (only when an active plan or recovery state already exists) -├─ ~go plan → Plan mode (Analysis → Design; prefer scripts/sopify_runtime.py or .sopify-runtime/scripts/sopify_runtime.py for raw input, and use the matching go_plan_runtime.py only for the plan-only slice) -├─ ~go → Full workflow mode -└─ No prefix → Semantic analysis - ↓ -Semantic analysis routing: -├─ Q&A → gate → consult handoff → host answers -├─ Replay/Review/Why this choice → Workflow learning -├─ Simple change → Quick fix -├─ Medium task → Light iteration -└─ Complex task → Full development workflow -``` - -**Route Types:** - -| Route | Condition | Behavior | -|-------|-----------|----------| -| Q&A | Pure question, no code changes | Run gate first, then answer through consult handoff in the host session | -| Quick Fix | ≤2 files, clear modification | Direct execution | -| Light Iteration | 3-5 files, clear requirements | Light plan + execution | -| Full Development | >5 files or architectural changes | Full 3-phase workflow | - -**Host Integration Contract:** -- `Codex/Skills` is prompt-layer guidance only; it is not the machine contract for the vendored runtime. -- `~/.codex/sopify/payload-manifest.json` is only the global preflight contract; it does not replace the workspace bundle manifest. -- When a workspace is missing a compatible bundle, the host must call `~/.codex/sopify/helpers/bootstrap_workspace.py` before trying to route through vendored runtime entries. -- Treat workspace `.sopify-runtime/manifest.json` as a thin stub only; it no longer promises `limits.runtime_gate_entry / limits.preferences_preload_entry`. -- Once Sopify is triggered, the host must combine the workspace stub with `~/.codex/sopify/payload-manifest.json`, resolve the selected global bundle, and read `runtime_gate_entry` from the selected bundle contract or workspace-preflight contract before executing the first gate hop. -- Repo-local development mode may fall back to `scripts/runtime_gate.py`; the host must not bypass the gate and use `scripts/sopify_runtime.py` as the first hop. -- Before every new Sopify LLM round, the host must execute the runtime gate first; this includes fresh requests, clarification/decision/execution-confirm resumes, and ordinary continuation into the next LLM turn. -- Consume only the stable JSON contract returned by the gate; continue into normal Sopify stages only when `status == ready`, `gate_passed == true`, `evidence.handoff_found == true`, and `evidence.strict_runtime_entry == true`. -- When `allowed_response_mode == checkpoint_only`, the host may only perform checkpoint responses; when `allowed_response_mode == error_visible_retry`, it may only show visible retry/error output; when `allowed_response_mode == action_proposal_retry`, the host must generate an ActionProposal per `action_proposal_schema` and retry the gate with `--action-proposal-json`. -- The runtime gate internally executes the long-term preference preload; discover that helper from the selected global bundle contract or workspace-preflight contract, and fall back to `scripts/preferences_preload_runtime.py` only when the vendored helper is unavailable in repo-local development mode. -- When first activation returns `ROOT_CONFIRM_REQUIRED`, the host must enter root selection before retrying: recommend the current directory, offer the repository root, and allow manual `activation_root` input. This outcome is a pre-runtime checkpoint, so `allowed_response_mode` must be `checkpoint_only`. `~go init` must not bypass that step. -- Consume only the `preferences` result exposed by the gate contract; inject `preferences.injection_text` only when `status == ready` and `preferences.status == loaded` and `preferences.injected == true`, and never re-read `preferences.md` to rebuild the prompt block manually. -- The preload downgrade policy is fixed to `fail-open with visibility`; `missing / invalid / read_error` must not block the main flow, but the host must retain observable `helper_path / workspace_root / plan_directory / preferences_path / status / error_code / injected` fields internally. -- The long-term preference block always follows the priority `current explicit task > preferences.md > default rules`; temporary instructions from the current task override long-term preferences and are not written back by default. -- For post-run continuation, treat `.sopify-skills/state/current_handoff.json` as the source of truth and fall back to the rendered `Next:` text only when the handoff file is missing. -- If handoff `artifacts.execution_gate` exists, treat it together with `.sopify-skills/state/current_run.json.stage` as the sole execution-gate contract; do not infer plan readiness from the plan path or the rendered `Next:` text. -- When `current_handoff.json.required_host_action == answer_questions`, treat `.sopify-skills/state/current_clarification.json` as the sole machine contract for the active missing-facts checkpoint. -- The preferred clarification UX is to show `missing_facts` plus `questions[*]` and collect a natural-language supplement from the user; while clarification is pending, do not materialize the formal plan or jump to `~go exec`. -- After the user supplements the facts, the host must re-enter the default runtime entry in the same workspace and let runtime continue planning; if `current_clarification.json` is cleared afterwards, that is the expected close-out behavior. -- `~go finalize` still goes through the default runtime entry and does not require an extra host bridge; first version only supports metadata-managed plans and rejects legacy plans instead of auto-migrating them. -- When `current_handoff.json.required_host_action == confirm_decision`, treat `current_handoff.json.artifacts.decision_checkpoint` plus `decision_submission_state` as the primary machine contract for the active design split; `.sopify-skills/state/current_decision.json` remains the fallback state file and legacy projection source. -- For a pending decision, the preferred host UX is to show the `question`, list `options[*]` in order, and highlight `recommended_option_id`; users may answer with `1/2/...` or explicitly use `~decide choose `. -- `~decide status|choose|cancel` is a debug/override surface only; the normal path is for the host to enter the confirmation loop automatically from the `confirm_decision` handoff. -- While a decision is pending, the host must not create or rewrite the formal plan on its own and must not treat the rendered `Next:` text as an executable machine instruction. -- After the user confirms, the host must re-enter the default runtime entry in the same workspace and let runtime materialize the single formal plan; if `current_decision.json` is cleared afterwards, that is the expected close-out behavior. -- When `current_handoff.json.required_host_action == continue_host_consult`, treat the current message turn's gate tool-call contract together with `.sopify-skills/state/current_handoff.json` as the machine source of truth for consult answers; do not perform another host-side routing decision before answering. -- Treat `~go exec` as an advanced recovery entry only; when there is no active plan or recovery state, the host must not present it as the normal implementation path. -- Even when the user explicitly types `~go exec`, the host must still honor the machine contract for `clarification_pending / decision_pending` instead of bypassing those checkpoints. - ---- - ## Phase Execution ### P1 | Requirements Analysis @@ -417,7 +349,7 @@ Next: Please verify the functionality | `kb` | Knowledge base operations | Init, update strategies | | `templates` | Create documents | All template definitions | -**Loading:** On-demand, loaded when entering corresponding phase. +**Loading:** The above are all current builtin skills — runtime-managed workflow skills loaded on-demand by the runtime engine. Standalone invocation is not supported. The authoritative skill catalog is `builtin_catalog.generated.json`. --- @@ -431,36 +363,10 @@ Next: Please verify the functionality ~go finalize # Explicitly close out the current metadata-managed plan ``` -**Runtime helpers:** -``` -scripts/sopify_runtime.py # default repo-local raw-input entry, routed by the runtime router -.sopify-runtime/scripts/sopify_runtime.py # default vendored raw-input entry after secondary integration -scripts/go_plan_runtime.py # helper for the plan-only slice -.sopify-runtime/scripts/go_plan_runtime.py # vendored helper for the plan-only slice -scripts/develop_callback_runtime.py # internal callback helper for user-facing branches during `continue_host_develop`, with inspect / submit -.sopify-runtime/scripts/develop_callback_runtime.py # vendored develop callback helper, without changing the default runtime entry -scripts/decision_bridge_runtime.py # internal host bridge helper for `confirm_decision`, with inspect / submit / prompt -.sopify-runtime/scripts/decision_bridge_runtime.py # vendored decision bridge helper, without changing the default runtime entry -scripts/runtime_gate.py # prompt-level runtime gate helper, with enter -.sopify-runtime/scripts/runtime_gate.py # vendored runtime gate helper; the first hop after Sopify triggers -scripts/preferences_preload_runtime.py # long-term preference preload helper for hosts, with inspect -.sopify-runtime/scripts/preferences_preload_runtime.py # vendored preferences preload helper, without changing the default runtime entry -scripts/check-install-payload-bundle-smoke.py # maintainer smoke; verifies install-once + trigger-time bootstrap + unchanged default entry -~/.codex/sopify/payload-manifest.json # host global payload metadata used during workspace preflight -~/.codex/sopify/helpers/bootstrap_workspace.py # host global helper used to bootstrap `.sopify-runtime/` into a workspace -.sopify-runtime/manifest.json # vendored bundle machine contract; hosts must read this first -.sopify-skills/state/current_handoff.json # structured handoff written by the runtime; hosts must read this first after execution -.sopify-skills/state/current_run.json # active run state; includes the current stage and execution_gate snapshot -.sopify-skills/state/current_clarification.json # clarification checkpoint state; read only when handoff requests answer_questions -.sopify-skills/state/current_decision.json # decision checkpoint fallback state; read when handoff lacks the full checkpoint -``` - -Note: the default entry is still `scripts/sopify_runtime.py`, but once Sopify is triggered the first host hop must execute `scripts/runtime_gate.py enter`; when vendored, the workspace `.sopify-runtime/manifest.json` is only a thin stub, so the host must combine it with `~/.codex/sopify/payload-manifest.json`, resolve the selected global bundle, and then read `runtime_gate_entry / limits.runtime_gate_contract_version / limits.runtime_gate_allowed_response_modes` from the selected bundle contract or workspace-preflight contract. If the workspace bundle is missing or incompatible, the host must preflight through `~/.codex/sopify/payload-manifest.json` and call `~/.codex/sopify/helpers/bootstrap_workspace.py` first; `go_plan_runtime.py` is now only for repo-local plan-only / debug flows, and `~go finalize` still routes through the default runtime entry. The runtime gate internally performs preload through `preferences_preload_entry / limits.preferences_preload_contract_version / limits.preferences_preload_statuses` exposed by the selected global bundle contract or an equivalent preflight contract, and emits a unified `status / gate_passed / allowed_response_mode / preferences / handoff / evidence` contract; continue into normal stages only when `status=ready`, `gate_passed=true`, `evidence.handoff_found=true`, and `evidence.strict_runtime_entry=true`; `checkpoint_only` may only drive checkpoint responses, `error_visible_retry` may only surface visible retry/error output, and `action_proposal_retry` requires the host to generate an ActionProposal per schema and retry. If first activation initially returns `ROOT_CONFIRM_REQUIRED`, the host must stop for root selection: recommend the current directory, offer the repository root, allow manual `activation_root`, then rerun the same request with that explicit root. This outcome is a pre-runtime checkpoint, so `allowed_response_mode` must be `checkpoint_only`. `~go init` must not bypass this step. After execution the host must read `.sopify-skills/state/current_handoff.json` before trusting `Next:`; if `required_host_action=answer_questions`, continue into `.sopify-skills/state/current_clarification.json`; if `required_host_action=confirm_decision`, first consume `current_handoff.json.artifacts.decision_checkpoint / decision_submission_state` and only fall back to `.sopify-skills/state/current_decision.json`; if `required_host_action=continue_host_develop` and implementation hits another user-facing branch, the host must route through `scripts/develop_callback_runtime.py inspect|submit` (vendored: `.sopify-runtime/scripts/develop_callback_runtime.py`) instead of ad-hoc questioning; the helper path, host hints, and minimum resume contract are exposed through the selected global bundle contract's `limits.develop_callback_entry / limits.develop_callback_hosts / limits.develop_resume_context_required_fields / limits.develop_resume_after_actions`; in the current documented scope, hosts may call `scripts/decision_bridge_runtime.py inspect` (vendored: `.sopify-runtime/scripts/decision_bridge_runtime.py`) and then write the normalized submission through `submit` or `prompt`. Maintainers can recheck the three hard constraints with `python3 scripts/check-install-payload-bundle-smoke.py`. +**Runtime helpers and state files:** See `.sopify-skills/blueprint/protocol.md §8.4–8.5` for the full runtime helper index and state file index. **Configuration File:** `sopify.config.yaml` (project root) **Knowledge Base Directory:** `.sopify-skills/` -**Blueprint Path:** `.sopify-skills/blueprint/` - **Plan Package Path:** `.sopify-skills/plan/YYYYMMDD_feature_name/` diff --git a/README.md b/README.md index 669c42f..fc0c990 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE) [![Docs](https://img.shields.io/badge/docs-CC%20BY%204.0-green.svg)](./LICENSE-docs) -[![Version](https://img.shields.io/badge/version-2026--05--09.175537-orange.svg)](#version-history) +[![Version](https://img.shields.io/badge/version-2026--05--11.202509-orange.svg)](#version-history) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md) English · [简体中文](./README.zh-CN.md) · [Quick Start](#quick-start) · [Contributors](./CONTRIBUTORS.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 9ae4b78..d5aa3d2 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,7 +8,7 @@ [![许可证](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE) [![文档](https://img.shields.io/badge/docs-CC%20BY%204.0-green.svg)](./LICENSE-docs) -[![版本](https://img.shields.io/badge/version-2026--05--09.175537-orange.svg)](#版本历史) +[![版本](https://img.shields.io/badge/version-2026--05--11.202509-orange.svg)](#版本历史) [![欢迎PR](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING_CN.md) [English](./README.md) · 简体中文 · [快速开始](#快速开始) · [贡献者](./CONTRIBUTORS.md) diff --git a/installer/hosts/claude.py b/installer/hosts/claude.py index ed33ff6..d4bb94e 100644 --- a/installer/hosts/claude.py +++ b/installer/hosts/claude.py @@ -2,7 +2,7 @@ from __future__ import annotations -from installer.models import EntryMode, FeatureId, HostCapability, SupportTier +from installer.models import EntryMode, EnhancementGroup, FeatureId, HostCapability, SupportTier from .base import HostAdapter, HostRegistration @@ -36,6 +36,11 @@ FeatureId.HOST_BRIDGE, FeatureId.SMOKE_VERIFIED, ), + declared_enhancements=( + EnhancementGroup.CONTINUATION, + EnhancementGroup.INTERACTION, + EnhancementGroup.AUDIT, + ), entry_modes=(EntryMode.PROMPT_ONLY,), doctor_checks=( "host_prompt_present", diff --git a/installer/hosts/codex.py b/installer/hosts/codex.py index daf169c..b72180f 100644 --- a/installer/hosts/codex.py +++ b/installer/hosts/codex.py @@ -2,7 +2,7 @@ from __future__ import annotations -from installer.models import EntryMode, FeatureId, HostCapability, SupportTier +from installer.models import EntryMode, EnhancementGroup, FeatureId, HostCapability, SupportTier from .base import HostAdapter, HostRegistration @@ -36,6 +36,11 @@ FeatureId.HOST_BRIDGE, FeatureId.SMOKE_VERIFIED, ), + declared_enhancements=( + EnhancementGroup.CONTINUATION, + EnhancementGroup.INTERACTION, + EnhancementGroup.AUDIT, + ), entry_modes=(EntryMode.PROMPT_ONLY,), doctor_checks=( "host_prompt_present", diff --git a/installer/inspection.py b/installer/inspection.py index 0981ca3..edbb058 100644 --- a/installer/inspection.py +++ b/installer/inspection.py @@ -31,7 +31,7 @@ ) from runtime.config import ConfigError, load_runtime_config from runtime.context_snapshot import resolve_context_snapshot -from runtime.state import SESSIONS_DIRNAME, StateStore +from runtime.state import StateStore STATUS_SCHEMA_VERSION = "2" DOCTOR_SCHEMA_VERSION = "1" @@ -75,6 +75,23 @@ "decision_missing_for_pending_handoff": "The handoff expects a decision checkpoint, but no valid decision state was found.", } +_STAGE_LABELS: dict[str, str] = { + "plan_generated": "plan ready", + "ready_for_execution": "awaiting execution", + "executing": "executing", + "completed": "completed", + "clarification_pending": "awaiting info", + "decision_pending": "awaiting decision", + "develop_pending": "awaiting host development", +} + +_CHECKPOINT_LABELS: dict[str, str] = { + "answer_questions": "awaiting supplemental info", + "confirm_decision": "awaiting decision confirmation", + "continue_host_develop": "ready to continue", + "continue_host_consult": "ready to continue", +} + @dataclass(frozen=True) class InspectionCheck: @@ -504,8 +521,8 @@ def render_status_text(payload: dict[str, object]) -> str: f" root: {workspace_state['root']}", f" sopify_skills_present: {workspace_state['sopify_skills_present']}", f" active_plan: {workspace_state['active_plan'] or '(none)'}", - f" current_run_stage: {workspace_state['current_run_stage'] or '(none)'}", - f" pending_checkpoint: {workspace_state['pending_checkpoint'] or '(none)'}", + f" current_run_stage: {_STAGE_LABELS.get(workspace_state['current_run_stage'], workspace_state['current_run_stage']) if workspace_state['current_run_stage'] else '(none)'}", + f" pending_checkpoint: {_CHECKPOINT_LABELS.get(workspace_state['pending_checkpoint'], workspace_state['pending_checkpoint']) if workspace_state['pending_checkpoint'] else '(none)'}", ] ) if workspace_state["quarantine_count"]: @@ -521,9 +538,10 @@ def render_status_text(payload: dict[str, object]) -> str: if workspace_state["state_conflicts"]: lines.append(f" state_conflict_count: {len(workspace_state['state_conflicts'])}") first_conflict = workspace_state["state_conflicts"][0] + conflict_explanation = str(first_conflict.get("explanation") or _describe_state_conflict(str(first_conflict.get("code") or ""))).strip() lines.append( - " state_conflict: {code} @ {path}".format( - code=first_conflict.get("code") or "unknown", + " state_conflict: {desc} @ {path}".format( + desc=conflict_explanation or "unknown conflict", path=first_conflict.get("path") or "(unknown)", ) ) @@ -586,6 +604,7 @@ def _render_structured_evidence_lines(evidence: object) -> tuple[str, ...]: def _inspect_runtime_workspace_state(workspace_root: Path) -> dict[str, object]: + """Thin projection of current global machine truth for doctor/status.""" fallback_state_root = workspace_root / ".sopify-skills" / "state" fallback_run = _read_json(fallback_state_root / "current_run.json") fallback_handoff = _read_json(fallback_state_root / "current_handoff.json") @@ -605,50 +624,22 @@ def _inspect_runtime_workspace_state(workspace_root: Path) -> dict[str, object]: return fallback_payload global_store = StateStore(config) - snapshots = [ - resolve_context_snapshot( - config=config, - review_store=global_store, - global_store=global_store, - ) - ] - sessions_root = config.state_dir / SESSIONS_DIRNAME - if sessions_root.is_dir(): - for session_dir in sorted(sessions_root.iterdir(), key=lambda item: item.name): - if not session_dir.is_dir(): - continue - try: - session_store = StateStore(config, session_id=session_dir.name) - except ValueError: - continue - snapshots.append( - resolve_context_snapshot( - config=config, - review_store=session_store, - global_store=global_store, - ) - ) + snapshot = resolve_context_snapshot( + config=config, + review_store=global_store, + global_store=global_store, + ) + + quarantined_items = [item.to_dict() for item in snapshot.quarantined_items] + state_conflicts = [] + for item in snapshot.conflict_items: + payload = item.to_dict() + payload["explanation"] = _describe_state_conflict(item.code) + state_conflicts.append(payload) - primary_snapshot = snapshots[0] - quarantined_items: dict[tuple[str, str, str, str, str], dict[str, object]] = {} - state_conflicts: dict[tuple[str, str, str, str], dict[str, object]] = {} - runtime_notes: list[str] = [] - for snapshot in snapshots: - for item in snapshot.quarantined_items: - key = (item.state_kind, item.path, item.reason, item.provenance_status, item.state_scope) - quarantined_items.setdefault(key, item.to_dict()) - for item in snapshot.conflict_items: - key = (item.code, item.message, item.path, item.state_scope) - payload = item.to_dict() - payload["explanation"] = _describe_state_conflict(item.code) - state_conflicts.setdefault(key, payload) - for note in snapshot.notes: - if note not in runtime_notes: - runtime_notes.append(note) - - current_run = primary_snapshot.current_run - current_plan = primary_snapshot.current_plan - current_handoff = primary_snapshot.current_handoff + current_run = snapshot.current_run + current_plan = snapshot.current_plan + current_handoff = snapshot.current_handoff active_plan = None if current_run is not None: active_plan = str(current_run.plan_path or current_run.plan_id or "") or None @@ -660,9 +651,9 @@ def _inspect_runtime_workspace_state(workspace_root: Path) -> dict[str, object]: "current_run_stage": current_run.stage if current_run is not None else None, "pending_checkpoint": current_handoff.required_host_action if current_handoff is not None else None, "quarantine_count": len(quarantined_items), - "quarantined_items": list(quarantined_items.values()), - "state_conflicts": list(state_conflicts.values()), - "runtime_notes": runtime_notes, + "quarantined_items": quarantined_items, + "state_conflicts": state_conflicts, + "runtime_notes": list(snapshot.notes), } @@ -694,8 +685,7 @@ def _runtime_workspace_checks(workspace_state: dict[str, object]) -> tuple[Inspe status=CHECK_FAIL, reason_code=REASON_RUNTIME_STATE_CONFLICT, evidence=tuple( - "{code} @ {path}: {explanation}".format( - code=item.get("code") or "unknown", + "{explanation} @ {path}".format( path=item.get("path") or "(unknown)", explanation=item.get("explanation") or _describe_state_conflict(str(item.get("code") or "")), ) diff --git a/installer/models.py b/installer/models.py index 6730ada..4dc3267 100644 --- a/installer/models.py +++ b/installer/models.py @@ -48,6 +48,19 @@ class FeatureId(StrEnum): SMOKE_VERIFIED = "smoke_verified" +class EnhancementGroup(StrEnum): + """Opt-in consumption enhancement groups declared by host adapters. + + Each group maps to a set of contract surfaces from the consumption matrix + (see blueprint/design.md). This enum is a declaration axis orthogonal to + FeatureId (installation capability axis). + """ + + CONTINUATION = "continuation" + INTERACTION = "interaction" + AUDIT = "audit" + + @dataclass(frozen=True) class HostCapability: """Product-facing capability declaration for one supported host.""" @@ -57,6 +70,7 @@ class HostCapability: install_enabled: bool declared_features: tuple[FeatureId, ...] verified_features: tuple[FeatureId, ...] + declared_enhancements: tuple[EnhancementGroup, ...] entry_modes: tuple[EntryMode, ...] doctor_checks: tuple[str, ...] smoke_targets: tuple[str, ...] @@ -68,6 +82,7 @@ def to_dict(self) -> dict[str, object]: "install_enabled": self.install_enabled, "declared_features": [feature.value for feature in self.declared_features], "verified_features": [feature.value for feature in self.verified_features], + "declared_enhancements": [group.value for group in self.declared_enhancements], "entry_modes": [mode.value for mode in self.entry_modes], "doctor_checks": list(self.doctor_checks), "smoke_targets": list(self.smoke_targets), diff --git a/runtime/gate_output.py b/runtime/gate_output.py index 07e0ae5..64d071b 100644 --- a/runtime/gate_output.py +++ b/runtime/gate_output.py @@ -69,10 +69,7 @@ def render_gate_text(payload: Mapping[str, Any]) -> str: f" allowed_response_mode: {allowed_response_mode}", ] if runtime: - route_name = str(runtime.get("route_name") or "").strip() reason = str(runtime.get("reason") or "").strip() - if route_name: - lines.append(f" route: {route_name}") if reason: lines.append(f" reason: {reason}") if preflight: diff --git a/runtime/output.py b/runtime/output.py index 7fd5594..dfc8e38 100644 --- a/runtime/output.py +++ b/runtime/output.py @@ -71,7 +71,6 @@ "risk_level": "风险级别", "risk": "关键风险", "mitigation": "缓解", - "entry_guard_reason": "守卫原因码", "execution_gate": "门禁", "missing_facts": "缺口", "missing": "未生成", @@ -95,12 +94,10 @@ "next_answer_questions": "回复补充信息继续规划,或输入 取消 终止本轮设计", "next_plan": "在宿主会话中继续评审或执行方案,或直接回复修改意见", "next_workflow": "在宿主会话中继续执行后续阶段,或显式使用 ~go plan 只规划", - "next_resume": "在宿主会话中继续 develop 阶段", "next_exec": "仅在已有活动 plan 或恢复态时使用 ~go exec;普通开发流继续按宿主会话推进", "next_cancel": "如需继续,重新发起 ~go plan 或 ~go", "next_archive_success": "请验证 blueprint 索引与 history 归档结果", "next_archive_retry": "补齐 blueprint 更新或切换到 metadata-managed plan 后重试", - "next_quick_fix": "在宿主会话中继续执行快速修复", "next_consult": "在宿主会话中继续问答,或改成明确变更请求", "next_decision": "回复 1/2(或 ~decide choose )确认方案,或输入 取消 终止本轮设计", "handoff_answer_questions": "已写入 clarification handoff,宿主应先补齐缺失事实信息", @@ -135,7 +132,6 @@ "risk_level": "Risk Level", "risk": "Key Risk", "mitigation": "Mitigation", - "entry_guard_reason": "Entry Guard Reason", "execution_gate": "Gate", "missing_facts": "Missing Facts", "missing": "not generated", @@ -159,12 +155,10 @@ "next_answer_questions": "Reply with the missing facts to continue planning, or type cancel to stop this round", "next_plan": "Continue plan review or execution in the host session, or reply with feedback", "next_workflow": "Continue the downstream stages in the host session, or use ~go plan for planning only", - "next_resume": "Continue the develop stage in the host session", "next_exec": "Use ~go exec only when an active plan or recovery state already exists; otherwise continue through the host flow", "next_cancel": "Start a new ~go plan or ~go flow when ready", "next_archive_success": "Review the blueprint index refresh and the history archive", "next_archive_retry": "Update the blueprint or switch to a metadata-managed plan and retry", - "next_quick_fix": "Continue the quick-fix flow in the host session", "next_consult": "Continue the discussion in the host session, or restate it as a change request", "next_decision": "Reply with 1/2 (or `~decide choose `) to confirm, or type cancel to abort this design round", "handoff_answer_questions": "clarification handoff written; the host should gather the missing factual details first", @@ -181,6 +175,33 @@ }, } +_ROUTE_FAMILIES = { + "completion": frozenset({"plan_only", "archive_lifecycle", "cancel_active"}), + "pending": frozenset({"clarification_pending", "decision_pending"}), + "action": frozenset({"workflow", "light_iterate", "quick_fix", "consult", "resume_active", "exec_plan"}), + "conflict": frozenset({"state_conflict"}), + "rejection": frozenset({"proposal_rejected"}), +} + +# Canonical family → symbol mapping (P4c-3a.1). +# Hosts can predict the status symbol from the route family alone. +_FAMILY_SYMBOL: dict[str, str] = { + "completion": "✓", + "pending": "?", + "action": "!", + "conflict": "!", + "rejection": "!", +} + +_ROUTE_TO_FAMILY: dict[str, str] = { + route: family for family, routes in _ROUTE_FAMILIES.items() for route in routes +} + +_GATE_STATUS_DISPLAY = { + "zh-CN": {"ready": "就绪", "blocked": "阻断", "decision_required": "待决策"}, + "en-US": {"ready": "Ready", "blocked": "Blocked", "decision_required": "Decision Required"}, +} + _TITLE_COLORS = { "green": "\033[32m", "blue": "\033[34m", @@ -208,8 +229,13 @@ def render_runtime_output( body = _core_lines(result, locale) next_hint = _next_hint(result, locale) + context_files = _collect_context_files(result) + lines = [title, ""] lines.extend(body) + if context_files: + lines.extend(["", f"Context: {len(context_files)} files"]) + lines.extend(f" - {path}" for path in context_files) lines.extend(["", "---", f"Changes: {len(changes)} files"]) if changes: lines.extend(f" - {path}" for path in changes) @@ -280,7 +306,6 @@ def _core_lines(result: RuntimeResult, language: str) -> list[str]: f"{labels['missing_facts']}: {missing_facts}", f"{labels['questions']}: {question_text or labels['missing']}", ] - _append_entry_guard_reason_line(lines, result=result, language=language) return lines if route_name == "decision_pending" and result.recovered_context.current_decision is not None: @@ -295,7 +320,6 @@ def _core_lines(result: RuntimeResult, language: str) -> list[str]: f"{labels['options']}: {option_text or labels['missing']}", f"{labels['status']}: {_decision_pending_status(language, recommended)}", ] - _append_entry_guard_reason_line(lines, result=result, language=language) return lines if route_name == "state_conflict": @@ -312,7 +336,6 @@ def _core_lines(result: RuntimeResult, language: str) -> list[str]: ] ) lines.append(f"{labels['quarantined']}: {len(quarantined_items)}") - _append_entry_guard_reason_line(lines, result=result, language=language) return lines if route_name == "archive_lifecycle": @@ -323,7 +346,6 @@ def _core_lines(result: RuntimeResult, language: str) -> list[str]: f"{labels['status']}: {labels['archive_success']}", ] return [ - f"{labels['route']}: {route_name}", f"{labels['status']}: {labels['archive_blocked']}", f"{labels['reason']}: {_diagnostic_reason(result)}", ] @@ -358,11 +380,9 @@ def _core_lines(result: RuntimeResult, language: str) -> list[str]: if route_name == "cancel_active": return [ f"{labels['status']}: {labels['cleared']}", - f"{labels['route']}: {route_name}", ] return [ - f"{labels['route']}: {route_name}", f"{labels['status']}: {_status_message(result, language)}", f"{labels['reason']}: {_diagnostic_reason(result)}", ] @@ -379,10 +399,6 @@ def _collect_changes(result: RuntimeResult) -> list[str]: if path not in seen: seen.add(path) ordered.append(path) - for path in result.recovered_context.loaded_files: - if path not in seen: - seen.add(path) - ordered.append(path) for path in result.generated_files: if path not in seen: seen.add(path) @@ -399,48 +415,43 @@ def _collect_changes(result: RuntimeResult) -> list[str]: return ordered +def _collect_context_files(result: RuntimeResult) -> list[str]: + """Return loaded context files that are not part of generated changes.""" + return list(result.recovered_context.loaded_files) + + def _next_hint(result: RuntimeResult, language: str) -> str: labels = _LABELS[language] if result.handoff is not None: return _handoff_next_hint(result, language) - if result.route.route_name == "archive_lifecycle": + route_name = result.route.route_name + if route_name == "archive_lifecycle": return labels["next_archive_success"] if result.plan_artifact is not None else labels["next_archive_retry"] - if result.route.route_name == "clarification_pending": - return labels["next_answer_questions"] - if result.route.route_name == "decision_pending": - return labels["next_decision"] - if result.route.route_name == "state_conflict": + if route_name in _ROUTE_FAMILIES["pending"]: + return labels["next_answer_questions"] if route_name == "clarification_pending" else labels["next_decision"] + if route_name in _ROUTE_FAMILIES["conflict"]: return labels["next_state_conflict"] - if result.route.route_name == "exec_plan": + if route_name == "exec_plan": return labels["next_exec"] - if result.route.route_name == "cancel_active": + if route_name == "cancel_active": return labels["next_cancel"] return labels["next_retry"] def _status_symbol(result: RuntimeResult) -> str: route_name = result.route.route_name - if route_name == "plan_only": - return "✓" if result.plan_artifact is not None else "!" - if route_name == "archive_lifecycle": - return "✓" if result.plan_artifact is not None else "!" - if route_name in {"clarification_pending"}: - return "?" - if route_name == "decision_pending": - return "?" - if route_name == "state_conflict": - if result.route.active_run_action == "abort_conflict" and not _state_conflict_payload(result): + family = _ROUTE_TO_FAMILY.get(route_name) + if family is not None: + symbol = _FAMILY_SYMBOL[family] + # Completion: missing expected artifact downgrades to warning + if family == "completion" and route_name in {"plan_only", "archive_lifecycle"} and result.plan_artifact is None: + return "!" + # Conflict: fully resolved upgrades to success + if family == "conflict" and result.route.active_run_action == "abort_conflict" and not _state_conflict_payload(result): return "✓" - return "!" - if route_name == "cancel_active": - return "✓" - if route_name == "proposal_rejected": - return "!" - if route_name in {"workflow", "light_iterate", "quick_fix", "consult", "resume_active", "exec_plan"}: - return "!" - if result.notes: - return "!" - return "✓" + return symbol + # Unclassified route: warning if notes, else success + return "!" if result.notes else "✓" def _status_message(result: RuntimeResult, language: str) -> str: @@ -491,51 +502,30 @@ def _handoff_label(result: RuntimeResult, language: str) -> str: return CURRENT_HANDOFF_RELATIVE_PATH +_HANDOFF_KIND_HINT = { + "plan": "next_plan", + "develop": "next_workflow", + "clarification": "next_answer_questions", + "decision": "next_decision", + "consult": "next_consult", + "reject": "next_reject", +} + + def _handoff_next_hint(result: RuntimeResult, language: str) -> str: labels = _LABELS[language] handoff = result.handoff if handoff is None: return labels["next_retry"] - required_host_action = str(handoff.required_host_action or "").strip() - if required_host_action == "continue_host_consult": - if handoff.handoff_kind == "reject": - return labels["next_reject"] - if handoff.handoff_kind == "archive": - receipt_status = str((handoff.artifacts or {}).get("archive_receipt_status", "")).strip() - if receipt_status == "completed": - return labels["next_archive_success"] - return labels["next_archive_retry"] - return labels["next_consult"] - if required_host_action == "answer_questions": - return labels["next_answer_questions"] - if required_host_action == "resolve_state_conflict": - return labels["next_state_conflict"] - if required_host_action == "continue_host_develop": - if result.route.route_name == "plan_only": - return labels["next_plan"] - if result.route.route_name == "resume_active": - return labels["next_resume"] - if result.route.route_name == "exec_plan": - return labels["next_exec"] - if result.route.route_name == "quick_fix": - return labels["next_quick_fix"] - return labels["next_workflow"] - if required_host_action == "confirm_decision": - return labels["next_decision"] - # Fallback: match by canonical handoff_kind (family) - if handoff.handoff_kind == "plan": - return labels["next_plan"] - if handoff.handoff_kind == "clarification": - return labels["next_answer_questions"] - if handoff.handoff_kind == "develop": - if result.route.route_name == "quick_fix": - return labels["next_quick_fix"] - return labels["next_resume"] if result.route.route_name == "resume_active" else labels["next_exec"] - if handoff.handoff_kind == "decision": - return labels["next_decision"] - if handoff.handoff_kind == "consult": - return labels["next_consult"] - return labels["next_retry"] + kind = handoff.handoff_kind + if kind == "archive": + receipt_status = str((handoff.artifacts or {}).get("archive_receipt_status", "")).strip() + return labels["next_archive_success"] if receipt_status == "completed" else labels["next_archive_retry"] + if kind == "state_conflict": + action = str(handoff.required_host_action or "").strip() + return labels["next_state_conflict"] if action == "resolve_state_conflict" else labels["next_workflow"] + hint_key = _HANDOFF_KIND_HINT.get(kind) + return labels[hint_key] if hint_key else labels["next_retry"] def _diagnostic_reason(result: RuntimeResult) -> str: @@ -543,21 +533,14 @@ def _diagnostic_reason(result: RuntimeResult) -> str: return result.notes[0] if result.route.reason: return result.route.reason - return result.route.route_name + return "—" def _execution_gate(result: RuntimeResult): - # Output only renders resolved runtime result facts. It may fall back from - # recovered context to persisted handoff artifacts, but it never re-opens - # checkpoint state from disk on its own. - current_run = result.recovered_context.current_run - if current_run is not None and current_run.execution_gate is not None: - return current_run.execution_gate - if result.handoff is not None: - execution_gate = result.handoff.artifacts.get("execution_gate") - if isinstance(execution_gate, dict): - return execution_gate - return None + if result.handoff is None: + return None + execution_gate = result.handoff.artifacts.get("execution_gate") + return execution_gate if isinstance(execution_gate, dict) else None def _priority_note(result: RuntimeResult) -> str | None: @@ -570,25 +553,6 @@ def _priority_note(result: RuntimeResult) -> str | None: return None -def _append_entry_guard_reason_line(lines: list[str], *, result: RuntimeResult, language: str) -> None: - reason_code = _entry_guard_reason_code(result) - if not reason_code: - return - labels = _LABELS[language] - lines.append(f"{labels['entry_guard_reason']}: {reason_code}") - - -def _entry_guard_reason_code(result: RuntimeResult) -> str | None: - handoff = result.handoff - if handoff is None: - return None - value = handoff.artifacts.get("entry_guard_reason_code") - if isinstance(value, str): - normalized = value.strip() - return normalized or None - return None - - def _execution_gate_line(result: RuntimeResult, language: str) -> str: labels = _LABELS[language] current_gate = _execution_gate(result) @@ -596,13 +560,11 @@ def _execution_gate_line(result: RuntimeResult, language: str) -> str: return f"{labels['execution_gate']}: {labels['missing']}" if hasattr(current_gate, "gate_status"): gate_status = current_gate.gate_status - blocking_reason = current_gate.blocking_reason - plan_completion = current_gate.plan_completion else: gate_status = str(current_gate.get("gate_status") or "blocked") - blocking_reason = str(current_gate.get("blocking_reason") or "none") - plan_completion = str(current_gate.get("plan_completion") or "incomplete") - return f"{labels['execution_gate']}: {gate_status} / {blocking_reason} / {plan_completion}" + display_map = _GATE_STATUS_DISPLAY.get(language, _GATE_STATUS_DISPLAY["en-US"]) + display = display_map.get(gate_status, display_map["blocked"]) + return f"{labels['execution_gate']}: {display}" def _decision_pending_status(language: str, recommended_option_id: str) -> str: @@ -626,10 +588,6 @@ def _phase_label(result: RuntimeResult, language: str) -> str: def _state_conflict_payload(result: RuntimeResult) -> dict[str, object]: - if result.recovered_context.state_conflict: - return dict(result.recovered_context.state_conflict) - if result.route.route_name == "state_conflict" and result.route.active_run_action == "abort_conflict": - return {} if result.handoff is not None: payload = result.handoff.artifacts.get("state_conflict") if isinstance(payload, dict): @@ -638,8 +596,6 @@ def _state_conflict_payload(result: RuntimeResult) -> dict[str, object]: def _quarantined_items(result: RuntimeResult) -> list[dict[str, object]]: - if result.recovered_context.quarantined_items: - return [dict(item) for item in result.recovered_context.quarantined_items] if result.handoff is not None: payload = result.handoff.artifacts.get("quarantined_items") if isinstance(payload, list): diff --git a/scripts/check-enhancement-declaration.py b/scripts/check-enhancement-declaration.py new file mode 100644 index 0000000..ce441e1 --- /dev/null +++ b/scripts/check-enhancement-declaration.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Validate host enhancement declarations against governance expectations. + +Checks that each registered host's declared_enhancements align with the +policy expectations derived from its support_tier and the consumption matrix +(see blueprint/design.md P4c-2 governance expectation table). + +Exit codes: + 0 — always (advisory check, never blocks) +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT)) + +from installer.hosts import iter_declared_hosts +from installer.models import EnhancementGroup, SupportTier + +ALL_GROUPS = frozenset(EnhancementGroup) + +TIER_EXPECTATIONS: dict[SupportTier, frozenset[EnhancementGroup]] = { + SupportTier.DEEP_VERIFIED: ALL_GROUPS, + SupportTier.BASELINE_SUPPORTED: frozenset({EnhancementGroup.CONTINUATION}), + SupportTier.EXPERIMENTAL: frozenset(), + SupportTier.DOCUMENTED_ONLY: frozenset(), +} + + +def check_host(host_id: str, tier: SupportTier, declared: frozenset[EnhancementGroup]) -> list[str]: + """Return advisory warnings for one host.""" + warnings: list[str] = [] + expected = TIER_EXPECTATIONS.get(tier, frozenset()) + + missing = expected - declared + if missing: + labels = ", ".join(sorted(g.value for g in missing)) + warnings.append(f"[{host_id}] tier={tier.value} expects [{labels}] but not declared") + + if tier == SupportTier.DEEP_VERIFIED and declared != ALL_GROUPS: + labels = ", ".join(sorted(g.value for g in ALL_GROUPS - declared)) + warnings.append(f"[{host_id}] deep_verified should declare all groups; missing: [{labels}]") + + return warnings + + +def main() -> int: + all_warnings: list[str] = [] + checked = 0 + + for cap in iter_declared_hosts(): + declared = frozenset(cap.declared_enhancements) + warnings = check_host(cap.host_id, cap.support_tier, declared) + all_warnings.extend(warnings) + checked += 1 + + status = "PASS" if not warnings else "WARN" + groups_str = ", ".join(g.value for g in cap.declared_enhancements) or "(none)" + print(f" {status} {cap.host_id} tier={cap.support_tier.value} enhancements=[{groups_str}]") + + print() + if all_warnings: + print(f"Advisory warnings ({len(all_warnings)}):") + for w in all_warnings: + print(f" ⚠ {w}") + print() + print("(advisory only — does not block)") + return 0 + + print(f"All {checked} host(s) pass enhancement declaration check.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_installer.py b/tests/test_installer.py index ae58b6b..b675c20 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -1262,36 +1262,20 @@ def test_codex_cn_prompt_install_keeps_workspace_preflight_contract(self) -> Non validate_host_install(CODEX_ADAPTER, home_root=home_root) prompt = (home_root / ".codex" / "AGENTS.md").read_text(encoding="utf-8") - self.assertIn("~/.codex/sopify/payload-manifest.json", prompt) - self.assertIn("~/.codex/sopify/helpers/bootstrap_workspace.py --workspace-root ", prompt) - self.assertIn("缺少或不满足兼容要求的 `.sopify-runtime/manifest.json`", prompt) - self.assertIn("第一步必须先执行 runtime gate", prompt) - self.assertIn("只作为 thin stub", prompt) - self.assertIn("selected global bundle", prompt) - self.assertIn("workspace preflight contract", prompt) - self.assertIn("runtime_gate_entry", prompt) - self.assertIn("scripts/runtime_gate.py enter --workspace-root --request \"\"", prompt) - self.assertIn("不得绕过 gate 直接调用 `scripts/sopify_runtime.py`", prompt) - self.assertIn("allowed_response_mode == checkpoint_only", prompt) - self.assertIn("allowed_response_mode == error_visible_retry", prompt) - self.assertIn("preferences_preload_entry", prompt) - self.assertIn("scripts/preferences_preload_runtime.py inspect --workspace-root ", prompt) - self.assertIn("fail-open with visibility", prompt) - self.assertIn("当前任务明确要求 > `preferences.md` > 默认规则", prompt) - self.assertIn("不得自行读取 `preferences.md` 原文做二次拼装", prompt) - self.assertIn("ROOT_CONFIRM_REQUIRED", prompt) - self.assertIn("activation_root", prompt) - self.assertIn("默认推荐“当前目录”", prompt) - self.assertIn("这一类返回属于 pre-runtime checkpoint", prompt) - self.assertIn("`allowed_response_mode` 应为 `checkpoint_only`", prompt) - self.assertIn("`~go init` 不得绕过这一步", prompt) - self.assertIn("scripts/develop_callback_runtime.py", prompt) - self.assertIn("resume_context", prompt) - self.assertIn("不得自由追问", prompt) - self.assertIn("不得手写 `current_decision.json / current_handoff.json`", prompt) - self.assertIn("scripts/develop_callback_runtime.py submit --payload-json ...", prompt) - self.assertIn("即使用户显式输入 `~go exec`", prompt) - self.assertIn("必须继续遵守对应 checkpoint 的机器契约", prompt) + # Gate-first obligation (§8.1) + self.assertIn("runtime gate", prompt) + self.assertIn("protocol.md §8.1", prompt) + self.assertIn("allowed_response_mode", prompt) + # Handoff-first obligation (§8.2) + self.assertIn("current_handoff.json", prompt) + self.assertIn("protocol.md §8.2", prompt) + self.assertIn("required_host_action", prompt) + # No self-routing / no truth-writing (§8.3) + self.assertIn("protocol.md §8.3", prompt) + # Host Integration Contract ref (§8) + self.assertIn("protocol.md §8", prompt) + # Runtime helper index ref (§8.4–8.5) + self.assertIn("protocol.md §8.4", prompt) def test_codex_cn_installed_prompt_assets_keep_footer_contract(self) -> None: # Footer contract aligned: replay reference removed from source and assertion. @@ -1316,36 +1300,20 @@ def test_claude_en_prompt_install_keeps_workspace_preflight_contract(self) -> No validate_host_install(CLAUDE_ADAPTER, home_root=home_root) prompt = (home_root / ".claude" / "CLAUDE.md").read_text(encoding="utf-8") - self.assertIn("~/.claude/sopify/payload-manifest.json", prompt) - self.assertIn("~/.claude/sopify/helpers/bootstrap_workspace.py --workspace-root ", prompt) - self.assertIn("does not yet have a compatible `.sopify-runtime/manifest.json`", prompt) - self.assertIn("first step must be the runtime gate", prompt) - self.assertIn("only a thin stub", prompt) - self.assertIn("selected global bundle", prompt) - self.assertIn("workspace-preflight contract", prompt) - self.assertIn("runtime_gate_entry", prompt) - self.assertIn("scripts/runtime_gate.py enter --workspace-root --request \"\"", prompt) - self.assertIn("must not bypass the gate", prompt) - self.assertIn("allowed_response_mode == checkpoint_only", prompt) - self.assertIn("allowed_response_mode == error_visible_retry", prompt) - self.assertIn("preferences_preload_entry", prompt) - self.assertIn("scripts/preferences_preload_runtime.py inspect --workspace-root ", prompt) - self.assertIn("fail-open with visibility", prompt) - self.assertIn("current explicit task > preferences.md > default rules", prompt) - self.assertIn("never re-read `preferences.md` to rebuild the prompt block manually", prompt) - self.assertIn("ROOT_CONFIRM_REQUIRED", prompt) - self.assertIn("activation_root", prompt) - self.assertIn("recommend the current directory", prompt) - self.assertIn("This outcome is a pre-runtime checkpoint", prompt) - self.assertIn("`allowed_response_mode` must be `checkpoint_only`", prompt) - self.assertIn("must not bypass this step", prompt) - self.assertIn("scripts/develop_callback_runtime.py", prompt) - self.assertIn("resume_context", prompt) - self.assertIn("must not ask a free-form question", prompt) - self.assertIn("hand-write `current_decision.json / current_handoff.json`", prompt) - self.assertIn("scripts/develop_callback_runtime.py submit --payload-json ...", prompt) - self.assertIn("Even when the user explicitly types `~go exec`", prompt) - self.assertIn("must still honor the machine contract", prompt) + # Gate-first obligation (§8.1) + self.assertIn("runtime gate", prompt) + self.assertIn("protocol.md §8.1", prompt) + self.assertIn("allowed_response_mode", prompt) + # Handoff-first obligation (§8.2) + self.assertIn("current_handoff.json", prompt) + self.assertIn("protocol.md §8.2", prompt) + self.assertIn("required_host_action", prompt) + # No self-routing / no truth-writing (§8.3) + self.assertIn("protocol.md §8.3", prompt) + # Host Integration Contract ref (§8) + self.assertIn("protocol.md §8", prompt) + # Runtime helper index ref (§8.4–8.5) + self.assertIn("protocol.md §8.4", prompt) def test_claude_en_installed_prompt_assets_keep_footer_contract(self) -> None: # Footer contract aligned: replay reference removed from source and assertion. diff --git a/tests/test_installer_status_doctor.py b/tests/test_installer_status_doctor.py index ff66ba8..a74d8d1 100644 --- a/tests/test_installer_status_doctor.py +++ b/tests/test_installer_status_doctor.py @@ -714,6 +714,108 @@ def test_doctor_cli_json_output_contains_checks_and_summary(self) -> None: self.assertIn("summary", payload) + def test_status_text_renders_human_labels_not_raw_taxonomy(self) -> None: + with tempfile.TemporaryDirectory() as home_dir, tempfile.TemporaryDirectory() as workspace_dir: + home_root = Path(home_dir) + workspace_root = Path(workspace_dir) + state_root = workspace_root / ".sopify-skills" / "state" + _write_json( + state_root / "current_run.json", + {"run_id": "run-1", "stage": "clarification_pending", "status": "active", "plan_id": "p", "plan_path": ".sopify-skills/plan/p"}, + ) + _write_json( + state_root / "current_handoff.json", + {"run_id": "run-1", "required_host_action": "answer_questions"}, + ) + + install_host_assets(CODEX_ADAPTER, repo_root=REPO_ROOT, home_root=home_root, language_directory="CN") + install_global_payload(CODEX_ADAPTER, repo_root=REPO_ROOT, home_root=home_root) + + status_payload = build_status_payload(home_root=home_root, workspace_root=workspace_root) + rendered = render_status_text(status_payload) + + self.assertIn("current_run_stage: awaiting info", rendered) + self.assertIn("pending_checkpoint: awaiting supplemental info", rendered) + self.assertNotIn("clarification_pending", rendered) + self.assertNotIn("answer_questions", rendered) + + def test_status_text_renders_mapped_checkpoint_labels(self) -> None: + with tempfile.TemporaryDirectory() as home_dir, tempfile.TemporaryDirectory() as workspace_dir: + home_root = Path(home_dir) + workspace_root = Path(workspace_dir) + _seed_workspace_state(workspace_root) + + install_host_assets(CODEX_ADAPTER, repo_root=REPO_ROOT, home_root=home_root, language_directory="CN") + install_global_payload(CODEX_ADAPTER, repo_root=REPO_ROOT, home_root=home_root) + + status_payload = build_status_payload(home_root=home_root, workspace_root=workspace_root) + rendered = render_status_text(status_payload) + + self.assertIn("pending_checkpoint: ready to continue", rendered) + self.assertNotIn("continue_host_develop", rendered) + + def test_status_text_state_conflict_shows_explanation_not_raw_code(self) -> None: + payload: dict[str, object] = { + "schema_version": "2", + "hosts": [], + "state": {"overall_status": "partial"}, + "workspace_state": { + "requested": True, + "root": "/tmp/ws", + "sopify_skills_present": True, + "active_plan": "some_plan", + "current_run_stage": "executing", + "pending_checkpoint": None, + "quarantine_count": 0, + "quarantined_items": [], + "state_conflicts": [ + { + "code": "run_stage_handoff_mismatch", + "path": "state/current_run.json", + "explanation": "The persisted run stage and handoff action disagree about the current checkpoint.", + } + ], + "runtime_notes": [], + }, + } + rendered = render_status_text(payload) + + self.assertIn("state_conflict_count: 1", rendered) + self.assertIn("The persisted run stage and handoff action disagree", rendered) + self.assertNotIn("run_stage_handoff_mismatch", rendered) + + def test_doctor_text_state_conflict_evidence_shows_explanation_not_raw_code(self) -> None: + payload: dict[str, object] = { + "schema_version": "2", + "hosts": [], + "state": {"overall_status": "partial"}, + "workspace_state": { + "requested": True, + "root": "/tmp/ws", + "sopify_skills_present": True, + "active_plan": "some_plan", + "current_run_stage": "executing", + "pending_checkpoint": None, + "quarantine_count": 0, + "quarantined_items": [], + "state_conflicts": [ + { + "code": "run_stage_handoff_mismatch", + "path": "state/current_run.json", + "explanation": "The persisted run stage and handoff action disagree about the current checkpoint.", + } + ], + "runtime_notes": [], + }, + } + from installer.inspection import _runtime_workspace_checks + checks = _runtime_workspace_checks(payload["workspace_state"]) + conflict_check = next(c for c in checks if c.check_id == "workspace_runtime_state_conflict") + for evidence_line in conflict_check.evidence: + self.assertNotRegex(evidence_line, r"^run_stage_handoff_mismatch") + self.assertIn("disagree about the current checkpoint", evidence_line) + + def _run_script(entrypoint, argv: list[str]) -> str: from io import StringIO from contextlib import redirect_stdout diff --git a/tests/test_runtime_output_rendering.py b/tests/test_runtime_output_rendering.py new file mode 100644 index 0000000..e744dca --- /dev/null +++ b/tests/test_runtime_output_rendering.py @@ -0,0 +1,287 @@ +# Test classification: contract +"""Regression tests for runtime output rendering (P4c surface cleanup).""" + +from __future__ import annotations + +import sys +import unittest +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Mapping, Optional + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from runtime._models.handoff import RecoveredContext, RuntimeHandoff, RuntimeResult +from runtime._models.core import RouteDecision +from runtime.output import render_runtime_output, _execution_gate_line, _GATE_STATUS_DISPLAY, _status_symbol +from runtime.gate_output import render_gate_text + + +def _minimal_route(route_name: str = "plan_only", **kwargs: Any) -> RouteDecision: + defaults: dict[str, Any] = { + "route_name": route_name, + "request_text": "test request", + "reason": "test reason", + } + defaults.update(kwargs) + return RouteDecision(**defaults) + + +def _minimal_result( + route_name: str = "plan_only", + loaded_files: tuple[str, ...] = (), + **kwargs: Any, +) -> RuntimeResult: + route_kwargs = {} + result_kwargs = {} + for k, v in kwargs.items(): + if k in {"reason", "request_text", "active_run_action"}: + route_kwargs[k] = v + else: + result_kwargs[k] = v + return RuntimeResult( + route=_minimal_route(route_name, **route_kwargs), + recovered_context=RecoveredContext(loaded_files=loaded_files), + **result_kwargs, + ) + + +class TestContextBlockRendering(unittest.TestCase): + """F6-regression: loaded_files must appear as a Context block, not disappear.""" + + def test_loaded_files_render_as_context_section(self) -> None: + result = _minimal_result(loaded_files=("plan/summary.md", "blueprint/README.md")) + rendered = render_runtime_output( + result, brand="test", language="en-US", title_color="none", use_color=False, + ) + self.assertIn("Context: 2 files", rendered) + self.assertIn("plan/summary.md", rendered) + self.assertIn("blueprint/README.md", rendered) + + def test_empty_loaded_files_omits_context_section(self) -> None: + result = _minimal_result(loaded_files=()) + rendered = render_runtime_output( + result, brand="test", language="en-US", title_color="none", use_color=False, + ) + self.assertNotIn("Context:", rendered) + + def test_loaded_files_not_mixed_into_changes(self) -> None: + result = _minimal_result(loaded_files=("plan/summary.md",)) + rendered = render_runtime_output( + result, brand="test", language="en-US", title_color="none", use_color=False, + ) + changes_idx = rendered.index("Changes:") + context_idx = rendered.index("Context:") + self.assertLess(context_idx, changes_idx, "Context block should appear before Changes") + + +def _handoff_with_gate(gate_status: str) -> RuntimeHandoff: + return RuntimeHandoff( + schema_version="1", + route_name="plan_only", + run_id="test-run", + artifacts={"execution_gate": {"gate_status": gate_status}}, + ) + + +class TestGateStatusFallback(unittest.TestCase): + """F6-regression: unknown gate_status must not leak raw internal codes.""" + + def test_known_status_renders_display_label(self) -> None: + for language, expected in (("en-US", "Ready"), ("zh-CN", "就绪")): + result = _minimal_result(handoff=_handoff_with_gate("ready")) + line = _execution_gate_line(result, language) + self.assertIn(expected, line, msg=f"language={language}") + + def test_unknown_gate_status_falls_back_to_blocked_en(self) -> None: + result = _minimal_result(handoff=_handoff_with_gate("experimental_new_state_xyz")) + line = _execution_gate_line(result, "en-US") + self.assertIn("Blocked", line) + self.assertNotIn("experimental_new_state_xyz", line) + + def test_unknown_gate_status_falls_back_to_blocked_zh(self) -> None: + result = _minimal_result(handoff=_handoff_with_gate("experimental_new_state_xyz")) + line = _execution_gate_line(result, "zh-CN") + self.assertIn("阻断", line) + self.assertNotIn("experimental_new_state_xyz", line) + + +class TestGateOutputNoRouteExposure(unittest.TestCase): + """F6-regression: render_gate_text must not expose internal route names.""" + + def test_route_field_not_in_gate_text(self) -> None: + payload: dict[str, Any] = { + "status": "ready", + "allowed_response_mode": "unrestricted", + "runtime": { + "route_name": "workflow", + "reason": "plan matched", + }, + } + text = render_gate_text(payload) + for line in text.splitlines(): + self.assertFalse( + line.strip().startswith("route:"), + msg=f"route: field leaked in gate text line: {line!r}", + ) + + def test_reason_still_visible_in_gate_text(self) -> None: + payload: dict[str, Any] = { + "status": "ready", + "runtime": { + "route_name": "plan_only", + "reason": "plan matched", + }, + } + text = render_gate_text(payload) + self.assertIn("reason: plan matched", text) + + def test_gate_text_without_runtime_section(self) -> None: + payload: dict[str, Any] = {"status": "error"} + text = render_gate_text(payload) + self.assertNotIn("route:", text) + self.assertIn("status: error", text) + + +class TestNextHintMapping(unittest.TestCase): + """3a.2-regression: Next hint uses handoff_kind, not route_name.""" + + def _render_next(self, handoff_kind: str, route_name: str = "plan_only", **handoff_kw: Any) -> str: + handoff = RuntimeHandoff( + schema_version="1", route_name=route_name, run_id="test", + handoff_kind=handoff_kind, **handoff_kw, + ) + result = _minimal_result(route_name=route_name, handoff=handoff) + rendered = render_runtime_output( + result, brand="test", language="en-US", title_color="none", use_color=False, + ) + for line in rendered.splitlines(): + if line.startswith("Next:"): + return line + return "" + + def test_develop_kinds_all_map_to_workflow_hint(self) -> None: + for route in ("quick_fix", "resume_active", "exec_plan"): + hint = self._render_next("develop", route_name=route) + self.assertIn("downstream stages", hint, msg=f"route={route}") + + def test_plan_kind_maps_to_plan_hint(self) -> None: + hint = self._render_next("plan") + self.assertIn("plan review", hint) + + def test_clarification_kind_maps_to_answer_hint(self) -> None: + hint = self._render_next("clarification", route_name="clarification_pending") + self.assertIn("missing facts", hint) + + def test_decision_kind_maps_to_decision_hint(self) -> None: + hint = self._render_next("decision", route_name="decision_pending") + self.assertIn("confirm", hint) + + def test_consult_kind_maps_to_consult_hint(self) -> None: + hint = self._render_next("consult", route_name="consult") + self.assertIn("discussion", hint) + + def test_reject_kind_maps_to_reject_hint(self) -> None: + hint = self._render_next("reject", route_name="proposal_rejected") + self.assertIn("rejected", hint) + + def test_archive_completed_maps_to_success_hint(self) -> None: + hint = self._render_next( + "archive", route_name="archive_lifecycle", + artifacts={"archive_receipt_status": "completed"}, + ) + self.assertIn("Review", hint) + + def test_archive_incomplete_maps_to_retry_hint(self) -> None: + hint = self._render_next("archive", route_name="archive_lifecycle") + self.assertIn("retry", hint) + + def test_state_conflict_active_maps_to_conflict_hint(self) -> None: + hint = self._render_next( + "state_conflict", route_name="state_conflict", + required_host_action="resolve_state_conflict", + ) + self.assertIn("cancel", hint) + + def test_state_conflict_cleared_maps_to_continue_hint(self) -> None: + hint = self._render_next( + "state_conflict", route_name="state_conflict", + required_host_action="continue_host_develop", + ) + self.assertIn("downstream stages", hint) + + +class TestStatusSymbolMapping(unittest.TestCase): + """3a.1-regression: _status_symbol uses _FAMILY_SYMBOL table with edge overrides.""" + + def test_completion_with_artifact_returns_check(self) -> None: + from runtime._models.artifacts import PlanArtifact + artifact = PlanArtifact( + plan_id="p1", title="t", summary="s", level="light", + path="plan.md", files=(), created_at="2026-01-01", + ) + result = _minimal_result("plan_only", plan_artifact=artifact) + self.assertEqual(_status_symbol(result), "✓") + + def test_completion_without_artifact_returns_warning(self) -> None: + result = _minimal_result("plan_only") + self.assertEqual(_status_symbol(result), "!") + + def test_archive_without_artifact_returns_warning(self) -> None: + result = _minimal_result("archive_lifecycle") + self.assertEqual(_status_symbol(result), "!") + + def test_conflict_abort_no_payload_returns_check(self) -> None: + handoff = RuntimeHandoff( + schema_version="1", route_name="state_conflict", run_id="test", + handoff_kind="state_conflict", artifacts={}, + ) + result = _minimal_result("state_conflict", active_run_action="abort_conflict", handoff=handoff) + self.assertEqual(_status_symbol(result), "✓") + + def test_conflict_with_payload_returns_warning(self) -> None: + handoff = RuntimeHandoff( + schema_version="1", route_name="state_conflict", run_id="test", + handoff_kind="state_conflict", + artifacts={"state_conflict": {"code": "c1", "message": "m"}}, + ) + result = _minimal_result("state_conflict", handoff=handoff) + self.assertEqual(_status_symbol(result), "!") + + def test_pending_returns_question(self) -> None: + result = _minimal_result("clarification_pending") + self.assertEqual(_status_symbol(result), "?") + + def test_action_returns_warning(self) -> None: + result = _minimal_result("workflow") + self.assertEqual(_status_symbol(result), "!") + + +class TestExecutionGateHandoffOnly(unittest.TestCase): + """3a.6-regression: _execution_gate reads only handoff artifacts.""" + + def test_no_handoff_returns_missing(self) -> None: + result = _minimal_result("plan_only") + line = _execution_gate_line(result, "en-US") + self.assertIn("not generated", line) + + def test_handoff_with_gate_renders_status(self) -> None: + handoff = _handoff_with_gate("ready") + result = _minimal_result("plan_only", handoff=handoff) + line = _execution_gate_line(result, "en-US") + self.assertIn("Ready", line) + + def test_handoff_without_gate_returns_missing(self) -> None: + handoff = RuntimeHandoff( + schema_version="1", route_name="plan_only", run_id="test", + artifacts={}, + ) + result = _minimal_result("plan_only", handoff=handoff) + line = _execution_gate_line(result, "en-US") + self.assertIn("not generated", line) + + +if __name__ == "__main__": + unittest.main()