Merge main and scope terminal design records

Move the pending activity, input and SSH authentication decisions into their
module-owned notes under the current governance contract. Import the shutdown
draft type directly after the incoming drag-preview cleanup removes its parent
import.
This commit is contained in:
kuddev committed 2026-09-19 21:01:03 +08:00
commit bb2dc13956
21 files changed
+554 -223

No files matched your search

+1
View File
@@ -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
+10 -21
View File
@@ -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 <PR-base-commit>` 验证;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` 的发布章节。
+4 -2
View File
@@ -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;
+35
View File
@@ -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.
@@ -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`.
@@ -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.
@@ -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.
@@ -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.
+8
View File
@@ -0,0 +1,8 @@
# Documentation rules
- `docs/` 记录经过核验的当前事实:系统行为、架构地图、协议、操作手册、验收合同和事故复盘的事实部分。
- 非平凡决策的原因、被否定方案和替代关系写入 `architecture/notes/`;计划、待办和临时工作记录不作为当前事实文档新增。
- 事实变化时更新正文;需要表达演进时写明版本、日期或状态,避免无锚点的“以前、现在、不再”。
- 不手抄可从 schema、类型定义、命令帮助或生成文件直接得到的 API 清单;链接单一权威来源。
- 每篇文档保持单一职责和可审查长度。测试语料、发布说明和验收材料按各自用途管理,不套用普通事实文档的机械上限。
- `release-notes/` 还必须遵循该目录更具体的 `AGENTS.md`。
+8 -167
View File
@@ -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.
+2 -1
View File
@@ -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.
+11 -6
View File
@@ -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
+9
View File
@@ -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` 中经过核验的最新发布结论为准,不凭空补头像或贡献者。
+10
View File
@@ -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/<capability>/`。
- Agent 集成、改键输入与 SSH 认证的因果记录分别位于 [`ai_hook`](../architecture/notes/nebula_app/ai_hook/)、[`input`](../architecture/notes/nebula_app/input/) 和 [`ssh_session`](../architecture/notes/nebula_app/ssh_session/)。
@@ -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<NebulaWorkspace>,
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<Self>) -> 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<Self>) -> 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<TabDragPreview> {
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<Self>) {
@@ -1,4 +1,6 @@
//! Application shutdown: approve drafts, capture durable state, then stop PTYs.
use gpui::SharedString;
use super::*;
use crate::i18n::Message;
+7
View File
@@ -0,0 +1,7 @@
# Settings core rules
- 本目录拥有设置模型、默认值、验证、兼容读取、语言注册和持久化合同,不拥有 UI 控件或应用编排。
- 保持零生产依赖合同。新依赖、动态解析或后台服务需要单独设计依据,不能为了一个界面入口扩大核心职责。
- 新设置必须定义默认值、合法范围、序列化/反序列化、旧配置兼容、重置和往返测试;开放会话是否即时生效必须明确。
- 语言和消息注册保持单一权威;普通翻译查询的既有零分配合同不能被格式化或冷路径逻辑悄悄改变。
- 持久化格式、兼容迁移或共享设置所有权的非平凡变化写入 `architecture/notes/nebula_settings/<capability>/`。
+8
View File
@@ -0,0 +1,8 @@
# Terminal core rules
- 本目录只拥有 grid、VT、PTY、输入编码、回放和终端行为;不得依赖产品视图、窗口状态或 GPUI 组件。
- 输入修复必须分别验证原生控制台读取方、翻译后字节流读取方和协商协议;不能用单一路径正常推断所有客户端兼容。
- 字节流、解析器和回放测试要覆盖分块边界、静默源、错误/EOF 和平台差异。间歇问题需要能稳定失败的判据。
- 热路径变更提供代表性成本证据;分配、吞吐或内存结论限定到实际测量的内容、尺寸、平台和构建配置。
- 不为 UI 需求在终端核心增加产品概念。确需新接口时,公开最小命令/结果并保持状态私有。
- 重大协议、缓冲、线程、所有权或兼容性决策写入 `architecture/notes/nebula_terminal/<capability>/`;普通回归修复只需测试和变更说明。
+10
View File
@@ -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/`。
+8
View File
@@ -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/<area>/`。
+79 -5
View File
@@ -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",