diff --git a/.gitignore b/.gitignore index 37f187e..c962ed6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ tocodex-docs/ # this private tree and must never be allowlisted — tracked files bypass these # rules until `git rm --cached`. /docs/** +!/docs/AGENTS.md !/docs/lua-configuration.md !/docs/math-rendering-test.md !/docs/chemistry-rendering-test.md diff --git a/AGENTS.md b/AGENTS.md index 653ed2e..c7d37df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,32 +1,21 @@ # Nebula 项目代理指引 -## 强制工程规范 +## 全局规则 - 修改前阅读 `CONTRIBUTING.md`、`docs/architecture.md` 和 `docs/project-constraints.md`;新 UI 文案同时遵循 `docs/internationalization.md`。 +- 规则按目录分层:先遵守本文件,再读取目标文件路径上最近的 `AGENTS.md`。模块细节留在模块目录,不回填到根规则。 - 行数预算与依赖方向由 `architecture/` 声明,使用 `python3 scripts/check_architecture.py --base ` 验证;800 行仅提示,2000 行是既有防灾上限。 - 禁止靠抬高预算、删测试、压缩格式、机械分片或静默排除目录让门禁通过。规范存在缺陷时,先给可复现反例,补正反测试并记录经维护者审查的规则修订。 - 核心规则保持单一权威实现;按职责和生命周期拆分,不因文件夹名称或历史模块别名推断耦合。新增长期依赖、跨层接口、持久化和线程模型变更须给出设计依据。 - 普通翻译查询维持已测零分配合同;参数格式化和冷路径另行评估。不能把单机微基准写成普遍性能保证。 - 不得声称仅加入 workflow 或 `CODEOWNERS` 就已启用 GitHub 强制保护;服务端设置及负例 PR 验证须另行获准并核验。 +- `docs/` 记录经过核验的当前事实;非平凡改动的决策、被否定方案和事故因果写入与代码职责同路径的 `architecture/notes/`。遵循 [note 规则](architecture/notes/AGENTS.md),普通样式修改和单文件修复不写 note。 -## 发布任务的持久化上下文 +## 分层入口 -- 处理版本号、构建、打包、自动更新、GitHub Release、Changelog 或 Release Note 前,必须先完整阅读 [`.agents/memory.md`](.agents/memory.md) 的发布章节。 -- `.agents/memory.md` 是经过实际发布核验的项目记忆,不是临时推理草稿。只有在代码、脚本、GitHub 元数据或真实运行结果已经核实后才能更新;过时结论必须直接修正并注明新的核验日期。 -- 发布相关命令必须使用 UTF-8。工作区可能包含用户的未跟踪探针、构建目录和截图,只能显式暂存本次文件,不得用清理、全量暂存或重置命令处理它们。 - -## Release Note 原则 - -1. 发布说明只写已经实现并有代码、构建或真实运行证据的用户可感知变化。调试探针、测试用强制弹窗、内部重构和未经验证的推断不得写成已交付功能。 -2. 沿用 `v1.3.0` 的双语结构:English 的 `Added / Fixed / Improved`、中文的 `新增 / 修复 / 改进`、`Contributors` 和最终资产 `SHA256`。中英文必须表达同一事实,不能一侧增加能力或承诺。 -3. 同一发布的 `CHANGELOG.md`、`docs/release-notes/vX.Y.Z.md` 和 GitHub Release 正文必须同步。应在正式构建前完成 Changelog,因为 ZIP 和安装器会把它打进产物。 -4. Issue 引用必须先读取对应 Issue 的标题、正文、状态和必要评论,再挂到最准确的一条说明上。禁止按编号猜测、复用旧版本链接,或把宽泛需求说成逐项 Bug 报告。 -5. 英文引用使用 `Addresses [#N](https://github.com/Kuddev/nebula/issues/N).`,中文使用 `对应 [#N](https://github.com/Kuddev/nebula/issues/N)。`。一个 Issue 涵盖多项紧密修复时,优先只在最具代表性的条目引用一次。 -6. 文案以用户结果为主,例如“100% 不透明度不再回到 82%”,不要用内部类型名、辅助函数或提交标题代替行为说明。仍开放或只部分覆盖的问题不得宣称彻底解决。 - -## 发布安全边界 - -- 正式发布不得使用 `-SkipBuild` 或 `-AllowStale` 绕过新鲜度检查,也不得把 workspace 默认构建产生的 legacy shell 当作 GPUI 产品包。 -- 不得因为某个磁盘空间更大就擅自使用它。构建和输出目录必须位于用户明确允许的路径;当前环境尤其不得操作 `E:\`。 -- GitHub 推送使用普通非强制推送。发布后若只有说明文档修订,可同步分支和 Release 正文,但不得擅自移动已经对应二进制的版本标签。 -- 发布结束前必须核验 Release 标题、标签、正文、资产真实文件名、空资产标签、大小、SHA256、分支与版本标签指向;不能只以创建命令返回成功作为完成依据。 +- 应用、窗口和 UI:遵循 [`nebula_app/AGENTS.md`](nebula_app/AGENTS.md)。 +- 终端核心、PTY、VT 和输入:遵循 [`nebula_terminal/AGENTS.md`](nebula_terminal/AGENTS.md)。 +- 设置、持久化和语言注册:遵循 [`nebula_settings/AGENTS.md`](nebula_settings/AGENTS.md)。 +- 检查器、门禁和治理脚本:遵循 [`scripts/AGENTS.md`](scripts/AGENTS.md)。 +- 文档事实与操作手册:遵循 [`docs/AGENTS.md`](docs/AGENTS.md)。 +- 版本号、构建、打包、自动更新、GitHub Release、Changelog 或 Release Note:先完整阅读 [`packaging/AGENTS.md`](packaging/AGENTS.md)、[`docs/release-notes/AGENTS.md`](docs/release-notes/AGENTS.md) 和本地 `.agents/memory.md` 的发布章节。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f568c83..9fb8726 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,8 @@ when changing them. Internal Rust crate and source-directory names remain stable - [Enforced contracts and review rules](docs/project-constraints.md) - UI changes must also satisfy the **UI 设计约束 / UI design constraints** chapter in that document: interaction states, result feedback, hit targets and visual acceptance. -- [Decision log and policy change process](docs/architecture-decisions.md) +- [Causal note policy](architecture/notes/AGENTS.md) for new non-trivial decisions +- [Legacy decision archive](docs/architecture-decisions.md) for accepted historical context - [Evidence behind the rules](docs/engineering-evidence.md) - [Internationalization](docs/internationalization.md) when changing UI text @@ -69,7 +70,8 @@ raise any existing tracked private artifact for an explicit maintainer decision. 1. State the user-visible problem and the behavior to preserve. Discuss a new cross-layer dependency, persisted format, core abstraction or threading model - before implementing it. Record significant decisions in the decision log. + before implementing it. Record significant decisions under the owning path in + `architecture/notes/`; ordinary fixes do not create a note. 2. Keep one conceptual change per PR. A necessary extraction and its behavior tests may accompany the feature; unrelated rewrites and formatting may not. 3. Put shared rules in their existing authority. UI modules adapt those rules; diff --git a/architecture/notes/AGENTS.md b/architecture/notes/AGENTS.md new file mode 100644 index 0000000..4863be9 --- /dev/null +++ b/architecture/notes/AGENTS.md @@ -0,0 +1,35 @@ +# Causal note rules + +## Purpose + +- `docs/` describes verified current behavior, architecture and operating procedures. +- `architecture/notes/` records why a non-trivial decision exists: context, rejected alternatives, consequences, evidence and replacement conditions. +- Plans, task lists and temporary work logs do not belong here. + +## When to write + +Write a note for dependency direction, core ownership, persistent formats, protocols, threading/lifetime, important performance contracts, governance changes or a cross-layer incident whose cause is likely to be forgotten. Do not write one for styling, ordinary single-file fixes, mechanical refactors or routine dependency updates. + +## Location and history + +- Mirror the owning code path, for example `architecture/notes/nebula_terminal/input/2026-09-19-example.md`. +- Use `YYYY-MM-DD-kebab-case.md`. Keep one decision or incident per file. +- Do not create a global `INDEX.md`. The nearest module `AGENTS.md` points agents to the relevant directory. +- Once accepted, do not rewrite the old rationale into a new conclusion. Create a new note with `Supersedes`; the old note may receive only a short `Superseded by` pointer. +- Notes are reviewed repository content and contain only rationale and evidence intended for repository review. + +## Required structure + +Each decision note stays at or below 200 physical lines and contains: + +1. `Status` +2. `Context` +3. `Evidence` +4. `Decision` +5. `Rejected alternatives` +6. `Consequences` +7. `Validation` +8. `Supersedes` +9. `Revisit when` + +State `None` where a relation does not exist. Link authoritative code, tests or factual docs instead of copying API inventories. Tests protect current behavior; the note preserves the reason and the discarded paths. diff --git a/architecture/notes/governance/2026-09-19-layered-rules-and-causal-notes.md b/architecture/notes/governance/2026-09-19-layered-rules-and-causal-notes.md new file mode 100644 index 0000000..7591be7 --- /dev/null +++ b/architecture/notes/governance/2026-09-19-layered-rules-and-causal-notes.md @@ -0,0 +1,81 @@ +# Layered agent rules and causal notes + +- Date: 2026-09-19 +- Scope: repository governance and agent context +- Owner: maintainer review + +## Status + +Accepted by direct maintainer instruction; implementation is included with this note. + +## Context + +The repository had one root `AGENTS.md`. Release-note formatting, release safety, +architecture gates, UI behavior and terminal-core constraints were all loaded for +every task. Verified facts, a consolidated ADR log, plans, temporary work logs and +hard lessons also shared the broad `docs/` area despite different lifecycles. + +## Evidence + +- Only the root `AGENTS.md` existed before this change. +- `docs/architecture-decisions.md` had grown into one consolidated decision file. +- Local planning, handoff and hard-lesson files used `docs/` despite having causal + or temporary lifecycles. +- Existing local-only agent context already followed a separate lifecycle outside + the reviewed engineering documents. +- Existing architecture contracts already required non-trivial decisions, + alternatives, consequences, validation and revisit conditions. + +## Decision + +Keep the root guide limited to repository-wide invariants and task entry points. +Place responsibility-specific rules in the nearest owning directory. Split release +build safety from release-note wording. Store sanitized, reviewable causal records +under `architecture/notes/`, mirroring the owning code path and without a global +index. Local-only operational context keeps its existing lifecycle. + +Current factual documents continue to update in place. Accepted decision notes keep +their original rationale; a later design creates a new note and declares the +supersession relationship. Ordinary fixes and styling changes do not create notes. + +The existing consolidated ADR and hard-lesson files remain historical material. +They are not bulk-rewritten or mechanically split; relevant history migrates only +when a future non-trivial change needs a new note. + +## Rejected alternatives + +- Keep adding every rule to root `AGENTS.md`: unrelated tasks continue paying the + context cost and module-specific instructions remain harder to discover. +- Rewrite all historical ADRs immediately: this creates a large conflict-prone + migration and risks changing the meaning of accepted decisions. +- Keep every causal note local and ignored: collaborators and clean clones lose the + reviewed reasoning behind shared code. +- Maintain one global notes index: parallel branches must edit the same file for + unrelated decisions, creating predictable merge conflicts. +- Put every plan and small fix in notes: the directory becomes a chronological + activity log instead of a durable causal record. + +## Consequences + +Agents must read the root guide plus the nearest module guide. New modules add an +`AGENTS.md` only when they have distinct contracts. Notes contain only information +intended for repository review. The old ADR log stays readable but is no longer the +required destination for new decisions. + +## Validation + +Governance tests verify that layered guides and the note policy are tracked, +decision notes use dated module paths, contain all required sections, stay within +the scoped line limit and do not introduce a global index. Positive and negative +note fixtures lock the checker behavior. + +## Supersedes + +Supersedes the storage process in the `Process` section of the consolidated +`docs/architecture-decisions.md`; it does not rewrite or invalidate ADR-0001 onward. + +## Revisit when + +Revisit the 200-line note limit only with a reproducible legitimate note that cannot +remain clear through links or a scoped evidence file. Revisit directory ownership +when a module boundary changes in `docs/architecture.md`. diff --git a/architecture/notes/nebula_app/ai_hook/2026-09-19-agent-activity-authority.md b/architecture/notes/nebula_app/ai_hook/2026-09-19-agent-activity-authority.md new file mode 100644 index 0000000..3cabea2 --- /dev/null +++ b/architecture/notes/nebula_app/ai_hook/2026-09-19-agent-activity-authority.md @@ -0,0 +1,93 @@ +# Agent activity authority across local and SSH panes + +## Status + +Implemented on 2026-09-19; reviewed with the terminal activity repair. This note +records ownership and fallback decisions, not a release or live-provider claim. + +## Context + +Separate UI state machines let screen text reopen attention after a hook had +completed a turn. Ordinary output containing `y/n` could look like a blocking +question, while completed shell commands could leave foreground identity or +progress behind. A remote pane adds another boundary: local process absence and +output silence cannot establish that its remote command finished. + +## Evidence + +The [shared lifecycle and regressions](../../../../nebula_app/src/ai_hook/lifecycle.rs) +cover conflicting hook/screen observations, completion and input barriers. +[Provider contracts](../../../../nebula_app/src/ai_hook/bridges.rs) distinguish +complete lifecycle/attention bridges from partial and legacy completion bridges. +[UI regressions](../../../../nebula_app/src/gpui_shell/terminal/view/activity_tests.rs) +exercise command, Agent and progress transitions together. + +## Decision + +One pane activity owner decides state. Provider adapters normalize bounded facts; +UI adapters validate routing and ownership, pass the shared ordering gate, apply +the event and project its effects. A rejected observation cannot advance ordering. + +Capabilities belong to the bridge actually installed. Complete hooks own the +states they report. A partial bridge may use a current input control near the +prompt only for a capability it lacks; ordinary output words have no authority. +This preserves genuine questions from partial bridges without allowing loose +screen matches to override a complete hook. Native event contracts must be +declared, and a real native event suppresses legacy notify duplicates per session. + +Shell completion clears foreground identity, Agent state and progress together. +Local process-probe errors remain unknown; child absence alone cannot finish a +shell builtin. Remote completion requires a remote lifecycle event, OSC 133 or a +pending command's known empty prompt. Silence, BEL and host-process absence are +not remote completion evidence. + +Remote installation uses the same owned edit policy as local installation. +[The planner](../../../../nebula_app/src/ai_hook/remote.rs) owns edits and removal; +[SSH orchestration](../../../../nebula_app/src/ssh_session/integration.rs) owns +authenticated exec/PTY operations. Per-channel tokens are passed at startup and +not persisted in remote files or made dependent on AcceptEnv. Remote identity is +PID plus start epoch, separate from host PIDs. Foreign, subagent and late-session +events are rejected before ordering; compaction preserves the active turn. + +## Rejected alternatives + +- More per-view flags would preserve competing state authorities. +- Treating every provider as a complete hook provider would lose real questions + from partial bridges; accepting every screen match would recreate false alarms. +- Remote silence and local process snapshots cannot distinguish remote work from + a finished command and therefore cannot replace remote lifecycle evidence. +- Replacing whole user settings or trust databases would exceed integration + ownership and make removal destructive. + +## Consequences + +Remote manifests record owned hook groups, chained notify settings and asset +hashes. Setup locking, compare-and-swap writes and rollback protect user edits; +removal restores owned settings and persists an opt-out. Integration rejection +falls back to an ordinary SSH shell, with a fresh channel after rejected exec. +Existing hook trust reviews and explicit opt-outs remain authoritative. + +Complete-hook sessions skip screen classification. Partial sessions inspect the +existing bounded tail. Remote senders and event caches have bounded inputs, +ordering state and execution lifetimes. No new crate or long-lived service is +introduced. Supported shell/Python details remain authoritative in the installer +and its tests rather than a second documentation inventory. + +## Validation + +Tests cover lifecycle authority, replay/late events, session isolation, native and +legacy notify coexistence, install conflicts, rollback and removal. Remote tests +exercise exec fallback, token routing, controlling-TTY delivery and shell prompt +state preservation. Both UI adapters consume the shared rules. Live authenticated +model turns and visual acceptance remain separate from these protocol and state +tests. + +## Supersedes + +None. This is the initial module-scoped record for the repair. + +## Revisit when + +A provider's verified hook contract changes, or a shell gains stronger lifecycle +signals. Missing remote identity still cannot authorize a session switch using +only cwd or screen text. diff --git a/architecture/notes/nebula_app/input/2026-09-19-modified-enter-compatibility.md b/architecture/notes/nebula_app/input/2026-09-19-modified-enter-compatibility.md new file mode 100644 index 0000000..1bcdcd2 --- /dev/null +++ b/architecture/notes/nebula_app/input/2026-09-19-modified-enter-compatibility.md @@ -0,0 +1,57 @@ +# Modified Enter compatibility at the terminal input boundary + +## Status + +Implemented on 2026-09-19. Native transport probes and input regressions support +the decision; they do not establish acceptance with every client version. + +## Context + +Claude clients without an enhanced keyboard protocol did not receive the expected +Shift+Enter newline. GPUI, legacy input and Runtime API could also choose different +encodings for the same terminal client. + +## Evidence + +The Windows ConPTY probe receives CR from Shift+VK_RETURN even when UnicodeChar +is LF, while native ReadKey retains Shift. Direct LF compatibility and negotiated +CSI-u survive that transport. The +[shared input implementation and regressions](../../../../nebula_app/src/input/terminal_input.rs) +and [GPUI keymap tests](../../../../nebula_app/src/gpui_shell/terminal/keymap.rs) +cover the byte/key boundary. + +## Decision + +The shared terminal input adapter owns the compatibility decision for all callers. +A negotiated keyboard protocol takes precedence. An unnegotiated Claude client +receives LF for Shift+Enter. Native Windows clients retain native key identity and +modifiers. Modified Enter never submits a shell-history entry. + +## Rejected alternatives + +- A separate special case in each UI would let the encodings drift again. +- Replacing every modified Enter with LF would discard native key identity and + bypass an explicitly negotiated keyboard protocol. +- Assuming UnicodeChar alone determines ConPTY output contradicts the native + input-record probe. + +## Consequences + +The fallback is scoped to the client and protocol state that need it. It does not +change persisted shortcuts or require a new input thread, dependency or terminal +protocol. UI adapters remain responsible for forwarding their actual key facts. + +## Validation + +Regressions verify unnegotiated newline bytes, negotiated protocol precedence, +native Windows modifiers and the command-history boundary. The GPUI tests exercise +the key-to-PTY route; the shared adapter is compiled for both shell configurations. + +## Supersedes + +None. This is the initial module-scoped record for the compatibility decision. + +## Revisit when + +Supported Claude clients negotiate the keyboard protocol consistently here. Remove +the narrow fallback when transport and client evidence make it unnecessary. diff --git a/architecture/notes/nebula_app/ssh_session/2026-09-19-system-agent-authentication.md b/architecture/notes/nebula_app/ssh_session/2026-09-19-system-agent-authentication.md new file mode 100644 index 0000000..7555b28 --- /dev/null +++ b/architecture/notes/nebula_app/ssh_session/2026-09-19-system-agent-authentication.md @@ -0,0 +1,101 @@ +# System SSH agent authentication and transport ownership + +## Status + +Implemented on 2026-09-19. This records the authentication repair and its limits; +it does not claim a release or acceptance with every credential-manager UI. + +## Context + +Auto had no system-agent step, so an identity held only by an agent failed both +saved connections and Test Connection. Legacy `agent` profiles decoded as Auto +but had lost that capability. Restoring platform APIs without compile-time guards +would repeat a cross-platform build defect. Treating a signer error as rejection +can leave the underlying SSH task waiting for a signature. + +## Evidence + +[The shared authentication plan](../../../../nebula_app/src/ssh_session.rs), +[native transport adapter](../../../../nebula_app/src/platform/ssh_agent.rs) and +[agent policy](../../../../nebula_app/src/ssh_session/agent.rs) define the current +boundaries. [Loopback and wire tests](../../../../nebula_app/src/ssh_session/agent/tests.rs) +exercise real SSH authentication with generated identities. The locked protocol +dependency supplies public-key, certificate and RSA signing support. + +## Decision + +One plan serves both entry points: explicit private keys, system agent, resolved +or default disk keys, saved password, keyboard-interactive, then a password prompt. +This keeps an explicitly selected key first while avoiding missing/default disk +keys consuming attempts before a usable agent. Deduplication spans both disk-key +groups. A draft password replaces the saved-password step rather than bypassing +the selected mode. Strict modes and the legacy profile migration retain their +existing semantics. + +The platform adapter owns endpoint selection, connection and bounded identity +enumeration. The SSH layer owns per-host selectors, the total discovery budget, +signing outcomes and diagnostics. Windows uses the standard OpenSSH pipe and then +Pageant; Unix uses `SSH_AUTH_SOCK`. APIs are compiled only for their platform. + +Each host, including a jump host, gets a fresh agent connection and its own identity +ranking. Public selectors rank matching identities first; comments do not affect +key equality. Certificate selectors match their underlying public key, but full +certificate blobs remain distinct from raw keys during duplicate suppression. +The agent helper never loads private material or persists identities. + +Discovery and public-selector reads have short, bounded budgets. Signing uses the +longer interactive authentication budget because a credential manager may request +confirmation. Test Connection and unattended jump authentication retain their +shorter outer authentication budget. Current values are defined in the policy and +lifecycle modules; confirmation time does not consume later discovery allowance. + +Unavailable, empty and server-rejected agents allow fallback. Partial success +continues to the second factor. A signer, session-channel or signing-timeout error +propagates immediately and drops the unpooled transport. Password retries, changing +agents or merely enqueueing Disconnect cannot repair a session awaiting Signed. + +## Rejected alternatives + +- Restoring unconditional Windows calls would break Unix builds again. +- Flattening explicit and resolved keys loses either the user's selected-key + priority or agent priority over default disk paths. +- Converting every agent error into `false` would reuse a potentially stuck SSH + authentication task after a signer error. +- A silent fixed identity cutoff can hide the required identity. Stable ranking, + duplicate suppression and returned/offered diagnostics make attempts reviewable. +- Agent forwarding and provider-specific discovery are separate policies and are + not implicitly introduced by restoring login authentication. + +## Consequences + +Auto can authenticate without a disk private key. The existing UI modes, stored +configuration, credential store and dependencies remain the same. Only completely +authenticated sessions enter the pool; an authenticated jump retains its own +lifetime when a target fails. + +The locked Pageant 0.2.1 window-message transport calls synchronous `SendMessageA` +from an async worker. A caller deadline cannot preempt that call or reclaim a +worker blocked by an unresponsive legacy provider. The named-pipe busy case is +cancellable and separately tested. A stronger legacy-transport guarantee requires +an upstream or native adapter change; protocol timeout tests do not prove it. + +## Validation + +Generated keys, an isolated agent wire service and real loopback SSH servers cover +both entry points, strict modes, disk-key ordering, fallback, discovery timeouts, +signer refusal/disconnect, cancellation, transport closure, certificates, both RSA +hashes and independent jump/target selectors. Native fixtures use private pipe or +socket names. The Unix environment test runs in a child so parallel tests cannot +change another test's agent. No fixture enumerates a user's real credentials. +Native CI and live credential-manager UI acceptance are separate evidence. + +## Supersedes + +None. This is the initial module-scoped record for the restored authentication path. + +## Revisit when + +Custom IdentityAgent, IdentitiesOnly or forwarding needs an explicit policy. +Reconsider identity-attempt selection with server-limit evidence; do not silently +reintroduce truncation. Harden the legacy native transport before claiming that +every provider call can be forcibly cancelled. diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 0000000..9671e01 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,8 @@ +# Documentation rules + +- `docs/` 记录经过核验的当前事实:系统行为、架构地图、协议、操作手册、验收合同和事故复盘的事实部分。 +- 非平凡决策的原因、被否定方案和替代关系写入 `architecture/notes/`;计划、待办和临时工作记录不作为当前事实文档新增。 +- 事实变化时更新正文;需要表达演进时写明版本、日期或状态,避免无锚点的“以前、现在、不再”。 +- 不手抄可从 schema、类型定义、命令帮助或生成文件直接得到的 API 清单;链接单一权威来源。 +- 每篇文档保持单一职责和可审查长度。测试语料、发布说明和验收材料按各自用途管理,不套用普通事实文档的机械上限。 +- `release-notes/` 还必须遵循该目录更具体的 `AGENTS.md`。 diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index 325a146..055d06e 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -1,5 +1,11 @@ # Architecture decisions / 架构决策记录 +> Historical consolidated archive. New non-trivial decisions after the 2026-09-19 +> governance migration live under the owning path in +> [`architecture/notes/`](../architecture/notes/AGENTS.md). Existing ADRs keep their +> accepted rationale; a replacement creates a new note with `Supersedes` rather than +> rewriting the old conclusion. + ## Process Record decisions that change dependency direction, core ownership, persistent @@ -12,6 +18,8 @@ Revisit condition.** Identify the accountable maintainer in the PR review. A pol change must include a failing legitimate example when correcting a false positive, plus a violation that must remain rejected. Do not use a policy edit to conceal an unrelated feature's growth. Remote approval/enforcement is not implied by this log. +The note location and supersession rules above replace only this section's former +single-file storage process; the records below remain unchanged historical context. ## ADR-0001 — Evidence-based architecture contracts @@ -756,170 +764,3 @@ settings files. tests; a successful compile is not visual acceptance. - **Revisit condition:** Reconsider schema versioning if preserving organization through edits by older application versions becomes a supported requirement. - -## ADR-0021 — Agent activity authority and terminal input compatibility - -- **Status:** Implemented in the working tree, 2026-09-19; pending repository - review. This record does not claim a release or live-client acceptance. -- **Context:** Two UI shells independently combined hook facts, screen words, - command boundaries, process snapshots and terminal progress. Old output could - replace a completed turn with attention, and command completion could leave - progress or Agent state running. Modifier-preserving Windows key records also - behave differently for native console readers and byte-stream readers. -- **Evidence:** The reported Codex screen contains TOML strings `[y/n]` above a - current `esc to interrupt` footer. The Windows ConPTY probe receives CR from - Shift+VK_RETURN even when its UnicodeChar is LF, while native ReadKey retains - Shift. LF compatibility and negotiated CSI-u CSI-u both survive that transport. -- **Decision:** `ai_hook::lifecycle::AgentActivity` owns each pane's Agent state, - primary process/session, input barrier and fallback authority. UI adapters - validate routing/ownership, run the shared ordering gate, apply an event, then - project status and notification effects. Rejected observations must not mutate - the ordering gate. `protocol`/`payload` normalize bounded provider data; - `ordering` owns replay/late-event filtering; Windows transport, repair scheduling, - Codex notify editing and managed skills keep their separate I/O lifecycles. -- **Capability contract:** Starts/completions, attention events, attention context, - background tasks and delivery ordering are separate capabilities of the bridge - we install. Claude/OpenCode use lifecycle and attention hooks; Pi owns turns but - may need screen evidence of a live input form during a turn. Codex keeps legacy - completion-only notify alongside versioned native hooks: clients advertising - hooks use the three-event turn contract; 0.154.0 and newer use the locally - verified lifecycle/permission contract. A narrowly matched PreToolUse for the - structured request_user_input tool supplies questions; ordinary tool prose has no attention - authority. PostToolUse resumes work after either permission or a question. - The bridge declares its installed contract in the envelope. A native event - without that declaration is rejected; - observing a real native event suppresses notify duplicates for that session. - Explicit feature opt-outs and Codex's own hook trust review remain authoritative. - Untyped legacy notifications - can request attention during work, but cannot reopen an idle or completed turn. -- **Fallback boundaries:** Only capabilities absent from the active bridge may - consume screen observations. Blocking matches require current controls near - the input region; output words alone are insufficient. Shell completion clears - foreground identity, Agent state and progress together. SSH uses OSC 133 or a - pending command's known empty prompt, never silence, BEL or host-process absence. - Local snapshots retain errors as unknown and cannot finish shell builtins merely - because there are no children. Close-warning exemptions are not activity rules. -- **Input boundary:** `input::terminal_input` owns the newline compatibility - decision shared by GPUI, legacy input and Runtime API. Negotiated CSI-u takes - precedence; unnegotiated Claude uses LF; native Windows clients retain Win32 - identity and modifiers. Modified Enter never submits a shell-history entry. -- **Alternatives:** More per-view flags or additional loose keywords keep the - conflicting authorities. Treating every provider as a full-hook provider loses - legitimate input requests; letting every screen override hooks reintroduces - false attention/completion. Globally substituting LF loses native key identity. -- **SSH installation boundary:** `ai_hook::remote` plans edits using shared JSON - ownership, Codex feature and notify policies. `ssh_session::integration` executes - the bounded Python file adapter on an existing authenticated connection, then - starts the integrated shell on its own PTY. The per-channel token is passed at - startup, never written into remote files and never dependent on AcceptEnv. - Unsupported/rejected setup falls back to an ordinary SSH shell; rejection of - the integrated exec opens a fresh channel. Remote process identity is PID plus - start epoch, separate from host PIDs. Same-process SessionStart permits clear/ - resume transitions; subagent contexts, foreign processes and late sessions are - rejected before ordering. Compaction preserves the current turn state. -- **Ownership and removal:** A versioned remote manifest records exact hook groups, - notify chaining and managed asset hashes. Compare-and-swap writes, a setup lock - and rollback protect concurrent edits; user edits cause a conflict rather than - replacement. Explicit removal restores owned settings and persists a remote - opt-out. `setup-ai --ssh DESTINATION [--remove]` uses that same planner. Python - 3.8+ and POSIX filesystem/TTY support are required. Bash replays login startup - and sources user rc once; it remains an rcfile shell (Bash cannot set its - read-only login_shell flag). Zsh retains its existing startup wrappers. - Other shells receive the Agent hook environment without a claimed OSC 133 - implementation. No remote Codex hook trust database is edited. -- **Cost and compatibility:** No new crate or long-lived worker. Windows native - hooks add an ownership receipt; SSH adds the remote manifest and opt-out above. - Short-lived remote hook processes serialize writes per terminal and allocate - sequence numbers per Agent process. Counter/lock files are capped, old entries - expire, and input, output and execution time have explicit budgets. - Existing bounded event caches and process-probe throttling remain. OpenCode's - sender retains at most 512 session turn records so a permission pause or nested - session cannot consume another session's pending completion. Complete-hook - sessions skip screen classification; partial bridges scan only the existing - bounded terminal tail. Managed integration files retain ownership checks, - edited-file preservation and reversible removal; historical fixtures are exact - bytes from v1.5.0, protected from Git newline conversion and verified by actual - checkouts under all three autocrlf settings. -- **Validation:** Shared lifecycle and provider parsing tests, current/expired form - fixtures, GPUI key-to-PTY and command/progress tests, both feature configurations, - architecture ratcheting and the native transport probe. New regressions cover - native/notify coexistence, turn/session isolation, installation conflicts and - removal, SSH exec fallback without AcceptEnv, token routing, real controlling - TTY delivery, prompt-command arrays/export/readonly/reload and rollback. Keep - live authenticated model turns separate from protocol/transport acceptance. -- **Revisit condition:** Extend the conservative Codex version contract only with - upstream and execution evidence. Remove the Claude fallback once supported - clients negotiate the keyboard protocol here. A missing remote process identity - still cannot authorize a session switch based on cwd or screen text. - -## ADR-0022 — System SSH agent authentication and transport ownership - -- **Status:** Implemented, 2026-09-19; pending repository review. This record does - not claim a release or acceptance with every third-party credential manager. -- **Context:** Auto authentication had no system-agent step, so a host whose key - existed only in an agent failed both connection entry points. Legacy `agent` - profiles still decoded as Auto but had lost their authentication capability. - Reintroducing platform APIs without compile-time guards would repeat a - cross-platform build defect. Treating a signer failure as ordinary rejection - can leave the underlying SSH task waiting for a signature indefinitely. -- **Policy authority:** `ssh_session::authentication_plan` continues to own the - order shared by formal and test connections: explicit disk keys, system agent, - resolved/default disk keys, saved password, keyboard-interactive, then a password - prompt. Key path deduplication spans both disk-key groups. A nonempty draft - password replaces the saved-password step in Test Connection; it cannot bypass - the selected authentication mode. Password mode retains password/PAM only, - PublicKey retains disk keys only, and KeyboardInteractive retains that method - only. The legacy profile migration remains unchanged. -- **Agent boundary:** `ssh_session::agent` owns the total discovery budget, public - identity selectors, signing and structured results. `platform::ssh_agent` owns - native endpoint selection, connection and bounded identity enumeration. Its - adapter returns protocol streams and identities without depending on the SSH - policy module. Windows tries the standard OpenSSH - pipe and then Pageant; Unix uses `SSH_AUTH_SOCK`. Platform methods are compiled - only for their target, with no endpoints on other targets. Existing russh - support supplies the protocol, dynamic stream, public keys, certificates and - RSA SHA-256/SHA-512 signatures. There is no new dependency, persisted setting, - credential store, worker, authentication mode or agent forwarding. -- **Identity scope:** Every host, including a jump host, opens its own agent - connection and ranks identities using that host's public selectors. Explicit - selectors precede resolved selectors; other identities retain their returned - order. Public-key comments do not affect equality; certificate selectors match - their underlying public key. No private material is loaded by this module. - Duplicate identity blobs are offered only once across endpoints, while a - certificate remains distinct from its raw key. There is no unexplained identity - cutoff; logs and final diagnostics report returned and offered counts. A server - that stops offering public-key authentication ends the agent step. Partial - success skips further public keys and continues to the second factor. -- **Budgets:** Async connection and identity enumeration share a deadline of - 1.5 seconds per endpoint and 3 seconds in total. Public selector reads have a separate 1 second - budget and 64 KiB per regular file. Signing may need user confirmation, so it - uses the existing 300 second formal authentication budget. Confirmation time - does not consume a later endpoint's discovery allowance. Test Connection and - unattended jump authentication retain a 12 second authentication limit, with - an agent-specific diagnostic if it expires during that step. -- **Native cancellation limit:** The locked Pageant 0.2.1 window-message transport - calls synchronous `SendMessageA` from an async worker. A caller deadline cannot - preempt that native call or reclaim a worker blocked by an unresponsive legacy - provider. The named-pipe busy case is cancellable and separately tested. A - stronger guarantee for the legacy transport requires an upstream or native - adapter change; protocol timeout tests alone do not establish that guarantee. -- **Failure and cancellation:** Unavailable, empty and rejected agents are safe - fallback results; partial success and authenticated are separate outcomes. - Signer, session-channel and signing-timeout errors propagate immediately. The - caller drops its unpooled transport on error or cancellation. It must not send - a password, change agents or merely enqueue Disconnect on a transport that - may still be waiting for Signed. Only fully authenticated handles enter the - pool; an already authenticated jump connection keeps its own lifetime. -- **Validation:** Tests use generated keys, an isolated agent wire service and - real loopback SSH servers. They cover both entry points, legacy migration, - strict modes, disk-key order, fallback, discovery timeout, signer refusal and - disconnect, signing timeout and cancellation, transport closure, certificates, - both RSA hashes, selectors and independent jump/target identities. Native - adapter fixtures use private pipe/socket names; the Unix environment test runs - in a child process so parallel tests cannot change each other's agent. No test - enumerates a user's real credentials. Platform compilation and native fixture - results must be reported separately from live credential-manager acceptance. -- **Revisit condition:** Custom IdentityAgent, IdentitiesOnly and agent forwarding - need their own explicit policies and tests. They are not implicitly enabled by - restoring Auto authentication. Reconsider identity attempt selection with - server-limit evidence; do not silently reintroduce a fixed truncation. diff --git a/docs/architecture.md b/docs/architecture.md index feeffca..cbe39be 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -93,7 +93,8 @@ whole product. direction, cost, ownership and why an existing module is insufficient. See [CONTRIBUTING](../CONTRIBUTING.md), [enforced contracts](project-constraints.md), -and [decision records](architecture-decisions.md). Architectural review evaluates +the [causal note policy](../architecture/notes/AGENTS.md), and the +[legacy decision archive](architecture-decisions.md). Architectural review evaluates cohesion and knowledge shared across boundaries; CI provides narrower mechanical checks, not a guarantee against every future design mistake. diff --git a/docs/project-constraints.md b/docs/project-constraints.md index 9d902dd..b52f628 100644 --- a/docs/project-constraints.md +++ b/docs/project-constraints.md @@ -94,11 +94,15 @@ a failing violation, and fixtures for known false positives. Guardrail bugs are bugs: fix them before calling the gate mandatory. A failing architecture check must not be ignored, but a demonstrably defective policy must be revisable. -Record important changes in [the decision log](architecture-decisions.md): context, -evidence, alternatives, consequences, validation and a replacement/removal condition. -Ordinary fixes do not need ceremonial ADRs. A change to a budget or rule is a -dedicated governance change, not an unexplained edit hidden inside a feature PR. -No automatic exception-adding or budget-increasing command is provided. +Record important changes under the owning path in +[`architecture/notes/`](../architecture/notes/AGENTS.md): context, evidence, rejected +alternatives, consequences, validation and a replacement/removal condition. Ordinary +fixes do not need ceremonial records. The consolidated +[decision log](architecture-decisions.md) remains the historical archive through the +governance migration; new conclusions supersede old records instead of rewriting +their rationale. A change to a budget or rule is a dedicated governance change, not +an unexplained edit hidden inside a feature PR. No automatic exception-adding or +budget-increasing command is provided. Emergency/security repairs must not be forced into a dangerous broad refactor just to preserve a flawed metric. Escalate the demonstrated conflict to a maintainer, @@ -109,7 +113,8 @@ Existing platform-cfg counting is a historical heuristic, not a proof of platfor decoupling. It is not promoted to this new gate: its comment/string/nesting behavior needs separate work. Fork patches retain exact-revision pins, thin adapter scope, recorded motivation and an upstream-removal condition. Release safety remains in -`AGENTS.md` and the existing verified release instructions. +the root task entry, `packaging/AGENTS.md`, `docs/release-notes/AGENTS.md` and the +existing verified release instructions. ## 5. UI 设计约束 / UI design constraints diff --git a/docs/release-notes/AGENTS.md b/docs/release-notes/AGENTS.md new file mode 100644 index 0000000..84bf8cd --- /dev/null +++ b/docs/release-notes/AGENTS.md @@ -0,0 +1,9 @@ +# Release Note rules + +1. 只写已实现且有代码、构建或真实运行证据的用户可感知变化。调试探针、测试用强制弹窗、内部重构和未经验证的推断不得写成已交付功能。 +2. 沿用 `v1.3.0` 的双语结构:English 的 `Added / Fixed / Improved`、中文的 `新增 / 修复 / 改进`、`Contributors` 和最终资产 `SHA256`。中英文表达同一事实。 +3. 同一发布的 `CHANGELOG.md`、`docs/release-notes/vX.Y.Z.md` 和 GitHub Release 正文保持同步。正式构建前完成 Changelog,因为 ZIP 和安装器会把它打进产物。 +4. 引用 Issue 前读取其标题、正文、状态和必要评论,再挂到最准确的一条说明上。不得按编号猜测、复用旧版本链接或把宽泛需求改写成逐项 Bug 报告。 +5. 英文使用 `Addresses [#N](https://github.com/Kuddev/nebula/issues/N).`,中文使用 `对应 [#N](https://github.com/Kuddev/nebula/issues/N)。`。同一 Issue 涵盖紧密相关修复时,优先只在代表性条目引用一次。 +6. 文案描述可核验的用户结果,不用内部类型名、辅助函数或提交标题代替行为说明。仍开放或只部分覆盖的问题必须准确标明范围,不能扩大完成度。 +7. Contributors 规则和已发布版本的例外以本地 `.agents/memory.md` 中经过核验的最新发布结论为准,不凭空补头像或贡献者。 diff --git a/nebula_app/AGENTS.md b/nebula_app/AGENTS.md new file mode 100644 index 0000000..96d053e --- /dev/null +++ b/nebula_app/AGENTS.md @@ -0,0 +1,10 @@ +# Nebula application rules + +- 本目录是应用编排和适配层;依赖方向保持为 UI/组合 → 应用能力 → 共享领域规则,不在视图里复制设置、持久化、终端状态或协议状态机。 +- `src/gpui_shell` 负责 GPUI 呈现、命令和订阅。交互改动必须覆盖默认、hover、pressed、键盘焦点、disabled/loading 以及成功/失败中实际需要的状态。 +- 操作形成“触发 → 执行 → 可感知结果”的闭环。成功只能在操作真实完成或底层接口明确接受后显示,不能把回调触发当成平台级成功。 +- 异步工作必须有所有者、取消/过期策略和视图销毁清理;旧结果不得覆盖新状态。渲染回调不得同步等待磁盘、网络、子进程或长锁。 +- 新 UI 文案使用现有 typed message ID、命名占位符和回退合同,遵循 [`../docs/internationalization.md`](../docs/internationalization.md)。 +- 测试真实控件和布局路径;编译或状态单测不能代替 hover、命中区域、键盘路径和视觉验收。 +- 只有跨层所有权、持久化、协议、线程/生命周期或重要性能取舍才写 note,路径镜像到 `architecture/notes/nebula_app//`。 +- Agent 集成、改键输入与 SSH 认证的因果记录分别位于 [`ai_hook`](../architecture/notes/nebula_app/ai_hook/)、[`input`](../architecture/notes/nebula_app/input/) 和 [`ssh_session`](../architecture/notes/nebula_app/ssh_session/)。 diff --git a/nebula_app/src/gpui_shell/workspace/windowing.rs b/nebula_app/src/gpui_shell/workspace/windowing.rs index cf77ae0..ad2f422 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing.rs @@ -25,8 +25,8 @@ use std::time::Duration; use super::DockTarget; use gpui::{ AnyWindowHandle, App, AppContext as _, Bounds, Context, Entity, Global, IntoElement, - ParentElement as _, Render, SharedString, Styled as _, Subscription, WeakEntity, Window, - WindowBounds, WindowOptions, div, point, px, size, + ParentElement as _, Render, Styled as _, Subscription, WeakEntity, Window, WindowBounds, + WindowOptions, div, point, px, size, }; use gpui_component::{ActiveTheme as _, Root, TitleBar}; use nebula_split::SplitTree; @@ -154,7 +154,6 @@ pub(crate) struct CrossWindowTabDrag { source: WeakEntity, source_window_id: u64, pane_id: u64, - title: SharedString, } impl CrossWindowTabDrag { @@ -177,23 +176,14 @@ struct NativeCursorTarget { client_y: i32, } -pub(crate) struct TabDragPreview { - title: SharedString, -} +/// GPUI requires a renderable entity for native cross-window dragging, while +/// the source tab row already follows the pointer in our own reorder layer. +/// Keep this proxy invisible so the tab title is not painted a second time. +pub(crate) struct TabDragPreview; impl Render for TabDragPreview { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .max_w(px(280.0)) - .px_3() - .py_2() - .rounded(px(6.0)) - .border_1() - .border_color(cx.theme().border) - .bg(cx.theme().popover) - .text_color(cx.theme().popover_foreground) - .shadow_md() - .child(self.title.clone()) + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size(px(1.0)).opacity(0.0) } } @@ -1560,15 +1550,14 @@ impl NebulaWorkspace { source: cx.entity().downgrade(), source_window_id: self.runtime_window_id, pane_id: *focused, - title: self.tab_title(ix, cx), }) } pub(crate) fn cross_window_drag_preview( - payload: &CrossWindowTabDrag, + _payload: &CrossWindowTabDrag, cx: &mut App, ) -> Entity { - cx.new(|_| TabDragPreview { title: payload.title.clone() }) + cx.new(|_| TabDragPreview) } pub(crate) fn schedule_move_tab_to_new_window(&self, ix: usize, cx: &mut Context) { diff --git a/nebula_app/src/gpui_shell/workspace/windowing/shutdown.rs b/nebula_app/src/gpui_shell/workspace/windowing/shutdown.rs index ebfb8e2..65f6969 100644 --- a/nebula_app/src/gpui_shell/workspace/windowing/shutdown.rs +++ b/nebula_app/src/gpui_shell/workspace/windowing/shutdown.rs @@ -1,4 +1,6 @@ //! Application shutdown: approve drafts, capture durable state, then stop PTYs. +use gpui::SharedString; + use super::*; use crate::i18n::Message; diff --git a/nebula_settings/AGENTS.md b/nebula_settings/AGENTS.md new file mode 100644 index 0000000..8941d87 --- /dev/null +++ b/nebula_settings/AGENTS.md @@ -0,0 +1,7 @@ +# Settings core rules + +- 本目录拥有设置模型、默认值、验证、兼容读取、语言注册和持久化合同,不拥有 UI 控件或应用编排。 +- 保持零生产依赖合同。新依赖、动态解析或后台服务需要单独设计依据,不能为了一个界面入口扩大核心职责。 +- 新设置必须定义默认值、合法范围、序列化/反序列化、旧配置兼容、重置和往返测试;开放会话是否即时生效必须明确。 +- 语言和消息注册保持单一权威;普通翻译查询的既有零分配合同不能被格式化或冷路径逻辑悄悄改变。 +- 持久化格式、兼容迁移或共享设置所有权的非平凡变化写入 `architecture/notes/nebula_settings//`。 diff --git a/nebula_terminal/AGENTS.md b/nebula_terminal/AGENTS.md new file mode 100644 index 0000000..4aa27ce --- /dev/null +++ b/nebula_terminal/AGENTS.md @@ -0,0 +1,8 @@ +# Terminal core rules + +- 本目录只拥有 grid、VT、PTY、输入编码、回放和终端行为;不得依赖产品视图、窗口状态或 GPUI 组件。 +- 输入修复必须分别验证原生控制台读取方、翻译后字节流读取方和协商协议;不能用单一路径正常推断所有客户端兼容。 +- 字节流、解析器和回放测试要覆盖分块边界、静默源、错误/EOF 和平台差异。间歇问题需要能稳定失败的判据。 +- 热路径变更提供代表性成本证据;分配、吞吐或内存结论限定到实际测量的内容、尺寸、平台和构建配置。 +- 不为 UI 需求在终端核心增加产品概念。确需新接口时,公开最小命令/结果并保持状态私有。 +- 重大协议、缓冲、线程、所有权或兼容性决策写入 `architecture/notes/nebula_terminal//`;普通回归修复只需测试和变更说明。 diff --git a/packaging/AGENTS.md b/packaging/AGENTS.md new file mode 100644 index 0000000..af7d031 --- /dev/null +++ b/packaging/AGENTS.md @@ -0,0 +1,10 @@ +# Build, packaging and release rules + +- 处理版本号、构建、打包、自动更新、GitHub Release、Changelog 或 Release Note 前,完整阅读本地 `.agents/memory.md` 的发布章节和 `docs/release-notes/AGENTS.md`。 +- `.agents/memory.md` 是经真实发布核验的本地记忆,不是临时推理草稿。只有代码、脚本、GitHub 元数据或真实运行结果已核实后才能更新;过时结论直接修正并注明核验日期。 +- 所有发布相关命令使用 UTF-8。工作区可能包含用户的未跟踪探针、构建目录和截图,只能显式暂存本次文件,不得清理、全量暂存或重置它们。 +- 正式发布不得使用 `-SkipBuild` 或 `-AllowStale` 绕过新鲜度检查,也不得把 workspace 默认构建产生的 legacy shell 当作 GPUI 产品包。 +- 构建和输出目录必须位于用户明确允许的路径。 +- GitHub 推送使用普通非强制推送。发布后二进制版本标签不可因说明文档修订而移动。 +- 结束前核验 Release 标题、标签、正文、真实资产文件名、空资产标签、大小、SHA256、分支与版本标签指向;创建命令成功不等于发布核验完成。 +- 构建图、资产集合、更新协议或发布事务的非平凡变化写入 `architecture/notes/packaging/`。 diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md new file mode 100644 index 0000000..c909599 --- /dev/null +++ b/scripts/AGENTS.md @@ -0,0 +1,8 @@ +# Maintained scripts and governance rules + +- 维护脚本是产品工程合同的一部分:默认离线、UTF-8、确定性,并对缺失输入、解析失败、读取失败和不完整结果 fail closed。 +- 新门禁必须说明不变量,提供一个合法通过样例、一个违规失败样例和已知误报的反例测试;先修门禁缺陷,再把它称为强制规则。 +- 不通过提高预算、扩大排除、自动加例外、删除测试或 `continue-on-error` 消除失败。规则错误时提交窄范围政策修订和回归。 +- 依赖和行数策略的单一权威仍在 `architecture/`;脚本读取同一来源,不复制第二份常量清单。 +- 发布脚本还必须遵循 [`../packaging/AGENTS.md`](../packaging/AGENTS.md)。 +- 门禁语义、扫描边界或治理流程的非平凡变化写入 `architecture/notes/scripts//`。 diff --git a/scripts/tests/test_architecture_governance.py b/scripts/tests/test_architecture_governance.py index 1bdd04e..e16ba87 100644 --- a/scripts/tests/test_architecture_governance.py +++ b/scripts/tests/test_architecture_governance.py @@ -8,8 +8,37 @@ ROOT = Path(__file__).resolve().parents[2] DOCUMENTS = ( "CONTRIBUTING.md", "docs/project-constraints.md", "docs/architecture.md", "docs/architecture-decisions.md", "docs/engineering-evidence.md", - "docs/internationalization.md", + "docs/internationalization.md", "architecture/notes/AGENTS.md", ) +AGENT_GUIDES = ( + "AGENTS.md", "nebula_app/AGENTS.md", "nebula_terminal/AGENTS.md", + "nebula_settings/AGENTS.md", "scripts/AGENTS.md", "packaging/AGENTS.md", + "docs/AGENTS.md", "docs/release-notes/AGENTS.md", +) +PUBLIC_GUIDES = DOCUMENTS + AGENT_GUIDES +NOTES_ROOT = ROOT / "architecture/notes" +NOTE_REQUIRED_SECTIONS = ( + "Status", "Context", "Evidence", "Decision", "Rejected alternatives", + "Consequences", "Validation", "Supersedes", "Revisit when", +) +MAX_NOTE_LINES = 200 + + +def decision_note_errors(text): + errors = [] + if not re.search(r"(?m)^# [^#\n]+$", text): + errors.append("missing one level-one title") + headings = { + match.group(1).strip().casefold() + for match in re.finditer(r"(?m)^##\s+(.+?)\s*$", text) + } + for section in NOTE_REQUIRED_SECTIONS: + if section.casefold() not in headings: + errors.append(f"missing section: {section}") + line_count = len(text.splitlines()) + if line_count > MAX_NOTE_LINES: + errors.append(f"{line_count} lines exceeds {MAX_NOTE_LINES}") + return errors class GovernanceTests(unittest.TestCase): @@ -30,10 +59,13 @@ class GovernanceTests(unittest.TestCase): self.assertEqual(self.ignored_paths(paths), set(ignored)) def test_contributor_documents_exist_and_are_not_ignored(self): - for name in DOCUMENTS: + for name in PUBLIC_GUIDES: with self.subTest(name=name): self.assertTrue((ROOT / name).is_file()) - self.assertEqual(self.ignored_paths(DOCUMENTS), set(), "public contributor docs must not be ignored") + self.assertEqual( + self.ignored_paths(PUBLIC_GUIDES), set(), + "public contributor and agent guides must not be ignored", + ) def test_private_investigations_remain_ignored(self): paths = ( @@ -72,7 +104,7 @@ class GovernanceTests(unittest.TestCase): def test_maintained_tests_dependencies_and_public_assets_stay_visible(self): paths = ( - *DOCUMENTS, ".github/CODEOWNERS", ".github/PULL_REQUEST_TEMPLATE.md", + *PUBLIC_GUIDES, ".github/CODEOWNERS", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/architecture.yml", "architecture/dependencies.toml", "architecture/file-budgets.txt", "scripts/check_architecture.py", "scripts/tests/test_architecture_governance.py", "scripts/tests/new_regression.py", @@ -84,11 +116,12 @@ class GovernanceTests(unittest.TestCase): "third_party/winit-0.30.13/src/lib.rs", "docs/screenshots/SHOTLIST.md", "docs/screenshots/hero.png", "docs/release-notes/v1.5.0.md", "docs/skills/pebrel-runtime/SKILL.md", + "architecture/notes/nebula_terminal/input/2026-09-19-example.md", ) self.assertEqual(self.ignored_paths(paths), set()) def test_relative_document_links_resolve(self): - for name in DOCUMENTS: + for name in PUBLIC_GUIDES: document = ROOT / name text = document.read_text(encoding="utf-8") for link in re.findall(r"\]\(([^)]+)\)", text): @@ -98,6 +131,47 @@ class GovernanceTests(unittest.TestCase): with self.subTest(document=name, target=target): self.assertTrue((document.parent / target).exists()) + def test_decision_note_contract_accepts_and_rejects_known_examples(self): + valid = "# Decision\n\n" + "\n\n".join( + f"## {section}\n\nRecorded." for section in NOTE_REQUIRED_SECTIONS + ) + self.assertEqual(decision_note_errors(valid), []) + + missing_alternative = valid.replace("## Rejected alternatives", "## Options") + self.assertIn( + "missing section: Rejected alternatives", + decision_note_errors(missing_alternative), + ) + + oversized = valid + "\n" + "\n".join("detail" for _ in range(MAX_NOTE_LINES)) + self.assertTrue( + any("exceeds" in error for error in decision_note_errors(oversized)), + "an oversized note must fail", + ) + + def test_reviewed_decision_notes_are_scoped_and_complete(self): + notes = [ + path for path in NOTES_ROOT.rglob("*.md") + if path.name != "AGENTS.md" + ] + self.assertTrue(notes, "the governance migration must include its decision note") + self.assertFalse( + any(path.name.casefold() == "index.md" for path in NOTES_ROOT.rglob("*.md")), + "decision notes must not use a global index", + ) + for path in notes: + relative = path.relative_to(NOTES_ROOT) + with self.subTest(note=relative.as_posix()): + self.assertGreaterEqual(len(relative.parts), 2, "notes must follow an owner path") + self.assertRegex( + path.name, + r"^\d{4}-\d{2}-\d{2}-[a-z0-9][a-z0-9-]*\.md$", + ) + self.assertEqual( + decision_note_errors(path.read_text(encoding="utf-8")), + [], + ) + def test_required_job_is_unfiltered_and_unprivileged(self): workflow = (ROOT / ".github/workflows/architecture.yml").read_text(encoding="utf-8") for required in (" pull_request:", " merge_group:", "contents: read",