Skip to content

feat(rule):收紧规则与记忆分层,并补齐全局规则读写入口#542

Merged
phantom5099 merged 7 commits into1024XEngineer:mainfrom
phantom5099:main
May 3, 2026
Merged

feat(rule):收紧规则与记忆分层,并补齐全局规则读写入口#542
phantom5099 merged 7 commits into1024XEngineer:mainfrom
phantom5099:main

Conversation

@phantom5099
Copy link
Copy Markdown
Collaborator

@phantom5099 phantom5099 commented May 1, 2026

1. 原始问题

当前项目里,AGENTS.mdmemoskills 都会影响模型上下文,但它们的语义边界并不清晰,主要体现在:

  1. memo 容易被拿来承载“默认行为”或“长期约束”,但它本质上只是参考性记忆。
  2. 旧的 AGENTS.md 加载逻辑会沿当前 workdir 向上发现,导致项目规则会随当前路径漂移,不适合作为稳定的项目级规则入口。
  3. 规则发现、规则编辑和路径遍历和 prompt 渲染耦合在 context 内部,导致 prompt 组装层承担了过多文件系统职责。
  4. 缺少正式的全局规则入口,跨项目默认行为只能依赖 User Memo 或每轮重复说明。
  5. 虽然后续 TUI 理应提供“直接编辑全局规则”的入口,但当前代码层原本只有“加载规则”,没有稳定的全局规则读写函数可供上层接入。

2. 采取的解决方案

本 PR 采用最小收紧方案,不引入新的规则文件格式,也不引入通用规则平台,而是先把“显式规则”和“参考记忆”拆开,并把规则来源固定到两个自然入口:

  1. 项目根 AGENTS.md
  2. 全局 ~/.neocode/AGENTS.md

围绕这个目标,本 PR 做了四件事:

  1. 引入轻量 internal/rules 包,专门负责读取规则快照。
  2. 删除旧的“沿当前目录向上发现多个 AGENTS.md”逻辑,改为只认项目根和全局两个规则入口。
  3. context 中统一渲染 ## Rules section,并明确 Project Rules > Global Rules > Memo
  4. 补齐规则文件的稳定入口函数,让后续 TUI 可以直接复用同一套读写逻辑,而不需要自己拼路径或自己落盘。

3. 具体修改范围

3.1 规则加载模型收紧

新增 internal/rules/loader.go,只负责加载两份自然文本规则文件:

  1. 项目根 AGENTS.md
  2. 全局 ~/.neocode/AGENTS.md

不再支持:

  1. 沿路径向上发现多个 AGENTS.md
  2. RULE.md
  3. frontmatter
  4. id / name / description 等结构化 metadata
  5. 目录级规则

3.2 Prompt 中显式分层

internal/context 中原有的项目规则注入逻辑被替换为统一的 Rules section,结构固定为:

  1. Project Rules
  2. Global Rules

同时继续保留 memo 注入,但明确其语义为“仅供参考,不视为当前指令”,并把 Rules 放在 Memo 之前,避免两者继续处于同一优先级平面。

3.3 项目规则解析稳定化

runtime 在构建上下文时显式传递稳定的 ProjectRoot,让规则解析不再依赖当前活跃子目录。这样即使在同一项目内切换不同文件或子目录,项目规则集合仍然保持稳定一致。

3.4 新增规则文件读写入口

新增 internal/rules/store.go,补齐后续上层接入所需的稳定入口:

  1. GlobalRulePath
  2. ProjectRulePath
  3. ReadGlobalRule
  4. ReadProjectRule
  5. WriteGlobalRule
  6. WriteProjectRule

这组函数的职责是:

  1. 统一规则文件路径解析
  2. 统一读取规则正文
  3. 统一写入规则文件
  4. 保证写入时使用 UTF-8 校验
  5. 通过同目录临时文件 + 原子替换减少半写入风险

这一步很关键,因为它把“后续 TUI 直接编辑全局规则”所需的底层能力先补齐到了 rules 包中,同时没有把路径规则、文件写入逻辑泄漏到 TUI 或 gateway。

4. 预期收益

4.1 语义边界更清楚

这次改动把:

  1. Rules 定义为显式约束与默认行为
  2. Memo 定义为偏好、事实和经验的参考信息

从而减少“参考信息”和“正式约束”混用的问题。

4.2 项目规则来源更稳定

项目规则现在只认项目根 AGENTS.md,不再随着当前 workdir 的嵌套位置而漂移,更符合“项目级规则”的直觉。

4.3 全局默认行为有正式落点

像“默认中文输出”这样的跨项目规则,后续可以稳定落到全局 AGENTS.md,不必继续依赖 User Memo

4.4 context 职责更纯

context 现在只负责:

  1. 调用规则加载器
  2. 渲染 Rules section
  3. 维持 rules 与 memo 的顺序关系

不再承担复杂的目录遍历与规则发现职责。

4.5 为后续 TUI 接入提前补齐底层能力

虽然本 PR 没有改 TUIgateway,但已经把后续最需要的“全局规则读写入口”提前补齐。后面如果 TUI 要支持“直接编辑全局规则”,可以直接复用 rules 包能力,而不是重新实现一套路径拼接和文件写入逻辑。

5. 测试与验证

本 PR 补充和调整了以下测试覆盖:

  1. 读取项目根 / 全局 AGENTS.md
  2. 仅全局规则存在时的加载行为
  3. projectRoot 为文件路径时回退父目录
  4. 长规则文件的截断行为
  5. Rules section 的渲染顺序
  6. RulesMemo 之前注入
  7. 项目规则不随嵌套 workdir 漂移
  8. 全局/项目规则写入后可读回
  9. 缺失规则文件时返回空文档
  10. 非 UTF-8 内容写入被拒绝

已验证:

$env:GOCACHE = (Join-Path (Resolve-Path .).Path '.cache\go-build')
go test ./internal/rules/... ./internal/context/... ./internal/runtime -run TestDoesNotExist
go test -cover ./internal/rules ./internal/context

当前覆盖率结果:

  1. internal/rules: 69.8%
  2. internal/context: 95.5%

说明:

  1. internal/runtime 使用 -run TestDoesNotExist 做编译型校验,确认 ProjectRoot 元数据接入没有打断 runtime 主链路。
  2. go test ./internal/runtime/... 里仍有现存的 Windows symlink 权限测试问题,与本 PR 无关。

6. 当前不做的内容

本 PR 明确不做:

  1. 不改 TUI
  2. 不改 gateway
  3. 不新增 /rules 命令或 RPC
  4. 不引入 RULE.md
  5. 不引入 frontmatter
  6. 不引入通用 rules registry
  7. 不支持目录级规则发现
  8. 不支持 session 级规则启停

这样做的原因是:这次改动的目标是先解决“规则与记忆分层不清”和“缺少全局规则正式入口”这两个核心问题,而不是提前搭建一套复杂规则平台。

7. 后续 gateway / TUI 需要接入的内容

虽然这次没有修改 gatewayTUI,但后续接入方向已经比较明确:

7.1 TUI 侧建议补一个“直接编辑全局规则”的入口

TUI 后续应支持:

  1. 打开当前全局规则内容
  2. 编辑并保存全局 AGENTS.md
  3. 在保存前后给出基础提示或反馈

底层可直接复用:

  1. GlobalRulePath
  2. ReadGlobalRule
  3. WriteGlobalRule

7.2 TUI 侧建议补一个“查看当前项目规则”的入口

TUI 可以进一步提供:

  1. 查看项目根规则文件路径
  2. 查看当前项目规则正文
  3. 快速跳转到项目根 AGENTS.md

底层可直接复用:

  1. ProjectRulePath
  2. ReadProjectRule

另:

  • 现在刚好还有个编辑项目级规则的入口,可以根据需要选择补不补上

7.3 gateway 后续如需透出调试能力,可基于同一套 rules API

如果后续需要把“当前实际生效的规则来源”暴露给 UI 或调试面板,gateway 应继续走已有边界,只透出规则快照或路径信息,而不是在 gateway 中自行实现规则读写逻辑。

8. 总结

这次 PR 的核心不是“新增更多规则能力”,而是用最小改动把规则和记忆从同一语义平面拆开,并把规则入口收紧为用户自然会写的两份 AGENTS.md。在此基础上,再补齐 rules 包的稳定读写入口,既解决了当前问题,也为后续 TUI 直接编辑全局规则预留了干净的接入点。

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Copy link
Copy Markdown

@fennoai fennoai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings not repeated inline:

  • Medium: internal/context/builder_test.go:38-60 has the same environment coupling as sources_test.go: NewBuilder() uses the default loader, so any real ~/.neocode/AGENTS.md makes TestDefaultBuilderBuild fail even when the package is correct.

I did not find additional performance, security, or documentation issues beyond the items above.

Comment thread internal/context/source_rules.go
Comment thread internal/rules/store.go
}

// ProjectRulePath 返回项目根规则文件 AGENTS.md 的固定路径。
func ProjectRulePath(projectRoot string) string {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProjectRulePath can still return a relative path here, and WriteProjectRule will write to it, but Loader.loadProjectDocument later rejects the same path because it is not absolute. That makes the new read/write API non-round-trippable for callers that pass relative roots. Either normalize to an absolute path here as well, or reject relatives consistently.

Comment thread internal/context/sources_test.go Outdated
t.Parallel()

sections, err := (&projectRulesSource{}).Sections(context.Background(), BuildInput{
sections, err := newRulesPromptSource(nil).Sections(context.Background(), BuildInput{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test now depends on the reviewer machine not having a real ~/.neocode/AGENTS.md. newRulesPromptSource(nil) uses the default loader, so the assertion fails as soon as global rules exist in the environment. The test should inject an isolated loader/base dir instead of consulting the real home directory.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 1, 2026

Codecov Report

❌ Patch coverage is 79.01786% with 47 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/rules/store.go 71.56% 15 Missing and 14 partials ⚠️
internal/rules/loader.go 78.04% 10 Missing and 8 partials ⚠️

📢 Thoughts on this report? Let us know!

@Yumiue
Copy link
Copy Markdown
Collaborator

Yumiue commented May 2, 2026

OK,我翻遍了opencode和claudecode,它们都没有在网页或应用整这个全局,项目规则的独立配置指令或交互的入口,而是把它当作一个用户自行了解编辑的内容。
那么既然不会被其他模块复用了,tui和gateway不用了,只在项目初始组装。,有必要单独拆解rule吗?不是context内容吗
这里有另一个想法,如果你支持热重载规则,那么就可以考虑拆出来,加一个relode指令,但是暴露出来的就这一个接口,为什么也不放在context里面呢。
请考虑

@phantom5099
Copy link
Copy Markdown
Collaborator Author

OK,我翻遍了opencode和claudecode,它们都没有在网页或应用整这个全局,项目规则的独立配置指令或交互的入口,而是把它当作一个用户自行了解编辑的内容。 那么既然不会被其他模块复用了,tui和gateway不用了,只在项目初始组装。,有必要单独拆解rule吗?不是context内容吗 这里有另一个想法,如果你支持热重载规则,那么就可以考虑拆出来,加一个relode指令,但是暴露出来的就这一个接口,为什么也不放在context里面呢。 请考虑

允许用户直接在应用内直接修改规则内容而不是还得专门找全局规则文件在哪,怎么修改,本质上是利好用户的行为,为什么非得参考opencode和Claudecode?不如看看windsurf呢?还有这个项目规则的独立配置指令或交互的入口,我还没用过Claude code,我记得有说是/memory 里是可以直接找这个相应的文件
以下是windsurf:
image

@phantom5099
Copy link
Copy Markdown
Collaborator Author

/code 提升覆盖测试率,并提交PR到Fork仓库里

@fennoai
Copy link
Copy Markdown

fennoai Bot commented May 2, 2026

Summary

已补齐这轮 rules/context 相关覆盖并推回当前 PR 分支。

  • 新增 internal/rules 的取消上下文、缺失文件、读取失败、HOME 回退、零预算截断等测试分支。
  • 修正 internal/context/sources_test.go,改为注入隔离的 rules loader,避免依赖评审机真实 ~/.neocode/AGENTS.md
  • 调整 internal/rules/store.goProjectRulePath,将相对项目根归一化为绝对路径,保证读写入口与 loader 的绝对路径要求一致。

Verification

  • go test ./internal/rules ./internal/context
  • go test -coverprofile=/tmp/rules.out ./internal/rules
  • go test -coverprofile=/tmp/context.out ./internal/context
  • 结果:internal/rules 覆盖率 79.3%internal/context 覆盖率 95.5%

Git

View job run

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: phantom5099 <245659304+phantom5099@users.noreply.github.com>
@Yumiue
Copy link
Copy Markdown
Collaborator

Yumiue commented May 2, 2026

OK,我翻遍了opencode和claudecode,它们都没有在网页或应用整这个全局,项目规则的独立配置指令或交互的入口,而是把它当作一个用户自行了解编辑的内容。 那么既然不会被其他模块复用了,tui和gateway不用了,只在项目初始组装。,有必要单独拆解rule吗?不是context内容吗 这里有另一个想法,如果你支持热重载规则,那么就可以考虑拆出来,加一个relode指令,但是暴露出来的就这一个接口,为什么也不放在context里面呢。 请考虑

允许用户直接在应用内直接修改规则内容而不是还得专门找全局规则文件在哪,怎么修改,本质上是利好用户的行为,为什么非得参考opencode和Claudecode?不如看看windsurf呢?还有这个项目规则的独立配置指令或交互的入口,我还没用过Claude code,我记得有说是/memory 里是可以直接找这个相应的文件 以下是windsurf: image

可以,确实我没发现claude的/memory指令确实有这个,打开文本编辑器去编辑的,我没什么问题了

@pionxe
Copy link
Copy Markdown
Collaborator

pionxe commented May 3, 2026

@xgopilot 请仔细审阅相关代码(特别是 internal/rules/loader.go, internal/rules/store.go 和 internal/context),并重点围绕以下 4 个维度输出你的审查意见:

  1. 跨平台 I/O 与路径安全 (Cross-Platform Compatibility)

检查 ~/.neocode/AGENTS.md 的路径解析是否使用了标准的 os.UserHomeDir(),以确保在 Windows (%USERPROFILE%) 和 Linux/Ubuntu 下均不产生异常。

评估基于“同目录写临时文件再原子替换(os.Rename)”的落盘机制。如果在 Windows 环境下,目标文件正被另一个高频进程读取,os.Rename 是否会触发 ERROR_ACCESS_DENIED?代码中是否包含了带 Backoff 的重试机制?

  1. Token 预算与上下文截断 (Token/Context Budgeting)

在 internal/context 中合并 Project Rules、Global Rules 和 Memo 时,检查其长度截断逻辑。

截断是基于字符、行还是 Token?截断机制是否安全(例如:是否会粗暴切断 Markdown 的代码块反引号,导致后续传入大模型时引发严重的格式解析混乱)?

  1. 边缘场景与架构退化 (Edge Cases & Regressions)

Monorepo 兼容: 废弃子目录规则后,目前的 ProjectRoot 发现机制是否健壮?在深层嵌套的复杂工程中,是否会因为根规则过于宽泛而干扰 Agent 针对特定子目录模块的判断?

热更新丢失: rules 包加载规则是否仅在初始化时发生?如果后续 TUI 通过新 API 修改了全局文件,当前常驻的 ReAct 循环或 Gateway 能否在下一轮交互中动态感知并重载?

  1. 代码规范与测试覆盖 (Go Idioms & Testing)

检查 store.go 中 API 的并发安全性。

确认非 UTF-8 校验逻辑是否严谨。

对于核心逻辑,提供优化建议或指出潜在的内存泄漏风险。

输出要求:
请以结构化的 Markdown 格式回复,分为【Blocker (阻碍合并)】、【Warning (潜在隐患)】和【Suggestion (优化建议)】三个级别,并附带简短的修复代码示例。

@fennoai
Copy link
Copy Markdown

fennoai Bot commented May 3, 2026

Blocker (阻碍合并)

  1. internal/rules/store.go:145 直接用 os.Rename(tempPath, path) 提交规则文件,在 Windows 下如果目标文件正被另一个进程读/扫/索引,常见结果就是 ERROR_ACCESS_DENIED。这里没有任何 backoff/retry,也没有针对 Windows 的替换语义兜底,所以“稳定的全局规则写入口”在 Windows 上仍然是脆弱的。
func commitWithRetry(tempPath, path string) error {
	var last error
	for i := 0; i < 5; i++ {
		if err := os.Rename(tempPath, path); err == nil {
			return nil
		} else {
			last = err
		}
		time.Sleep(time.Duration(25*(1<<i)) * time.Millisecond)
	}
	return fmt.Errorf("rules: commit file %s: %w", path, last)
}
  1. internal/rules/loader.go:14,116-126 把规则文件硬裁成 4000 runes,internal/context/source_rules.go:74-77 再把这段原样拼进 ## Rules。这不是 token budget,而是单文件 rune budget;并且会在任意位置截断 Markdown。若规则正文在 4000 rune 附近落在 ``` 代码块中间,后续 ## Memo / ## System State 可能都会被模型当成代码块内容,直接改变 prompt 结构。
func truncateRuleMarkdown(s string, max int) (string, bool) {
	trimmed, truncated := truncateRunes(s, max)
	if !truncated {
		return trimmed, false
	}
	if strings.Count(trimmed, "```")%2 == 1 {
		trimmed += "\n```"
	}
	return strings.TrimRight(trimmed, "\n") + "\n[truncated]\n", true
}

Warning (潜在隐患)

  1. internal/rules/loader.go:105-113 没有优先走标准 os.UserHomeDir(),而是先读 HOME,只有 HOME 不是绝对路径时才 fallback。你这次需求里明确把全局规则入口定义成 ~/.neocode/AGENTS.md,那这里最好直接以 os.UserHomeDir() 为主,否则在 Windows + Git Bash / MSYS / 自定义 HOME 环境下,规则位置可能和 %USERPROFILE% 不一致。

  2. 目前没有“合并后的总预算”。internal/rules/loader.go:14 只限制单个 rule 文件 4000 runes,但 internal/context/source_rules.go:35-55 会把 Project Rules + Global Rules 全部拼入 system prompt;Memo 又是独立 source,internal/context/prompt.go:35-44 也没有总量裁剪。结果是这次虽然做了“分层”,但没有做“总预算控制”,高配 memo + 双规则文件时 system prompt 仍然会膨胀。

  3. UTF-8 校验只覆盖写路径。internal/rules/store.go:116-119 会拒绝写入无效 UTF-8,但 internal/rules/store.go:99-112 / internal/rules/loader.go:83,95 在读路径上直接 string(data),外部手工写入的脏文件会被静默转成 RuneError 后继续注入 prompt。这个“校验”还不算严谨。

  4. WriteGlobalRule / WriteProjectRule 没有任何同路径互斥。临时文件名是唯一的,所以不会写出半文件,但并发写同一路径仍然是 last-writer-wins,调用方拿不到冲突信号。若后续 TUI、Gateway 或后台同步同时写规则,这个 API 语义是不稳定的。

  5. Monorepo 退化是设计上真实存在的。internal/runtime/run.go:553ProjectRoot 固定成 cfg.Workdirinternal/context/source_rules.go:27-31 又明确优先吃这个稳定根。这样确实消除了“随子目录漂移”,但也等于彻底放弃子模块级规则覆盖。在深层 monorepo 中,根 AGENTS.md 过宽时会持续影响所有子模块判断。这个不是实现 bug,但需要在 PR 描述和后续产品设计里明确承认是 tradeoff。

Suggestion (优化建议)

  1. 热更新本身没有丢。DefaultBuilder.Build() 每次都会调用 source,rulesPromptSource.Sections() 每次都会调用 loader.Load()runtime 也在每轮构建上下文时重新 Build();因此下一轮交互能感知规则文件变化。这里建议补一个测试,直接覆盖“写入 global rule 后下一次 Build 可见新内容”,把这个行为固定下来。

  2. resolveBaseDir 建议改成“os.UserHomeDir() 为主,显式 baseDir 为辅”,不要自己拼 HOME 优先级。

func resolveBaseDir(baseDir string) string {
	if trimmed := strings.TrimSpace(baseDir); trimmed != "" {
		return filepath.Clean(trimmed)
	}
	home, err := os.UserHomeDir()
	if err != nil || strings.TrimSpace(home) == "" {
		return ""
	}
	return filepath.Join(home, defaultRulesDir)
}
  1. 读路径建议补 UTF-8 断言,并把错误包成可定位的 rule read error,而不是默默吞掉乱码。
if !utf8.Valid(data) {
	return Document{}, fmt.Errorf("rules: read %s: content is not valid UTF-8", path)
}
  1. 规则截断最好至少同时满足两件事:

    • 有“总预算”而不只是“单文件预算”;
    • 截断后保证 Markdown 基本闭合,避免把后续 section 吃进代码块。
  2. 没看到明显的长期内存泄漏风险;反而当前更现实的问题是 os.ReadFile + []rune 双份分配会在超大规则文件下放大峰值内存。若后面要把规则编辑入口真正暴露给 TUI,建议顺手把读取上限也做成硬限制,避免一次读入异常大文件。

简短结论

当前实现的热更新路径是通的,但跨平台写入和截断安全性还不够:Windows 原子替换缺少重试,规则截断会破坏 Markdown 结构,而且 system prompt 没有合并后的总预算控制。这三点里前两点我建议按 blocker 处理。

已本地验证:go test ./internal/rules ./internal/context

View job run

@phantom5099
Copy link
Copy Markdown
Collaborator Author

Blocker (阻碍合并)

  1. internal/rules/store.go:145 直接用 os.Rename(tempPath, path) 提交规则文件,在 Windows 下如果目标文件正被另一个进程读/扫/索引,常见结果就是 ERROR_ACCESS_DENIED。这里没有任何 backoff/retry,也没有针对 Windows 的替换语义兜底,所以“稳定的全局规则写入口”在 Windows 上仍然是脆弱的。
func commitWithRetry(tempPath, path string) error {
	var last error
	for i := 0; i < 5; i++ {
		if err := os.Rename(tempPath, path); err == nil {
			return nil
		} else {
			last = err
		}
		time.Sleep(time.Duration(25*(1<<i)) * time.Millisecond)
	}
	return fmt.Errorf("rules: commit file %s: %w", path, last)
}
  1. internal/rules/loader.go:14,116-126 把规则文件硬裁成 4000 runes,internal/context/source_rules.go:74-77 再把这段原样拼进 ## Rules。这不是 token budget,而是单文件 rune budget;并且会在任意位置截断 Markdown。若规则正文在 4000 rune 附近落在 ``` 代码块中间,后续 ## Memo / ## System State 可能都会被模型当成代码块内容,直接改变 prompt 结构。
func truncateRuleMarkdown(s string, max int) (string, bool) {
	trimmed, truncated := truncateRunes(s, max)
	if !truncated {
		return trimmed, false
	}
	if strings.Count(trimmed, "```")%2 == 1 {
		trimmed += "\n```"
	}
	return strings.TrimRight(trimmed, "\n") + "\n[truncated]\n", true
}

Warning (潜在隐患)

  1. internal/rules/loader.go:105-113 没有优先走标准 os.UserHomeDir(),而是先读 HOME,只有 HOME 不是绝对路径时才 fallback。你这次需求里明确把全局规则入口定义成 ~/.neocode/AGENTS.md,那这里最好直接以 os.UserHomeDir() 为主,否则在 Windows + Git Bash / MSYS / 自定义 HOME 环境下,规则位置可能和 %USERPROFILE% 不一致。
  2. 目前没有“合并后的总预算”。internal/rules/loader.go:14 只限制单个 rule 文件 4000 runes,但 internal/context/source_rules.go:35-55 会把 Project Rules + Global Rules 全部拼入 system prompt;Memo 又是独立 source,internal/context/prompt.go:35-44 也没有总量裁剪。结果是这次虽然做了“分层”,但没有做“总预算控制”,高配 memo + 双规则文件时 system prompt 仍然会膨胀。
  3. UTF-8 校验只覆盖写路径。internal/rules/store.go:116-119 会拒绝写入无效 UTF-8,但 internal/rules/store.go:99-112 / internal/rules/loader.go:83,95 在读路径上直接 string(data),外部手工写入的脏文件会被静默转成 RuneError 后继续注入 prompt。这个“校验”还不算严谨。
  4. WriteGlobalRule / WriteProjectRule 没有任何同路径互斥。临时文件名是唯一的,所以不会写出半文件,但并发写同一路径仍然是 last-writer-wins,调用方拿不到冲突信号。若后续 TUI、Gateway 或后台同步同时写规则,这个 API 语义是不稳定的。
  5. Monorepo 退化是设计上真实存在的。internal/runtime/run.go:553ProjectRoot 固定成 cfg.Workdirinternal/context/source_rules.go:27-31 又明确优先吃这个稳定根。这样确实消除了“随子目录漂移”,但也等于彻底放弃子模块级规则覆盖。在深层 monorepo 中,根 AGENTS.md 过宽时会持续影响所有子模块判断。这个不是实现 bug,但需要在 PR 描述和后续产品设计里明确承认是 tradeoff。

Suggestion (优化建议)

  1. 热更新本身没有丢。DefaultBuilder.Build() 每次都会调用 source,rulesPromptSource.Sections() 每次都会调用 loader.Load()runtime 也在每轮构建上下文时重新 Build();因此下一轮交互能感知规则文件变化。这里建议补一个测试,直接覆盖“写入 global rule 后下一次 Build 可见新内容”,把这个行为固定下来。
  2. resolveBaseDir 建议改成“os.UserHomeDir() 为主,显式 baseDir 为辅”,不要自己拼 HOME 优先级。
func resolveBaseDir(baseDir string) string {
	if trimmed := strings.TrimSpace(baseDir); trimmed != "" {
		return filepath.Clean(trimmed)
	}
	home, err := os.UserHomeDir()
	if err != nil || strings.TrimSpace(home) == "" {
		return ""
	}
	return filepath.Join(home, defaultRulesDir)
}
  1. 读路径建议补 UTF-8 断言,并把错误包成可定位的 rule read error,而不是默默吞掉乱码。
if !utf8.Valid(data) {
	return Document{}, fmt.Errorf("rules: read %s: content is not valid UTF-8", path)
}
  1. 规则截断最好至少同时满足两件事:

    • 有“总预算”而不只是“单文件预算”;
    • 截断后保证 Markdown 基本闭合,避免把后续 section 吃进代码块。
  2. 没看到明显的长期内存泄漏风险;反而当前更现实的问题是 os.ReadFile + []rune 双份分配会在超大规则文件下放大峰值内存。若后面要把规则编辑入口真正暴露给 TUI,建议顺手把读取上限也做成硬限制,避免一次读入异常大文件。

简短结论

当前实现的热更新路径是通的,但跨平台写入和截断安全性还不够:Windows 原子替换缺少重试,规则截断会破坏 Markdown 结构,而且 system prompt 没有合并后的总预算控制。这三点里前两点我建议按 blocker 处理。

已本地验证:go test ./internal/rules ./internal/context

View job run

@xgopilot 根据优化建议修复问题,并提交PR到Fork仓库中,PR中需要用中文说明具体做了什么修改,收益是什么

@fennoai
Copy link
Copy Markdown

fennoai Bot commented May 3, 2026

已完成

已按评论里的优化建议完成修复,并推送到 Fork 分支,新增 Fork PR:phantom5099#67

修改内容

  • internal/rules/store.go:提交规则文件时改为带退避重试的提交流程,并在 Windows 路径上增加“删除旧文件后再提交”的替换兜底;同时为读取路径补上 UTF-8 校验,避免脏文件静默注入 prompt。
  • internal/rules/loader.go:全局规则目录解析改为优先使用 os.UserHomeDir();规则注入改为“合并后总预算”裁剪,并在截断时补齐未闭合的 Markdown 代码块围栏,避免把后续 Memo/其他 section 吃进代码块。
  • 测试补齐:新增规则热更新可见性、合并预算、Markdown 围栏闭合、UTF-8 读校验、Windows 提交重试/替换兜底等回归测试。

收益

  • 规则写入在 Windows 下对杀毒/索引等瞬时占用更稳,降低 ERROR_ACCESS_DENIED 类失败概率。
  • 规则截断不再破坏 prompt section 结构,项目规则仍优先于全局规则,且总注入体积可控。
  • 读写两侧都明确了 UTF-8 约束,全局规则路径解析也和 ~/.neocode/AGENTS.md 语义保持一致。

验证

  • go test ./internal/rules ./internal/context
  • go test ./internal/runtime -run TestDoesNotExist

提交信息:4ef04c3c fix(rules): 收紧规则读写健壮性

View job run

xgopilot and others added 2 commits May 3, 2026 09:05
Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: phantom5099 <245659304+phantom5099@users.noreply.github.com>
fix(rules): 收紧规则读写健壮性
@phantom5099 phantom5099 merged commit ee5cbfc into 1024XEngineer:main May 3, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants