From ee49186b5ab7af0551a5a0239ffef504407a7486 Mon Sep 17 00:00:00 2001 From: wz Date: Fri, 10 Apr 2026 11:54:03 +0800 Subject: [PATCH 1/2] add bootstrap installer --- INSTALL.md | 30 +++++++ README.md | 5 ++ memory-system/AGENTS.md | 2 +- memory-system/README.md | 2 +- .../test_refresh_strict_original_memory.py | 2 +- method-forge/AGENTS.md | 2 +- method-forge/README.md | 58 ++++++------- method-forge/docs/method/activation-rules.md | 6 +- .../docs/method/autonomous-execution.md | 10 +-- method-forge/docs/method/consumer-adoption.md | 18 ++-- method-forge/docs/method/skill-contracts.md | 2 +- .../docs/method/workflow-health-check.md | 4 +- method-forge/docs/method/workflow.md | 4 +- .../presets/minimal-change-package/README.md | 36 ++++---- .../orchestrations/route-request/README.md | 6 +- .../orchestrations/spec-flow/README.md | 8 +- .../verify-and-memory/README.md | 2 +- .../SKILL.md | 10 +-- .../skills/method-forge-code-review/SKILL.md | 2 +- .../skills/method-forge-execute/SKILL.md | 10 +-- .../method-forge-feature-intake/SKILL.md | 2 +- .../method-forge-memory-promote/SKILL.md | 2 +- .../skills/method-forge-plan-review/SKILL.md | 2 +- .../skills/method-forge-plan-write/SKILL.md | 2 +- .../skills/method-forge-spec-clarify/SKILL.md | 2 +- .../method-forge-task-breakdown/SKILL.md | 2 +- .../method-forge-verify-change/SKILL.md | 2 +- scripts/bootstrap.sh | 34 ++++++++ scripts/install-skills.sh | 84 +++++++++++++++++++ 29 files changed, 252 insertions(+), 99 deletions(-) create mode 100644 INSTALL.md create mode 100755 scripts/bootstrap.sh create mode 100755 scripts/install-skills.sh diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..fc43be2 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,30 @@ +# Install + +This repository is the source of truth for the consolidated Codex enhancement stack. + +## One-command bootstrap + +From a fresh clone: + +```bash +./scripts/bootstrap.sh +``` + +That installs the `method-forge` skills into `"$CODEX_HOME/skills"` by symlink, so the local Codex App can discover them. + +## Recommended flow for teammates + +1. Clone the repository. +2. Run `./scripts/bootstrap.sh`. +3. Open the repository root as a Codex workspace. +4. When the repo changes, pull the latest revision and re-run the bootstrap script. + +## Notes + +- The repository keeps the three subsystems together under: + - `knowledge-base/` + - `memory-system/` + - `method-forge/` +- The bootstrap script only installs skills. It does not overwrite existing Codex memory data. +- If you want the global memory system refreshed for a workspace, use the memory-system refresh workflow in `memory-system/`. + diff --git a/README.md b/README.md index d9f44d7..5252349 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,8 @@ The intended priority order is: Use the subdirectory that matches the task. If a task spans systems, keep the work in this repo and follow the narrowest applicable instructions first. +## Install + +Start here: [INSTALL.md](INSTALL.md) + +The one-command bootstrap installs the `method-forge` skills into `"$CODEX_HOME/skills"` without touching existing Codex memory data. diff --git a/memory-system/AGENTS.md b/memory-system/AGENTS.md index 9dfa1b0..0034a2e 100644 --- a/memory-system/AGENTS.md +++ b/memory-system/AGENTS.md @@ -22,7 +22,7 @@ - 完成重要设计或实现后,运行: ```bash -python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root "/Users/wz/project/codex-enhanced-system/memory-system" +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root "$(git rev-parse --show-toplevel)" ``` ## 当前原则 diff --git a/memory-system/README.md b/memory-system/README.md index 32a3727..4667253 100644 --- a/memory-system/README.md +++ b/memory-system/README.md @@ -28,7 +28,7 @@ 全局运行入口是: ```bash -python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root "/Users/wz/project/codex-enhanced-system/memory-system" +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root "$(git rev-parse --show-toplevel)" ``` 本工作区里的实现源码当前保存在: diff --git a/memory-system/tests/test_refresh_strict_original_memory.py b/memory-system/tests/test_refresh_strict_original_memory.py index 86a7f80..8cb9f26 100644 --- a/memory-system/tests/test_refresh_strict_original_memory.py +++ b/memory-system/tests/test_refresh_strict_original_memory.py @@ -8,7 +8,7 @@ from pathlib import Path -SCRIPT_PATH = Path("/Users/wz/project/codex-enhanced-system/memory-system/scripts/refresh_strict_original_memory.py") +SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "refresh_strict_original_memory.py" def load_module(): diff --git a/method-forge/AGENTS.md b/method-forge/AGENTS.md index f3e3ffb..1d67fca 100644 --- a/method-forge/AGENTS.md +++ b/method-forge/AGENTS.md @@ -1,6 +1,6 @@ # method-forge Rules -- 开工前先读共享 memory guides;重要工作完成后运行 `python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system/method-forge`。 +- 开工前先读共享 memory guides;重要工作完成后运行 `python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root "$(git rev-parse --show-toplevel)"`。 - 本 workspace 只实现方法层,不重复实现 Codex App 原生的 multi-agent、worktrees、diff review、git/PR、background automations、sandbox/approvals、skill loading。 - 会话内流程编排统一叫 `orchestrations`;`automations` 只指 Codex App 原生后台任务。 - 本文件只放硬规则;解释性内容一律放到 `docs/method/`。 diff --git a/method-forge/README.md b/method-forge/README.md index 7ee2be9..388ae1a 100644 --- a/method-forge/README.md +++ b/method-forge/README.md @@ -71,15 +71,15 @@ route-request ## 快速使用 -1. 从 [orchestrations/route-request/README.md](/Users/wz/project/codex-enhanced-system/method-forge/orchestrations/route-request/README.md) 开始,对请求做 intake 和分流。 -2. 当 `need_spec=true` 时,进入 [orchestrations/spec-flow/README.md](/Users/wz/project/codex-enhanced-system/method-forge/orchestrations/spec-flow/README.md)。 -3. 以 [docs/templates/](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates) 中的模板产出中间文档。 -4. 实现完成后,始终走 [orchestrations/verify-and-memory/README.md](/Users/wz/project/codex-enhanced-system/method-forge/orchestrations/verify-and-memory/README.md)。 +1. 从 [orchestrations/route-request/README.md](orchestrations/route-request/README.md) 开始,对请求做 intake 和分流。 +2. 当 `need_spec=true` 时,进入 [orchestrations/spec-flow/README.md](orchestrations/spec-flow/README.md)。 +3. 以 [docs/templates/](docs/templates) 中的模板产出中间文档。 +4. 实现完成后,始终走 [orchestrations/verify-and-memory/README.md](orchestrations/verify-and-memory/README.md)。 如果你不想手动分步骤,可以直接触发入口 skill: -- [method-forge-execute/SKILL.md](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-execute/SKILL.md) -- [method-forge-autonomous-execution/SKILL.md](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-autonomous-execution/SKILL.md) +- [method-forge-execute/SKILL.md](skills/method-forge-execute/SKILL.md) +- [method-forge-autonomous-execution/SKILL.md](skills/method-forge-autonomous-execution/SKILL.md) 一句话用法: @@ -89,28 +89,28 @@ route-request ## 核心文档 -- 流程说明:[docs/method/workflow.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/workflow.md) -- Skill 契约:[docs/method/skill-contracts.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/skill-contracts.md) -- Orchestration 规则:[docs/method/orchestration-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/orchestration-rules.md) -- Codex 原生边界:[docs/method/codex-native-boundaries.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/codex-native-boundaries.md) -- 单入口执行 skill:[skills/method-forge-execute/SKILL.md](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-execute/SKILL.md) -- autonomous 执行扩展:[docs/method/autonomous-execution.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/autonomous-execution.md) -- 自动激活规则:[docs/method/activation-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/activation-rules.md) -- autonomous 入口 skill:[skills/method-forge-autonomous-execution/SKILL.md](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-autonomous-execution/SKILL.md) -- 恢复规则:[docs/method/resume-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/resume-rules.md) -- 防死循环规则:[docs/method/loop-guard-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/loop-guard-rules.md) -- 运行态模板:[docs/templates/run-state-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/run-state-template.md) -- heartbeat prompt 模板:[docs/templates/autonomous-heartbeat-prompt-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/autonomous-heartbeat-prompt-template.md) -- `v1` 验收报告:[docs/method/v1-acceptance-report.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/v1-acceptance-report.md) -- `v1.5` 补强报告:[docs/method/v1.5-hardening-report.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/v1.5-hardening-report.md) -- 失败回退规则:[docs/method/failure-rework-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/failure-rework-rules.md) -- 模板 lint 规则:[docs/method/template-lint-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/template-lint-rules.md) -- 流程健康检查:[docs/method/workflow-health-check.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/workflow-health-check.md) -- 消费方接入说明:[docs/method/consumer-adoption.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/consumer-adoption.md) -- 消费方 `AGENTS` 草案:[docs/templates/consumer-agents-rules-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/consumer-agents-rules-template.md) -- 健康检查模板:[docs/templates/workflow-health-report-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/workflow-health-report-template.md) -- 健康检查样例:[docs/method/examples/health-check/workflow-health-report.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/examples/health-check/workflow-health-report.md) -- 轻量 preset 入口:[docs/presets/minimal-change-package/README.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/presets/minimal-change-package/README.md) +- 流程说明:[docs/method/workflow.md](docs/method/workflow.md) +- Skill 契约:[docs/method/skill-contracts.md](docs/method/skill-contracts.md) +- Orchestration 规则:[docs/method/orchestration-rules.md](docs/method/orchestration-rules.md) +- Codex 原生边界:[docs/method/codex-native-boundaries.md](docs/method/codex-native-boundaries.md) +- 单入口执行 skill:[skills/method-forge-execute/SKILL.md](skills/method-forge-execute/SKILL.md) +- autonomous 执行扩展:[docs/method/autonomous-execution.md](docs/method/autonomous-execution.md) +- 自动激活规则:[docs/method/activation-rules.md](docs/method/activation-rules.md) +- autonomous 入口 skill:[skills/method-forge-autonomous-execution/SKILL.md](skills/method-forge-autonomous-execution/SKILL.md) +- 恢复规则:[docs/method/resume-rules.md](docs/method/resume-rules.md) +- 防死循环规则:[docs/method/loop-guard-rules.md](docs/method/loop-guard-rules.md) +- 运行态模板:[docs/templates/run-state-template.md](docs/templates/run-state-template.md) +- heartbeat prompt 模板:[docs/templates/autonomous-heartbeat-prompt-template.md](docs/templates/autonomous-heartbeat-prompt-template.md) +- `v1` 验收报告:[docs/method/v1-acceptance-report.md](docs/method/v1-acceptance-report.md) +- `v1.5` 补强报告:[docs/method/v1.5-hardening-report.md](docs/method/v1.5-hardening-report.md) +- 失败回退规则:[docs/method/failure-rework-rules.md](docs/method/failure-rework-rules.md) +- 模板 lint 规则:[docs/method/template-lint-rules.md](docs/method/template-lint-rules.md) +- 流程健康检查:[docs/method/workflow-health-check.md](docs/method/workflow-health-check.md) +- 消费方接入说明:[docs/method/consumer-adoption.md](docs/method/consumer-adoption.md) +- 消费方 `AGENTS` 草案:[docs/templates/consumer-agents-rules-template.md](docs/templates/consumer-agents-rules-template.md) +- 健康检查模板:[docs/templates/workflow-health-report-template.md](docs/templates/workflow-health-report-template.md) +- 健康检查样例:[docs/method/examples/health-check/workflow-health-report.md](docs/method/examples/health-check/workflow-health-report.md) +- 轻量 preset 入口:[docs/presets/minimal-change-package/README.md](docs/presets/minimal-change-package/README.md) ## 当前版本 @@ -142,7 +142,7 @@ route-request 如果你希望在每个 worker 里支持显式 autonomous 请求或恢复既有 autonomous run,还需要: - 让入口 skill 全局可见 -- 采用 [activation-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/activation-rules.md) 中的触发规则 +- 采用 [activation-rules.md](docs/method/activation-rules.md) 中的触发规则 当前也已经补上一个消费方可直接复用的轻量落地包: diff --git a/method-forge/docs/method/activation-rules.md b/method-forge/docs/method/activation-rules.md index d98c83f..699f6cb 100644 --- a/method-forge/docs/method/activation-rules.md +++ b/method-forge/docs/method/activation-rules.md @@ -138,6 +138,6 @@ autonomous mode 的监听者使用 Codex 原生 heartbeat automation,内层默 ## 8. 配套文件 -- autonomous 扩展:[docs/method/autonomous-execution.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/autonomous-execution.md) -- autonomous 入口 skill:[skills/method-forge-autonomous-execution/SKILL.md](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-autonomous-execution/SKILL.md) -- 消费方 `AGENTS` 草案:[docs/templates/consumer-agents-rules-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/consumer-agents-rules-template.md) +- autonomous 扩展:[docs/method/autonomous-execution.md](autonomous-execution.md) +- autonomous 入口 skill:[skills/method-forge-autonomous-execution/SKILL.md](../../skills/method-forge-autonomous-execution/SKILL.md) +- 消费方 `AGENTS` 草案:[docs/templates/consumer-agents-rules-template.md](../templates/consumer-agents-rules-template.md) diff --git a/method-forge/docs/method/autonomous-execution.md b/method-forge/docs/method/autonomous-execution.md index 7dd9692..302c6d3 100644 --- a/method-forge/docs/method/autonomous-execution.md +++ b/method-forge/docs/method/autonomous-execution.md @@ -137,8 +137,8 @@ autonomous 周期恢复时应按以下顺序读取: ## 10. 配套文件 -- 恢复规则:[docs/method/resume-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/resume-rules.md) -- 防死循环规则:[docs/method/loop-guard-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/loop-guard-rules.md) -- 运行态模板:[docs/templates/run-state-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/run-state-template.md) -- 周期报告模板:[docs/templates/autonomous-cycle-report-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/autonomous-cycle-report-template.md) -- heartbeat prompt 草案:[docs/templates/autonomous-heartbeat-prompt-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/autonomous-heartbeat-prompt-template.md) +- 恢复规则:[docs/method/resume-rules.md](resume-rules.md) +- 防死循环规则:[docs/method/loop-guard-rules.md](loop-guard-rules.md) +- 运行态模板:[docs/templates/run-state-template.md](../templates/run-state-template.md) +- 周期报告模板:[docs/templates/autonomous-cycle-report-template.md](../templates/autonomous-cycle-report-template.md) +- heartbeat prompt 草案:[docs/templates/autonomous-heartbeat-prompt-template.md](../templates/autonomous-heartbeat-prompt-template.md) diff --git a/method-forge/docs/method/consumer-adoption.md b/method-forge/docs/method/consumer-adoption.md index 4b5a171..6b2b2bf 100644 --- a/method-forge/docs/method/consumer-adoption.md +++ b/method-forge/docs/method/consumer-adoption.md @@ -109,15 +109,15 @@ 当前已提供以下配套: -- 接入说明:[docs/method/consumer-adoption.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/consumer-adoption.md) -- autonomous 扩展说明:[docs/method/autonomous-execution.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/autonomous-execution.md) -- 自动激活规则:[docs/method/activation-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/activation-rules.md) -- autonomous 入口 skill:[skills/method-forge-autonomous-execution/SKILL.md](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-autonomous-execution/SKILL.md) -- 单入口 skill:[skills/method-forge-execute/SKILL.md](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-execute/SKILL.md) -- preset 入口:[docs/presets/minimal-change-package/README.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/presets/minimal-change-package/README.md) -- `AGENTS.md` 硬规则草案:[docs/templates/consumer-agents-rules-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/consumer-agents-rules-template.md) -- 变更包索引模板:[docs/templates/package-index-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/package-index-template.md) -- 落地检查清单模板:[docs/templates/adoption-checklist-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/adoption-checklist-template.md) +- 接入说明:[docs/method/consumer-adoption.md](consumer-adoption.md) +- autonomous 扩展说明:[docs/method/autonomous-execution.md](autonomous-execution.md) +- 自动激活规则:[docs/method/activation-rules.md](activation-rules.md) +- autonomous 入口 skill:[skills/method-forge-autonomous-execution/SKILL.md](../../skills/method-forge-autonomous-execution/SKILL.md) +- 单入口 skill:[skills/method-forge-execute/SKILL.md](../../skills/method-forge-execute/SKILL.md) +- preset 入口:[docs/presets/minimal-change-package/README.md](../presets/minimal-change-package/README.md) +- `AGENTS.md` 硬规则草案:[docs/templates/consumer-agents-rules-template.md](../templates/consumer-agents-rules-template.md) +- 变更包索引模板:[docs/templates/package-index-template.md](../templates/package-index-template.md) +- 落地检查清单模板:[docs/templates/adoption-checklist-template.md](../templates/adoption-checklist-template.md) ## 8. 当前边界 diff --git a/method-forge/docs/method/skill-contracts.md b/method-forge/docs/method/skill-contracts.md index ddc104d..ee3d233 100644 --- a/method-forge/docs/method/skill-contracts.md +++ b/method-forge/docs/method/skill-contracts.md @@ -6,7 +6,7 @@ - 输入尽量来自已存在的请求、仓库上下文和上一步文档 - 输出以单一主文档为准,不把真相源散落在聊天文本里 -- 默认使用 [docs/templates/](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates) 中的模板 +- 默认使用 [docs/templates/](../templates) 中的模板 - 不直接写长期 memory - 不改造 knowledge base 本体 - 不包装 Codex 原生工程能力 diff --git a/method-forge/docs/method/workflow-health-check.md b/method-forge/docs/method/workflow-health-check.md index 3bdcfdd..0669dfa 100644 --- a/method-forge/docs/method/workflow-health-check.md +++ b/method-forge/docs/method/workflow-health-check.md @@ -73,5 +73,5 @@ workflow health check 用于判断一套 `method-forge` 流程是否还在健康 当前已提供: -- 模板:[docs/templates/workflow-health-report-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/workflow-health-report-template.md) -- 样例:[docs/method/examples/health-check/workflow-health-report.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/examples/health-check/workflow-health-report.md) +- 模板:[docs/templates/workflow-health-report-template.md](../templates/workflow-health-report-template.md) +- 样例:[docs/method/examples/health-check/workflow-health-report.md](examples/health-check/workflow-health-report.md) diff --git a/method-forge/docs/method/workflow.md b/method-forge/docs/method/workflow.md index 74700c8..c1f73fc 100644 --- a/method-forge/docs/method/workflow.md +++ b/method-forge/docs/method/workflow.md @@ -32,8 +32,8 @@ docs/specs// 如果要把这套方法层接到别的 workspace,上述目录约定的轻量落地方式见: -- [docs/method/consumer-adoption.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/consumer-adoption.md) -- [docs/presets/minimal-change-package/README.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/presets/minimal-change-package/README.md) +- [docs/method/consumer-adoption.md](consumer-adoption.md) +- [docs/presets/minimal-change-package/README.md](../presets/minimal-change-package/README.md) ## 3. 主流程 diff --git a/method-forge/docs/presets/minimal-change-package/README.md b/method-forge/docs/presets/minimal-change-package/README.md index bce455e..986b557 100644 --- a/method-forge/docs/presets/minimal-change-package/README.md +++ b/method-forge/docs/presets/minimal-change-package/README.md @@ -47,27 +47,27 @@ docs/specs// ## Use These Templates -- [consumer-agents-rules-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/consumer-agents-rules-template.md) -- [package-index-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/package-index-template.md) -- [intake-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/intake-template.md) -- [spec-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/spec-template.md) -- [plan-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/plan-template.md) -- [plan-review-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/plan-review-template.md) -- [tasks-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/tasks-template.md) -- [verify-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/verify-template.md) -- [code-review-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/code-review-template.md) -- [memory-candidate-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/memory-candidate-template.md) -- [workflow-health-report-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/workflow-health-report-template.md) -- [run-state-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/run-state-template.md) -- [autonomous-cycle-report-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/autonomous-cycle-report-template.md) -- [autonomous-heartbeat-prompt-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/autonomous-heartbeat-prompt-template.md) -- [adoption-checklist-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/adoption-checklist-template.md) +- [consumer-agents-rules-template.md](../../templates/consumer-agents-rules-template.md) +- [package-index-template.md](../../templates/package-index-template.md) +- [intake-template.md](../../templates/intake-template.md) +- [spec-template.md](../../templates/spec-template.md) +- [plan-template.md](../../templates/plan-template.md) +- [plan-review-template.md](../../templates/plan-review-template.md) +- [tasks-template.md](../../templates/tasks-template.md) +- [verify-template.md](../../templates/verify-template.md) +- [code-review-template.md](../../templates/code-review-template.md) +- [memory-candidate-template.md](../../templates/memory-candidate-template.md) +- [workflow-health-report-template.md](../../templates/workflow-health-report-template.md) +- [run-state-template.md](../../templates/run-state-template.md) +- [autonomous-cycle-report-template.md](../../templates/autonomous-cycle-report-template.md) +- [autonomous-heartbeat-prompt-template.md](../../templates/autonomous-heartbeat-prompt-template.md) +- [adoption-checklist-template.md](../../templates/adoption-checklist-template.md) ## Notes -- 若消费方希望一句话触发整条流程,可以直接使用 [method-forge-execute](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-execute/SKILL.md) 的自然语言入口。 -- 若消费方希望任务自动续跑到本轮结束,可再叠加 [method-forge-autonomous-execution](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-autonomous-execution/SKILL.md) 和 heartbeat automation prompt。 -- 消费方可以先把 [consumer-agents-rules-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/consumer-agents-rules-template.md) 贴进自己的 `AGENTS.md` 再细化。 +- 若消费方希望一句话触发整条流程,可以直接使用 [method-forge-execute](../../../skills/method-forge-execute/SKILL.md) 的自然语言入口。 +- 若消费方希望任务自动续跑到本轮结束,可再叠加 [method-forge-autonomous-execution](../../../skills/method-forge-autonomous-execution/SKILL.md) 和 heartbeat automation prompt。 +- 消费方可以先把 [consumer-agents-rules-template.md](../../templates/consumer-agents-rules-template.md) 贴进自己的 `AGENTS.md` 再细化。 - `package-index.md` 是导航页,不是第二份 spec。 - 若消费方只做轻任务,不必强制补齐所有文件。 - 仍然遵守 `method-forge` 的边界:不重做 Codex 原生 multi-agent、git/PR、automations、sandbox。 diff --git a/method-forge/orchestrations/route-request/README.md b/method-forge/orchestrations/route-request/README.md index e874ce4..ad882f2 100644 --- a/method-forge/orchestrations/route-request/README.md +++ b/method-forge/orchestrations/route-request/README.md @@ -23,11 +23,11 @@ ## Flow -1. 调用 [method-forge-feature-intake](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-feature-intake/SKILL.md)。 +1. 调用 [method-forge-feature-intake](../../skills/method-forge-feature-intake/SKILL.md)。 2. 若 `need_research=true`,先补 research 或 knowledge-base 输入。 3. 若 `need_spec=false`,进入直接实现路径。 -4. 若 `need_spec=true`,进入 [spec-flow](/Users/wz/project/codex-enhanced-system/method-forge/orchestrations/spec-flow/README.md)。 -5. 实现完成后统一进入 [verify-and-memory](/Users/wz/project/codex-enhanced-system/method-forge/orchestrations/verify-and-memory/README.md)。 +4. 若 `need_spec=true`,进入 [spec-flow](../spec-flow/README.md)。 +5. 实现完成后统一进入 [verify-and-memory](../verify-and-memory/README.md)。 ## Branch Rules diff --git a/method-forge/orchestrations/spec-flow/README.md b/method-forge/orchestrations/spec-flow/README.md index 1c6bac1..e3a700e 100644 --- a/method-forge/orchestrations/spec-flow/README.md +++ b/method-forge/orchestrations/spec-flow/README.md @@ -18,13 +18,13 @@ spec-clarify ## Flow -1. 调用 [method-forge-spec-clarify](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-spec-clarify/SKILL.md),产出 `spec.md`。 +1. 调用 [method-forge-spec-clarify](../../skills/method-forge-spec-clarify/SKILL.md),产出 `spec.md`。 2. 对高风险或边界复杂任务,可在 `spec.md` 后暂停确认。 -3. 调用 [method-forge-plan-write](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-plan-write/SKILL.md),产出 `plan.md`。 +3. 调用 [method-forge-plan-write](../../skills/method-forge-plan-write/SKILL.md),产出 `plan.md`。 4. 对高风险或顺序敏感任务,可在 `plan.md` 后暂停确认。 -5. 调用 [method-forge-plan-review](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-plan-review/SKILL.md),产出 `plan-review.md`。 +5. 调用 [method-forge-plan-review](../../skills/method-forge-plan-review/SKILL.md),产出 `plan-review.md`。 6. 若 `approval_status=needs-revision`,回到 `plan-write` 修订。 -7. 若 `approval_status=approved`,调用 [method-forge-task-breakdown](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-task-breakdown/SKILL.md),产出 `tasks.md`。 +7. 若 `approval_status=approved`,调用 [method-forge-task-breakdown](../../skills/method-forge-task-breakdown/SKILL.md),产出 `tasks.md`。 ## Exit Criteria diff --git a/method-forge/orchestrations/verify-and-memory/README.md b/method-forge/orchestrations/verify-and-memory/README.md index 6790c37..1ae4a90 100644 --- a/method-forge/orchestrations/verify-and-memory/README.md +++ b/method-forge/orchestrations/verify-and-memory/README.md @@ -14,7 +14,7 @@ ## Flow -1. 调用 [method-forge-verify-change](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-verify-change/SKILL.md)。 +1. 调用 [method-forge-verify-change](../../skills/method-forge-verify-change/SKILL.md)。 2. 产出 `verify.md`,记录行为、测试、风险、文档同步和最终状态。 3. 当 `memory_candidate=yes` 时,只整理候选结论,不直接写长期 memory。 4. 当 `final_status` 不是 `passed` 时,明确阻塞项并回到实现或文档修订。 diff --git a/method-forge/skills/method-forge-autonomous-execution/SKILL.md b/method-forge/skills/method-forge-autonomous-execution/SKILL.md index f069943..2223905 100644 --- a/method-forge/skills/method-forge-autonomous-execution/SKILL.md +++ b/method-forge/skills/method-forge-autonomous-execution/SKILL.md @@ -69,8 +69,8 @@ description: Use when the user explicitly asks for autonomous execution, backgro ## References -- [docs/method/autonomous-execution.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/autonomous-execution.md) -- [docs/method/resume-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/resume-rules.md) -- [docs/method/loop-guard-rules.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/method/loop-guard-rules.md) -- [docs/templates/run-state-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/run-state-template.md) -- [docs/templates/autonomous-heartbeat-prompt-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/autonomous-heartbeat-prompt-template.md) +- [docs/method/autonomous-execution.md](../../docs/method/autonomous-execution.md) +- [docs/method/resume-rules.md](../../docs/method/resume-rules.md) +- [docs/method/loop-guard-rules.md](../../docs/method/loop-guard-rules.md) +- [docs/templates/run-state-template.md](../../docs/templates/run-state-template.md) +- [docs/templates/autonomous-heartbeat-prompt-template.md](../../docs/templates/autonomous-heartbeat-prompt-template.md) diff --git a/method-forge/skills/method-forge-code-review/SKILL.md b/method-forge/skills/method-forge-code-review/SKILL.md index ff188a2..81b3077 100644 --- a/method-forge/skills/method-forge-code-review/SKILL.md +++ b/method-forge/skills/method-forge-code-review/SKILL.md @@ -22,7 +22,7 @@ ## Output - `code-review.md` -- 模板来源:[docs/templates/code-review-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/code-review-template.md) +- 模板来源:[docs/templates/code-review-template.md](../../docs/templates/code-review-template.md) ## Procedure diff --git a/method-forge/skills/method-forge-execute/SKILL.md b/method-forge/skills/method-forge-execute/SKILL.md index c9c5d58..be576d1 100644 --- a/method-forge/skills/method-forge-execute/SKILL.md +++ b/method-forge/skills/method-forge-execute/SKILL.md @@ -35,13 +35,13 @@ description: Use when the user says phrases like "按method-forge方式执行", 1. 读取用户需求与已有设计材料,先建立或更新当前变更包。 2. 默认在当前 workspace 使用 `docs/specs/-/` 作为变更包目录,并先创建或更新 `package-index.md`。 -3. 按 [method-forge-feature-intake](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-feature-intake/SKILL.md) 产出 `intake.md`,判断轻任务还是复杂任务。 -4. 若 `need_spec=true`,按 [spec-flow](/Users/wz/project/codex-enhanced-system/method-forge/orchestrations/spec-flow/README.md) 归一化已有设计材料,补齐 `spec.md`、`plan.md`、`plan-review.md`、`tasks.md`。 +3. 按 [method-forge-feature-intake](../method-forge-feature-intake/SKILL.md) 产出 `intake.md`,判断轻任务还是复杂任务。 +4. 若 `need_spec=true`,按 [spec-flow](../../orchestrations/spec-flow/README.md) 归一化已有设计材料,补齐 `spec.md`、`plan.md`、`plan-review.md`、`tasks.md`。 5. 若用户已有设计文档,优先映射与补缺,不机械复制原文。 6. 若实现上下文足够,直接在同一 worker 里执行任务。 -7. 高风险改动按需追加 [method-forge-code-review](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-code-review/SKILL.md)。 -8. 完成后统一走 [verify-and-memory](/Users/wz/project/codex-enhanced-system/method-forge/orchestrations/verify-and-memory/README.md),产出 `verify.md`。 -9. 若 `verify.md` 判定存在稳定候选,再按 [method-forge-memory-promote](/Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-memory-promote/SKILL.md) 产出 `memory-candidate.md`。 +7. 高风险改动按需追加 [method-forge-code-review](../method-forge-code-review/SKILL.md)。 +8. 完成后统一走 [verify-and-memory](../../orchestrations/verify-and-memory/README.md),产出 `verify.md`。 +9. 若 `verify.md` 判定存在稳定候选,再按 [method-forge-memory-promote](../method-forge-memory-promote/SKILL.md) 产出 `memory-candidate.md`。 ## Outputs diff --git a/method-forge/skills/method-forge-feature-intake/SKILL.md b/method-forge/skills/method-forge-feature-intake/SKILL.md index 7acc2f3..ec35a57 100644 --- a/method-forge/skills/method-forge-feature-intake/SKILL.md +++ b/method-forge/skills/method-forge-feature-intake/SKILL.md @@ -19,7 +19,7 @@ ## Output - `intake.md` -- 模板来源:[docs/templates/intake-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/intake-template.md) +- 模板来源:[docs/templates/intake-template.md](../../docs/templates/intake-template.md) ## Procedure diff --git a/method-forge/skills/method-forge-memory-promote/SKILL.md b/method-forge/skills/method-forge-memory-promote/SKILL.md index 4118358..75eb3f5 100644 --- a/method-forge/skills/method-forge-memory-promote/SKILL.md +++ b/method-forge/skills/method-forge-memory-promote/SKILL.md @@ -18,7 +18,7 @@ ## Output - `memory-candidate.md` -- 模板来源:[docs/templates/memory-candidate-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/memory-candidate-template.md) +- 模板来源:[docs/templates/memory-candidate-template.md](../../docs/templates/memory-candidate-template.md) ## Procedure diff --git a/method-forge/skills/method-forge-plan-review/SKILL.md b/method-forge/skills/method-forge-plan-review/SKILL.md index df0bd8a..7128788 100644 --- a/method-forge/skills/method-forge-plan-review/SKILL.md +++ b/method-forge/skills/method-forge-plan-review/SKILL.md @@ -18,7 +18,7 @@ ## Output - `plan-review.md` -- 模板来源:[docs/templates/plan-review-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/plan-review-template.md) +- 模板来源:[docs/templates/plan-review-template.md](../../docs/templates/plan-review-template.md) ## Procedure diff --git a/method-forge/skills/method-forge-plan-write/SKILL.md b/method-forge/skills/method-forge-plan-write/SKILL.md index 02e0325..7718c7a 100644 --- a/method-forge/skills/method-forge-plan-write/SKILL.md +++ b/method-forge/skills/method-forge-plan-write/SKILL.md @@ -18,7 +18,7 @@ ## Output - `plan.md` -- 模板来源:[docs/templates/plan-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/plan-template.md) +- 模板来源:[docs/templates/plan-template.md](../../docs/templates/plan-template.md) ## Procedure diff --git a/method-forge/skills/method-forge-spec-clarify/SKILL.md b/method-forge/skills/method-forge-spec-clarify/SKILL.md index 75454d1..1d978be 100644 --- a/method-forge/skills/method-forge-spec-clarify/SKILL.md +++ b/method-forge/skills/method-forge-spec-clarify/SKILL.md @@ -18,7 +18,7 @@ ## Output - `spec.md` -- 模板来源:[docs/templates/spec-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/spec-template.md) +- 模板来源:[docs/templates/spec-template.md](../../docs/templates/spec-template.md) ## Procedure diff --git a/method-forge/skills/method-forge-task-breakdown/SKILL.md b/method-forge/skills/method-forge-task-breakdown/SKILL.md index 2c9106b..87b3f7f 100644 --- a/method-forge/skills/method-forge-task-breakdown/SKILL.md +++ b/method-forge/skills/method-forge-task-breakdown/SKILL.md @@ -17,7 +17,7 @@ ## Output - `tasks.md` -- 模板来源:[docs/templates/tasks-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/tasks-template.md) +- 模板来源:[docs/templates/tasks-template.md](../../docs/templates/tasks-template.md) ## Procedure diff --git a/method-forge/skills/method-forge-verify-change/SKILL.md b/method-forge/skills/method-forge-verify-change/SKILL.md index c6529ec..72af7bd 100644 --- a/method-forge/skills/method-forge-verify-change/SKILL.md +++ b/method-forge/skills/method-forge-verify-change/SKILL.md @@ -25,7 +25,7 @@ ## Output - `verify.md` -- 模板来源:[docs/templates/verify-template.md](/Users/wz/project/codex-enhanced-system/method-forge/docs/templates/verify-template.md) +- 模板来源:[docs/templates/verify-template.md](../../docs/templates/verify-template.md) ## Procedure diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..3023bcb --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" +codex_home="${CODEX_HOME:-$HOME/.codex}" +skills_home="$codex_home/skills" + +if [[ ! -d "$repo_root/.git" ]]; then + echo "This script must be run from a checkout of codex-enhanced-system." >&2 + exit 1 +fi + +mkdir -p "$codex_home" +mkdir -p "$skills_home" + +echo "Installing method-forge skills into: $skills_home" +"$script_dir/install-skills.sh" \ + --source "$repo_root/method-forge/skills" \ + --target "$skills_home" \ + --mode symlink + +cat < --target [--mode symlink|copy] + +Installs each direct subdirectory containing a SKILL.md file from the source +directory into the target Codex skills directory. +EOF +} + +source_dir="" +target_dir="" +mode="symlink" + +while [[ $# -gt 0 ]]; do + case "$1" in + --source) + source_dir="${2:-}" + shift 2 + ;; + --target) + target_dir="${2:-}" + shift 2 + ;; + --mode) + mode="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "$source_dir" || -z "$target_dir" ]]; then + usage >&2 + exit 1 +fi + +if [[ ! -d "$source_dir" ]]; then + echo "Source directory not found: $source_dir" >&2 + exit 1 +fi + +mkdir -p "$target_dir" + +installed=0 +while IFS= read -r -d '' skill_dir; do + skill_name="$(basename "$skill_dir")" + skill_target="$target_dir/$skill_name" + + case "$mode" in + symlink) + ln -sfn "$skill_dir" "$skill_target" + ;; + copy) + rm -rf "$skill_target" + cp -R "$skill_dir" "$skill_target" + ;; + *) + echo "Unsupported mode: $mode" >&2 + exit 1 + ;; + esac + + installed=$((installed + 1)) + printf 'Installed skill: %s\n' "$skill_name" +done < <(find "$source_dir" -mindepth 1 -maxdepth 1 -type d -name '*' -print0) + +if [[ "$installed" -eq 0 ]]; then + echo "No skills found in: $source_dir" >&2 + exit 1 +fi + +printf 'Skills installed into: %s\n' "$target_dir" + From 749d992927acd8f30b4c224624a478667b529ceb Mon Sep 17 00:00:00 2001 From: wz Date: Sat, 11 Apr 2026 22:24:48 +0800 Subject: [PATCH 2/2] align knowledge base and autonomous workflow --- .../2026-04-11-kb-drift-remediation/intake.md | 26 + .../package-index.md | 38 + .../plan-review.md | 11 + .../2026-04-11-kb-drift-remediation/plan.md | 13 + .../runtime/run-state.md | 45 + .../2026-04-11-kb-drift-remediation/spec.md | 35 + .../2026-04-11-kb-drift-remediation/tasks.md | 15 + .../2026-04-11-kb-drift-remediation/verify.md | 33 + .../intake.md | 24 + .../package-index.md | 38 + .../plan-review.md | 11 + .../plan.md | 10 + .../runtime/run-state.md | 45 + .../spec.md | 31 + .../tasks.md | 11 + .../verify.md | 36 + .../2026-04-11-kb-drift-review/intake.md | 26 + .../package-index.md | 38 + .../2026-04-11-kb-drift-review/plan-review.md | 11 + docs/specs/2026-04-11-kb-drift-review/plan.md | 13 + .../runtime/run-state.md | 45 + docs/specs/2026-04-11-kb-drift-review/spec.md | 43 + .../specs/2026-04-11-kb-drift-review/tasks.md | 17 + .../2026-04-11-kb-drift-review/verify.md | 33 + .../intake.md | 24 + .../package-index.md | 38 + .../plan-review.md | 11 + .../plan.md | 10 + .../runtime/run-state.md | 43 + .../spec.md | 31 + .../tasks.md | 13 + .../verify.md | 36 + .../intake.md | 26 + .../package-index.md | 38 + .../plan-review.md | 11 + .../plan.md | 10 + .../runtime/run-state.md | 45 + .../spec.md | 33 + .../tasks.md | 17 + .../verify.md | 33 + .../2026-04-11-kb-maintain-json/intake.md | 26 + .../package-index.md | 38 + .../plan-review.md | 11 + .../specs/2026-04-11-kb-maintain-json/plan.md | 10 + .../runtime/run-state.md | 45 + .../specs/2026-04-11-kb-maintain-json/spec.md | 37 + .../2026-04-11-kb-maintain-json/tasks.md | 15 + .../2026-04-11-kb-maintain-json/verify.md | 33 + .../intake.md | 26 + .../package-index.md | 38 + .../plan-review.md | 11 + .../plan.md | 13 + .../runtime/run-state.md | 45 + .../spec.md | 33 + .../tasks.md | 15 + .../verify.md | 27 + .../2026-04-11-kb-query-provenance/intake.md | 54 + .../package-index.md | 40 + .../plan-review.md | 35 + .../2026-04-11-kb-query-provenance/plan.md | 54 + .../runtime/run-state.md | 43 + .../2026-04-11-kb-query-provenance/spec.md | 68 + .../2026-04-11-kb-query-provenance/tasks.md | 43 + .../2026-04-11-kb-query-provenance/verify.md | 70 + .../intake.md | 28 + .../package-index.md | 38 + .../plan-review.md | 11 + .../plan.md | 13 + .../runtime/run-state.md | 45 + .../spec.md | 37 + .../tasks.md | 15 + .../verify.md | 36 + .../2026-04-11-kb-report-index-sync/intake.md | 24 + .../package-index.md | 38 + .../plan-review.md | 11 + .../2026-04-11-kb-report-index-sync/plan.md | 10 + .../runtime/run-state.md | 43 + .../2026-04-11-kb-report-index-sync/spec.md | 33 + .../2026-04-11-kb-report-index-sync/tasks.md | 15 + .../2026-04-11-kb-report-index-sync/verify.md | 36 + .../intake.md | 28 + .../package-index.md | 38 + .../plan-review.md | 13 + .../2026-04-11-kb-write-path-closeout/plan.md | 13 + .../runtime/run-state.md | 43 + .../2026-04-11-kb-write-path-closeout/spec.md | 35 + .../tasks.md | 15 + .../verify.md | 39 + .../intake.md | 24 + .../package-index.md | 38 + .../plan-review.md | 11 + .../plan.md | 13 + .../runtime/run-state.md | 43 + .../spec.md | 29 + .../tasks.md | 11 + .../verify.md | 24 + knowledge-base/AGENTS.md | 6 +- knowledge-base/KB_COMMANDS.md | 112 +- knowledge-base/kb | 1295 ++++++++++++++++- .../raw/repos/codex-memory-kit/.gitignore | 5 + ...dex.mult-agent.feishu-bridge.plist.example | 31 + .../repos/codex-memory-kit/package-lock.json | 12 + .../raw/repos/codex-memory-kit/package.json | 10 + .../0001-strict-formal-memory-adapter.patch | 992 +++++++++++++ .../0002-strict-memory-refresh-bridge.patch | 372 +++++ .../0003-team-complete-memory-refresh.patch | 616 ++++++++ ...0004-team-verification-evidence-gate.patch | 489 +++++++ ...-codex-upstream-strict-formal-memory.patch | 971 ++++++++++++ ...pstream-strict-memory-refresh-bridge.patch | 358 +++++ ...pstream-team-complete-memory-refresh.patch | 600 ++++++++ ...ream-team-verification-evidence-gate.patch | 472 ++++++ .../repos/codex-memory-kit/src/constants.js | 24 + .../src/contracts/strict-integration-mode.js | 51 + .../raw/repos/codex-memory-kit/src/index.js | 113 ++ .../src/integration/external-memory.js | 109 ++ .../src/integration/project-memory-view.js | 78 + .../src/integration/workspace-resolver.js | 71 + .../src/overlay/build-overlay-context.js | 112 ++ .../src/policy/error-recovery.js | 200 +++ .../src/policy/hitl-checkpoints.js | 166 +++ .../src/policy/legacy-memory-bypass.js | 91 ++ .../codex-memory-kit/src/policy/path-guard.js | 85 ++ .../src/policy/permission-gate.js | 104 ++ .../src/runtime/agent-startup-context.js | 82 ++ .../src/runtime/guarded-action-runner.js | 271 ++++ .../src/runtime/leader-refresh-trigger.js | 182 +++ .../src/runtime/memory-intake-queue.js | 170 +++ .../src/runtime/project-memory-commands.js | 233 +++ .../src/runtime/promotion-audit-trail.js | 123 ++ .../src/runtime/promotion-gate.js | 305 ++++ .../src/runtime/runtime-facade.js | 423 ++++++ .../src/runtime/state-store.js | 179 +++ .../src/runtime/verification-state.js | 325 +++++ .../src/team/team-contract.js | 41 + .../test/agent-startup-context.test.js | 50 + .../test/error-recovery.test.js | 56 + .../test/external-memory.test.js | 48 + .../test/guarded-action-runner.test.js | 111 ++ .../codex-memory-kit/test/helpers/fixtures.js | 104 ++ .../test/hitl-checkpoints.test.js | 82 ++ .../test/leader-refresh-trigger.test.js | 183 +++ .../test/legacy-memory-bypass.test.js | 85 ++ .../test/memory-intake-queue.test.js | 111 ++ .../test/overlay-context.test.js | 32 + .../codex-memory-kit/test/path-guard.test.js | 57 + .../test/permission-gate.test.js | 74 + .../test/project-memory-commands.test.js | 119 ++ .../test/project-memory-view.test.js | 37 + .../test/promotion-audit-trail.test.js | 65 + .../test/promotion-gate.test.js | 152 ++ .../test/runtime-facade.test.js | 214 +++ .../codex-memory-kit/test/state-store.test.js | 101 ++ .../test/strict-integration-mode.test.js | 29 + .../test/team-contract.test.js | 81 ++ .../test/verification-state.test.js | 87 ++ .../test/workspace-resolver.test.js | 45 + knowledge-base/tests/test_kb_query.py | 1004 +++++++++++++ .../concept-codex-native-memory-governance.md | 5 +- .../concept-verification-evidence-gate.md | 4 +- .../wiki/entities/entity-codex-memory-kit.md | 4 +- knowledge-base/wiki/hot.md | 5 +- knowledge-base/wiki/index.md | 14 +- knowledge-base/wiki/log.md | 15 +- knowledge-base/wiki/overview.md | 10 +- .../reports/report-drift-review-2026-04-11.md | 33 + .../sources/source-codex-memory-kit-readme.md | 4 +- ...my-codex-memory-integration-development.md | 4 +- ...-codex-upstream-first-integration-apply.md | 4 +- ...codex-upstream-first-integration-status.md | 4 +- ...ource-oh-my-codex-upstream-review-notes.md | 4 +- ...codex-native-memory-governance-baseline.md | 6 +- .../synthesis-upstream-integration-rollout.md | 8 +- .../synthesis-upstream-reviewer-packet.md | 4 +- memory-system/AGENTS.md | 6 +- .../codex/memory/instructions/user/GUIDE.md | 2 +- method-forge/AGENTS.md | 6 +- method-forge/README.md | 3 +- method-forge/docs/method/activation-rules.md | 44 +- .../docs/method/autonomous-execution.md | 18 +- method-forge/docs/method/consumer-adoption.md | 6 +- method-forge/docs/method/resume-rules.md | 2 + method-forge/docs/method/skill-contracts.md | 2 +- .../presets/minimal-change-package/README.md | 3 +- .../autonomous-heartbeat-prompt-template.md | 3 +- .../consumer-agents-rules-template.md | 5 +- .../SKILL.md | 13 +- 186 files changed, 15676 insertions(+), 118 deletions(-) create mode 100644 docs/specs/2026-04-11-kb-drift-remediation/intake.md create mode 100644 docs/specs/2026-04-11-kb-drift-remediation/package-index.md create mode 100644 docs/specs/2026-04-11-kb-drift-remediation/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-drift-remediation/plan.md create mode 100644 docs/specs/2026-04-11-kb-drift-remediation/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-drift-remediation/spec.md create mode 100644 docs/specs/2026-04-11-kb-drift-remediation/tasks.md create mode 100644 docs/specs/2026-04-11-kb-drift-remediation/verify.md create mode 100644 docs/specs/2026-04-11-kb-drift-report-stabilization/intake.md create mode 100644 docs/specs/2026-04-11-kb-drift-report-stabilization/package-index.md create mode 100644 docs/specs/2026-04-11-kb-drift-report-stabilization/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-drift-report-stabilization/plan.md create mode 100644 docs/specs/2026-04-11-kb-drift-report-stabilization/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-drift-report-stabilization/spec.md create mode 100644 docs/specs/2026-04-11-kb-drift-report-stabilization/tasks.md create mode 100644 docs/specs/2026-04-11-kb-drift-report-stabilization/verify.md create mode 100644 docs/specs/2026-04-11-kb-drift-review/intake.md create mode 100644 docs/specs/2026-04-11-kb-drift-review/package-index.md create mode 100644 docs/specs/2026-04-11-kb-drift-review/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-drift-review/plan.md create mode 100644 docs/specs/2026-04-11-kb-drift-review/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-drift-review/spec.md create mode 100644 docs/specs/2026-04-11-kb-drift-review/tasks.md create mode 100644 docs/specs/2026-04-11-kb-drift-review/verify.md create mode 100644 docs/specs/2026-04-11-kb-index-updated-at-sync/intake.md create mode 100644 docs/specs/2026-04-11-kb-index-updated-at-sync/package-index.md create mode 100644 docs/specs/2026-04-11-kb-index-updated-at-sync/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-index-updated-at-sync/plan.md create mode 100644 docs/specs/2026-04-11-kb-index-updated-at-sync/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-index-updated-at-sync/spec.md create mode 100644 docs/specs/2026-04-11-kb-index-updated-at-sync/tasks.md create mode 100644 docs/specs/2026-04-11-kb-index-updated-at-sync/verify.md create mode 100644 docs/specs/2026-04-11-kb-maintain-health-summary/intake.md create mode 100644 docs/specs/2026-04-11-kb-maintain-health-summary/package-index.md create mode 100644 docs/specs/2026-04-11-kb-maintain-health-summary/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-maintain-health-summary/plan.md create mode 100644 docs/specs/2026-04-11-kb-maintain-health-summary/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-maintain-health-summary/spec.md create mode 100644 docs/specs/2026-04-11-kb-maintain-health-summary/tasks.md create mode 100644 docs/specs/2026-04-11-kb-maintain-health-summary/verify.md create mode 100644 docs/specs/2026-04-11-kb-maintain-json/intake.md create mode 100644 docs/specs/2026-04-11-kb-maintain-json/package-index.md create mode 100644 docs/specs/2026-04-11-kb-maintain-json/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-maintain-json/plan.md create mode 100644 docs/specs/2026-04-11-kb-maintain-json/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-maintain-json/spec.md create mode 100644 docs/specs/2026-04-11-kb-maintain-json/tasks.md create mode 100644 docs/specs/2026-04-11-kb-maintain-json/verify.md create mode 100644 docs/specs/2026-04-11-kb-provenance-health-checks/intake.md create mode 100644 docs/specs/2026-04-11-kb-provenance-health-checks/package-index.md create mode 100644 docs/specs/2026-04-11-kb-provenance-health-checks/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-provenance-health-checks/plan.md create mode 100644 docs/specs/2026-04-11-kb-provenance-health-checks/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-provenance-health-checks/spec.md create mode 100644 docs/specs/2026-04-11-kb-provenance-health-checks/tasks.md create mode 100644 docs/specs/2026-04-11-kb-provenance-health-checks/verify.md create mode 100644 docs/specs/2026-04-11-kb-query-provenance/intake.md create mode 100644 docs/specs/2026-04-11-kb-query-provenance/package-index.md create mode 100644 docs/specs/2026-04-11-kb-query-provenance/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-query-provenance/plan.md create mode 100644 docs/specs/2026-04-11-kb-query-provenance/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-query-provenance/spec.md create mode 100644 docs/specs/2026-04-11-kb-query-provenance/tasks.md create mode 100644 docs/specs/2026-04-11-kb-query-provenance/verify.md create mode 100644 docs/specs/2026-04-11-kb-query-ranking-governance/intake.md create mode 100644 docs/specs/2026-04-11-kb-query-ranking-governance/package-index.md create mode 100644 docs/specs/2026-04-11-kb-query-ranking-governance/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-query-ranking-governance/plan.md create mode 100644 docs/specs/2026-04-11-kb-query-ranking-governance/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-query-ranking-governance/spec.md create mode 100644 docs/specs/2026-04-11-kb-query-ranking-governance/tasks.md create mode 100644 docs/specs/2026-04-11-kb-query-ranking-governance/verify.md create mode 100644 docs/specs/2026-04-11-kb-report-index-sync/intake.md create mode 100644 docs/specs/2026-04-11-kb-report-index-sync/package-index.md create mode 100644 docs/specs/2026-04-11-kb-report-index-sync/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-report-index-sync/plan.md create mode 100644 docs/specs/2026-04-11-kb-report-index-sync/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-report-index-sync/spec.md create mode 100644 docs/specs/2026-04-11-kb-report-index-sync/tasks.md create mode 100644 docs/specs/2026-04-11-kb-report-index-sync/verify.md create mode 100644 docs/specs/2026-04-11-kb-write-path-closeout/intake.md create mode 100644 docs/specs/2026-04-11-kb-write-path-closeout/package-index.md create mode 100644 docs/specs/2026-04-11-kb-write-path-closeout/plan-review.md create mode 100644 docs/specs/2026-04-11-kb-write-path-closeout/plan.md create mode 100644 docs/specs/2026-04-11-kb-write-path-closeout/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-kb-write-path-closeout/spec.md create mode 100644 docs/specs/2026-04-11-kb-write-path-closeout/tasks.md create mode 100644 docs/specs/2026-04-11-kb-write-path-closeout/verify.md create mode 100644 docs/specs/2026-04-11-method-forge-autonomous-continuity/intake.md create mode 100644 docs/specs/2026-04-11-method-forge-autonomous-continuity/package-index.md create mode 100644 docs/specs/2026-04-11-method-forge-autonomous-continuity/plan-review.md create mode 100644 docs/specs/2026-04-11-method-forge-autonomous-continuity/plan.md create mode 100644 docs/specs/2026-04-11-method-forge-autonomous-continuity/runtime/run-state.md create mode 100644 docs/specs/2026-04-11-method-forge-autonomous-continuity/spec.md create mode 100644 docs/specs/2026-04-11-method-forge-autonomous-continuity/tasks.md create mode 100644 docs/specs/2026-04-11-method-forge-autonomous-continuity/verify.md create mode 100644 knowledge-base/raw/repos/codex-memory-kit/.gitignore create mode 100644 knowledge-base/raw/repos/codex-memory-kit/launchd/com.codex.mult-agent.feishu-bridge.plist.example create mode 100644 knowledge-base/raw/repos/codex-memory-kit/package-lock.json create mode 100644 knowledge-base/raw/repos/codex-memory-kit/package.json create mode 100644 knowledge-base/raw/repos/codex-memory-kit/patches/0001-strict-formal-memory-adapter.patch create mode 100644 knowledge-base/raw/repos/codex-memory-kit/patches/0002-strict-memory-refresh-bridge.patch create mode 100644 knowledge-base/raw/repos/codex-memory-kit/patches/0003-team-complete-memory-refresh.patch create mode 100644 knowledge-base/raw/repos/codex-memory-kit/patches/0004-team-verification-evidence-gate.patch create mode 100644 knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-strict-formal-memory.patch create mode 100644 knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-strict-memory-refresh-bridge.patch create mode 100644 knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-team-complete-memory-refresh.patch create mode 100644 knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-team-verification-evidence-gate.patch create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/constants.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/contracts/strict-integration-mode.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/index.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/integration/external-memory.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/integration/project-memory-view.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/integration/workspace-resolver.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/overlay/build-overlay-context.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/policy/error-recovery.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/policy/hitl-checkpoints.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/policy/legacy-memory-bypass.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/policy/path-guard.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/policy/permission-gate.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/agent-startup-context.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/guarded-action-runner.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/leader-refresh-trigger.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/memory-intake-queue.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/project-memory-commands.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/promotion-audit-trail.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/promotion-gate.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/runtime-facade.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/state-store.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/runtime/verification-state.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/src/team/team-contract.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/agent-startup-context.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/error-recovery.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/external-memory.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/guarded-action-runner.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/helpers/fixtures.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/hitl-checkpoints.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/leader-refresh-trigger.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/legacy-memory-bypass.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/memory-intake-queue.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/overlay-context.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/path-guard.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/permission-gate.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/project-memory-commands.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/project-memory-view.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/promotion-audit-trail.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/promotion-gate.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/runtime-facade.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/state-store.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/strict-integration-mode.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/team-contract.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/verification-state.test.js create mode 100644 knowledge-base/raw/repos/codex-memory-kit/test/workspace-resolver.test.js create mode 100644 knowledge-base/tests/test_kb_query.py create mode 100644 knowledge-base/wiki/reports/report-drift-review-2026-04-11.md diff --git a/docs/specs/2026-04-11-kb-drift-remediation/intake.md b/docs/specs/2026-04-11-kb-drift-remediation/intake.md new file mode 100644 index 0000000..71ed810 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-remediation/intake.md @@ -0,0 +1,26 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户继续要求推进实现。 +- The user asked to keep pushing the implementation forward. + +## Problem / 问题 + +- 新增的 `kb drift-review` 已经在真实仓库上抓到了非空信号。 +- 如果只停留在“看到了 signal”,这套治理链路还没形成闭环。 + +- The new `kb drift-review` command already surfaced non-empty signals on the real repository. +- If we stop at “we saw the signal,” the governance loop is still not closed. + +## Scope / 范围 + +- 复核被 `source-lag` 标到的 canonical 页 +- 刷新 `hot/index/overview/log` +- 生成并收口 drift review report +- 用 `drift-review` 与 `maintain` 验证最终状态 + +- Review the canonical pages flagged by `source-lag` +- Refresh `hot/index/overview/log` +- Generate and close out the drift-review report +- Verify the final state with `drift-review` and `maintain` diff --git a/docs/specs/2026-04-11-kb-drift-remediation/package-index.md b/docs/specs/2026-04-11-kb-drift-remediation/package-index.md new file mode 100644 index 0000000..7a31552 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-remediation/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-drift-remediation` +- title: `knowledge-base drift signal remediation` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-drift-remediation/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-drift-remediation/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-drift-remediation/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-drift-remediation/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-drift-remediation/tasks.md` | +| implementation | `done` | `knowledge-base/wiki/concepts/*`, `knowledge-base/wiki/entities/*`, `knowledge-base/wiki/syntheses/*`, `knowledge-base/wiki/hot.md`, `knowledge-base/wiki/index.md`, `knowledge-base/wiki/overview.md`, `knowledge-base/wiki/log.md`, `knowledge-base/wiki/reports/report-drift-review-2026-04-11.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-drift-remediation/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-drift-remediation/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 根据 `kb drift-review` 的真实信号,定向刷新陈旧 canonical 页、导航页和 drift review report。 +- goal_en: Use the real `kb drift-review` signals to refresh stale canonical pages, guide pages, and the drift-review report itself. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 如有需要,可继续把这批 refreshed pages 的细化结论再蒸馏成新的概念页或报告。 +- next_step_en: If needed, continue by distilling the refreshed conclusions into new concept pages or reports. + +## Notes / 备注 + +- 本轮不是新增检测器,而是根据检测结果做内容层修复与导航层收口。 +- This slice does not add a new detector; it remediates content and guide surfaces based on the detector output. diff --git a/docs/specs/2026-04-11-kb-drift-remediation/plan-review.md b/docs/specs/2026-04-11-kb-drift-remediation/plan-review.md new file mode 100644 index 0000000..6f15604 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-remediation/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 方案把 drift remediation 分成“内容页刷新”和“导航层收口”两部分,符合当前 signal 的真实来源。 +- 通过要求 `drift-review` 回到 `stable`,我们能直接验证这次内容修复是否真的奏效。 +- report 自身也要收口,否则会留下“修完以后 report 反而变旧”的假信号。 + +- The plan splits drift remediation into “canonical content refresh” and “guide-layer closeout,” which matches the real source of the current signals. +- Requiring `drift-review` to return to `stable` gives a direct check that the remediation actually worked. +- The report itself must also be closed out; otherwise it leaves a false signal where the remediation is done but the report is stale. diff --git a/docs/specs/2026-04-11-kb-drift-remediation/plan.md b/docs/specs/2026-04-11-kb-drift-remediation/plan.md new file mode 100644 index 0000000..31d6669 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-remediation/plan.md @@ -0,0 +1,13 @@ +# Plan / 计划 + +1. 复核被 `source-lag` 标到的 canonical 页,与新 source 页对齐稳定结论。 + Review the canonical pages flagged by `source-lag` and align their stable conclusions with newer source pages. + +2. 刷新 `hot/index/overview/log`,把新的 drift review surface 纳入导航层。 + Refresh `hot/index/overview/log` and integrate the new drift-review surface into the guide layer. + +3. 写出 drift review report,并把 report 自身收口到最终稳定状态。 + Write the drift-review report and close the report itself to the final stable state. + +4. 用 `drift-review`、`maintain` 和格式检查验证结果。 + Verify the result with `drift-review`, `maintain`, and formatting checks. diff --git a/docs/specs/2026-04-11-kb-drift-remediation/runtime/run-state.md b/docs/specs/2026-04-11-kb-drift-remediation/runtime/run-state.md new file mode 100644 index 0000000..145a396 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-remediation/runtime/run-state.md @@ -0,0 +1,45 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-drift-remediation` +- title: `knowledge-base drift signal remediation` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-drift-remediation/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue only if new drift or content gaps appear` | +| stop_reason | `current remediation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T20:17:56+0800` +- last_progress_at: `2026-04-11T20:30:08+0800` +- last_resumed_at: `2026-04-11T20:30:08+0800` +- updated_at: `2026-04-11T20:30:08+0800` + +## Notes / 备注 + +- 本轮根据真实 drift signals 做了内容刷新与导航层收口,并把 drift review report 本身也收敛到稳定状态。 +- verify、diff check 和 memory refresh 已完成。 +- This slice refreshed content and guide surfaces based on real drift signals and also closed the drift-review report itself to a stable state. +- Verify, diff check, and memory refresh are complete. diff --git a/docs/specs/2026-04-11-kb-drift-remediation/spec.md b/docs/specs/2026-04-11-kb-drift-remediation/spec.md new file mode 100644 index 0000000..d6086de --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-remediation/spec.md @@ -0,0 +1,35 @@ +# Spec / 规格 + +## Goals / 目标 + +- 让先前的 drift signals 在内容层得到真实响应,而不是只通过忽略或降级处理。 +- Ensure the previous drift signals receive a real content-level response rather than being merely ignored or downgraded. + +- 让 `drift-review` 在当前仓库上重新回到 `stable`,同时保持 `maintain` 为 `healthy`。 +- Bring `drift-review` back to `stable` on the current repository while keeping `maintain` at `healthy`. + +## Non-Goals / 非目标 + +- 不扩展新的 drift heuristics +- 不扩大领域范围 +- 不改 query / maintain 的接口契约 + +- Do not expand the drift heuristics +- Do not widen the domain scope +- Do not change the `query` / `maintain` interface contracts + +## Functional Rules / 功能规则 + +- 只有在 newer source 提供了稳定新增信息时,才刷新 canonical 页内容 +- guide 页刷新要同时更新导航入口,不做纯时间戳漂白 +- drift review report 应反映最终当前状态,而不是停留在“写入前”的瞬时残余信号 +- 最终状态要求: + - `kb drift-review` -> `stable` + - `kb maintain` -> `healthy` + +- Refresh canonical page content only when newer sources add stable information +- Refresh guide pages together with their navigation entry points instead of performing timestamp-only whitening +- The drift-review report should reflect the final current state instead of a transient pre-write residual signal +- Final-state requirements: + - `kb drift-review` -> `stable` + - `kb maintain` -> `healthy` diff --git a/docs/specs/2026-04-11-kb-drift-remediation/tasks.md b/docs/specs/2026-04-11-kb-drift-remediation/tasks.md new file mode 100644 index 0000000..50486ab --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-remediation/tasks.md @@ -0,0 +1,15 @@ +# Tasks / 任务拆解 + +- [x] 刷新被 `source-lag` 标到的 canonical 页 +- [x] 刷新 `hot/index/overview/log` +- [x] 生成并收口 `report-drift-review-2026-04-11.md` +- [x] 重新跑 `kb drift-review` +- [x] 重新跑 `kb maintain` +- [x] 补双语变更包文档 + +- [x] Refresh the canonical pages flagged by `source-lag` +- [x] Refresh `hot/index/overview/log` +- [x] Generate and close out `report-drift-review-2026-04-11.md` +- [x] Re-run `kb drift-review` +- [x] Re-run `kb maintain` +- [x] Add the bilingual change-package docs diff --git a/docs/specs/2026-04-11-kb-drift-remediation/verify.md b/docs/specs/2026-04-11-kb-drift-remediation/verify.md new file mode 100644 index 0000000..8a760a9 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-remediation/verify.md @@ -0,0 +1,33 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review --json +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain --json +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/concepts/concept-codex-native-memory-governance.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/concepts/concept-verification-evidence-gate.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/entities/entity-codex-memory-kit.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/syntheses/synthesis-codex-native-memory-governance-baseline.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/syntheses/synthesis-upstream-integration-rollout.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/syntheses/synthesis-upstream-reviewer-packet.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/hot.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/index.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/overview.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/log.md /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/reports/report-drift-review-2026-04-11.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-drift-remediation +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `kb drift-review`: `drift_verdict: stable` +- `kb drift-review --json`: `status: ok`, `signals: []` +- `kb maintain`: `health_verdict: healthy` +- `kb maintain --json`: `status: ok`, `issues: []` +- `git diff --check`: 无输出,目标内容与变更包格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `kb drift-review`: `drift_verdict: stable` +- `kb drift-review --json`: `status: ok`, `signals: []` +- `kb maintain`: `health_verdict: healthy` +- `kb maintain --json`: `status: ok`, `issues: []` +- `git diff --check`: no output, so the target content and change package passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- 当前仓库这批 drift signals 已经被实质性消化,检测器重新回到稳定态,且相关内容页与变更包都完成了格式校验和 memory 刷新。 +- The current repository has substantively absorbed this batch of drift signals, the detector is back to a stable state, and the related content plus change package completed formatting checks and memory refresh. diff --git a/docs/specs/2026-04-11-kb-drift-report-stabilization/intake.md b/docs/specs/2026-04-11-kb-drift-report-stabilization/intake.md new file mode 100644 index 0000000..eb71687 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-report-stabilization/intake.md @@ -0,0 +1,24 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户继续要求推进实现。 +- The user asked to continue advancing the implementation. + +## Problem / 问题 + +- `kb drift-review --write-report` 之前按写入前的 signals 直接渲染报告。 +- 这会导致命令本身刚刚解决掉的 `report-lag` 仍然被写进新报告里,形成一次需要手工回写的“旧快照”。 + +- `kb drift-review --write-report` previously rendered the report directly from the pre-write signals. +- That meant the command could archive the very `report-lag` it had just resolved, leaving a stale snapshot that required manual cleanup. + +## Scope / 范围 + +- 收敛 drift-review report 的写回语义 +- 保持真实仓库的 stable/healthy 状态不被打破 +- 补回归测试和双语文档 + +- Stabilize the drift-review report writeback semantics +- Preserve the real repository's stable/healthy state +- Add regression coverage and bilingual docs diff --git a/docs/specs/2026-04-11-kb-drift-report-stabilization/package-index.md b/docs/specs/2026-04-11-kb-drift-report-stabilization/package-index.md new file mode 100644 index 0000000..9fb9c34 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-report-stabilization/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-drift-report-stabilization` +- title: `knowledge-base drift report writeback stabilization` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-drift-report-stabilization/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-drift-report-stabilization/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-drift-report-stabilization/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-drift-report-stabilization/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-drift-report-stabilization/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py`, `knowledge-base/KB_COMMANDS.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-drift-report-stabilization/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-drift-report-stabilization/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 让 `kb drift-review --write-report` 在一次写入后就收敛到最终稳定状态,而不是把写入前的 `report-lag` 残留写进归档报告。 +- goal_en: Make `kb drift-review --write-report` converge to the final post-write state in one pass instead of archiving a stale pre-write `report-lag` snapshot. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 如有需要,可把同样的“写后稳定”策略评估到其他 report-producing commands。 +- next_step_en: If needed, evaluate the same “post-write stable” approach for other report-producing commands. + +## Notes / 备注 + +- 本轮是对 drift-review report 写回语义的补强,不改变 drift heuristics 本身。 +- This slice strengthens the drift-review report writeback semantics without changing the drift heuristics themselves. diff --git a/docs/specs/2026-04-11-kb-drift-report-stabilization/plan-review.md b/docs/specs/2026-04-11-kb-drift-report-stabilization/plan-review.md new file mode 100644 index 0000000..d6111c6 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-report-stabilization/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 这个问题属于“写回语义”而不是“检测逻辑”错误,所以应该小范围修补命令路径,而不是重写 drift heuristics。 +- 通过测试夹具覆盖“只有 report-lag 的场景”,可以在不污染真实知识库的前提下验证写后稳定行为。 +- 用户面文档需要同步更新,否则命令行为变化会只停留在代码层。 + +- This is a writeback-semantics issue rather than a detector-logic bug, so a focused command-path fix is better than reworking the drift heuristics. +- Covering the “report-lag only” scenario in tests verifies the post-write stable behavior without polluting the live knowledge base. +- The user-facing command docs should be updated as well so the behavior change does not live only in code. diff --git a/docs/specs/2026-04-11-kb-drift-report-stabilization/plan.md b/docs/specs/2026-04-11-kb-drift-report-stabilization/plan.md new file mode 100644 index 0000000..5ff62c3 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-report-stabilization/plan.md @@ -0,0 +1,10 @@ +# Plan / 计划 + +1. 找出 `drift-review --write-report` 把写入前 signals 直接归档的路径。 + Find the path where `drift-review --write-report` archives the pre-write signals directly. + +2. 调整写入逻辑,让归档报告和写后摘要都收敛到最终状态。 + Adjust the write path so the archived report and the post-write summary converge to the final state. + +3. 补回归测试、命令文档和验证记录。 + Add regression tests, command docs, and verification records. diff --git a/docs/specs/2026-04-11-kb-drift-report-stabilization/runtime/run-state.md b/docs/specs/2026-04-11-kb-drift-report-stabilization/runtime/run-state.md new file mode 100644 index 0000000..1b11b0d --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-report-stabilization/runtime/run-state.md @@ -0,0 +1,45 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-drift-report-stabilization` +- title: `knowledge-base drift report writeback stabilization` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-drift-report-stabilization/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue only if another report-producing path shows the same stale-write pattern` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T20:28:38+0800` +- last_progress_at: `2026-04-11T20:30:08+0800` +- last_resumed_at: `2026-04-11T20:30:08+0800` +- updated_at: `2026-04-11T20:30:08+0800` + +## Notes / 备注 + +- 本轮修复了 drift-review report 的写回语义,让归档内容不再保留被同一次写入解决掉的 `report-lag`。 +- verify、diff check 和 memory refresh 已完成。 +- This slice fixes the drift-review report writeback semantics so archived content no longer preserves a `report-lag` that the same write already resolved. +- Verify, diff check, and memory refresh are complete. diff --git a/docs/specs/2026-04-11-kb-drift-report-stabilization/spec.md b/docs/specs/2026-04-11-kb-drift-report-stabilization/spec.md new file mode 100644 index 0000000..1c42db7 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-report-stabilization/spec.md @@ -0,0 +1,31 @@ +# Spec / 规格 + +## Goals / 目标 + +- 让 `kb drift-review --write-report` 产出的报告反映写入后的最终状态,而不是写入前的瞬时状态。 +- Ensure that `kb drift-review --write-report` archives the final post-write state rather than a transient pre-write state. + +- 保持 `drift-review` 的检测逻辑不变,只修正 report 写入语义。 +- Keep the `drift-review` detection logic unchanged and only correct the report writeback semantics. + +## Non-Goals / 非目标 + +- 不新增 drift signal 类型 +- 不修改 `maintain` 的行为 +- 不在真实仓库里额外生成多余的 drift report 只为了验证 + +- Do not add new drift signal types +- Do not change `maintain` +- Do not generate extra drift reports in the live repository only for verification + +## Functional Rules / 功能规则 + +- `drift-review --write-report` 的归档内容应等价于“写入后再次观察到的状态” +- 因为写入新 report 会解决 `report-lag`,归档报告不应继续记录这条已被自身解决的信号 +- 命令在真实仓库上的文本/JSON摘要也应与写入后的状态保持一致 +- `--dry-run` 继续只做路径与摘要预览,不实际改动仓库 + +- The archived result of `drift-review --write-report` should be equivalent to the state observed after the write +- Because a fresh report resolves `report-lag`, the archived report should not keep recording that self-resolved signal +- The command's real text/JSON summary should also stay aligned with the post-write state +- `--dry-run` remains a non-mutating preview of the path and summary diff --git a/docs/specs/2026-04-11-kb-drift-report-stabilization/tasks.md b/docs/specs/2026-04-11-kb-drift-report-stabilization/tasks.md new file mode 100644 index 0000000..e4a1ab5 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-report-stabilization/tasks.md @@ -0,0 +1,11 @@ +# Tasks / 任务拆解 + +- [x] 调整 `drift-review --write-report` 的写回逻辑 +- [x] 补“写后稳定”回归测试 +- [x] 更新 `knowledge-base/KB_COMMANDS.md` +- [x] 跑验证 + +- [x] Adjust the `drift-review --write-report` writeback path +- [x] Add a post-write stability regression test +- [x] Update `knowledge-base/KB_COMMANDS.md` +- [x] Run verification diff --git a/docs/specs/2026-04-11-kb-drift-report-stabilization/verify.md b/docs/specs/2026-04-11-kb-drift-report-stabilization/verify.md new file mode 100644 index 0000000..dbfb581 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-report-stabilization/verify.md @@ -0,0 +1,36 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 -m unittest /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review --json +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review --write-report --dry-run +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/kb /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py /Users/wz/project/codex-enhanced-system/knowledge-base/KB_COMMANDS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-drift-report-stabilization +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `unittest`: `Ran 18 tests ... OK` +- `kb drift-review`: `drift_verdict: stable` +- `kb drift-review --json`: `status: ok`, `signals: []` +- `kb drift-review --write-report --dry-run`: 预览写入 `wiki/reports/report-drift-review-2026-04-11-2.md` +- `kb maintain`: `health_verdict: healthy` +- `git diff --check`: 无输出,目标文件格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `unittest`: `Ran 18 tests ... OK` +- `kb drift-review`: `drift_verdict: stable` +- `kb drift-review --json`: `status: ok`, `signals: []` +- `kb drift-review --write-report --dry-run`: previewed a write to `wiki/reports/report-drift-review-2026-04-11-2.md` +- `kb maintain`: `health_verdict: healthy` +- `git diff --check`: no output, so the target files passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- `drift-review --write-report` 现在会把归档报告收敛到写后状态,同时没有打破当前仓库的 `stable` / `healthy` 基线。 +- `drift-review --write-report` now converges the archived report to the post-write state without breaking the repository's current `stable` / `healthy` baseline. diff --git a/docs/specs/2026-04-11-kb-drift-review/intake.md b/docs/specs/2026-04-11-kb-drift-review/intake.md new file mode 100644 index 0000000..b0d74c3 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-review/intake.md @@ -0,0 +1,26 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户继续要求推进代码实现。 +- The user asked to continue advancing the implementation. + +## Problem / 问题 + +- `maintain` 现在已经能给健康结论,但它仍然以“维护错误/告警”为主视角。 +- 对于“支持页后来更新了、导航页可能落后了、report 层整体偏旧了”这类信号,更适合单独做 drift review,而不是混进硬错误检查里。 + +- `maintain` can now provide a health verdict, but it still looks at the world mainly through maintenance errors and warnings. +- Signals such as “support pages were updated later,” “guide pages may be lagging,” or “the report layer is overall older” fit a dedicated drift review better than they fit hard maintenance errors. + +## Scope / 范围 + +- 新增 `kb drift-review` / `kb drift` +- 输出 drift verdict、signal counts、signal groups、recommendations +- 支持 JSON 和 report 写入 +- 补测试和双语文档 + +- Add `kb drift-review` / `kb drift` +- Return a drift verdict, signal counts, signal groups, and recommendations +- Support JSON and report writing +- Add tests and bilingual docs diff --git a/docs/specs/2026-04-11-kb-drift-review/package-index.md b/docs/specs/2026-04-11-kb-drift-review/package-index.md new file mode 100644 index 0000000..387a024 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-review/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-drift-review` +- title: `knowledge-base drift review command` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-drift-review/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-drift-review/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-drift-review/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-drift-review/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-drift-review/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py`, `knowledge-base/KB_COMMANDS.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-drift-review/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-drift-review/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 给 `knowledge-base` 增加一个显式的 drift review 入口,把“可能需要复核”的信号从普通维护错误中分离出来。 +- goal_en: Add an explicit drift-review entry point to `knowledge-base` so “may need review” signals are separated from normal maintenance errors. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 对当前 drift signals 做定向内容复核或生成正式 drift review 报告。 +- next_step_en: Perform targeted content review on the current drift signals or generate a formal drift-review report. + +## Notes / 备注 + +- 本轮在真实仓库上跑出了非空 drift signals,说明该入口已经有实际使用价值。 +- This slice produced non-empty drift signals on the real repository, which shows the new entry point already has practical value. diff --git a/docs/specs/2026-04-11-kb-drift-review/plan-review.md b/docs/specs/2026-04-11-kb-drift-review/plan-review.md new file mode 100644 index 0000000..05099a0 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-review/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 方案把 drift 视为“review signal”而不是“hard failure”,更符合知识库治理的实际语义。 +- heuristics 都依赖现有 frontmatter 与引用图,不需要额外数据库或状态层。 +- 当前仓库本身就存在近期 source 更新,因此这套命令很可能一落地就能产出真实信号。 + +- The plan treats drift as a review signal rather than a hard failure, which better matches the real semantics of knowledge-base governance. +- The heuristics rely only on existing frontmatter and the reference graph, so they do not require extra databases or state layers. +- The current repository already contains recent source updates, so the command is likely to produce real signals immediately after landing. diff --git a/docs/specs/2026-04-11-kb-drift-review/plan.md b/docs/specs/2026-04-11-kb-drift-review/plan.md new file mode 100644 index 0000000..a60683e --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-review/plan.md @@ -0,0 +1,13 @@ +# Plan / 计划 + +1. 为 drift review 建立 signal model、payload 和 renderer。 + Build a signal model, payload, and renderer for drift review. + +2. 定义低噪音 drift heuristics:source lag、guide lag、metadata lag、report lag。 + Define low-noise drift heuristics: source lag, guide lag, metadata lag, and report lag. + +3. 接入 CLI 命令、JSON 输出和 report 生成。 + Wire the feature into the CLI command, JSON output, and report generation. + +4. 补测试、文档和验证。 + Add tests, docs, and verification. diff --git a/docs/specs/2026-04-11-kb-drift-review/runtime/run-state.md b/docs/specs/2026-04-11-kb-drift-review/runtime/run-state.md new file mode 100644 index 0000000..0e255c5 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-review/runtime/run-state.md @@ -0,0 +1,45 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-drift-review` +- title: `knowledge-base drift review command` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-drift-review/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `review or remediate the current drift signals` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T19:30:01+0800` +- last_progress_at: `2026-04-11T20:02:13+0800` +- last_resumed_at: `2026-04-11T20:02:13+0800` +- updated_at: `2026-04-11T20:02:13+0800` + +## Notes / 备注 + +- 本轮新增了独立 drift review 入口,并在真实仓库上验证到了非空 drift signals。 +- verify、diff check 和 memory refresh 已完成。 +- This slice adds a dedicated drift-review entry point and verified non-empty drift signals on the real repository. +- Verify, diff check, and memory refresh are complete. diff --git a/docs/specs/2026-04-11-kb-drift-review/spec.md b/docs/specs/2026-04-11-kb-drift-review/spec.md new file mode 100644 index 0000000..abc47bf --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-review/spec.md @@ -0,0 +1,43 @@ +# Spec / 规格 + +## Goals / 目标 + +- 让 `knowledge-base` 有一个独立的 drift review 入口,用来识别“需要人工复核”的页面,而不是把这些信号全塞进 `maintain`。 +- Give `knowledge-base` a dedicated drift-review entry point for identifying pages that may need human review instead of forcing all such signals into `maintain`. + +- 让 drift review 同时适合 CLI 阅读、脚本消费和 report 留档。 +- Make drift review suitable for CLI reading, script consumption, and report archival at the same time. + +## Non-Goals / 非目标 + +- 不自动修复 drift +- 不重写 canonical 页内容 +- 不把 drift signal 直接当成维护失败 + +- Do not auto-fix drift +- Do not rewrite canonical page content +- Do not treat drift signals as direct maintenance failures + +## Functional Rules / 功能规则 + +- 新增 `drift-review` 子命令及 `drift` 别名 +- drift review 至少识别这些 signals: + - `source-lag` + - `guide-lag` + - `metadata-lag` + - `report-lag` +- 输出 `drift_verdict` +- 支持 `--json` +- 支持 `--write-report` +- drift review 默认返回 0,不把“需要复核”视为命令失败 + +- Add a `drift-review` subcommand with the `drift` alias +- The drift review recognizes at least these signal types: + - `source-lag` + - `guide-lag` + - `metadata-lag` + - `report-lag` +- Return `drift_verdict` +- Support `--json` +- Support `--write-report` +- Return 0 by default so “needs review” is not treated as a command failure diff --git a/docs/specs/2026-04-11-kb-drift-review/tasks.md b/docs/specs/2026-04-11-kb-drift-review/tasks.md new file mode 100644 index 0000000..cb816c6 --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-review/tasks.md @@ -0,0 +1,17 @@ +# Tasks / 任务拆解 + +- [x] 为 drift review 定义 signal model 与 payload +- [x] 实现 source-lag / guide-lag / metadata-lag / report-lag +- [x] 增加 `drift-review` / `drift` CLI +- [x] 支持 JSON 与 report 输出 +- [x] 补测试 +- [x] 更新 `knowledge-base/KB_COMMANDS.md` +- [x] 跑验证 + +- [x] Define the signal model and payload for drift review +- [x] Implement source-lag / guide-lag / metadata-lag / report-lag +- [x] Add the `drift-review` / `drift` CLI +- [x] Support JSON and report output +- [x] Add tests +- [x] Update `knowledge-base/KB_COMMANDS.md` +- [x] Run verification diff --git a/docs/specs/2026-04-11-kb-drift-review/verify.md b/docs/specs/2026-04-11-kb-drift-review/verify.md new file mode 100644 index 0000000..936812d --- /dev/null +++ b/docs/specs/2026-04-11-kb-drift-review/verify.md @@ -0,0 +1,33 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 -m unittest /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review --json +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review --write-report --dry-run +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/kb /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py /Users/wz/project/codex-enhanced-system/knowledge-base/KB_COMMANDS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-drift-review +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `unittest`: `Ran 17 tests ... OK` +- `drift-review`: 当前仓库返回 `review-needed`,共 `warn=6`、`info=3` +- `drift-review --json`: 返回了 `drift_verdict`、`signal_counts`、`signal_groups`、`signals`、`recommendations` +- `drift-review --write-report --dry-run`: 返回了待写入的 drift review report 路径 +- `git diff --check`: 无输出,目标文件格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `unittest`: `Ran 17 tests ... OK` +- `drift-review`: the current repository returns `review-needed` with `warn=6` and `info=3` +- `drift-review --json`: returns `drift_verdict`, `signal_counts`, `signal_groups`, `signals`, and `recommendations` +- `drift-review --write-report --dry-run`: returns the drift-review report path that would be written +- `git diff --check`: no output, so the target files passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- `drift-review` 已经不仅是新命令,而且能在真实仓库中识别出一批值得复核的页面。 +- `drift-review` is not merely a new command; it already identifies a real set of pages worth reviewing in the live repository. diff --git a/docs/specs/2026-04-11-kb-index-updated-at-sync/intake.md b/docs/specs/2026-04-11-kb-index-updated-at-sync/intake.md new file mode 100644 index 0000000..54a5ae0 --- /dev/null +++ b/docs/specs/2026-04-11-kb-index-updated-at-sync/intake.md @@ -0,0 +1,24 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户继续要求推进实现。 +- The user asked to keep the implementation moving. + +## Problem / 问题 + +- 前一刀已经把 command-generated report 的 index 收口做齐了,但 `reindex` 和 `delete` 仍然可能在改写 `wiki/index.md` 后不更新 `updated_at`。 +- 这会让 index 内容已变、metadata 仍旧的情况继续存在。 + +- The previous slice already closed index registration for command-generated reports, but `reindex` and `delete` could still rewrite `wiki/index.md` without refreshing `updated_at`. +- That leaves a state where the index content changes while the metadata still looks stale. + +## Scope / 范围 + +- 让 `reindex` 在实际改写 index 时刷新 `updated_at` +- 让删除路径移除 index 条目时也刷新 `updated_at` +- 补测试、文档和双语变更包 + +- Make `reindex` refresh `updated_at` when it actually rewrites the index +- Make the delete path refresh `updated_at` when it removes index entries +- Add tests, docs, and a bilingual change package diff --git a/docs/specs/2026-04-11-kb-index-updated-at-sync/package-index.md b/docs/specs/2026-04-11-kb-index-updated-at-sync/package-index.md new file mode 100644 index 0000000..0c772de --- /dev/null +++ b/docs/specs/2026-04-11-kb-index-updated-at-sync/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-index-updated-at-sync` +- title: `knowledge-base index updated_at sync` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-index-updated-at-sync/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-index-updated-at-sync/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-index-updated-at-sync/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-index-updated-at-sync/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-index-updated-at-sync/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py`, `knowledge-base/KB_COMMANDS.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-index-updated-at-sync/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-index-updated-at-sync/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 让 `reindex` 和 `delete` 在实际改写 `wiki/index.md` 时同步刷新 `updated_at`,把 index 维护语义和前面 report 写入链路统一起来。 +- goal_en: Make `reindex` and `delete` refresh `updated_at` whenever they actually rewrite `wiki/index.md`, aligning index-maintenance semantics with the earlier report-write path. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 如有需要,可继续检查其他会改 canonical guide surfaces 的命令是否也应统一 metadata 语义。 +- next_step_en: If needed, continue by checking whether other commands that mutate canonical guide surfaces should share the same metadata semantics. + +## Notes / 备注 + +- 本轮不改知识图谱逻辑,只补齐 `wiki/index.md` 的元数据一致性。 +- This slice does not change knowledge-graph logic; it only closes the metadata consistency gap for `wiki/index.md`. diff --git a/docs/specs/2026-04-11-kb-index-updated-at-sync/plan-review.md b/docs/specs/2026-04-11-kb-index-updated-at-sync/plan-review.md new file mode 100644 index 0000000..8d9810d --- /dev/null +++ b/docs/specs/2026-04-11-kb-index-updated-at-sync/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 这是 metadata closeout 问题,不需要引入新的 graph state。 +- 用现有 `render_page` 渲染 index 可以保持 frontmatter 和 body 更新路径一致。 +- 通过测试覆盖 `reindex` 与 index-entry removal 两条路径,就能验证真实语义而不必在 live repo 做破坏性实验。 + +- This is a metadata-closeout problem and does not require a new graph state layer. +- Rendering the index through the existing `render_page` path keeps frontmatter and body updates consistent. +- Covering both `reindex` and the index-entry removal path in tests verifies the semantics without needing destructive experiments in the live repo. diff --git a/docs/specs/2026-04-11-kb-index-updated-at-sync/plan.md b/docs/specs/2026-04-11-kb-index-updated-at-sync/plan.md new file mode 100644 index 0000000..653ff5a --- /dev/null +++ b/docs/specs/2026-04-11-kb-index-updated-at-sync/plan.md @@ -0,0 +1,10 @@ +# Plan / 计划 + +1. 让 `reindex` 改为基于 index page body 重建,并在实际写入时刷新 metadata。 + Make `reindex` rebuild from the index page body and refresh metadata on real writes. + +2. 让移除 index 条目的路径也共享同样的 metadata 刷新语义。 + Give the index-entry removal path the same metadata refresh semantics. + +3. 补回归测试、命令文档和验证记录。 + Add regression coverage, command docs, and verification records. diff --git a/docs/specs/2026-04-11-kb-index-updated-at-sync/runtime/run-state.md b/docs/specs/2026-04-11-kb-index-updated-at-sync/runtime/run-state.md new file mode 100644 index 0000000..c7c6ec2 --- /dev/null +++ b/docs/specs/2026-04-11-kb-index-updated-at-sync/runtime/run-state.md @@ -0,0 +1,43 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-index-updated-at-sync` +- title: `knowledge-base index updated_at sync` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-index-updated-at-sync/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue only if another index-mutating path still misses metadata refresh` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T20:52:43+0800` +- last_progress_at: `2026-04-11T20:55:14+0800` +- last_resumed_at: `2026-04-11T20:55:14+0800` +- updated_at: `2026-04-11T20:55:14+0800` + +## Notes / 备注 + +- 本轮把 `reindex` 与删除路径的 index metadata 语义补齐到了和 report 写入链路一致。 +- This slice aligns the index metadata semantics of `reindex` and the delete path with the earlier report-write path. diff --git a/docs/specs/2026-04-11-kb-index-updated-at-sync/spec.md b/docs/specs/2026-04-11-kb-index-updated-at-sync/spec.md new file mode 100644 index 0000000..2ff5b86 --- /dev/null +++ b/docs/specs/2026-04-11-kb-index-updated-at-sync/spec.md @@ -0,0 +1,31 @@ +# Spec / 规格 + +## Goals / 目标 + +- 让所有实际改写 `wiki/index.md` 的主路径都同步刷新它自己的 `updated_at`。 +- Ensure that the main paths which actually rewrite `wiki/index.md` also refresh its own `updated_at`. + +- 保持当前知识库的 `healthy` / `stable` 基线不被打破。 +- Preserve the current `healthy` / `stable` baseline of the knowledge base. + +## Non-Goals / 非目标 + +- 不改 `query`、`maintain`、`drift-review` 的检测逻辑 +- 不自动改写 `hot.md`、`overview.md` 或 `log.md` +- 不扩大 index 自动润色范围 + +- Do not change the detection logic of `query`, `maintain`, or `drift-review` +- Do not automatically rewrite `hot.md`, `overview.md`, or `log.md` +- Do not widen the scope of index auto-polishing + +## Functional Rules / 功能规则 + +- `reindex --write` / `reindex --write --prune` 若实际写入 `wiki/index.md`,必须同步刷新 `updated_at` +- 删除路径若实际移除了 index 条目,也必须同步刷新 `updated_at` +- dry-run 继续保持无副作用 +- 新逻辑不能打破 `kb maintain` 的 `healthy` 与 `kb drift-review` 的 `stable` + +- If `reindex --write` / `reindex --write --prune` actually writes `wiki/index.md`, it must also refresh `updated_at` +- If the delete path actually removes index entries, it must also refresh `updated_at` +- Dry-run remains side-effect free +- The new logic must not break `kb maintain`'s `healthy` or `kb drift-review`'s `stable` diff --git a/docs/specs/2026-04-11-kb-index-updated-at-sync/tasks.md b/docs/specs/2026-04-11-kb-index-updated-at-sync/tasks.md new file mode 100644 index 0000000..b079928 --- /dev/null +++ b/docs/specs/2026-04-11-kb-index-updated-at-sync/tasks.md @@ -0,0 +1,13 @@ +# Tasks / 任务拆解 + +- [x] 让 `reindex` 改写时刷新 `wiki/index.md.updated_at` +- [x] 让 index-entry removal 路径改写时刷新 `wiki/index.md.updated_at` +- [x] 补回归测试 +- [x] 更新 `knowledge-base/KB_COMMANDS.md` +- [x] 跑验证 + +- [x] Make `reindex` refresh `wiki/index.md.updated_at` on write +- [x] Make the index-entry removal path refresh `wiki/index.md.updated_at` on write +- [x] Add regression tests +- [x] Update `knowledge-base/KB_COMMANDS.md` +- [x] Run verification diff --git a/docs/specs/2026-04-11-kb-index-updated-at-sync/verify.md b/docs/specs/2026-04-11-kb-index-updated-at-sync/verify.md new file mode 100644 index 0000000..d7b1024 --- /dev/null +++ b/docs/specs/2026-04-11-kb-index-updated-at-sync/verify.md @@ -0,0 +1,36 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 -m unittest /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb reindex --write --dry-run +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb delete /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/reports/report-drift-review-2026-04-11.md --dry-run +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/kb /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py /Users/wz/project/codex-enhanced-system/knowledge-base/KB_COMMANDS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-index-updated-at-sync +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `unittest`: `Ran 21 tests ... OK` +- `kb maintain`: `health_verdict: healthy` +- `kb drift-review`: `drift_verdict: stable` +- `kb reindex --write --dry-run`: 预览更新 `wiki/index.md` +- `kb delete ... --dry-run`: 预览移除一个 report 的 index 条目 +- `git diff --check`: 无输出,目标文件与变更包格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `unittest`: `Ran 21 tests ... OK` +- `kb maintain`: `health_verdict: healthy` +- `kb drift-review`: `drift_verdict: stable` +- `kb reindex --write --dry-run`: previewed an update to `wiki/index.md` +- `kb delete ... --dry-run`: previewed removing a report's index entry +- `git diff --check`: no output, so the target files and change package passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- `wiki/index.md` 的 `updated_at` 现在会跟随实际改写路径同步刷新,index metadata 语义和前面的 report 写入链路已经对齐。 +- `wiki/index.md.updated_at` now refreshes along the real write paths, so the index metadata semantics are aligned with the earlier report-write closeout. diff --git a/docs/specs/2026-04-11-kb-maintain-health-summary/intake.md b/docs/specs/2026-04-11-kb-maintain-health-summary/intake.md new file mode 100644 index 0000000..9714a73 --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-health-summary/intake.md @@ -0,0 +1,26 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户继续要求推进代码实现。 +- The user asked to continue pushing the implementation forward. + +## Problem / 问题 + +- `maintain` 已经能输出结构化结果,但还不够“可读即判断”。 +- 现在用户或后续 automation 还要自己解释每条 issue 属于哪类问题、整体健康处于什么状态、下一步应该先修什么。 + +- `maintain` already returns structured data, but it is not yet “readable enough to judge immediately.” +- A user or later automation still has to infer what kind of issue each item represents, what the overall health state is, and what should be fixed first. + +## Scope / 范围 + +- 给维护结果增加 `health_verdict` +- 按类别归组 issue +- 生成建议列表 +- 把这些信息同步到文本、JSON 和 report 输出 + +- Add `health_verdict` to maintenance results +- Group issues by category +- Generate recommendation lists +- Sync those signals into text, JSON, and report output diff --git a/docs/specs/2026-04-11-kb-maintain-health-summary/package-index.md b/docs/specs/2026-04-11-kb-maintain-health-summary/package-index.md new file mode 100644 index 0000000..cfeea4d --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-health-summary/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-maintain-health-summary` +- title: `knowledge-base categorized maintenance health summaries` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-maintain-health-summary/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-maintain-health-summary/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-maintain-health-summary/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-maintain-health-summary/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-maintain-health-summary/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py`, `knowledge-base/KB_COMMANDS.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-maintain-health-summary/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-maintain-health-summary/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 让 `maintain` 输出不只列出 issue,还能给出健康结论、问题分组和可执行建议。 +- goal_en: Make `maintain` output provide not just issues, but also a health verdict, issue grouping, and actionable recommendations. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 继续做 drift review 入口或更强的 maintenance report 导航整合。 +- next_step_en: Continue with a drift-review entry point or stronger maintenance-report navigation integration. + +## Notes / 备注 + +- 本轮同时增强了文本输出、JSON 输出和自动生成的 maintenance report。 +- This slice enhances the text output, JSON output, and generated maintenance report together. diff --git a/docs/specs/2026-04-11-kb-maintain-health-summary/plan-review.md b/docs/specs/2026-04-11-kb-maintain-health-summary/plan-review.md new file mode 100644 index 0000000..68cd38f --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-health-summary/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 方案只增强表达层,不改变维护规则本身,兼容性风险低。 +- 通过统一 payload builder 派生文本、JSON 和 report,可以避免三套输出慢慢漂移。 +- recommendation 采用基于 category 的轻量规则,比硬编码完整修复流程更稳。 + +- The plan enhances only the expression layer and does not change the maintenance rules themselves, so compatibility risk stays low. +- Deriving text, JSON, and report output from one payload builder helps prevent the three surfaces from drifting apart. +- Recommendations use lightweight category-based rules, which is more stable than hardcoding a full repair workflow. diff --git a/docs/specs/2026-04-11-kb-maintain-health-summary/plan.md b/docs/specs/2026-04-11-kb-maintain-health-summary/plan.md new file mode 100644 index 0000000..6a61926 --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-health-summary/plan.md @@ -0,0 +1,10 @@ +# Plan / 计划 + +1. 为维护 issue 增加 category 与 health verdict helper。 + Add category and health-verdict helpers for maintenance issues. + +2. 更新 payload builder、文本 summary 和 generated report。 + Update the payload builder, text summary, and generated report. + +3. 补测试、命令文档和验证。 + Add tests, command docs, and verification. diff --git a/docs/specs/2026-04-11-kb-maintain-health-summary/runtime/run-state.md b/docs/specs/2026-04-11-kb-maintain-health-summary/runtime/run-state.md new file mode 100644 index 0000000..f29c343 --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-health-summary/runtime/run-state.md @@ -0,0 +1,45 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-maintain-health-summary` +- title: `knowledge-base categorized maintenance health summaries` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-maintain-health-summary/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue with the next maintenance or drift slice` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T19:28:13+0800` +- last_progress_at: `2026-04-11T19:30:01+0800` +- last_resumed_at: `2026-04-11T19:30:01+0800` +- updated_at: `2026-04-11T19:30:01+0800` + +## Notes / 备注 + +- 本轮把维护输出推进到“health verdict + issue grouping + recommendations”。 +- verify、diff check 和 memory refresh 已完成。 +- This slice advances maintenance output to “health verdict + issue grouping + recommendations.” +- Verify, diff check, and memory refresh are complete. diff --git a/docs/specs/2026-04-11-kb-maintain-health-summary/spec.md b/docs/specs/2026-04-11-kb-maintain-health-summary/spec.md new file mode 100644 index 0000000..dfe1fbf --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-health-summary/spec.md @@ -0,0 +1,33 @@ +# Spec / 规格 + +## Goals / 目标 + +- `maintain` 的人类输出能够让用户一眼看出当前是 `healthy`、`needs-attention` 还是 `failing`。 +- The human-oriented `maintain` output should make it obvious at a glance whether the current state is `healthy`, `needs-attention`, or `failing`. + +- `maintain --json` 和 generated report 都能表达 issue category 与 recommendation,而不是只有平铺列表。 +- `maintain --json` and the generated report should express issue categories and recommendations instead of only a flat list. + +## Non-Goals / 非目标 + +- 不新增维护规则本身 +- 不做自动修复 +- 不引入新的独立 drift 子命令 + +- Do not add new maintenance rules themselves +- Do not perform automatic fixes +- Do not introduce a separate drift subcommand + +## Functional Rules / 功能规则 + +- 维护输出新增 `health_verdict` +- issue 至少分为:`provenance`、`guide-surface`、`frontmatter`、`links`、`index`、`general` +- JSON 输出新增 `issue_groups` 与 `recommendations` +- 文本输出显示 `health_verdict`、`issue_groups`,并在 issue 行中显示 category +- generated report 新增 `Issue Summary`、`Findings By Category` 和按类别生成的 recommendation + +- Maintenance output adds `health_verdict` +- Issues are categorized into at least: `provenance`, `guide-surface`, `frontmatter`, `links`, `index`, and `general` +- JSON output adds `issue_groups` and `recommendations` +- Text output shows `health_verdict`, `issue_groups`, and includes the category on each issue line +- The generated report adds `Issue Summary`, `Findings By Category`, and category-aware recommendations diff --git a/docs/specs/2026-04-11-kb-maintain-health-summary/tasks.md b/docs/specs/2026-04-11-kb-maintain-health-summary/tasks.md new file mode 100644 index 0000000..b599f74 --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-health-summary/tasks.md @@ -0,0 +1,17 @@ +# Tasks / 任务拆解 + +- [x] 为 maintenance issue 增加 category helper +- [x] 为 maintenance output 增加 `health_verdict` +- [x] 为 JSON 输出增加 `issue_groups` 和 `recommendations` +- [x] 为文本 summary 和 generated report 增加 category-aware 展示 +- [x] 补测试 +- [x] 更新 `knowledge-base/KB_COMMANDS.md` +- [x] 跑验证 + +- [x] Add a category helper for maintenance issues +- [x] Add `health_verdict` to maintenance output +- [x] Add `issue_groups` and `recommendations` to JSON output +- [x] Add category-aware rendering to the text summary and generated report +- [x] Add tests +- [x] Update `knowledge-base/KB_COMMANDS.md` +- [x] Run verification diff --git a/docs/specs/2026-04-11-kb-maintain-health-summary/verify.md b/docs/specs/2026-04-11-kb-maintain-health-summary/verify.md new file mode 100644 index 0000000..3b84b0b --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-health-summary/verify.md @@ -0,0 +1,33 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 -m unittest /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain --json +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain --write-report --dry-run +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/kb /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py /Users/wz/project/codex-enhanced-system/knowledge-base/KB_COMMANDS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-maintain-health-summary +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `unittest`: `Ran 13 tests ... OK` +- `kb maintain`: 文本输出现在包含 `health_verdict` +- `kb maintain --json`: 返回 `health_verdict`、`issue_groups`、`recommendations` +- `kb maintain --write-report --dry-run`: 文本摘要仍正常,并保留 report 写入路径 +- `git diff --check`: 无输出,目标文件格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `unittest`: `Ran 13 tests ... OK` +- `kb maintain`: text output now includes `health_verdict` +- `kb maintain --json`: returns `health_verdict`, `issue_groups`, and `recommendations` +- `kb maintain --write-report --dry-run`: the text summary still behaves correctly and keeps the report-write path visible +- `git diff --check`: no output, so the target files passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- 维护输出已经从“列问题”升级成“给健康结论 + 归类问题 + 指出下一步”。 +- Maintenance output has been upgraded from “listing issues” to “providing a health verdict, grouping the issues, and pointing to the next action.” diff --git a/docs/specs/2026-04-11-kb-maintain-json/intake.md b/docs/specs/2026-04-11-kb-maintain-json/intake.md new file mode 100644 index 0000000..81311ee --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-json/intake.md @@ -0,0 +1,26 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户要求在做完 provenance lint 与健康检查后继续写代码。 +- The user asked to keep coding after provenance lint and health checks were completed. + +## Problem / 问题 + +- 现在 `kb maintain` 虽然能输出更强的健康检查结果,但默认仍只有文本摘要。 +- 查询已经有 `--json`,维护命令还没有同等级的结构化出口,不利于后续 automation 和程序化消费。 + +- `kb maintain` now returns stronger health-check results, but it still only emits a text summary by default. +- Query already has `--json`, while maintenance has no equivalent structured output yet, which makes later automation and programmatic consumption harder. + +## Scope / 范围 + +- 为 `kb maintain` 增加 `--json` +- 输出 `counts`、`issue_counts`、`issues` +- 在 `--write-report` 场景下补 `report_path` 与 `report_action` +- 补测试与双语文档 + +- Add `--json` to `kb maintain` +- Return `counts`, `issue_counts`, and `issues` +- Add `report_path` and `report_action` when `--write-report` is in play +- Add tests and bilingual docs diff --git a/docs/specs/2026-04-11-kb-maintain-json/package-index.md b/docs/specs/2026-04-11-kb-maintain-json/package-index.md new file mode 100644 index 0000000..8ecdeea --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-json/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-maintain-json` +- title: `knowledge-base machine-readable maintenance output` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-maintain-json/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-maintain-json/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-maintain-json/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-maintain-json/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-maintain-json/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py`, `knowledge-base/KB_COMMANDS.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-maintain-json/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-maintain-json/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 给 `kb maintain` 增加 `--json` 结构化输出,并在需要时把 report 写入元信息一起暴露出来。 +- goal_en: Add `--json` structured output to `kb maintain` and expose report-write metadata when needed. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 继续补更丰富的 maintenance report summary 或 drift review 入口。 +- next_step_en: Continue with richer maintenance-report summaries or a drift-review entry point. + +## Notes / 备注 + +- 本轮把维护结果变成更容易被脚本、automation 或后续 review 消费的格式。 +- This slice turns maintenance results into a format that scripts, automation, and later review flows can consume more easily. diff --git a/docs/specs/2026-04-11-kb-maintain-json/plan-review.md b/docs/specs/2026-04-11-kb-maintain-json/plan-review.md new file mode 100644 index 0000000..fa162fb --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-json/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 方案没有重做一套新维护接口,而是在现有 `maintain` 上补结构化出口,变更面小。 +- 通过共享 payload/renderer,可以避免文本输出和 JSON 输出逻辑漂移。 +- report 元信息只在相关场景出现,避免默认输出膨胀。 + +- The plan does not rebuild maintenance as a new interface; it adds a structured exit to the existing `maintain` command, keeping the change surface small. +- Shared payload and renderer helpers avoid drift between text and JSON output. +- Report metadata appears only when relevant, which keeps the default output compact. diff --git a/docs/specs/2026-04-11-kb-maintain-json/plan.md b/docs/specs/2026-04-11-kb-maintain-json/plan.md new file mode 100644 index 0000000..7470e95 --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-json/plan.md @@ -0,0 +1,10 @@ +# Plan / 计划 + +1. 为 `maintain` 提炼统一的 payload builder 与 summary renderer。 + Extract a shared payload builder and summary renderer for `maintain`. + +2. 为命令增加 `--json` 参数,并接入 report 元信息。 + Add the `--json` flag and wire in report metadata. + +3. 补测试、命令文档与验证。 + Add tests, command docs, and verification. diff --git a/docs/specs/2026-04-11-kb-maintain-json/runtime/run-state.md b/docs/specs/2026-04-11-kb-maintain-json/runtime/run-state.md new file mode 100644 index 0000000..876f962 --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-json/runtime/run-state.md @@ -0,0 +1,45 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-maintain-json` +- title: `knowledge-base machine-readable maintenance output` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-maintain-json/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue with the next knowledge-base maintenance or drift slice` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T18:46:53+0800` +- last_progress_at: `2026-04-11T18:47:56+0800` +- last_resumed_at: `2026-04-11T18:47:56+0800` +- updated_at: `2026-04-11T18:47:56+0800` + +## Notes / 备注 + +- 本轮为 `maintain` 补了结构化出口,方便后续自动流程消费健康检查结果。 +- verify、diff check 和 memory refresh 已完成。 +- This slice adds a structured exit to `maintain`, making the health-check results easier for later automated flows to consume. +- Verify, diff check, and memory refresh are complete. diff --git a/docs/specs/2026-04-11-kb-maintain-json/spec.md b/docs/specs/2026-04-11-kb-maintain-json/spec.md new file mode 100644 index 0000000..586edc1 --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-json/spec.md @@ -0,0 +1,37 @@ +# Spec / 规格 + +## Goals / 目标 + +- 让 `kb maintain` 的结果可以被脚本稳定读取,而不必解析人类文本输出。 +- Let scripts read `kb maintain` results reliably without scraping the human-oriented text output. + +- 保持现有文本输出不退化,同时让 `--write-report` 的副作用可见。 +- Preserve the current text output while making `--write-report` side effects visible. + +## Non-Goals / 非目标 + +- 不改变维护检查规则本身 +- 不引入新的 maintenance 子命令 +- 不做自动 report 上传或调度 + +- Do not change the maintenance rules themselves +- Do not add a new maintenance subcommand +- Do not add automatic report upload or scheduling + +## Functional Rules / 功能规则 + +- `kb maintain --json` 返回: + - `status` + - `counts` + - `issue_counts` + - `issues` +- 若同时指定 `--write-report`,则额外返回 `report_path` 与 `report_action` +- 普通文本输出继续保留,并在有 issue 时展示聚合的 `issue_counts` + +- `kb maintain --json` returns: + - `status` + - `counts` + - `issue_counts` + - `issues` +- When `--write-report` is also requested, the output additionally returns `report_path` and `report_action` +- The normal text output stays intact and now also shows aggregated `issue_counts` when issues exist diff --git a/docs/specs/2026-04-11-kb-maintain-json/tasks.md b/docs/specs/2026-04-11-kb-maintain-json/tasks.md new file mode 100644 index 0000000..f35da55 --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-json/tasks.md @@ -0,0 +1,15 @@ +# Tasks / 任务拆解 + +- [x] 为 `maintain` 增加 payload builder 与 issue 聚合 +- [x] 为 `maintain` 增加 `--json` +- [x] 为 report 写入场景暴露 `report_path` / `report_action` +- [x] 补单测 +- [x] 更新 `knowledge-base/KB_COMMANDS.md` +- [x] 跑 CLI 验证 + +- [x] Add payload building and issue aggregation for `maintain` +- [x] Add `--json` to `maintain` +- [x] Expose `report_path` / `report_action` for report-writing flows +- [x] Add unit tests +- [x] Update `knowledge-base/KB_COMMANDS.md` +- [x] Run CLI verification diff --git a/docs/specs/2026-04-11-kb-maintain-json/verify.md b/docs/specs/2026-04-11-kb-maintain-json/verify.md new file mode 100644 index 0000000..ed50eee --- /dev/null +++ b/docs/specs/2026-04-11-kb-maintain-json/verify.md @@ -0,0 +1,33 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 -m unittest /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain --json +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain --json --write-report --dry-run +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/kb /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py /Users/wz/project/codex-enhanced-system/knowledge-base/KB_COMMANDS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-maintain-json +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `unittest`: `Ran 11 tests ... OK` +- `kb maintain`: 文本输出仍正常,当前仓库 `issues: none` +- `kb maintain --json`: 返回 `status / counts / issue_counts / issues` +- `kb maintain --json --write-report --dry-run`: 额外返回 `report_path` 与 `report_action` +- `git diff --check`: 无输出,目标文件格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `unittest`: `Ran 11 tests ... OK` +- `kb maintain`: text output remains healthy and the current repository still reports `issues: none` +- `kb maintain --json`: returns `status / counts / issue_counts / issues` +- `kb maintain --json --write-report --dry-run`: additionally returns `report_path` and `report_action` +- `git diff --check`: no output, so the target files passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- `maintain` 现在具备和 `query` 类似的结构化出口,后续 automation 或 review 流可以直接消费。 +- `maintain` now has a structured exit similar to `query`, so later automation or review flows can consume it directly. diff --git a/docs/specs/2026-04-11-kb-provenance-health-checks/intake.md b/docs/specs/2026-04-11-kb-provenance-health-checks/intake.md new file mode 100644 index 0000000..631eac2 --- /dev/null +++ b/docs/specs/2026-04-11-kb-provenance-health-checks/intake.md @@ -0,0 +1,26 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户要求继续做 `provenance lint 强化 + 知识页健康检查`,并在做完后继续写代码。 +- The user asked to continue with `provenance lint hardening + knowledge-page health checks`, then keep coding after that slice. + +## Problem / 问题 + +- 现有 `maintain` 已经能检查 `source_refs` 的目标位置是否合法,但还不能判断“支持质量是否足够好”。 +- `wiki/index.md`、`wiki/overview.md`、`wiki/hot.md`、`wiki/log.md` 这些导航层页面还没有被系统检查,未来容易静默漂移。 + +- The current `maintain` command already validates whether `source_refs` point to valid locations, but it still cannot judge whether the support quality is good enough. +- The guide-layer pages `wiki/index.md`, `wiki/overview.md`, `wiki/hot.md`, and `wiki/log.md` are not yet checked systematically and could silently drift over time. + +## Scope / 范围 + +- 为 `entity / concept / synthesis` 增加更强的 `source_refs` 质量检查 +- 为重复 / 自指的 `source_refs` 与 `related` 增加 lint +- 为 guide-surface 页面增加健康检查 +- 补测试、双语文档和验证 + +- Add stronger `source_refs` quality checks for `entity / concept / synthesis` +- Add lint for duplicate or self-referential `source_refs` and `related` +- Add health checks for guide-surface pages +- Add tests, bilingual docs, and verification diff --git a/docs/specs/2026-04-11-kb-provenance-health-checks/package-index.md b/docs/specs/2026-04-11-kb-provenance-health-checks/package-index.md new file mode 100644 index 0000000..e0c5e88 --- /dev/null +++ b/docs/specs/2026-04-11-kb-provenance-health-checks/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-provenance-health-checks` +- title: `knowledge-base provenance lint hardening and guide-surface health checks` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-provenance-health-checks/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-provenance-health-checks/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-provenance-health-checks/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-provenance-health-checks/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-provenance-health-checks/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py`, `knowledge-base/KB_COMMANDS.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-provenance-health-checks/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-provenance-health-checks/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 强化 `kb maintain`,不只检查 provenance 路径合法性,还检查 canonical 支撑质量与知识页导航层健康。 +- goal_en: Harden `kb maintain` so it checks not only provenance-path legality, but also canonical support quality and guide-surface health. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 继续把维护输出做成更可消费的健康摘要或 drift review 入口。 +- next_step_en: Continue by turning maintenance output into a more consumable health summary or drift-review entry point. + +## Notes / 备注 + +- 本轮不重写现有 wiki 内容,只增强 lint 与维护检查。 +- This slice does not rewrite existing wiki content; it only strengthens lint and maintenance checks. diff --git a/docs/specs/2026-04-11-kb-provenance-health-checks/plan-review.md b/docs/specs/2026-04-11-kb-provenance-health-checks/plan-review.md new file mode 100644 index 0000000..6a00afc --- /dev/null +++ b/docs/specs/2026-04-11-kb-provenance-health-checks/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 方案仍然保持“只做检查,不做自动修复”,风险低且和现有工作流一致。 +- 通过把 guide-surface 规则限制在少数关键入口页面上,可以避免把 lint 写成过度脆弱的内容模板检查。 +- provenance 强化聚焦在“canonical source support 是否存在”,比强制具体数量或引用格式更稳。 + +- The plan still follows the “check only, no automatic repair” approach, which keeps risk low and aligns with the current workflow. +- By limiting guide-surface rules to a small set of key entry pages, the lint avoids becoming an overly brittle content-template checker. +- Provenance hardening focuses on whether canonical source support exists, which is more stable than forcing exact counts or formats. diff --git a/docs/specs/2026-04-11-kb-provenance-health-checks/plan.md b/docs/specs/2026-04-11-kb-provenance-health-checks/plan.md new file mode 100644 index 0000000..bbe7809 --- /dev/null +++ b/docs/specs/2026-04-11-kb-provenance-health-checks/plan.md @@ -0,0 +1,13 @@ +# Plan / 计划 + +1. 在 `knowledge-base/kb` 里补 provenance 质量 lint 与 guide-surface health-check helper。 + Add provenance-quality lint and guide-surface health-check helpers in `knowledge-base/kb`. + +2. 更新 `check_maintenance()`,把新规则接入默认维护检查。 + Update `check_maintenance()` so the new rules run in the default maintenance pass. + +3. 补充单测,覆盖通过场景、弱 provenance 场景和 hot-path 漂移场景。 + Add unit tests covering the passing case, weak-provenance case, and hot-path drift case. + +4. 更新双语命令文档并完成验证。 + Update bilingual command docs and finish verification. diff --git a/docs/specs/2026-04-11-kb-provenance-health-checks/runtime/run-state.md b/docs/specs/2026-04-11-kb-provenance-health-checks/runtime/run-state.md new file mode 100644 index 0000000..4e3d129 --- /dev/null +++ b/docs/specs/2026-04-11-kb-provenance-health-checks/runtime/run-state.md @@ -0,0 +1,45 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-provenance-health-checks` +- title: `knowledge-base provenance lint hardening and guide-surface health checks` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-provenance-health-checks/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue with the next maintenance-surface coding slice` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T18:43:33+0800` +- last_progress_at: `2026-04-11T18:47:56+0800` +- last_resumed_at: `2026-04-11T18:47:56+0800` +- updated_at: `2026-04-11T18:47:56+0800` + +## Notes / 备注 + +- 本轮把 provenance lint 从“路径合法”推进到“支撑质量 + 导航健康”。 +- verify、diff check 和 memory refresh 已完成。 +- This slice advanced provenance lint from “path validity” to “support quality + guide-surface health”. +- Verify, diff check, and memory refresh are complete. diff --git a/docs/specs/2026-04-11-kb-provenance-health-checks/spec.md b/docs/specs/2026-04-11-kb-provenance-health-checks/spec.md new file mode 100644 index 0000000..36f3384 --- /dev/null +++ b/docs/specs/2026-04-11-kb-provenance-health-checks/spec.md @@ -0,0 +1,33 @@ +# Spec / 规格 + +## Goals / 目标 + +- `kb maintain` 不仅能发现引用断裂,还能发现“canonical answer 页只靠 raw 支撑”的弱 provenance。 +- `kb maintain` should detect not only broken references, but also weak provenance where canonical answer pages rely only on raw support. + +- `kb maintain` 能在导航层页面漂移前发出提醒。 +- `kb maintain` should warn before guide-surface pages drift too far. + +## Non-Goals / 非目标 + +- 不做自动修复 wiki 页面 +- 不引入定时健康检查 automation +- 不实现完整的 page-evolution 状态机 + +- No automatic wiki-page repair +- No scheduled health-check automation +- No full page-evolution state machine + +## Functional Rules / 功能规则 + +- `entity / concept / synthesis` 页面若 `source_refs` 没有任何 `wiki/sources/` 支撑页,则发出 warning。 +- `source_refs` 与 `related` 若出现重复目标或自指目标,则发出 warning。 +- `index / overview / hot / log` 四个 guide-surface 页面必须存在,并拥有基本 frontmatter、可解析链接和关键互链。 +- `overview` 必须继续覆盖至少一个 `domain` 与 `report` 入口。 +- `hot` 必须继续覆盖至少一个 `domain`、`synthesis`、`report` 入口。 + +- Emit a warning when an `entity / concept / synthesis` page has `source_refs` but none of them point to `wiki/sources/`. +- Emit a warning when `source_refs` or `related` contain duplicate or self-referential targets. +- Require the four guide-surface pages `index / overview / hot / log` to exist with basic frontmatter, resolvable links, and key cross-links. +- Require `overview` to keep at least one `domain` and one `report` entry point. +- Require `hot` to keep at least one `domain`, `synthesis`, and `report` entry point. diff --git a/docs/specs/2026-04-11-kb-provenance-health-checks/tasks.md b/docs/specs/2026-04-11-kb-provenance-health-checks/tasks.md new file mode 100644 index 0000000..0acd1b0 --- /dev/null +++ b/docs/specs/2026-04-11-kb-provenance-health-checks/tasks.md @@ -0,0 +1,15 @@ +# Tasks / 任务拆解 + +- [x] 为 `kb` 增加 provenance 质量 lint helper +- [x] 为 `kb` 增加 guide-surface health check helper +- [x] 将新规则接入 `check_maintenance()` +- [x] 补充单测覆盖成功与告警场景 +- [x] 更新 `knowledge-base/KB_COMMANDS.md` +- [x] 跑验证并刷新 memory + +- [x] Add provenance-quality lint helpers to `kb` +- [x] Add guide-surface health-check helpers to `kb` +- [x] Wire the new rules into `check_maintenance()` +- [x] Add unit tests for passing and warning cases +- [x] Update `knowledge-base/KB_COMMANDS.md` +- [x] Run verification and refresh memory diff --git a/docs/specs/2026-04-11-kb-provenance-health-checks/verify.md b/docs/specs/2026-04-11-kb-provenance-health-checks/verify.md new file mode 100644 index 0000000..affca99 --- /dev/null +++ b/docs/specs/2026-04-11-kb-provenance-health-checks/verify.md @@ -0,0 +1,27 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 -m unittest /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/kb /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py /Users/wz/project/codex-enhanced-system/knowledge-base/KB_COMMANDS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-provenance-health-checks +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `unittest`: `Ran 9 tests ... OK` +- `kb maintain`: `issues: none` +- `git diff --check`: 无输出,目标文件格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `unittest`: `Ran 9 tests ... OK` +- `kb maintain`: `issues: none` +- `git diff --check`: no output, so the target files passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- provenance lint 与 guide-surface health check 已接入默认维护流程,且当前仓库在新规则下保持通过。 +- Provenance lint and guide-surface health checks are now part of the default maintenance flow, and the current repository still passes under the new rules. diff --git a/docs/specs/2026-04-11-kb-query-provenance/intake.md b/docs/specs/2026-04-11-kb-query-provenance/intake.md new file mode 100644 index 0000000..6a1272d --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-provenance/intake.md @@ -0,0 +1,54 @@ +# Intake / Intake + +## Request Summary / 请求摘要 + +- request: 用户在完成规划后要求“开始落地代码”,当前第一批实现聚焦 `knowledge-base` 的 query 与 provenance 能力。 +- request_en: After planning was finalized, the user requested to start implementation; the first implementation slice focuses on `knowledge-base` query and provenance capabilities. +- requester: `user` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Classification / 分类 + +| Field | Value | +| --- | --- | +| task_type | `complex-change` | +| risk_level | `medium` | +| need_spec | `true` | +| need_research | `false` | +| need_memory_lookup | `false` | +| suggested_path | `spec-flow` | +| next_step | implement `kb query`, docs, and verification | + +## Goal / 目标 + +- primary_goal: 让知识库先具备一个可用的查询入口,并把 `source_refs` / `related` 作为第一层 provenance 暴露出来。 +- primary_goal_en: Give the knowledge base a usable query entry point and expose `source_refs` / `related` as the first layer of provenance. +- success_signal: 可以直接运行 `python3 knowledge-base/kb query ...` 拿到排序结果、摘要片段和出处字段。 +- success_signal_en: `python3 knowledge-base/kb query ...` returns ranked results, snippets, and provenance fields. + +## Constraints And Signals / 约束与信号 + +- hard_constraints: 不引入外部依赖;保留 `raw/` 与 `wiki/` 的现有边界;不破坏现有命令。 +- hard_constraints_en: No new external dependencies; preserve the current `raw/` and `wiki/` boundary; do not break existing commands. +- dependencies: 现有 `knowledge-base/kb` CLI、canonical page frontmatter、`source_refs` 与 `related` 字段。 +- dependencies_en: The existing `knowledge-base/kb` CLI, canonical page frontmatter, and the `source_refs` / `related` fields. +- known_risks: 前端没有统一 query contract;现有 `related` 字段存在无后缀短引用,解析时需要兼容。 +- known_risks_en: There is no existing query contract yet; current `related` fields use extensionless shorthand references that must be resolved compatibly. + +## Reasoning / 判断依据 + +### Why This Task Type / 为什么是复杂变更 + +- 需要同时改 CLI、引用解析、命令文档和测试,属于跨文件行为变更。 +- This touches CLI behavior, reference resolution, command docs, and tests, so it is a cross-file behavioral change. + +### Why This Risk Level / 为什么是中风险 + +- 风险主要在于误改现有 frontmatter 引用语义,导致维护命令或 provenance 输出失真。 +- The main risk is changing existing frontmatter reference semantics in a way that distorts maintenance commands or provenance output. + +### Research Or Memory Notes / 研究与记忆说明 + +- 本轮不再继续 research;直接实现前面规划过的第一批高价值切片。 +- No additional research is required in this round; implement the first high-value slice that was already planned. diff --git a/docs/specs/2026-04-11-kb-query-provenance/package-index.md b/docs/specs/2026-04-11-kb-query-provenance/package-index.md new file mode 100644 index 0000000..90351eb --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-provenance/package-index.md @@ -0,0 +1,40 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-query-provenance` +- title: `knowledge-base query command with lightweight provenance` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-query-provenance/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-query-provenance/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-query-provenance/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-query-provenance/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-query-provenance/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py` | +| verify | `done` | `docs/specs/2026-04-11-kb-query-provenance/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-query-provenance/runtime/run-state.md` | +| autonomous-cycle-report | `not-applicable` | | +| workflow-health-report | `not-applicable` | | + +## Summary / 摘要 + +- goal: 为 `knowledge-base` 落地一个可运行的 `query/search/ask` 查询入口,并附带基础出处信息。 +- goal_en: Land a working `query/search/ask` command for `knowledge-base` with basic provenance output. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 在下一轮继续补 query 健康检查、结果去重和更强的 provenance lint。 +- next_step_en: In the next slice, extend query health checks, result dedupe, and stronger provenance linting. + +## Notes / 备注 + +- 本轮只实现第一批最小闭环,不包含向量检索、自动 citation 重写或 page evolution 自动化。 +- This round ships the first minimal vertical slice only; it does not include vector search, automatic citation rewriting, or page-evolution automation. diff --git a/docs/specs/2026-04-11-kb-query-provenance/plan-review.md b/docs/specs/2026-04-11-kb-query-provenance/plan-review.md new file mode 100644 index 0000000..4df9ffc --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-provenance/plan-review.md @@ -0,0 +1,35 @@ +# Plan Review / 计划评审 + +## Findings / 发现 + +- 当前方案保持在单脚本增强、测试补充和命令文档更新范围内,没有引入新的平台层。 +- The plan stays within a single-script enhancement plus tests and docs; it does not introduce a new platform layer. + +## Missing Tests / 缺失测试 + +- 需要覆盖无后缀短引用解析。 +- Need coverage for extensionless shorthand reference resolution. +- 需要覆盖 `--json` 的结构化输出。 +- Need coverage for structured `--json` output. + +## Overengineering Check / 过度设计检查 + +- 通过:本轮不做 embedding、数据库或自动 citation 编排。 +- Pass: this round does not introduce embeddings, a database, or automatic citation composition. + +## Ambiguities / 歧义 + +- `report` 是否默认参与搜索可以后续再调,不阻塞首轮实现。 +- Whether `report` pages should participate by default can be revisited later and does not block the first implementation slice. + +## Ordering Issues / 顺序问题 + +- 先做引用解析,再做 query provenance 输出,顺序合理。 +- It is correct to fix reference resolution before rendering query provenance. + +## Approval / 审批 + +| Field | Value | +| --- | --- | +| approval_status | `approved` | +| reviewer_notes | 以最小闭环先落 `query + provenance + tests`,其余 query 治理能力后续补齐 | diff --git a/docs/specs/2026-04-11-kb-query-provenance/plan.md b/docs/specs/2026-04-11-kb-query-provenance/plan.md new file mode 100644 index 0000000..54707ae --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-provenance/plan.md @@ -0,0 +1,54 @@ +# Plan / 计划 + +## Architecture / 架构 + +- 在 `knowledge-base/kb` 内直接增加 query 子命令,复用现有 page 解析、canonical page 枚举和 frontmatter 结构。 +- Add the query subcommand directly inside `knowledge-base/kb`, reusing existing page parsing, canonical-page enumeration, and frontmatter structures. + +## Data Flow / 数据流 + +- 用户输入查询词。 +- The user provides search terms. +- CLI 枚举 canonical pages,按标题、路径、标题层级、稳定结论和正文计算分值。 +- The CLI enumerates canonical pages and scores them across title, path, heading, stable-claim, and body matches. +- 结果返回 `snippet`、`source_refs` 和 `related`。 +- The result includes `snippet`, `source_refs`, and `related`. +- 若使用 `--json`,则返回结构化 JSON 供后续 automation 或脚本消费。 +- With `--json`, the command returns structured JSON for later automation or scripts. + +## Touch Points / 触点 + +- files_or_modules: `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py` +- docs_to_update: `knowledge-base/KB_COMMANDS.md`, `docs/specs/2026-04-11-kb-query-provenance/*` +- external_inputs: 当前 canonical pages 与其 frontmatter + +## Implementation Order / 实现顺序 + +1. 在 `kb` 中补 query 结果模型、匹配打分和 provenance 渲染。 +2. 在 `kb` 中统一 frontmatter 短引用解析,并把 `maintain` 升级为 provenance-aware 检查。 +3. 补测试与命令文档,并做本地验证。 + +## Risks / 风险 + +- risk: 旧页面的 `related` 或 `source_refs` 使用无后缀短引用,解析不一致会导致 provenance 失真。 + mitigation: 统一引入 `resolve_repo_reference()`,优先复用到 provenance 相关路径解析。 +- risk: 查询结果过于依赖正文命中,导致噪音偏高。 + mitigation: 提高标题、标题层级和稳定结论的权重,并限制 body hit 的加分上限。 + +## Test Strategy / 测试策略 + +- unit_or_local_checks: `python3 -m unittest knowledge-base/tests/test_kb_query.py` +- integration_or_manual_checks: `python3 knowledge-base/kb query "formal memory" --limit 3` +- integration_or_manual_checks_2: `python3 knowledge-base/kb maintain` +- failure_cases_to_cover: 无后缀短引用、`--json` 输出、`--type` 过滤、`source_refs` 目标分类 + +## Rollout Strategy / 发布策略 + +- release_or_merge_notes: 这是增量命令增强,不需要迁移已有知识页。 +- rollback_notes: 若查询结果不稳,可先回退 `kb` 的 query 子命令与引用解析改动,不影响已有 add/log/maintain/reindex/delete/distill-memory。 + +## Out Of Scope / 范围外 + +- 复杂 ranking 调优 +- Query result dedupe for bilingual mirror pages +- 自动健康检查与 query 合约版本化 diff --git a/docs/specs/2026-04-11-kb-query-provenance/runtime/run-state.md b/docs/specs/2026-04-11-kb-query-provenance/runtime/run-state.md new file mode 100644 index 0000000..ba1d5dc --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-provenance/runtime/run-state.md @@ -0,0 +1,43 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-query-provenance` +- title: `knowledge-base query command with lightweight provenance` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-query-provenance/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `start the next knowledge-base query/provenance slice when requested` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T18:14:52+0800` +- last_progress_at: `2026-04-11T18:20:03+0800` +- last_resumed_at: `2026-04-11T18:20:03+0800` +- updated_at: `2026-04-11T18:20:03+0800` + +## Notes / 备注 + +- 本轮按最小可交付切片完成,尚未扩展到 query 健康检查与 page evolution。 +- This round completed the smallest useful vertical slice and has not yet expanded into query health checks or page evolution. diff --git a/docs/specs/2026-04-11-kb-query-provenance/spec.md b/docs/specs/2026-04-11-kb-query-provenance/spec.md new file mode 100644 index 0000000..5d10a8e --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-provenance/spec.md @@ -0,0 +1,68 @@ +# Spec / 规格 + +## Goal / 目标 + +- 为 `knowledge-base` 增加 `query/search/ask` 命令,支持按标题、路径、标题层级、稳定结论和正文做轻量检索,并输出基础 provenance。 +- Add a `query/search/ask` command to `knowledge-base` that performs lightweight search over titles, paths, headings, stable claims, and body text, then returns basic provenance. + +## User Value / 用户价值 + +- 用户现在可以先“查到什么页最相关”,再顺着 `source_refs` 与 `related` 去追溯,不必只靠手工打开 `index.md` 和 `overview.md`。 +- Users can now first ask “which pages are most relevant,” then follow `source_refs` and `related` to trace support, instead of relying only on manual navigation through `index.md` and `overview.md`. + +## In Scope / 范围内 + +- 新增 `kb query` 命令及 `search` / `ask` 别名。 +- Add the `kb query` command and the `search` / `ask` aliases. +- 结果排序、命中字段说明、摘要片段输出。 +- Add ranking, matched-field explanations, and snippet output. +- 输出 `source_refs` 与 `related`。 +- Return `source_refs` and `related`. +- 兼容无 `.md` 后缀的 frontmatter 短引用解析。 +- Resolve extensionless frontmatter shorthand references. +- 为 `maintain` 补 provenance-aware 的 `source_refs` 目标校验。 +- Add provenance-aware `source_refs` target validation to `maintain`. +- 补充命令文档与回归测试。 +- Add command docs and regression tests. + +## Out Of Scope / 范围外 + +- 向量检索、embedding、数据库索引。 +- Vector search, embeddings, or database-backed indexing. +- 自动 citation 编排或跨页证据聚合。 +- Automatic citation composition or cross-page evidence aggregation. +- page evolution 状态机。 +- A page-evolution state machine. +- 自动健康检查调度。 +- Automated health-check scheduling. + +## Constraints / 约束 + +- technical_constraints: 继续使用标准库;不引入新的服务依赖;保持脚本可直接执行。 +- technical_constraints_en: Stay within the standard library, add no service dependencies, and keep the script directly executable. +- workflow_constraints: 不动现有知识页内容,只增强 CLI 和说明文档。 +- workflow_constraints_en: Do not rewrite existing knowledge pages; only enhance the CLI and its documentation. +- policy_constraints: provenance 只做轻量展示,不把 raw 命中直接提升为 canonical answer。 +- policy_constraints_en: Provenance remains lightweight and must not promote raw hits into canonical answers by itself. + +## Acceptance Criteria / 验收标准 + +- `python3 knowledge-base/kb query "formal memory"` 可以返回排序结果。 +- `python3 knowledge-base/kb query "formal memory"` returns ranked results. +- 结果里包含 `matched_fields`、`snippet`、`source_refs`、`related`。 +- Results include `matched_fields`, `snippet`, `source_refs`, and `related`. +- `--json` 输出可被机器读取。 +- `--json` output is machine-readable. +- 无后缀短引用在 provenance 解析中可正确补成 `.md`。 +- Extensionless shorthand references are resolved to `.md` correctly in provenance handling. +- `python3 knowledge-base/kb maintain` 在当前仓库上可通过,并能检查 `source_refs` 的目标位置是否合法。 +- `python3 knowledge-base/kb maintain` passes on the current repository and validates whether `source_refs` point to valid support locations. +- 新增测试覆盖查询和引用解析。 +- New tests cover both query behavior and reference resolution. + +## Open Questions / 未决问题 + +- 下一轮是否把 `report` 结果默认降权,而不是简单排除。 +- Whether the next slice should down-rank `report` results by default instead of simply excluding them. +- 下一轮是否把 bilingual 镜像页做结果去重。 +- Whether the next slice should dedupe bilingual mirrored pages in search results. diff --git a/docs/specs/2026-04-11-kb-query-provenance/tasks.md b/docs/specs/2026-04-11-kb-query-provenance/tasks.md new file mode 100644 index 0000000..14ca9fb --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-provenance/tasks.md @@ -0,0 +1,43 @@ +# Tasks / 任务 + +## Task List / 任务列表 + +### T1 + +- id: `T1` +- goal: 在 `knowledge-base/kb` 里实现 query 命令与 provenance 输出。 +- goal_en: Implement the query command and provenance output in `knowledge-base/kb`. +- files: `knowledge-base/kb` +- depends_on: `none` +- verification: 直接运行 `python3 knowledge-base/kb query ...` +- done_definition: 查询结果可读、可排序,并附带 `source_refs` / `related` + +### T2 + +- id: `T2` +- goal: 补上无后缀短引用解析与回归测试。 +- goal_en: Add extensionless shorthand reference resolution, provenance-aware maintain checks, and regression tests. +- files: `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py` +- depends_on: `T1` +- verification: `python3 -m unittest knowledge-base/tests/test_kb_query.py`, `python3 knowledge-base/kb maintain` +- done_definition: 测试覆盖查询与引用解析,`maintain` 能验证 `source_refs` 目标,且全部通过 + +### T3 + +- id: `T3` +- goal: 补命令文档与变更包验证记录。 +- goal_en: Add command documentation and package verification records. +- files: `knowledge-base/KB_COMMANDS.md`, `docs/specs/2026-04-11-kb-query-provenance/*` +- depends_on: `T1`, `T2` +- verification: 文档与实际命令一致,verify 记录完整 +- done_definition: 文档可作为下一轮续跑的真相源 + +## Dependency Summary / 依赖摘要 + +- `T1 -> T2 -> T3` + +## Execution Notes / 执行说明 + +- sequencing_notes: 先稳定行为,再补测试和文档。 +- parallelism_notes: 当前改动集中在一个 CLI 脚本,平行化收益不高。 +- rollback_notes: 若 query 行为不稳定,可只回退 `kb` 与新测试,不影响其他现有命令。 diff --git a/docs/specs/2026-04-11-kb-query-provenance/verify.md b/docs/specs/2026-04-11-kb-query-provenance/verify.md new file mode 100644 index 0000000..a48c2ed --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-provenance/verify.md @@ -0,0 +1,70 @@ +# Verify / 验证 + +## Change Summary / 变更摘要 + +- 新增 `knowledge-base/kb query` 命令,支持 `search` / `ask` 别名。 +- Added the `knowledge-base/kb query` command with `search` / `ask` aliases. +- 查询结果现在会输出 `matched_fields`、`snippet`、`source_refs` 和 `related`。 +- Query results now include `matched_fields`, `snippet`, `source_refs`, and `related`. +- provenance 解析现在兼容无 `.md` 后缀的 frontmatter 短引用。 +- Provenance resolution now supports extensionless frontmatter shorthand references. +- `maintain` 现在会额外检查 `source_refs` 是否仍然落在合理支持位置。 +- `maintain` now additionally validates whether `source_refs` still point to valid support locations. +- 新增 `knowledge-base/tests/test_kb_query.py` 回归测试。 +- Added `knowledge-base/tests/test_kb_query.py` regression tests. + +## Status / 状态 + +| Field | Value | +| --- | --- | +| behavior_check | `pass` | +| test_check | `pass` | +| regression_risk | `medium` | +| doc_sync_needed | `completed` | +| memory_candidate | `no` | +| final_status | `passed` | + +## Behavior Validation / 行为验证 + +- 运行:`python3 knowledge-base/kb query "formal memory" --limit 3` +- Result: 返回了排序结果、命中字段、摘要片段和 provenance 字段。 +- Result_en: Returned ranked results with matched fields, snippets, and provenance. + +- 运行:`python3 knowledge-base/kb query "formal memory" --json --limit 2` +- Result: 返回结构化 JSON,可直接给后续自动流程消费。 +- Result_en: Returned structured JSON suitable for later automated consumers. + +- 运行:`python3 knowledge-base/kb maintain` +- Result: 当前仓库输出 `issues: none`。 +- Result_en: The current repository reported `issues: none`. + +## Test Validation / 测试验证 + +- 运行:`python3 -m unittest knowledge-base/tests/test_kb_query.py` +- Result: `Ran 4 tests ... OK` + +## Regression Risk / 回归风险 + +- 风险点主要在 `extract_frontmatter_links()` 的引用解析行为变化。 +- The main regression surface is the changed reference-resolution behavior inside `extract_frontmatter_links()`. +- 当前通过新增测试覆盖了 query 和 shorthand resolution,但还没有覆盖 `maintain` 的更广泛页面集。 +- The new tests cover query behavior and shorthand resolution, but they do not yet cover the full `maintain` flow across the broader page set. + +## Documentation Sync / 文档同步 + +- 已更新 `knowledge-base/KB_COMMANDS.md` 的命令说明。 +- Updated the command documentation in `knowledge-base/KB_COMMANDS.md`. +- 已新增本轮双语变更包文档。 +- Added the bilingual change-package documents for this round. + +## Memory Candidate / 记忆候选 + +- eligible: `no` +- candidate_summary: +- why_stable: +- why_not: 这是 repo 内的增量功能交付,不属于应进入长期 memory 的跨项目稳定偏好或长期原则。 + +## Open Issues / 未决问题 + +- 下一轮可以继续补 query 结果去重、report 降权和 provenance lint。 +- The next slice can continue with result dedupe, report down-ranking, and provenance linting. diff --git a/docs/specs/2026-04-11-kb-query-ranking-governance/intake.md b/docs/specs/2026-04-11-kb-query-ranking-governance/intake.md new file mode 100644 index 0000000..0f7698d --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-ranking-governance/intake.md @@ -0,0 +1,28 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户要求“不断点续跑,按切片逐个验证,不把验证完全省掉”。 +- The user asked to keep rolling slice by slice, with verification on every slice instead of skipping validation. + +## Problem / 问题 + +- 现有 `kb query` 已可用,但默认排序仍偏词面命中,容易把 `source` 大量顶到前面。 +- `report` 即使被纳入查询,也不该和答案型 canonical 页处在同一优先级。 +- 未来双语页或镜像页增多后,查询结果会出现明显重复。 + +- The current `kb query` works, but ranking still leans too much on lexical hits and can crowd the top with `source` pages. +- Even when included, `report` pages should not share the same default priority as answer-like canonical pages. +- As bilingual or mirrored pages increase, obvious duplicate results will show up in queries. + +## Scope / 范围 + +- 调整 `kb query` 排序分数 +- 为镜像重复结果增加保守去重 +- 保持 CLI 和 JSON 输出可解释 +- 补测试与双语命令文档 + +- Adjust `kb query` ranking scores +- Add conservative dedupe for mirrored duplicate results +- Keep CLI and JSON output explainable +- Add tests and bilingual command docs diff --git a/docs/specs/2026-04-11-kb-query-ranking-governance/package-index.md b/docs/specs/2026-04-11-kb-query-ranking-governance/package-index.md new file mode 100644 index 0000000..e900f5d --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-ranking-governance/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-query-ranking-governance` +- title: `knowledge-base query ranking governance and conservative dedupe` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-query-ranking-governance/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-query-ranking-governance/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-query-ranking-governance/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-query-ranking-governance/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-query-ranking-governance/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py`, `knowledge-base/KB_COMMANDS.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-query-ranking-governance/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-query-ranking-governance/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 收紧 `kb query` 的默认结果质量,让答案型页面优先、`report` 降权,并保守折叠语言镜像重复结果。 +- goal_en: Tighten default `kb query` result quality so answer-like pages rank first, `report` pages are downweighted, and locale-mirror duplicates are conservatively collapsed. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 继续做 provenance lint 强化和知识页健康检查。 +- next_step_en: Continue with stronger provenance linting and knowledge-page health checks. + +## Notes / 备注 + +- 本轮只做查询结果治理,不改 wiki 内容结构,也不引入向量检索。 +- This slice only improves query-result governance; it does not change wiki content structure or add vector search. diff --git a/docs/specs/2026-04-11-kb-query-ranking-governance/plan-review.md b/docs/specs/2026-04-11-kb-query-ranking-governance/plan-review.md new file mode 100644 index 0000000..a505562 --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-ranking-governance/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 方案保持在 `kb query` 这条竖线内,没有扩大到 wiki 内容重写。 +- locale mirror dedupe 采用保守识别,只处理带明显语言后缀的标题或 slug,避免误伤不同主题页。 +- 排序治理保留 `match_score`,避免最终分数完全黑盒化。 + +- The plan stays inside the `kb query` vertical slice and does not expand into wiki content rewriting. +- Locale-mirror dedupe uses conservative detection and only handles titles or slugs with obvious language suffixes, reducing the chance of collapsing distinct topics. +- Ranking governance retains `match_score` so the final score does not become fully opaque. diff --git a/docs/specs/2026-04-11-kb-query-ranking-governance/plan.md b/docs/specs/2026-04-11-kb-query-ranking-governance/plan.md new file mode 100644 index 0000000..63c5c57 --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-ranking-governance/plan.md @@ -0,0 +1,13 @@ +# Plan / 计划 + +1. 在 `knowledge-base/kb` 中拆分 `match_score` 与最终排序分,并加入类型、状态、质量、locale 变体修正。 + In `knowledge-base/kb`, split `match_score` from the final ranking score and add type, status, quality, and locale-variant adjustments. + +2. 为 query 结果增加保守 dedupe,只折叠明显的 locale mirror 结果。 + Add conservative dedupe for query results and collapse only obvious locale-mirror duplicates. + +3. 更新输出契约、命令文档和测试。 + Update the output contract, command docs, and tests. + +4. 跑单测与真实 CLI 查询验证。 + Run unit tests and real CLI query verification. diff --git a/docs/specs/2026-04-11-kb-query-ranking-governance/runtime/run-state.md b/docs/specs/2026-04-11-kb-query-ranking-governance/runtime/run-state.md new file mode 100644 index 0000000..8a9c73d --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-ranking-governance/runtime/run-state.md @@ -0,0 +1,45 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-query-ranking-governance` +- title: `knowledge-base query ranking governance and conservative dedupe` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-query-ranking-governance/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue with provenance lint hardening when requested` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `1` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | `unit-test expected mirror winner mismatch; fixed with locale-variant base-page preference` | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T18:34:22+0800` +- last_progress_at: `2026-04-11T18:35:56+0800` +- last_resumed_at: `2026-04-11T18:35:56+0800` +- updated_at: `2026-04-11T18:35:56+0800` + +## Notes / 备注 + +- 本轮把 query 结果治理推进到“类型优先级 + report 降权 + locale mirror dedupe”。 +- verify、维护检查和 memory refresh 均已完成。 +- This slice advanced query-result governance to “type priority + report downweight + locale-mirror dedupe”. +- Verify, maintenance checks, and memory refresh are all complete. diff --git a/docs/specs/2026-04-11-kb-query-ranking-governance/spec.md b/docs/specs/2026-04-11-kb-query-ranking-governance/spec.md new file mode 100644 index 0000000..8d4e996 --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-ranking-governance/spec.md @@ -0,0 +1,37 @@ +# Spec / 规格 + +## Goals / 目标 + +- `query` 默认优先返回更接近“答案页”的结果。 +- `report` 只有在显式纳入时参与结果集,但仍保持降权。 +- 明显的 locale mirror 结果默认收口为一条,并保留被折叠路径信息。 + +- `query` should prefer results that behave more like answer pages by default. +- `report` pages may join the result set only when explicitly included, but they remain downweighted. +- Obvious locale-mirror results should collapse into one default result while preserving the suppressed paths. + +## Non-Goals / 非目标 + +- 不做语义向量检索 +- 不自动重写 wiki 页面内容 +- 不做跨文件 citation 修复 + +- No semantic vector retrieval +- No automatic wiki page rewriting +- No cross-file citation repair + +## Functional Rules / 功能规则 + +- 查询结果增加类型治理分:`concept`、`synthesis`、`entity`、`domain` 优先于 `source`,`report` 明显降权。 +- 查询结果增加轻量质量分:有 `source_refs` 和稳定结论的页更容易排前。 +- 带 locale 后缀的标题或 slug 视为镜像候选;若同 kind 下出现同基准 stem 或 title,则默认只保留最高优先结果。 +- 默认输出保留 `score`,并新增 `match_score` 区分词面命中分。 +- 去重后若有被折叠页面,输出 `suppressed_duplicates`。 +- 提供 `--no-dedupe` 以便显式查看原始重复结果。 + +- Add type-governance ranking so `concept`, `synthesis`, `entity`, and `domain` outrank `source`, while `report` is materially downweighted. +- Add small quality bonuses so pages with `source_refs` and stable claims surface more easily. +- Treat locale-suffixed titles or slugs as mirror candidates; when the same kind shares a normalized base stem or title, keep only the highest-priority result by default. +- Keep `score` in the default output and add `match_score` so lexical matching remains visible. +- Emit `suppressed_duplicates` when dedupe collapses mirrored pages. +- Provide `--no-dedupe` for explicit raw duplicate inspection. diff --git a/docs/specs/2026-04-11-kb-query-ranking-governance/tasks.md b/docs/specs/2026-04-11-kb-query-ranking-governance/tasks.md new file mode 100644 index 0000000..977012c --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-ranking-governance/tasks.md @@ -0,0 +1,15 @@ +# Tasks / 任务拆解 + +- [x] 在 `knowledge-base/kb` 中加入 query 类型治理分、质量分和 locale 变体惩罚 +- [x] 在 `knowledge-base/kb` 中实现默认 dedupe 与 `--no-dedupe` +- [x] 在 JSON 和文本输出中暴露 `match_score` 与 `suppressed_duplicates` +- [x] 在 `knowledge-base/tests/test_kb_query.py` 中补充排序与去重测试 +- [x] 在 `knowledge-base/KB_COMMANDS.md` 中更新双语命令说明 +- [x] 运行 CLI 与单测验证 + +- [x] Add query type-governance, quality bonuses, and locale-variant penalty in `knowledge-base/kb` +- [x] Implement default dedupe and `--no-dedupe` in `knowledge-base/kb` +- [x] Expose `match_score` and `suppressed_duplicates` in JSON and text output +- [x] Add ranking and dedupe tests in `knowledge-base/tests/test_kb_query.py` +- [x] Update bilingual command guidance in `knowledge-base/KB_COMMANDS.md` +- [x] Run CLI and unit-test verification diff --git a/docs/specs/2026-04-11-kb-query-ranking-governance/verify.md b/docs/specs/2026-04-11-kb-query-ranking-governance/verify.md new file mode 100644 index 0000000..49b8461 --- /dev/null +++ b/docs/specs/2026-04-11-kb-query-ranking-governance/verify.md @@ -0,0 +1,36 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 -m unittest /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb query integration --include-reports --limit 8 +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb query memory --include-reports --limit 8 +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb query memory --include-reports --limit 8 --json +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/kb /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py /Users/wz/project/codex-enhanced-system/knowledge-base/KB_COMMANDS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-query-ranking-governance +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `unittest`: `Ran 7 tests ... OK` +- `query integration`: 顶部结果依次为 `concept -> synthesis -> source`,说明答案型页面已优先于 `source` +- `query memory`: 顶部结果依次为 `concept -> concept -> synthesis -> entity -> source` +- `query memory --json`: 已返回 `match_score` 与 `suppressed_duplicates` 字段 +- `kb maintain`: `issues: none` +- `git diff --check`: 无输出,说明本轮目标文件没有空白或 patch 格式问题 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `unittest`: `Ran 7 tests ... OK` +- `query integration`: the top results are `concept -> synthesis -> source`, showing answer-like pages now outrank `source` +- `query memory`: the top results are `concept -> concept -> synthesis -> entity -> source` +- `query memory --json`: the output now includes `match_score` and `suppressed_duplicates` +- `kb maintain`: `issues: none` +- `git diff --check`: no output, so the target files are free of whitespace or patch-format issues +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- 本轮切片通过,查询结果默认质量已明显收紧,且保留了显式查看原始重复项的出口。 +- This slice passed. Default query-result quality is noticeably tighter, while still keeping an explicit escape hatch for raw duplicate inspection. diff --git a/docs/specs/2026-04-11-kb-report-index-sync/intake.md b/docs/specs/2026-04-11-kb-report-index-sync/intake.md new file mode 100644 index 0000000..dec37d6 --- /dev/null +++ b/docs/specs/2026-04-11-kb-report-index-sync/intake.md @@ -0,0 +1,24 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户继续要求推进实现。 +- The user asked to keep the implementation moving. + +## Problem / 问题 + +- `maintain --write-report` 在写入新 report 后会立刻留下 `missing from wiki/index.md` 的维护残留。 +- `drift-review --write-report` 也存在同类风险,只是之前靠手工导航收口规避了它。 + +- `maintain --write-report` immediately left a `missing from wiki/index.md` maintenance residue after writing a new report. +- `drift-review --write-report` carried the same class of risk, even though earlier manual guide updates had masked it. + +## Scope / 范围 + +- 让 report-producing commands 自动同步 `wiki/index.md` +- 保持现有 `stable` / `healthy` 基线不被打破 +- 补测试、命令文档和双语变更包 + +- Make report-producing commands auto-sync `wiki/index.md` +- Preserve the existing `stable` / `healthy` baseline +- Add tests, command docs, and a bilingual change package diff --git a/docs/specs/2026-04-11-kb-report-index-sync/package-index.md b/docs/specs/2026-04-11-kb-report-index-sync/package-index.md new file mode 100644 index 0000000..c101953 --- /dev/null +++ b/docs/specs/2026-04-11-kb-report-index-sync/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-report-index-sync` +- title: `knowledge-base report index sync` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-report-index-sync/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-report-index-sync/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-report-index-sync/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-report-index-sync/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-report-index-sync/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py`, `knowledge-base/KB_COMMANDS.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-report-index-sync/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-report-index-sync/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 让 `maintain --write-report` 和 `drift-review --write-report` 在实际写入新 report 时自动把它补进 `wiki/index.md`,避免写入动作自己制造新的 index drift。 +- goal_en: Make `maintain --write-report` and `drift-review --write-report` auto-register new reports in `wiki/index.md` so the write itself does not create fresh index drift. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 如有需要,可继续检查其他会修改 `wiki/index.md` 的命令是否也应统一更新 `updated_at` 语义。 +- next_step_en: If needed, continue by checking whether other commands that mutate `wiki/index.md` should share the same `updated_at` semantics. + +## Notes / 备注 + +- 本轮把 report 写入后的 index 收口做成了共享能力,并让写后摘要反映最终状态。 +- This slice turns post-write report registration into a shared capability and makes the write summaries reflect the final state. diff --git a/docs/specs/2026-04-11-kb-report-index-sync/plan-review.md b/docs/specs/2026-04-11-kb-report-index-sync/plan-review.md new file mode 100644 index 0000000..7d93cef --- /dev/null +++ b/docs/specs/2026-04-11-kb-report-index-sync/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 这是一个 command writeback closeout 问题,最合适的修法是共享一个小型 index registration helper,而不是把逻辑散落到各个命令里。 +- 通过测试夹具覆盖真实写入,可以验证写后健康状态,同时避免为了验证在真实仓库里新增无意义 report。 +- 文档需要同步说明“`--write-report` 不只写 report,也会把 report 纳入 index”,否则行为变化会很隐蔽。 + +- This is a command writeback closeout problem, so the best fix is a small shared index-registration helper rather than duplicating logic across commands. +- Covering the real write path in tests verifies the post-write healthy state without creating meaningless extra reports in the live repository. +- The docs should explicitly say that `--write-report` not only writes a report but also registers it in the index, otherwise the behavior change stays too implicit. diff --git a/docs/specs/2026-04-11-kb-report-index-sync/plan.md b/docs/specs/2026-04-11-kb-report-index-sync/plan.md new file mode 100644 index 0000000..4506167 --- /dev/null +++ b/docs/specs/2026-04-11-kb-report-index-sync/plan.md @@ -0,0 +1,10 @@ +# Plan / 计划 + +1. 复现并确认 command-generated report 的 index residue。 + Reproduce and confirm the index residue left by command-generated reports. + +2. 提取共享的 index registration helper,并接入 `maintain` 与 `drift-review`。 + Extract a shared index-registration helper and wire it into `maintain` and `drift-review`. + +3. 补回归测试、文档和验证记录。 + Add regression coverage, docs, and verification records. diff --git a/docs/specs/2026-04-11-kb-report-index-sync/runtime/run-state.md b/docs/specs/2026-04-11-kb-report-index-sync/runtime/run-state.md new file mode 100644 index 0000000..e4568a9 --- /dev/null +++ b/docs/specs/2026-04-11-kb-report-index-sync/runtime/run-state.md @@ -0,0 +1,43 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-report-index-sync` +- title: `knowledge-base report index sync` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-report-index-sync/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue only if another command-generated write path leaves similar governance residue` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T20:43:03+0800` +- last_progress_at: `2026-04-11T20:44:26+0800` +- last_resumed_at: `2026-04-11T20:44:26+0800` +- updated_at: `2026-04-11T20:44:26+0800` + +## Notes / 备注 + +- 本轮把 report 写入后的 index registration 抽成共享能力,并让 `maintain` 与 `drift-review` 的写后状态都收敛到最终健康态。 +- This slice extracts post-write report index registration into a shared capability and makes the write results of both `maintain` and `drift-review` converge to the final healthy state. diff --git a/docs/specs/2026-04-11-kb-report-index-sync/spec.md b/docs/specs/2026-04-11-kb-report-index-sync/spec.md new file mode 100644 index 0000000..a5f92de --- /dev/null +++ b/docs/specs/2026-04-11-kb-report-index-sync/spec.md @@ -0,0 +1,33 @@ +# Spec / 规格 + +## Goals / 目标 + +- 让 command-generated reports 在写入后立即成为 index graph 的有效成员,而不是新的治理噪音来源。 +- Ensure command-generated reports become valid members of the index graph immediately after writing instead of turning into new governance noise. + +- 让 `maintain` 和 `drift-review` 的写后摘要反映最终状态,而不是停留在写前计数或遗漏 index 收口。 +- Make the post-write summaries of `maintain` and `drift-review` reflect the final state instead of keeping pre-write counts or skipping index closeout. + +## Non-Goals / 非目标 + +- 不扩展新的 maintenance 或 drift heuristics +- 不自动把新 report 推进到 `hot.md` 或 `overview.md` +- 不改动 report 内容本身的业务结论 + +- Do not add new maintenance or drift heuristics +- Do not auto-promote new reports into `hot.md` or `overview.md` +- Do not change the business conclusions inside the reports themselves + +## Functional Rules / 功能规则 + +- 实际执行 `--write-report` 时,应自动把新 report 写入 `wiki/index.md` 对应 section +- 若 index 发生实际写入,应同步刷新 `wiki/index.md` 的 `updated_at` +- `maintain --write-report` 的摘要应基于写后重新计算的 counts/issues +- `drift-review --write-report` 的写后状态不应引入新的 maintenance residue +- `--dry-run` 保持为非修改式预览 + +- On a real `--write-report`, the command should auto-register the new report under the correct section of `wiki/index.md` +- If the index changes, the command should also refresh `wiki/index.md`'s `updated_at` +- The `maintain --write-report` summary should use post-write recomputed counts and issues +- The post-write state of `drift-review --write-report` should not introduce fresh maintenance residue +- `--dry-run` remains a non-mutating preview diff --git a/docs/specs/2026-04-11-kb-report-index-sync/tasks.md b/docs/specs/2026-04-11-kb-report-index-sync/tasks.md new file mode 100644 index 0000000..bcc805d --- /dev/null +++ b/docs/specs/2026-04-11-kb-report-index-sync/tasks.md @@ -0,0 +1,15 @@ +# Tasks / 任务拆解 + +- [x] 复现 `maintain --write-report` 的 index residue +- [x] 抽取共享的 index registration helper +- [x] 接入 `maintain` 与 `drift-review` +- [x] 补回归测试 +- [x] 更新 `knowledge-base/KB_COMMANDS.md` +- [x] 跑验证 + +- [x] Reproduce the `maintain --write-report` index residue +- [x] Extract a shared index-registration helper +- [x] Wire it into `maintain` and `drift-review` +- [x] Add regression tests +- [x] Update `knowledge-base/KB_COMMANDS.md` +- [x] Run verification diff --git a/docs/specs/2026-04-11-kb-report-index-sync/verify.md b/docs/specs/2026-04-11-kb-report-index-sync/verify.md new file mode 100644 index 0000000..c1495a6 --- /dev/null +++ b/docs/specs/2026-04-11-kb-report-index-sync/verify.md @@ -0,0 +1,36 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 -m unittest /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain --write-report --dry-run +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review --write-report --dry-run +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/kb /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py /Users/wz/project/codex-enhanced-system/knowledge-base/KB_COMMANDS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-report-index-sync +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `unittest`: `Ran 19 tests ... OK` +- `kb maintain`: `health_verdict: healthy` +- `kb drift-review`: `drift_verdict: stable` +- `kb maintain --write-report --dry-run`: 预览写入 `wiki/reports/report-maintenance-2026-04-11.md` +- `kb drift-review --write-report --dry-run`: 预览写入 `wiki/reports/report-drift-review-2026-04-11-2.md` +- `git diff --check`: 无输出,目标文件与变更包格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `unittest`: `Ran 19 tests ... OK` +- `kb maintain`: `health_verdict: healthy` +- `kb drift-review`: `drift_verdict: stable` +- `kb maintain --write-report --dry-run`: previewed a write to `wiki/reports/report-maintenance-2026-04-11.md` +- `kb drift-review --write-report --dry-run`: previewed a write to `wiki/reports/report-drift-review-2026-04-11-2.md` +- `git diff --check`: no output, so the target files and change package passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- command-generated reports 现在会在写入路径里自动收口到 `wiki/index.md`,不会再因为归档动作本身留下新的 index drift。 +- Command-generated reports now close themselves into `wiki/index.md` on the write path, so the archival step no longer leaves behind fresh index drift by itself. diff --git a/docs/specs/2026-04-11-kb-write-path-closeout/intake.md b/docs/specs/2026-04-11-kb-write-path-closeout/intake.md new file mode 100644 index 0000000..46f5d36 --- /dev/null +++ b/docs/specs/2026-04-11-kb-write-path-closeout/intake.md @@ -0,0 +1,28 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户继续要求推进实现。 +- The user asked to keep the implementation moving. + +## Problem / 问题 + +- `cmd_log` 追加日志时不会同步刷新 `wiki/log.md.updated_at`。 +- `cmd_add` 创建 canonical page 后仍要求手动再跑一次 `reindex --write`,否则新页会立刻处于 “missing from wiki/index.md”。 +- `cmd_add` 和 `cmd_delete` 仍然需要再单独补一条结构化 `log`,自动流程不够顺手。 + +- `cmd_log` appended log entries without refreshing `wiki/log.md.updated_at`. +- `cmd_add` still required a manual `reindex --write` after creating a canonical page, otherwise the new page immediately landed in a “missing from wiki/index.md” state. +- `cmd_add` and `cmd_delete` still required a separate structured `log` step, which kept automated flows from feeling fully smooth. + +## Scope / 范围 + +- 让 `cmd_log` 在实际写入时刷新 `wiki/log.md.updated_at` +- 让 `cmd_add` 创建 canonical page 后自动补入 `wiki/index.md` +- 给 `cmd_add` / `cmd_delete` 增加可选的 `--write-log` +- 补测试、命令文档和双语变更包 + +- Make `cmd_log` refresh `wiki/log.md.updated_at` on real writes +- Make `cmd_add` auto-register canonical pages in `wiki/index.md` +- Add optional `--write-log` support to `cmd_add` / `cmd_delete` +- Add tests, command docs, and a bilingual change package diff --git a/docs/specs/2026-04-11-kb-write-path-closeout/package-index.md b/docs/specs/2026-04-11-kb-write-path-closeout/package-index.md new file mode 100644 index 0000000..44a67a7 --- /dev/null +++ b/docs/specs/2026-04-11-kb-write-path-closeout/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-kb-write-path-closeout` +- title: `knowledge-base write path closeout` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-kb-write-path-closeout/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-kb-write-path-closeout/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-kb-write-path-closeout/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-kb-write-path-closeout/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-kb-write-path-closeout/tasks.md` | +| implementation | `done` | `knowledge-base/kb`, `knowledge-base/tests/test_kb_query.py`, `knowledge-base/KB_COMMANDS.md` | +| verify | `done` | `docs/specs/2026-04-11-kb-write-path-closeout/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-kb-write-path-closeout/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 收口 `kb` 剩余的低摩擦写路径,让 `log`、`add` 和可选的 `add/delete --write-log` 在真实写入后不再留下显而易见的 metadata/index/log 残留。 +- goal_en: Close out the remaining low-friction `kb` write paths so `log`, `add`, and the optional `add/delete --write-log` flow no longer leave obvious metadata, index, or logging residue after real writes. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 如有需要,可继续评估是否把更多 guide-surface 写路径统一成同样的一步式 closeout 语义。 +- next_step_en: If needed, continue by evaluating whether more guide-surface write paths should share the same one-step closeout semantics. + +## Notes / 备注 + +- 本轮聚焦 `write path closeout`,不是新增新的 graph heuristic。 +- This slice focuses on write-path closeout rather than adding new graph heuristics. diff --git a/docs/specs/2026-04-11-kb-write-path-closeout/plan-review.md b/docs/specs/2026-04-11-kb-write-path-closeout/plan-review.md new file mode 100644 index 0000000..6c20080 --- /dev/null +++ b/docs/specs/2026-04-11-kb-write-path-closeout/plan-review.md @@ -0,0 +1,13 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 这些都属于“写入后还差半步”的 closeout 问题,优先修命令写入路径最合适。 +- `cmd_add` 自动补 index 与前面 report 写入链路保持一致,能减少最常见的低摩擦残留。 +- `--write-log` 作为可选增强,比直接改成默认更稳,兼顾自动流程和手工使用体验。 +- 用单元测试覆盖真实写入语义,再用 live repo 命令做 dry-run 验证,可以避免不必要的真实内容扰动。 + +- These cases are all “one more step is still missing after write” closeout problems, so fixing the command write paths is the right first move. +- Auto-registering `cmd_add` in the index keeps it consistent with the earlier report-write path and removes a common low-friction residue. +- Optional `--write-log` is safer than forcing a new default, because it supports automated flows without making manual usage heavier. +- Covering real write semantics in unit tests and using dry-run checks in the live repo avoids unnecessary content churn. diff --git a/docs/specs/2026-04-11-kb-write-path-closeout/plan.md b/docs/specs/2026-04-11-kb-write-path-closeout/plan.md new file mode 100644 index 0000000..cdf339e --- /dev/null +++ b/docs/specs/2026-04-11-kb-write-path-closeout/plan.md @@ -0,0 +1,13 @@ +# Plan / 计划 + +1. 补齐 `cmd_log` 的 metadata 写回语义。 + Fix the metadata writeback semantics of `cmd_log`. + +2. 让 `cmd_add` 自动把新 canonical page 纳入 `wiki/index.md`。 + Make `cmd_add` auto-register new canonical pages in `wiki/index.md`. + +3. 给 `cmd_add` / `cmd_delete` 增加可选的一步式 `--write-log`。 + Add optional one-step `--write-log` support to `cmd_add` / `cmd_delete`. + +4. 补回归测试、命令文档和验证记录。 + Add regression coverage, command docs, and verification records. diff --git a/docs/specs/2026-04-11-kb-write-path-closeout/runtime/run-state.md b/docs/specs/2026-04-11-kb-write-path-closeout/runtime/run-state.md new file mode 100644 index 0000000..ae93f6c --- /dev/null +++ b/docs/specs/2026-04-11-kb-write-path-closeout/runtime/run-state.md @@ -0,0 +1,43 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-kb-write-path-closeout` +- title: `knowledge-base write path closeout` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-kb-write-path-closeout/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue only if another kb write path still leaves obvious post-write residue` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T22:10:07+0800` +- last_progress_at: `2026-04-11T22:16:00+0800` +- last_resumed_at: `2026-04-11T22:16:00+0800` +- updated_at: `2026-04-11T22:16:00+0800` + +## Notes / 备注 + +- 本轮把 `cmd_log`、`cmd_add` 以及可选的 `add/delete --write-log` 一起收口到了更低摩擦的状态。 +- This slice closes out `cmd_log`, `cmd_add`, and the optional `add/delete --write-log` flow into a lower-friction state. diff --git a/docs/specs/2026-04-11-kb-write-path-closeout/spec.md b/docs/specs/2026-04-11-kb-write-path-closeout/spec.md new file mode 100644 index 0000000..1575b50 --- /dev/null +++ b/docs/specs/2026-04-11-kb-write-path-closeout/spec.md @@ -0,0 +1,35 @@ +# Spec / 规格 + +## Goals / 目标 + +- 让常用 `kb` 写路径在单次写入后就尽量收敛到更健康的状态,而不是依赖额外人工补一步。 +- Make common `kb` write paths converge to a healthier state after a single write instead of depending on an extra manual cleanup step. + +- 保持当前真实仓库的 `healthy` / `stable` 基线不被打破。 +- Preserve the current `healthy` / `stable` baseline of the live repository. + +## Non-Goals / 非目标 + +- 不新增新的 maintenance 或 drift heuristics +- 不把 `--write-log` 变成强制默认行为 +- 不扩大 canonical page 模板范围 + +- Do not add new maintenance or drift heuristics +- Do not turn `--write-log` into a mandatory default behavior +- Do not widen the canonical page template scope + +## Functional Rules / 功能规则 + +- `cmd_log` 的真实写入必须同步刷新 `wiki/log.md.updated_at` +- `cmd_add` 的真实写入必须自动把新 canonical page 补入 `wiki/index.md` +- `cmd_add --write-log` 与 `cmd_delete --write-log` 应能自动补默认结构化日志 +- `cmd_add --dry-run` 和 `cmd_log --dry-run` 继续保持无副作用 +- `cmd_delete --dry-run` 与 `--write-log` 组合时也必须保持无副作用 +- 新逻辑不能打破 `kb maintain` 的 `healthy` 与 `kb drift-review` 的 `stable` + +- A real `cmd_log` write must refresh `wiki/log.md.updated_at` +- A real `cmd_add` write must auto-register the new canonical page in `wiki/index.md` +- `cmd_add --write-log` and `cmd_delete --write-log` should be able to append default structured log entries automatically +- `cmd_add --dry-run` and `cmd_log --dry-run` remain side-effect free +- `cmd_delete --dry-run` combined with `--write-log` must also remain side-effect free +- The new logic must not break `kb maintain`'s `healthy` or `kb drift-review`'s `stable` diff --git a/docs/specs/2026-04-11-kb-write-path-closeout/tasks.md b/docs/specs/2026-04-11-kb-write-path-closeout/tasks.md new file mode 100644 index 0000000..ae0037f --- /dev/null +++ b/docs/specs/2026-04-11-kb-write-path-closeout/tasks.md @@ -0,0 +1,15 @@ +# Tasks / 任务拆解 + +- [x] 让 `cmd_log` 写入时刷新 `wiki/log.md.updated_at` +- [x] 让 `cmd_add` 创建后自动补入 `wiki/index.md` +- [x] 给 `cmd_add` / `cmd_delete` 增加可选 `--write-log` +- [x] 补回归测试 +- [x] 更新 `knowledge-base/KB_COMMANDS.md` +- [x] 跑验证 + +- [x] Make `cmd_log` refresh `wiki/log.md.updated_at` on write +- [x] Make `cmd_add` auto-register new pages in `wiki/index.md` +- [x] Add optional `--write-log` to `cmd_add` / `cmd_delete` +- [x] Add regression tests +- [x] Update `knowledge-base/KB_COMMANDS.md` +- [x] Run verification diff --git a/docs/specs/2026-04-11-kb-write-path-closeout/verify.md b/docs/specs/2026-04-11-kb-write-path-closeout/verify.md new file mode 100644 index 0000000..75cec4f --- /dev/null +++ b/docs/specs/2026-04-11-kb-write-path-closeout/verify.md @@ -0,0 +1,39 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +python3 -m unittest /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb maintain +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb drift-review +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb log maintenance --summary "metadata sync check" --note "Dry-run validation for log updated_at semantics." --dry-run +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb add concept --slug temp-sanity-check --title "Temp Sanity Check" --source-ref wiki/sources/source-codex-memory-kit-readme.md --write-log --dry-run +python3 /Users/wz/project/codex-enhanced-system/knowledge-base/kb delete /Users/wz/project/codex-enhanced-system/knowledge-base/wiki/reports/report-drift-review-2026-04-11.md --write-log --dry-run +git diff --check -- /Users/wz/project/codex-enhanced-system/knowledge-base/kb /Users/wz/project/codex-enhanced-system/knowledge-base/tests/test_kb_query.py /Users/wz/project/codex-enhanced-system/knowledge-base/KB_COMMANDS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-kb-write-path-closeout +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `unittest`: `Ran 25 tests ... OK` +- `kb maintain`: `health_verdict: healthy` +- `kb drift-review`: `drift_verdict: stable` +- `kb log ... --dry-run`: 正常预览新增 log block +- `kb add ... --write-log --dry-run`: 正常预览新 canonical page scaffold +- `kb delete ... --write-log --dry-run`: 正常预览删除摘要与自动 log block +- `git diff --check`: 无输出,目标文件与变更包格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `unittest`: `Ran 25 tests ... OK` +- `kb maintain`: `health_verdict: healthy` +- `kb drift-review`: `drift_verdict: stable` +- `kb log ... --dry-run`: correctly previewed the new log block +- `kb add ... --write-log --dry-run`: correctly previewed the new canonical page scaffold +- `kb delete ... --write-log --dry-run`: correctly previewed the delete summary plus the auto-log block +- `git diff --check`: no output, so the target files and change package passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- `cmd_log`、`cmd_add` 和可选的 `add/delete --write-log` 现在都更接近单步闭环:真实写入后不再留下显而易见的 metadata/index/log 残留。 +- `cmd_log`, `cmd_add`, and the optional `add/delete --write-log` flow are now all closer to a one-step closeout: real writes no longer leave obvious metadata, index, or logging residue behind. diff --git a/docs/specs/2026-04-11-method-forge-autonomous-continuity/intake.md b/docs/specs/2026-04-11-method-forge-autonomous-continuity/intake.md new file mode 100644 index 0000000..6fa3a30 --- /dev/null +++ b/docs/specs/2026-04-11-method-forge-autonomous-continuity/intake.md @@ -0,0 +1,24 @@ +# Intake / 需求摄取 + +## Request / 请求 + +- 用户指出:已经激活了自动流程,为什么还是频繁停下来,需要再次说“继续”才继续。 +- The user pointed out that autonomous mode had already been activated, so it should not have kept stopping and waiting for another “continue”. + +## Problem / 问题 + +- 当前规则强调 `completed` 后要停止,但没有足够明确地区分“顶层任务完成”和“单个微切片完成”。 +- 这让执行者容易把 autonomous 误用成“每完成一刀就停”的半自动流程。 + +- The current rules correctly say to stop on `completed`, but they do not distinguish clearly enough between “the top-level task is done” and “one micro-slice is done.” +- That makes it too easy to use autonomous as a semi-manual flow that stops after every slice. + +## Scope / 范围 + +- 明确 `completed` 的语义边界 +- 明确“已知下一安全切片时要保持 running” +- 同步到 method-forge 文档、skill、模板和消费方 AGENTS + +- Clarify the semantic boundary of `completed` +- Clarify that the run should stay `running` when the next safe slice is already known +- Sync the rule into method-forge docs, the skill, templates, and consumer AGENTS diff --git a/docs/specs/2026-04-11-method-forge-autonomous-continuity/package-index.md b/docs/specs/2026-04-11-method-forge-autonomous-continuity/package-index.md new file mode 100644 index 0000000..d8dba1c --- /dev/null +++ b/docs/specs/2026-04-11-method-forge-autonomous-continuity/package-index.md @@ -0,0 +1,38 @@ +# Package Index / 变更包索引 + +## Change / 变更信息 + +- change_id: `2026-04-11-method-forge-autonomous-continuity` +- title: `method-forge autonomous continuity semantics` +- owner: `Codex` +- workspace: `/Users/wz/project/codex-enhanced-system` +- date: `2026-04-11` + +## Status / 状态 + +| Artifact | Status | Path | +| --- | --- | --- | +| intake | `done` | `docs/specs/2026-04-11-method-forge-autonomous-continuity/intake.md` | +| spec | `done` | `docs/specs/2026-04-11-method-forge-autonomous-continuity/spec.md` | +| plan | `done` | `docs/specs/2026-04-11-method-forge-autonomous-continuity/plan.md` | +| plan-review | `done` | `docs/specs/2026-04-11-method-forge-autonomous-continuity/plan-review.md` | +| tasks | `done` | `docs/specs/2026-04-11-method-forge-autonomous-continuity/tasks.md` | +| implementation | `done` | `method-forge/docs/method/*.md`, `method-forge/skills/method-forge-autonomous-execution/SKILL.md`, `method-forge/AGENTS.md`, `method-forge/docs/templates/consumer-agents-rules-template.md`, `knowledge-base/AGENTS.md`, `memory-system/AGENTS.md` | +| verify | `done` | `docs/specs/2026-04-11-method-forge-autonomous-continuity/verify.md` | +| code-review | `not-applicable` | | +| memory-candidate | `not-applicable` | | +| run-state | `done` | `docs/specs/2026-04-11-method-forge-autonomous-continuity/runtime/run-state.md` | + +## Summary / 摘要 + +- goal: 把“autonomous 不应在每个微切片后停下来等用户再说继续”写成显式规则,而不是继续依赖执行者习惯。 +- goal_en: Turn “autonomous should not stop after every micro-slice and wait for the user to say continue again” into an explicit rule instead of relying on executor habit. +- current_phase: `completed` +- current_phase_en: `completed` +- next_step: 后续继续观察真实 autonomous run 是否还会出现“微切片误标 completed”的残留。 +- next_step_en: Continue observing real autonomous runs for any remaining cases where a micro-slice is incorrectly marked `completed`. + +## Notes / 备注 + +- 本轮是方法层语义修正,不涉及新的 automation 平台实现。 +- This slice is a method-layer semantics correction and does not introduce a new automation platform. diff --git a/docs/specs/2026-04-11-method-forge-autonomous-continuity/plan-review.md b/docs/specs/2026-04-11-method-forge-autonomous-continuity/plan-review.md new file mode 100644 index 0000000..955bbcf --- /dev/null +++ b/docs/specs/2026-04-11-method-forge-autonomous-continuity/plan-review.md @@ -0,0 +1,11 @@ +# Plan Review / 计划复核 + +## Review / 复核 + +- 这是语义约束缺口,不是实现引擎 bug,因此应优先修文档真相源和消费方规则。 +- 同步消费方 AGENTS 很重要,否则 method-forge 自己修了,实际工作区仍可能继续按旧习惯停下来。 +- 不需要发明新状态,只需要把 `running` 与 `completed` 的使用边界写清楚。 + +- This is a semantic-constraint gap rather than an engine bug, so the source-of-truth docs and consumer rules should be fixed first. +- Syncing consumer AGENTS matters; otherwise method-forge can be fixed while actual workspaces still stop under the old habit. +- No new status is needed; the important part is clarifying how `running` and `completed` should be used. diff --git a/docs/specs/2026-04-11-method-forge-autonomous-continuity/plan.md b/docs/specs/2026-04-11-method-forge-autonomous-continuity/plan.md new file mode 100644 index 0000000..e37319d --- /dev/null +++ b/docs/specs/2026-04-11-method-forge-autonomous-continuity/plan.md @@ -0,0 +1,13 @@ +# Plan / 计划 + +1. 找出当前 method-forge 规则里容易把微切片误当成 `completed` 的位置。 + Find the places in the current method-forge rules that make it easy to treat a micro-slice as `completed`. + +2. 把“顶层任务完成”和“微切片完成”的边界补清楚。 + Clarify the boundary between “top-level task complete” and “micro-slice complete.” + +3. 同步 skill、模板和消费方 AGENTS。 + Sync the rule into the skill, templates, and consumer AGENTS. + +4. 做文本级验证并刷新 memory。 + Run text-level verification and refresh memory. diff --git a/docs/specs/2026-04-11-method-forge-autonomous-continuity/runtime/run-state.md b/docs/specs/2026-04-11-method-forge-autonomous-continuity/runtime/run-state.md new file mode 100644 index 0000000..296be90 --- /dev/null +++ b/docs/specs/2026-04-11-method-forge-autonomous-continuity/runtime/run-state.md @@ -0,0 +1,43 @@ +# Run State / 运行状态 + +## Task / 任务 + +- task_id: `2026-04-11-method-forge-autonomous-continuity` +- title: `method-forge autonomous continuity semantics` +- workspace: `/Users/wz/project/codex-enhanced-system` +- change_package: `docs/specs/2026-04-11-method-forge-autonomous-continuity/` +- execution_mode: `heartbeat` +- engine: `method-forge-execute` + +## Status / 状态 + +| Field | Value | +| --- | --- | +| status | `completed` | +| current_step | `verify completed` | +| last_success_step | `verify` | +| next_action | `continue only if real autonomous runs still expose micro-slice stop residue` | +| stop_reason | `current implementation slice completed successfully` | + +## Loop Guard / 循环保护 + +| Field | Value | +| --- | --- | +| retry_count | `0` | +| same_error_repeat_count | `0` | +| no_progress_cycle_count | `0` | +| total_cycle_count | `1` | +| last_error_signature | | +| human_confirmation_needed | `no` | + +## Timestamps / 时间戳 + +- started_at: `2026-04-11T20:52:43+0800` +- last_progress_at: `2026-04-11T20:55:14+0800` +- last_resumed_at: `2026-04-11T20:55:14+0800` +- updated_at: `2026-04-11T20:55:14+0800` + +## Notes / 备注 + +- 本轮把“微切片完成不等于顶层任务 completed”同步成了方法层显式规则。 +- This slice turns “a finished micro-slice does not equal a completed top-level task” into an explicit method-layer rule. diff --git a/docs/specs/2026-04-11-method-forge-autonomous-continuity/spec.md b/docs/specs/2026-04-11-method-forge-autonomous-continuity/spec.md new file mode 100644 index 0000000..8297c80 --- /dev/null +++ b/docs/specs/2026-04-11-method-forge-autonomous-continuity/spec.md @@ -0,0 +1,29 @@ +# Spec / 规格 + +## Goals / 目标 + +- 让 autonomous run 以“用户当前顶层目标”为停止粒度,而不是以“单个微切片”作为默认停止粒度。 +- Make autonomous runs stop at the granularity of the user's current top-level goal instead of defaulting to the granularity of a single micro-slice. + +- 把“下一安全切片已知时保持 `running`”写成显式规则,减少执行歧义。 +- Make “stay `running` when the next safe slice is already known” an explicit rule to reduce execution ambiguity. + +## Non-Goals / 非目标 + +- 不重做 Codex 原生 heartbeat / automation 平台 +- 不改变 loop guard 上限 +- 不删除 `blocked` / `waiting-human` / `waiting-external` 这些停机状态 + +- Do not rebuild the native Codex heartbeat / automation platform +- Do not change the loop-guard budgets +- Do not remove the `blocked` / `waiting-human` / `waiting-external` stop states + +## Functional Rules / 功能规则 + +- `completed` 只能表示顶层任务或当前变更包目标已经结束 +- 若同一用户目标下下一安全切片已明确,不应因为某个微切片完成或通过 `verify` 就退出 +- heartbeat prompt、resume rules、skill、消费方 AGENTS 必须对齐这一语义 + +- `completed` may only mean the top-level task or current change-package goal is finished +- If the next safe slice inside the same user goal is already known, the run should not exit just because one micro-slice finished or passed `verify` +- The heartbeat prompt, resume rules, skill, and consumer AGENTS must align on this semantic diff --git a/docs/specs/2026-04-11-method-forge-autonomous-continuity/tasks.md b/docs/specs/2026-04-11-method-forge-autonomous-continuity/tasks.md new file mode 100644 index 0000000..5210327 --- /dev/null +++ b/docs/specs/2026-04-11-method-forge-autonomous-continuity/tasks.md @@ -0,0 +1,11 @@ +# Tasks / 任务拆解 + +- [x] 补清 `completed` 的顶层任务语义 +- [x] 补清“下一安全切片已知时保持 running”的规则 +- [x] 同步 heartbeat prompt、resume rules、skill、AGENTS 和模板 +- [x] 跑文本验证 + +- [x] Clarify the top-level-task semantics of `completed` +- [x] Clarify the rule to stay `running` when the next safe slice is already known +- [x] Sync the heartbeat prompt, resume rules, skill, AGENTS, and templates +- [x] Run text verification diff --git a/docs/specs/2026-04-11-method-forge-autonomous-continuity/verify.md b/docs/specs/2026-04-11-method-forge-autonomous-continuity/verify.md new file mode 100644 index 0000000..4d300c0 --- /dev/null +++ b/docs/specs/2026-04-11-method-forge-autonomous-continuity/verify.md @@ -0,0 +1,24 @@ +# Verify / 验证 + +## Commands / 验证命令 + +```bash +rg -n "micro-slice|微切片|下一安全切片|top-level task|keep it `running`" /Users/wz/project/codex-enhanced-system/method-forge /Users/wz/project/codex-enhanced-system/knowledge-base/AGENTS.md /Users/wz/project/codex-enhanced-system/memory-system/AGENTS.md +git diff --check -- /Users/wz/project/codex-enhanced-system/method-forge/AGENTS.md /Users/wz/project/codex-enhanced-system/method-forge/docs/method/autonomous-execution.md /Users/wz/project/codex-enhanced-system/method-forge/docs/method/activation-rules.md /Users/wz/project/codex-enhanced-system/method-forge/docs/method/resume-rules.md /Users/wz/project/codex-enhanced-system/method-forge/docs/templates/autonomous-heartbeat-prompt-template.md /Users/wz/project/codex-enhanced-system/method-forge/docs/templates/consumer-agents-rules-template.md /Users/wz/project/codex-enhanced-system/method-forge/skills/method-forge-autonomous-execution/SKILL.md /Users/wz/project/codex-enhanced-system/knowledge-base/AGENTS.md /Users/wz/project/codex-enhanced-system/memory-system/AGENTS.md /Users/wz/project/codex-enhanced-system/docs/specs/2026-04-11-method-forge-autonomous-continuity +python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root /Users/wz/project/codex-enhanced-system +``` + +## Results / 结果 + +- `rg`: 目标规则已同步到 method-forge 方法文档、skill、模板以及 `knowledge-base` / `memory-system` 的消费方 AGENTS +- `git diff --check`: 无输出,目标文件与变更包格式通过 +- `refresh_memory.py`: 成功刷新 workspace memory + +- `rg`: the target rule is now present in the method-forge method docs, skill, templates, and the consumer AGENTS for `knowledge-base` and `memory-system` +- `git diff --check`: no output, so the target files and change package passed the formatting check +- `refresh_memory.py`: workspace memory refresh completed successfully + +## Conclusion / 结论 + +- autonomous 的停止粒度现在被明确收敛到“顶层任务完成”,不应再把单个微切片完成误当成需要停机的 `completed`。 +- The stopping granularity of autonomous is now explicitly tightened to “top-level task complete,” so a finished micro-slice should no longer be mistaken for a stop-worthy `completed` state. diff --git a/knowledge-base/AGENTS.md b/knowledge-base/AGENTS.md index 59d42e9..f769e44 100644 --- a/knowledge-base/AGENTS.md +++ b/knowledge-base/AGENTS.md @@ -141,14 +141,14 @@ Now that `wiki/reports/` exists, record important manual lint results there befo ## Method Forge Autonomous Coding -- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”“进入实现阶段”等实现意图时,默认进入 `method-forge` 的实现流程,并优先在当前线程使用 `method-forge-execute` 推进,不隐式创建后台 automation。 -- 只有当用户明确要求自动续跑、后台继续、autonomous/heartbeat 模式,或当前任务本来就在恢复一个已存在的 autonomous run 时,才进入 `method-forge` autonomous mode。 -- autonomous mode 使用 Codex 原生 heartbeat / background automation 作为监听者,不重做第二套调度器,也不替代原生 automation 创建入口。 +- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”“进入实现阶段”等实现意图时,默认进入 `method-forge` autonomous mode,除非用户明确要求不要自动、先别写代码或只做方案。 +- autonomous mode 使用 Codex 原生 heartbeat / background automation 作为监听者,不重做第二套调度器。 - autonomous mode 的内层默认执行引擎是 `method-forge-execute`;在 autonomous 已经启动后,后续续跑周期不需要用户再次指定“按 method-forge 执行”。 - 若当前任务还没有标准变更包,先建立 `docs/specs//`、`package-index.md` 和 `runtime/run-state.md`。 - 若已有需求或设计草稿,先由 `method-forge-execute` 归一化为 `intake/spec/plan/tasks`,再进入实现。 - 实现完成后必须产出 `verify.md`;高风险改动可在 verify 前追加 `code-review.md`。 - 若 `verify.md` 认为存在稳定可复用结论,只能提出 `memory-candidate.md`,不得直接写长期 memory。 - autonomous mode 必须执行 loop guard:同一步最多重试 3 次,同一错误签名最多重复 2 次,连续无进展最多 2 轮,单任务最多 12 个周期。 +- 不要把单个微切片或一次 `verify` 通过误记为 `completed`;若同一用户目标下下一安全切片已明确,应保持 `running` 并自动推进。 / Do not mark the run `completed` just because a single micro-slice or one `verify` pass finished; if the next safe slice inside the same user goal is already known, keep it `running` and continue automatically. - 触发 `blocked`、`waiting-human`、`waiting-external` 或 `completed` 后必须停止自动推进,并在 `run-state.md` 写明 `stop_reason` 与 `next_action`。 - 若本文件已有更具体的 knowledge-base 写入、晋升或单一写入者规则,以更具体规则为准,但不得取消 `verify` 或 loop guard。 diff --git a/knowledge-base/KB_COMMANDS.md b/knowledge-base/KB_COMMANDS.md index 0d73726..5c4cb34 100644 --- a/knowledge-base/KB_COMMANDS.md +++ b/knowledge-base/KB_COMMANDS.md @@ -10,9 +10,11 @@ 常用别名: +- `search` / `ask` = `query` - `new` = `add` - `note` = `log` - `check` / `lint` = `maintain` +- `drift` = `drift-review` - `update-index` = `reindex` - `remove` = `delete` - `distill` / `promote-memory` = `distill-memory` @@ -23,10 +25,45 @@ 快速看当前知识库规模。 +Quickly inspect the current knowledge-base size. + ```bash ./kb status ``` +### `query` + +查询 canonical knowledge pages,并输出轻量级 provenance 信息。 + +Search canonical knowledge pages and return lightweight provenance details. + +```bash +./kb query "formal memory" +./kb query authority --type concept --limit 3 +./kb query "formal memory" --json +./kb query authority --include-reports --no-dedupe +``` + +说明: + +- 默认搜索 `source / entity / concept / synthesis / domain`,不包含 `report` +- `--type` 可以重复传入,用来限制结果类型 +- `--include-reports` 可以把 `report` 页一起纳入 +- 默认会对明显的语言镜像结果做保守去重;如果想看原始重复项,用 `--no-dedupe` +- 排序会优先更像答案页的类型:`concept / synthesis / entity / domain` 会优先于 `source`,`report` 即使纳入也会降权 +- 输出会附带 `source_refs`、`related`,必要时还会显示 `suppressed_duplicates` +- `--json` 适合脚本、自动流程或后续 automation 消费 + +Notes: + +- By default the command searches `source / entity / concept / synthesis / domain`, not `report` +- You can pass `--type` multiple times to narrow the result types +- `--include-reports` adds report pages into the search set +- Conservative locale-mirror dedupe is on by default; use `--no-dedupe` if you want the raw duplicate results +- Ranking now prefers answer-like pages: `concept / synthesis / entity / domain` rank ahead of `source`, while `report` pages are downweighted even when included +- The output includes `source_refs`, `related`, and `suppressed_duplicates` when dedupe collapses mirrors +- `--json` is intended for scripts, automated flows, or later automation consumers + ### `add` 新增一个 page scaffold。 @@ -40,7 +77,9 @@ - `source` 支持直接把外部文件拷进 `raw/` - 其他类型默认只生成 canonical 模板页 -- 新增后再跑一次 `reindex`、`log`、`maintain` +- 新增 canonical page 后现在会自动补进 `wiki/index.md` +- 如果想一步补上结构化日志,可加 `--write-log` +- 新增后建议继续补 `log`,然后跑 `maintain` ### `log` @@ -50,6 +89,14 @@ ./kb log ingest --summary "add source-my-doc" --note "Added wiki/sources/source-my-doc.md" --note "Imported raw/inbox/my-doc.md" ``` +说明: + +- 实际写入 `wiki/log.md` 时,现在也会同步刷新它自己的 `updated_at` + +Notes: + +- A real write to `wiki/log.md` now also refreshes its own `updated_at` + ### `maintain` 做轻量维护检查。 @@ -57,6 +104,7 @@ ```bash ./kb maintain ./kb maintain --write-report +./kb maintain --json ``` 当前会检查: @@ -64,9 +112,68 @@ - frontmatter 是否存在 - 必要字段是否齐全 - `source/entity/concept/synthesis` 是否缺 `source_refs` +- `source` 的 `source_refs` 是否仍然指向 `raw/` +- `entity/concept/synthesis` 的 `source_refs` 是否仍然落在 `wiki/sources/` 或 `raw/` +- `entity/concept/synthesis` 是否至少保留一个 canonical `wiki/sources/` 支撑页,而不只是 raw 路径 +- `source_refs` / `related` 是否出现重复或自指 - 内部链接是否断裂 - canonical 页是否漏进 `wiki/index.md` - `wiki/index.md` 是否有 stale entry +- `wiki/index.md`、`wiki/overview.md`、`wiki/hot.md`、`wiki/log.md` 这些导航/健康页是否还完整互链 +- `wiki/hot.md` 和 `wiki/overview.md` 是否还保留对关键 domain / synthesis / report 健康入口的覆盖 +- 文本输出现在会附带 `health_verdict` 和 `issue_groups`,更容易快速判断当前健康状态 +- `--json` 可输出结构化 `health_verdict`、`counts`、`issue_counts`、`issue_groups`、`issues` 和 `recommendations`,便于脚本或后续 automation 消费 +- `--write-report` 现在会把新生成的 maintenance report 自动补进 `wiki/index.md`,避免写完马上留下新的 index drift + +Current checks include: + +- whether frontmatter exists +- whether required fields are present +- whether `source / entity / concept / synthesis` pages are missing `source_refs` +- whether `source` page `source_refs` still resolve into `raw/` +- whether `entity / concept / synthesis` page `source_refs` still resolve into `wiki/sources/` or `raw/` +- whether `entity / concept / synthesis` pages still retain at least one canonical `wiki/sources/` support page instead of only raw pointers +- whether `source_refs` or `related` introduce duplicate or self-referential targets +- whether internal links are broken +- whether canonical pages are missing from `wiki/index.md` +- whether `wiki/index.md` contains stale entries +- whether `wiki/index.md`, `wiki/overview.md`, `wiki/hot.md`, and `wiki/log.md` still form a healthy navigation surface +- whether `wiki/hot.md` and `wiki/overview.md` still cover key domain / synthesis / report entry points +- text output now includes `health_verdict` and `issue_groups` so the current health state is easier to scan quickly +- `--json` emits structured `health_verdict`, `counts`, `issue_counts`, `issue_groups`, `issues`, and `recommendations` for scripts or later automation consumers +- `--write-report` now also auto-registers the new maintenance report in `wiki/index.md` so the write does not immediately create fresh index drift + +### `drift-review` + +做轻量 drift review,专门看“哪些页可能需要复核”,而不是把它们直接当成维护错误。 + +Run a lightweight drift review that focuses on “which pages may need review,” without treating them as hard maintenance errors. + +```bash +./kb drift-review +./kb drift-review --json +./kb drift-review --write-report --dry-run +``` + +当前会产出这些 drift signals: + +- `source-lag`: `entity / concept / synthesis / domain` 早于其 canonical source support 页 +- `guide-lag`: `index / overview / hot` 早于它们链接到的 canonical 页 +- `metadata-lag`: 例如 `log.md` 的 `updated_at` 早于最新 dated log entry +- `report-lag`: report 层整体早于最新 canonical graph +- 输出会带 `drift_verdict`、`signal_counts`、`signal_groups` 和 `recommendations` +- `--write-report` 可生成 `report-drift-review-.md`,并在实际写入时把报告内容收敛到写入后的最终状态,而不是保留写入前的旧 `report-lag` 快照 +- 实际写入时也会把新 report 自动补进 `wiki/index.md`,避免 report 归档本身又制造新的 index drift + +Current drift signals include: + +- `source-lag`: an `entity / concept / synthesis / domain` page is older than its canonical source support pages +- `guide-lag`: `index / overview / hot` is older than the canonical pages it points to +- `metadata-lag`: for example, `log.md` has an `updated_at` older than the latest dated log entry +- `report-lag`: the report layer as a whole is older than the latest canonical graph +- the output includes `drift_verdict`, `signal_counts`, `signal_groups`, and `recommendations` +- `--write-report` can generate a `report-drift-review-.md`, and an actual write now converges the archived report to the post-write final state instead of preserving a stale pre-write `report-lag` snapshot +- an actual write also auto-registers the new report in `wiki/index.md` so the archive step does not create fresh index drift on its own ### `reindex` @@ -83,6 +190,7 @@ - 默认只检查 - `--write` 会补上缺失条目 - `--prune` 会移除已经失效的 canonical 条目 +- 只要 `reindex` 实际改写了 `wiki/index.md`,现在也会同步刷新 `updated_at` - 自动补的描述是保守占位文案,之后可以手工润色 ### `delete` @@ -97,6 +205,8 @@ 说明: - 会顺手移除 `wiki/index.md` 里的对应条目 +- 若确实移除了 index 条目,也会同步刷新 `wiki/index.md` 的 `updated_at` +- 如果想一步补上结构化删除记录,可加 `--write-log` - `--with-raw` 会把 `source_refs` 指向的 raw 文件一起移到 trash - 删完建议补一条 `log`,再跑一次 `maintain` diff --git a/knowledge-base/kb b/knowledge-base/kb index ada085b..6a2dac8 100755 --- a/knowledge-base/kb +++ b/knowledge-base/kb @@ -61,6 +61,7 @@ PAGE_LAYOUT = { REQUIRED_FRONTMATTER_KEYS = {"title", "type", "status", "created_at", "updated_at", "source_refs", "related"} SOURCE_REF_REQUIRED_TYPES = {"source", "entity", "concept", "synthesis"} +ANSWER_PAGE_TYPES = {"entity", "concept", "synthesis"} CANONICAL_TYPES = {"source", "entity", "concept", "synthesis", "domain", "report"} CANONICAL_DIRS = {spec["dir"].name for spec in PAGE_LAYOUT.values()} STABLE_HEADINGS = [ @@ -73,6 +74,45 @@ STABLE_HEADINGS = [ "Current Judgment", "Recommendation", ] +QUERY_KIND_BONUS = { + "concept": 8, + "synthesis": 7, + "entity": 5, + "domain": 3, + "source": 0, + "report": -8, +} +QUERY_STATUS_BONUS = { + "active": 1, + "draft": -2, + "archived": -4, +} +LOCALE_VARIANT_MARKERS = [ + "zh-cn", + "zh-hans", + "zh-hant", + "zh", + "cn", + "chinese", + "中文", + "中文版", + "简体中文", + "繁体中文", + "简中", + "繁中", + "en-us", + "en-gb", + "en-uk", + "english", + "en", + "英文版", + "英文", + "bilingual", + "双语", +] +LOCALE_VARIANT_PATTERN = "|".join(re.escape(item) for item in LOCALE_VARIANT_MARKERS) +TRAILING_LOCALE_PAREN_RE = re.compile(rf"(?:\s*\((?:{LOCALE_VARIANT_PATTERN})\))+$", re.IGNORECASE) +TRAILING_LOCALE_TOKEN_RE = re.compile(rf"(?:[\s\-_]+(?:{LOCALE_VARIANT_PATTERN}))+$", re.IGNORECASE) @dataclass @@ -93,6 +133,29 @@ class Issue: message: str +@dataclass +class QueryResult: + path: Path + kind: str + title: str + match_score: int + score: int + matched_fields: list[str] + snippet: str | None + source_refs: list[str] + related: list[str] + suppressed_duplicates: list[str] + + +@dataclass +class DriftSignal: + level: str + category: str + page: str + message: str + related_paths: list[str] + + def now_local() -> datetime: return datetime.now().astimezone() @@ -105,6 +168,21 @@ def timestamp_str() -> str: return now_local().isoformat(timespec="seconds") +def parse_timestamp(value: object) -> datetime | None: + text = str(value or "").strip() + if not text: + return None + for candidate in (text, f"{text}T00:00:00+00:00"): + try: + parsed = datetime.fromisoformat(candidate) + if parsed.tzinfo is None: + return parsed.replace(tzinfo=now_local().tzinfo) + return parsed + except ValueError: + continue + return None + + def die(message: str, code: int = 1) -> None: print(f"error: {message}", file=sys.stderr) raise SystemExit(code) @@ -143,6 +221,13 @@ def dump_text(path: Path, content: str) -> None: path.write_text(content, encoding="utf-8") +def render_page(meta: dict, body: str) -> str: + body_text = body.lstrip("\n").rstrip() + if body_text: + return render_frontmatter(meta) + "\n\n" + body_text + "\n" + return render_frontmatter(meta) + "\n" + + def split_frontmatter(text: str) -> tuple[dict, str, bool]: if not text.startswith("---\n"): return {}, text, False @@ -233,6 +318,19 @@ def normalize_reference_for_page(page_path: Path, ref: str) -> str: return ref +def resolve_repo_reference(page_path: Path, ref: str) -> Path: + raw_target = ref.strip().split("#", 1)[0] + candidate = (page_path.parent / raw_target).resolve() + if candidate.exists(): + return candidate + if not candidate.suffix: + suffix_candidate = candidate.with_suffix(".md") + if suffix_candidate.exists(): + return suffix_candidate + return suffix_candidate + return candidate + + def repo_rel(path: Path) -> str: return path.relative_to(REPO_ROOT).as_posix() @@ -357,12 +455,22 @@ def cmd_add(args: argparse.Namespace) -> int: ensure_parent(raw_destination) shutil.copy2(import_from, raw_destination) dump_text(destination, content) + ensure_index_entry(destination) print(f"created: {repo_rel(destination)}") if import_from and raw_destination is not None: print(f"copied raw: {repo_rel(raw_destination)}") + if args.write_log: + log_notes = [f"Added {repo_rel(destination)}"] + if raw_destination is not None: + log_notes.append(f"Imported {repo_rel(raw_destination)}") + if args.dry_run: + print("would append log:") + print(write_log_entry("ingest", f"add {destination.stem}", log_notes, dry_run=True)) + else: + write_log_entry("ingest", f"add {destination.stem}", log_notes, dry_run=False) + print(f"log updated: {repo_rel(LOG_PATH)}") print("next:") - print(f"- {REPO_ROOT / 'kb'} reindex --write") print(f"- {REPO_ROOT / 'kb'} log ingest --summary \"add {destination.stem}\" --note \"Added {repo_rel(destination)}\"") print(f"- {REPO_ROOT / 'kb'} maintain") return 0 @@ -378,18 +486,28 @@ def append_log_entry(content: str, action: str, summary: str, notes: list[str]) return content + "\n" + suffix -def cmd_log(args: argparse.Namespace) -> int: +def write_log_entry(action: str, summary: str, notes: list[str], dry_run: bool) -> str: if not LOG_PATH.exists(): die(f"log file missing: {repo_rel(LOG_PATH)}") - notes = list(args.note or []) if not notes: die("log needs at least one `--note`") - current = load_text(LOG_PATH) - updated = append_log_entry(current, args.action, args.summary, notes) + log_page = load_page(LOG_PATH) + current_body = log_page.body + updated_body = append_log_entry(current_body, action, summary, notes) + if dry_run: + return updated_body[len(current_body):].strip() + updated_meta = dict(log_page.meta) + updated_meta["updated_at"] = today_str() + dump_text(LOG_PATH, render_page(updated_meta, updated_body)) + return repo_rel(LOG_PATH) + + +def cmd_log(args: argparse.Namespace) -> int: + notes = list(args.note or []) + result = write_log_entry(args.action, args.summary, notes, dry_run=args.dry_run) if args.dry_run: - print(updated[len(current):].strip()) + print(result) return 0 - dump_text(LOG_PATH, updated) print(f"updated: {repo_rel(LOG_PATH)}") return 0 @@ -418,11 +536,134 @@ def extract_frontmatter_links(page: Page) -> list[tuple[str, Path]]: target = str(raw_target).strip() if not target: continue - resolved = (page.path.parent / target).resolve() + resolved = resolve_repo_reference(page.path, target) links.append((target, resolved)) return links +def path_within(path: Path, root: Path) -> bool: + try: + path.relative_to(root) + return True + except ValueError: + return False + + +def duplicate_resolved_refs(page: Page, key: str) -> list[str]: + values = page.meta.get(key, []) + if not isinstance(values, list): + return [] + + duplicates: list[str] = [] + seen: set[str] = set() + for raw_value in values: + target = str(raw_value).strip() + if not target: + continue + resolved = resolve_repo_reference(page.path, target) + try: + normalized = repo_rel(resolved) + except ValueError: + normalized = str(resolved) + if normalized in seen and normalized not in duplicates: + duplicates.append(normalized) + continue + seen.add(normalized) + return duplicates + + +def resolved_frontmatter_refs(page: Page, key: str) -> list[tuple[str, Path]]: + values = page.meta.get(key, []) + if not isinstance(values, list): + return [] + refs: list[tuple[str, Path]] = [] + for raw_value in values: + target = str(raw_value).strip() + if not target: + continue + refs.append((target, resolve_repo_reference(page.path, target))) + return refs + + +def page_reference_targets(page: Page) -> set[Path]: + targets = {resolved for _, resolved in extract_markdown_links(page)} + targets.update(resolved for _, resolved in extract_frontmatter_links(page)) + return targets + + +def guide_page_specs() -> dict[str, dict[str, object]]: + return { + "index": { + "path": INDEX_PATH, + "expected_type": "guide", + "required_links": [WIKI_ROOT / "overview.md", WIKI_ROOT / "hot.md", LOG_PATH], + }, + "overview": { + "path": WIKI_ROOT / "overview.md", + "expected_type": "guide", + "required_links": [INDEX_PATH, WIKI_ROOT / "hot.md", LOG_PATH], + "required_kinds": ["domain", "report"], + }, + "hot": { + "path": WIKI_ROOT / "hot.md", + "expected_type": "guide", + "required_links": [INDEX_PATH, WIKI_ROOT / "overview.md", LOG_PATH], + "required_kinds": ["domain", "synthesis", "report"], + }, + "log": { + "path": LOG_PATH, + "expected_type": "report", + "required_links": [INDEX_PATH, WIKI_ROOT / "overview.md"], + }, + } + + +def check_guide_health() -> list[Issue]: + issues: list[Issue] = [] + for spec in guide_page_specs().values(): + path = spec["path"] + assert isinstance(path, Path) + label = repo_rel(path) + if not path.exists(): + issues.append(Issue("error", f"{label}: missing required knowledge-base guide page")) + continue + + page = load_page(path) + if not page.has_frontmatter: + issues.append(Issue("error", f"{label}: missing frontmatter")) + continue + + missing_keys = sorted(REQUIRED_FRONTMATTER_KEYS - set(page.meta)) + if missing_keys: + issues.append(Issue("error", f"{label}: missing frontmatter keys {', '.join(missing_keys)}")) + + expected_type = str(spec["expected_type"]) + actual_type = str(page.meta.get("type") or "") + if actual_type != expected_type: + rendered_actual = actual_type or "missing" + issues.append(Issue("warn", f"{label}: expected type `{expected_type}` but found `{rendered_actual}`")) + + for raw_target, resolved in extract_frontmatter_links(page): + if not resolved.exists(): + issues.append(Issue("error", f"{label}: broken frontmatter ref -> {raw_target}")) + for raw_target, resolved in extract_markdown_links(page): + if not resolved.exists(): + issues.append(Issue("error", f"{label}: broken link -> {raw_target}")) + + targets = page_reference_targets(page) + for required_path in spec.get("required_links", []): + assert isinstance(required_path, Path) + if required_path not in targets: + issues.append(Issue("warn", f"{label}: missing health link -> {repo_rel(required_path)}")) + for required_kind in spec.get("required_kinds", []): + kind = str(required_kind) + required_dir = PAGE_LAYOUT[kind]["dir"] + if not any(path_within(target, required_dir) for target in targets): + issues.append(Issue("warn", f"{label}: missing health link for `{kind}` pages")) + + return issues + + def index_link_targets() -> set[str]: page = load_page(INDEX_PATH) targets: set[str] = set() @@ -455,11 +696,43 @@ def check_maintenance() -> tuple[dict[str, int], list[Issue]]: issues.append(Issue("error", f"{page.repo_rel}: missing frontmatter keys {', '.join(missing_keys)}")) if kind in SOURCE_REF_REQUIRED_TYPES: refs = page.meta.get("source_refs", []) + resolved_refs = resolved_frontmatter_refs(page, "source_refs") if not isinstance(refs, list) or not refs: issues.append(Issue("error", f"{page.repo_rel}: empty source_refs")) - for raw_target, resolved in extract_frontmatter_links(page): - if not resolved.exists(): - issues.append(Issue("error", f"{page.repo_rel}: broken frontmatter ref -> {raw_target}")) + elif kind == "source": + for raw_target, resolved in resolved_refs: + if not resolved.exists(): + issues.append(Issue("error", f"{page.repo_rel}: broken frontmatter ref -> {raw_target}")) + elif not path_within(resolved, RAW_ROOT): + issues.append(Issue("error", f"{page.repo_rel}: source_refs should resolve inside raw/ -> {raw_target}")) + else: + source_dir = PAGE_LAYOUT["source"]["dir"] + for raw_target, resolved in resolved_refs: + if not resolved.exists(): + issues.append(Issue("error", f"{page.repo_rel}: broken frontmatter ref -> {raw_target}")) + elif not path_within(resolved, source_dir) and not path_within(resolved, RAW_ROOT): + issues.append(Issue("error", f"{page.repo_rel}: source_refs should resolve to wiki/sources/ or raw/ -> {raw_target}")) + if kind in ANSWER_PAGE_TYPES: + canonical_support = [resolved for _, resolved in resolved_refs if path_within(resolved, source_dir)] + if refs and not canonical_support: + issues.append(Issue("warn", f"{page.repo_rel}: source_refs should include at least one wiki/sources page")) + duplicate_refs = duplicate_resolved_refs(page, "source_refs") + for duplicate in duplicate_refs: + issues.append(Issue("warn", f"{page.repo_rel}: duplicate source_ref -> {duplicate}")) + for raw_target, resolved in resolved_refs: + if resolved == page.path: + issues.append(Issue("warn", f"{page.repo_rel}: source_refs should not point to itself -> {raw_target}")) + related_values = page.meta.get("related", []) + if isinstance(related_values, list): + resolved_related = resolved_frontmatter_refs(page, "related") + for raw_target, resolved in resolved_related: + if not resolved.exists(): + issues.append(Issue("error", f"{page.repo_rel}: broken frontmatter ref -> {raw_target}")) + elif resolved == page.path: + issues.append(Issue("warn", f"{page.repo_rel}: related should not point to itself -> {raw_target}")) + duplicate_related = duplicate_resolved_refs(page, "related") + for duplicate in duplicate_related: + issues.append(Issue("warn", f"{page.repo_rel}: duplicate related ref -> {duplicate}")) for raw_target, resolved in extract_markdown_links(page): if not resolved.exists(): issues.append(Issue("error", f"{page.repo_rel}: broken link -> {raw_target}")) @@ -472,11 +745,13 @@ def check_maintenance() -> tuple[dict[str, int], list[Issue]]: if canonical_page_for_path(resolved) and not resolved.exists(): issues.append(Issue("warn", f"wiki/index.md: stale index entry -> {raw_target}")) + issues.extend(check_guide_health()) return counts, issues def render_maintenance_report(counts: dict[str, int], issues: list[Issue]) -> str: date = today_str() + payload = build_maintenance_payload(counts, issues) meta = { "title": f"Maintenance Report {date}", "type": "report", @@ -495,6 +770,7 @@ def render_maintenance_report(counts: dict[str, int], issues: list[Issue]) -> st "", "- Report type: `command-generated maintenance snapshot`", f"- Checked on: `{date}`", + f"- Health verdict: `{payload['health_verdict']}`", "", "## Current Counts", "", @@ -505,48 +781,538 @@ def render_maintenance_report(counts: dict[str, int], issues: list[Issue]) -> st f"- Domains: `{counts['domain']}`", f"- Reports: `{counts['report']}`", "", - "## Findings", + "## Issue Summary", "", ] + issue_counts = payload["issue_counts"] + issue_groups = payload["issue_groups"] + recommendations = payload["recommendations"] + if issue_counts: + lines.extend(f"- {level}: `{count}`" for level, count in sorted(issue_counts.items())) + else: + lines.append("- issue counts: none") + if issue_groups: + lines.append("") + lines.append("## Findings By Category") + lines.append("") + for category, category_issues in issue_groups.items(): + lines.append(f"### {category}") + lines.append("") + lines.extend(f"- [{item['level']}] {item['message']}" for item in category_issues) + lines.append("") + lines.extend( + [ + "## Findings", + "", + ] + ) if issues: lines.extend(f"- [{issue.level}] {issue.message}" for issue in issues) else: lines.append("- No issues found in this maintenance pass") - lines.extend(["", "## Recommendation", "", "- Fix any `error` items first, then rerun `kb maintain`"]) + lines.extend(["", "## Recommendation", ""]) + if recommendations: + lines.extend(f"- {item}" for item in recommendations) + else: + lines.append("- Fix any `error` items first, then rerun `kb maintain`") return "\n".join(lines) + "\n" -def cmd_maintain(args: argparse.Namespace) -> int: - counts, issues = check_maintenance() - print("maintenance summary") - print(f"- sources: {counts['source']}") - print(f"- entities: {counts['entity']}") - print(f"- concepts: {counts['concept']}") - print(f"- syntheses: {counts['synthesis']}") - print(f"- domains: {counts['domain']}") - print(f"- reports: {counts['report']}") +def maintenance_issue_counts(issues: list[Issue]) -> dict[str, int]: + counts: dict[str, int] = {} + for issue in issues: + counts[issue.level] = counts.get(issue.level, 0) + 1 + return counts + + +def maintenance_health_verdict(issues: list[Issue]) -> str: + if any(issue.level == "error" for issue in issues): + return "failing" if issues: - print("- issues:") + return "needs-attention" + return "healthy" + + +def categorize_issue(issue: Issue) -> str: + message = issue.message + if "health link" in message or "guide page" in message: + return "guide-surface" + if "source_refs" in message or "source_ref" in message or "duplicate related ref" in message or "related should not point to itself" in message: + return "provenance" + if "frontmatter" in message: + return "frontmatter" + if "broken link" in message: + return "links" + if "wiki/index.md" in message or "missing from wiki/index.md" in message or "stale index entry" in message: + return "index" + return "general" + + +def group_maintenance_issues(issues: list[Issue]) -> dict[str, list[dict[str, str]]]: + grouped: dict[str, list[dict[str, str]]] = {} + for issue in issues: + category = categorize_issue(issue) + grouped.setdefault(category, []).append({"level": issue.level, "message": issue.message}) + return grouped + + +def maintenance_recommendations(issues: list[Issue]) -> list[str]: + categories = set(group_maintenance_issues(issues)) + recommendations: list[str] = [] + if any(issue.level == "error" for issue in issues): + recommendations.append("Fix any `error` items first, then rerun `kb maintain`.") + if "provenance" in categories: + recommendations.append("Repair weak or duplicated provenance so answer-like pages keep clear canonical support.") + if "guide-surface" in categories: + recommendations.append("Restore missing guide-surface links so `index`, `overview`, `hot`, and `log` stay navigable together.") + if "index" in categories: + recommendations.append("Reconcile `wiki/index.md` with the canonical graph, then rerun `kb reindex --write` if needed.") + if "links" in categories: + recommendations.append("Fix broken internal links before treating the graph as healthy.") + if "frontmatter" in categories: + recommendations.append("Restore missing or invalid frontmatter fields so maintenance and query tooling can trust the page metadata.") + if not recommendations: + recommendations.append("No action needed; rerun `kb maintain` after future graph changes.") + return recommendations + + +def build_maintenance_payload( + counts: dict[str, int], + issues: list[Issue], + report_path: Path | None = None, + report_action: str | None = None, +) -> dict[str, object]: + issue_groups = group_maintenance_issues(issues) + payload: dict[str, object] = { + "status": "error" if any(issue.level == "error" for issue in issues) else "ok", + "health_verdict": maintenance_health_verdict(issues), + "counts": counts, + "issue_counts": maintenance_issue_counts(issues), + "issue_groups": issue_groups, + "issues": [{"level": issue.level, "category": categorize_issue(issue), "message": issue.message} for issue in issues], + "recommendations": maintenance_recommendations(issues), + } + if report_path is not None: + payload["report_path"] = repo_rel(report_path) + if report_action is not None: + payload["report_action"] = report_action + return payload + + +def render_maintenance_summary( + counts: dict[str, int], + issues: list[Issue], + as_json: bool, + report_path: Path | None = None, + report_action: str | None = None, +) -> str: + if as_json: + return json.dumps( + build_maintenance_payload(counts, issues, report_path=report_path, report_action=report_action), + ensure_ascii=False, + indent=2, + ) + + lines = [ + "maintenance summary", + f"- health_verdict: {maintenance_health_verdict(issues)}", + f"- sources: {counts['source']}", + f"- entities: {counts['entity']}", + f"- concepts: {counts['concept']}", + f"- syntheses: {counts['synthesis']}", + f"- domains: {counts['domain']}", + f"- reports: {counts['report']}", + ] + issue_counts = maintenance_issue_counts(issues) + if issue_counts: + rendered_counts = ", ".join(f"{level}={count}" for level, count in sorted(issue_counts.items())) + lines.append(f"- issue_counts: {rendered_counts}") + issue_groups = group_maintenance_issues(issues) + if issue_groups: + rendered_groups = ", ".join(f"{category}={len(items)}" for category, items in sorted(issue_groups.items())) + lines.append(f"- issue_groups: {rendered_groups}") + if report_path is not None: + verb = "would write" if report_action == "would_write" else "wrote" + lines.append(f"- report: {verb} {repo_rel(report_path)}") + if issues: + lines.append("- issues:") for issue in issues: - print(f" - [{issue.level}] {issue.message}") + lines.append(f" - [{issue.level}][{categorize_issue(issue)}] {issue.message}") else: - print("- issues: none") + lines.append("- issues: none") + return "\n".join(lines) + + +def next_maintenance_report_path() -> Path: + report_path = WIKI_ROOT / "reports" / f"report-maintenance-{today_str()}.md" + suffix = 2 + while report_path.exists(): + report_path = WIKI_ROOT / "reports" / f"report-maintenance-{today_str()}-{suffix}.md" + suffix += 1 + return report_path + + +def next_drift_review_report_path() -> Path: + report_path = WIKI_ROOT / "reports" / f"report-drift-review-{today_str()}.md" + suffix = 2 + while report_path.exists(): + report_path = WIKI_ROOT / "reports" / f"report-drift-review-{today_str()}-{suffix}.md" + suffix += 1 + return report_path + + +def cmd_maintain(args: argparse.Namespace) -> int: + counts, issues = check_maintenance() + summary_counts = counts + summary_issues = issues + report_path: Path | None = None + report_action: str | None = None if args.write_report: - report_name = f"report-maintenance-{today_str()}.md" - report_path = WIKI_ROOT / "reports" / report_name - suffix = 2 - while report_path.exists(): - report_path = WIKI_ROOT / "reports" / f"report-maintenance-{today_str()}-{suffix}.md" - suffix += 1 + report_path = next_maintenance_report_path() content = render_maintenance_report(counts, issues) if args.dry_run: - print("") - print(f"would write: {repo_rel(report_path)}") + report_action = "would_write" + else: + dump_text(report_path, content) + ensure_index_entry(report_path) + report_action = "written" + summary_counts, summary_issues = check_maintenance() + print( + render_maintenance_summary( + summary_counts, + summary_issues, + as_json=args.json, + report_path=report_path, + report_action=report_action, + ) + ) + return 0 if not any(issue.level == "error" for issue in summary_issues) else 1 + + +def signal_sort_key(signal: DriftSignal) -> tuple[int, str, str]: + priority = {"warn": 0, "info": 1}.get(signal.level, 2) + return (priority, signal.category, signal.page) + + +def page_updated_at(page: Page) -> datetime | None: + return parse_timestamp(page.meta.get("updated_at")) + + +def canonical_source_support_pages(page: Page) -> list[Page]: + source_dir = PAGE_LAYOUT["source"]["dir"] + supports: list[Page] = [] + for _, resolved in resolved_frontmatter_refs(page, "source_refs"): + if resolved.exists() and path_within(resolved, source_dir): + supports.append(load_page(resolved)) + return supports + + +def latest_log_entry_date(log_page: Page) -> datetime | None: + candidates: list[datetime] = [] + for match in re.finditer(r"^## \[(\d{4}-\d{2}-\d{2})\]", log_page.body, re.MULTILINE): + parsed = parse_timestamp(match.group(1)) + if parsed is not None: + candidates.append(parsed) + if not candidates: + return None + return max(candidates) + + +def guide_target_pages(page: Page) -> list[Page]: + targets: list[Page] = [] + seen: set[Path] = set() + for _, resolved in extract_markdown_links(page): + if not resolved.exists() or not canonical_page_for_path(resolved) or resolved in seen: + continue + seen.add(resolved) + targets.append(load_page(resolved)) + return targets + + +def latest_report_update() -> datetime | None: + updates = [page_updated_at(load_page(path)) for path in sorted(PAGE_LAYOUT["report"]["dir"].glob("*.md"))] + known = [item for item in updates if item is not None] + if not known: + return None + return max(known) + + +def latest_canonical_update() -> datetime | None: + updates = [page_updated_at(load_page(path)) for path in canonical_pages()] + known = [item for item in updates if item is not None] + if not known: + return None + return max(known) + + +def drift_review_signals() -> list[DriftSignal]: + signals: list[DriftSignal] = [] + + for path in canonical_pages(): + kind = canonical_kind_for_path(path) + if kind not in {"entity", "concept", "synthesis", "domain"}: + continue + page = load_page(path) + page_updated = page_updated_at(page) + if page_updated is None: + continue + newer_supports: list[str] = [] + for support_page in canonical_source_support_pages(page): + support_updated = page_updated_at(support_page) + if support_updated is not None and support_updated > page_updated: + newer_supports.append(repo_rel(support_page.path)) + if newer_supports: + signals.append( + DriftSignal( + level="warn", + category="source-lag", + page=page.repo_rel, + message="page may be stale because supporting source pages were updated later", + related_paths=sorted(newer_supports), + ) + ) + + for guide_path in (INDEX_PATH, WIKI_ROOT / "overview.md", WIKI_ROOT / "hot.md"): + if not guide_path.exists(): + continue + page = load_page(guide_path) + guide_updated = page_updated_at(page) + if guide_updated is None: + continue + newer_targets: list[str] = [] + for target_page in guide_target_pages(page): + target_updated = page_updated_at(target_page) + if target_updated is not None and target_updated > guide_updated: + newer_targets.append(repo_rel(target_page.path)) + if newer_targets: + signals.append( + DriftSignal( + level="info", + category="guide-lag", + page=page.repo_rel, + message="guide page may need review because linked canonical pages were updated later", + related_paths=sorted(newer_targets), + ) + ) + + if LOG_PATH.exists(): + log_page = load_page(LOG_PATH) + log_updated = page_updated_at(log_page) + latest_entry = latest_log_entry_date(log_page) + if log_updated is not None and latest_entry is not None and latest_entry > log_updated: + signals.append( + DriftSignal( + level="info", + category="metadata-lag", + page=log_page.repo_rel, + message="frontmatter updated_at is older than the latest dated log entry", + related_paths=[], + ) + ) + + latest_report = latest_report_update() + latest_canonical = latest_canonical_update() + if latest_report is not None and latest_canonical is not None and latest_canonical > latest_report: + report_paths = [repo_rel(path) for path in sorted(PAGE_LAYOUT["report"]["dir"].glob("*.md"))] + signals.append( + DriftSignal( + level="info", + category="report-lag", + page="wiki/reports", + message="report layer may need refresh because canonical pages were updated later than the newest report", + related_paths=report_paths, + ) + ) + + return sorted(signals, key=signal_sort_key) + + +def drift_signal_counts(signals: list[DriftSignal]) -> dict[str, int]: + counts: dict[str, int] = {} + for signal in signals: + counts[signal.level] = counts.get(signal.level, 0) + 1 + return counts + + +def drift_signal_groups(signals: list[DriftSignal]) -> dict[str, list[dict[str, object]]]: + grouped: dict[str, list[dict[str, object]]] = {} + for signal in signals: + grouped.setdefault(signal.category, []).append( + { + "level": signal.level, + "page": signal.page, + "message": signal.message, + "related_paths": signal.related_paths, + } + ) + return grouped + + +def drift_review_verdict(signals: list[DriftSignal]) -> str: + if any(signal.level == "warn" for signal in signals): + return "review-needed" + if signals: + return "watch" + return "stable" + + +def drift_review_recommendations(signals: list[DriftSignal]) -> list[str]: + categories = set(signal.category for signal in signals) + recommendations: list[str] = [] + if "source-lag" in categories: + recommendations.append("Review answer-like pages whose supporting source pages changed later, then refresh their conclusions if needed.") + if "guide-lag" in categories: + recommendations.append("Review guide-surface pages so navigation still reflects the newest canonical entry points.") + if "metadata-lag" in categories: + recommendations.append("Refresh `updated_at` metadata when guide or log pages receive meaningful changes.") + if "report-lag" in categories: + recommendations.append("Consider writing a fresh report if the report layer is older than the current canonical graph.") + if not recommendations: + recommendations.append("No drift signals detected; rerun `kb drift-review` after future graph changes.") + return recommendations + + +def build_drift_review_payload( + signals: list[DriftSignal], + report_path: Path | None = None, + report_action: str | None = None, +) -> dict[str, object]: + payload: dict[str, object] = { + "status": "attention" if signals else "ok", + "drift_verdict": drift_review_verdict(signals), + "signal_counts": drift_signal_counts(signals), + "signal_groups": drift_signal_groups(signals), + "signals": [ + { + "level": signal.level, + "category": signal.category, + "page": signal.page, + "message": signal.message, + "related_paths": signal.related_paths, + } + for signal in signals + ], + "recommendations": drift_review_recommendations(signals), + } + if report_path is not None: + payload["report_path"] = repo_rel(report_path) + if report_action is not None: + payload["report_action"] = report_action + return payload + + +def render_drift_review_summary( + signals: list[DriftSignal], + as_json: bool, + report_path: Path | None = None, + report_action: str | None = None, +) -> str: + payload = build_drift_review_payload(signals, report_path=report_path, report_action=report_action) + if as_json: + return json.dumps(payload, ensure_ascii=False, indent=2) + + lines = [ + "drift review", + f"- drift_verdict: {payload['drift_verdict']}", + ] + signal_counts = payload["signal_counts"] + if signal_counts: + rendered_counts = ", ".join(f"{level}={count}" for level, count in sorted(signal_counts.items())) + lines.append(f"- signal_counts: {rendered_counts}") + else: + lines.append("- signal_counts: none") + signal_groups = payload["signal_groups"] + if signal_groups: + rendered_groups = ", ".join(f"{category}={len(items)}" for category, items in sorted(signal_groups.items())) + lines.append(f"- signal_groups: {rendered_groups}") + if report_path is not None: + verb = "would write" if report_action == "would_write" else "wrote" + lines.append(f"- report: {verb} {repo_rel(report_path)}") + if signals: + lines.append("- signals:") + for signal in signals: + lines.append(f" - [{signal.level}][{signal.category}] {signal.page}: {signal.message}") + if signal.related_paths: + lines.append(f" related: {', '.join(signal.related_paths)}") + else: + lines.append("- signals: none") + return "\n".join(lines) + + +def render_drift_review_report(signals: list[DriftSignal]) -> str: + date = today_str() + payload = build_drift_review_payload(signals) + meta = { + "title": f"Drift Review {date}", + "type": "report", + "status": "active", + "created_at": date, + "updated_at": date, + "source_refs": [], + "related": ["../index.md", "../overview.md", "../hot.md", "../log.md"], + } + lines = [ + render_frontmatter(meta), + "", + f"# Drift Review {date}", + "", + "## Scope", + "", + "- Report type: `command-generated drift review`", + f"- Checked on: `{date}`", + f"- Drift verdict: `{payload['drift_verdict']}`", + "", + "## Signal Summary", + "", + ] + signal_counts = payload["signal_counts"] + if signal_counts: + lines.extend(f"- {level}: `{count}`" for level, count in sorted(signal_counts.items())) + else: + lines.append("- signal counts: none") + signal_groups = payload["signal_groups"] + if signal_groups: + lines.extend(["", "## Signals By Category", ""]) + for category, items in signal_groups.items(): + lines.append(f"### {category}") + lines.append("") + for item in items: + lines.append(f"- [{item['level']}] {item['page']}: {item['message']}") + if item["related_paths"]: + lines.append(f" - related: {', '.join(item['related_paths'])}") + lines.append("") + lines.extend(["## Recommendations", ""]) + lines.extend(f"- {item}" for item in payload["recommendations"]) + return "\n".join(lines).rstrip() + "\n" + + +def projected_signals_after_drift_report_write(signals: list[DriftSignal]) -> list[DriftSignal]: + # Writing a fresh drift-review report resolves the report-lag it is + # explicitly documenting, so the archived report should reflect the + # post-write repository state instead of the stale pre-write snapshot. + return [signal for signal in signals if signal.category != "report-lag"] + + +def cmd_drift_review(args: argparse.Namespace) -> int: + signals = drift_review_signals() + summary_signals = signals + report_path: Path | None = None + report_action: str | None = None + + if args.write_report: + report_path = next_drift_review_report_path() + if args.dry_run: + report_action = "would_write" else: + content = render_drift_review_report(projected_signals_after_drift_report_write(signals)) dump_text(report_path, content) - print(f"wrote: {repo_rel(report_path)}") - return 0 if not any(issue.level == "error" for issue in issues) else 1 + ensure_index_entry(report_path) + report_action = "written" + summary_signals = drift_review_signals() + + print(render_drift_review_summary(summary_signals, as_json=args.json, report_path=report_path, report_action=report_action)) + return 0 def section_bullets_for_heading(content_lines: list[str]) -> set[str]: @@ -587,6 +1353,57 @@ def split_h2_sections(lines: list[str]) -> tuple[list[str], list[tuple[str, list return preamble, sections +def ensure_index_entry(path: Path) -> bool: + if not INDEX_PATH.exists(): + return False + + kind = canonical_kind_for_path(path) + if kind is None: + return False + + index_page = load_page(INDEX_PATH) + body_lines = index_page.body.lstrip("\n").splitlines() + preamble, sections = split_h2_sections(body_lines) + heading = PAGE_LAYOUT[kind]["index_heading"] + target_link = wiki_link(path) + changed = False + found_section = False + rebuilt_sections: list[tuple[str, list[str]]] = [] + + for section_heading, content_lines in sections: + if section_heading != heading: + rebuilt_sections.append((section_heading, content_lines)) + continue + + found_section = True + current_targets = section_bullets_for_heading(content_lines) + new_lines = list(content_lines) + if target_link not in current_targets: + if new_lines and new_lines[-1].strip(): + new_lines.append("") + new_lines.append(autogenerated_index_line(path)) + changed = True + rebuilt_sections.append((section_heading, new_lines)) + + if not found_section: + rebuilt_sections.append((heading, ["", autogenerated_index_line(path)])) + changed = True + + if not changed: + return False + + rebuilt: list[str] = [] + rebuilt.extend(preamble) + for section_heading, content_lines in rebuilt_sections: + rebuilt.append(section_heading) + rebuilt.extend(content_lines) + + updated_meta = dict(index_page.meta) + updated_meta["updated_at"] = today_str() + dump_text(INDEX_PATH, render_page(updated_meta, "\n".join(rebuilt))) + return True + + def desired_paths_by_heading() -> dict[str, list[Path]]: mapping: dict[str, list[Path]] = {} for kind, spec in PAGE_LAYOUT.items(): @@ -597,8 +1414,9 @@ def desired_paths_by_heading() -> dict[str, list[Path]]: def cmd_reindex(args: argparse.Namespace) -> int: if not INDEX_PATH.exists(): die("wiki/index.md is missing") - lines = load_text(INDEX_PATH).splitlines() - preamble, sections = split_h2_sections(lines) + index_page = load_page(INDEX_PATH) + body_lines = index_page.body.lstrip("\n").splitlines() + preamble, sections = split_h2_sections(body_lines) desired = desired_paths_by_heading() missing_total: list[str] = [] stale_total: list[str] = [] @@ -650,13 +1468,15 @@ def cmd_reindex(args: argparse.Namespace) -> int: for heading, content_lines in updated_sections: rebuilt.append(heading) rebuilt.extend(content_lines) - updated_text = "\n".join(rebuilt).rstrip() + "\n" + updated_body = "\n".join(rebuilt).rstrip() + "\n" if args.dry_run: print("") print("would update wiki/index.md") return 0 - dump_text(INDEX_PATH, updated_text) + updated_meta = dict(index_page.meta) + updated_meta["updated_at"] = today_str() + dump_text(INDEX_PATH, render_page(updated_meta, updated_body)) print(f"updated: {repo_rel(INDEX_PATH)}") return 0 @@ -674,11 +1494,14 @@ def remove_index_lines_for_path(path: Path, dry_run: bool) -> int: if not INDEX_PATH.exists(): return 0 link = wiki_link(path) - lines = load_text(INDEX_PATH).splitlines() - kept = [line for line in lines if link not in line] - removed = len(lines) - len(kept) + index_page = load_page(INDEX_PATH) + body_lines = index_page.body.lstrip("\n").splitlines() + kept = [line for line in body_lines if link not in line] + removed = len(body_lines) - len(kept) if removed and not dry_run: - dump_text(INDEX_PATH, "\n".join(kept).rstrip() + "\n") + updated_meta = dict(index_page.meta) + updated_meta["updated_at"] = today_str() + dump_text(INDEX_PATH, render_page(updated_meta, "\n".join(kept))) return removed @@ -716,6 +1539,15 @@ def cmd_delete(args: argparse.Namespace) -> int: print(f"{'would remove' if args.dry_run else 'removed'} {removed} index entr{'y' if removed == 1 else 'ies'}") else: print("index entries removed: none") + if args.write_log: + log_notes = [f"Deleted {repo_rel(path)}"] + log_notes.extend(f"Deleted raw {repo_rel(raw_path)}" for raw_path, _ in raw_moves) + if args.dry_run: + print("would append log:") + print(write_log_entry("maintenance", f"delete {path.stem}", log_notes, dry_run=True)) + else: + write_log_entry("maintenance", f"delete {path.stem}", log_notes, dry_run=False) + print(f"log updated: {repo_rel(LOG_PATH)}") print("next:") print(f"- {REPO_ROOT / 'kb'} maintain") print(f"- {REPO_ROOT / 'kb'} log maintenance --summary \"delete {path.stem}\" --note \"Deleted {repo_rel(path)}\"") @@ -762,6 +1594,365 @@ def extract_candidate_facts(page: Page, limit: int) -> list[str]: return deduped +def normalize_search_text(text: str) -> str: + return re.sub(r"\s+", " ", text).strip().lower() + + +def query_terms_from_parts(parts: list[str]) -> list[str]: + text = " ".join(parts) + return [term for term in (normalize_search_text(item) for item in re.split(r"\s+", text)) if term] + + +def page_heading_lines(page: Page) -> list[str]: + headings: list[str] = [] + for line in page.body.splitlines(): + if line.startswith("#"): + headings.append(line.lstrip("#").strip()) + return headings + + +def page_text_lines(page: Page) -> list[str]: + lines: list[str] = [] + for line in page.body.splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if stripped.startswith("- "): + stripped = stripped[2:].strip() + if stripped and "TODO" not in stripped: + lines.append(stripped.replace("`", "")) + return lines + + +def matches_any_term(text: str, terms: list[str]) -> bool: + normalized = normalize_search_text(text) + return any(term in normalized for term in terms) + + +def truncate_snippet(text: str, limit: int = 180) -> str: + compact = re.sub(r"\s+", " ", text).strip() + if len(compact) <= limit: + return compact + return compact[: limit - 3].rstrip() + "..." + + +def dedupe_strings(values: Iterable[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for value in values: + compact = str(value).strip() + if not compact or compact in seen: + continue + seen.add(compact) + deduped.append(compact) + return deduped + + +def resolve_meta_paths(page: Page, key: str) -> list[str]: + values = page.meta.get(key, []) + if not isinstance(values, list): + return [] + resolved_values: list[str] = [] + for raw_value in values: + target = str(raw_value).strip() + if not target: + continue + resolved = resolve_repo_reference(page.path, target) + try: + resolved_values.append(repo_rel(resolved)) + except ValueError: + resolved_values.append(target) + return resolved_values + + +def strip_locale_suffix(text: str) -> tuple[str, bool]: + normalized = normalize_search_text(text).replace("_", "-") + base = normalized + changed = False + while True: + updated = TRAILING_LOCALE_PAREN_RE.sub("", base) + updated = TRAILING_LOCALE_TOKEN_RE.sub("", updated) + updated = re.sub(r"\s+", " ", updated).strip(" -_") + if updated == base: + break + base = updated + changed = True + return base, changed + + +def page_topic_stem(path: Path) -> str: + stem = path.stem + for spec in PAGE_LAYOUT.values(): + prefix = str(spec["prefix"]) + if stem.startswith(prefix): + stem = stem[len(prefix) :] + break + return stem.replace("_", " ").replace("-", " ") + + +def query_dedupe_identity(result: QueryResult) -> dict[str, object]: + title_key, title_variant = strip_locale_suffix(result.title) + stem_key, stem_variant = strip_locale_suffix(page_topic_stem(result.path)) + return { + "title_key": title_key, + "title_variant": title_variant, + "stem_key": stem_key, + "stem_variant": stem_variant, + } + + +def query_quality_bonus(page: Page, source_refs: list[str], stable_facts: list[str]) -> int: + bonus = 0 + if source_refs: + bonus += 1 + if stable_facts: + bonus += 1 + return bonus + + +def query_locale_variant_penalty(page: Page) -> int: + title_variant = strip_locale_suffix(str(page.meta.get("title") or ""))[1] + stem_variant = strip_locale_suffix(page_topic_stem(page.path))[1] + return -2 if title_variant or stem_variant else 0 + + +def query_rank_score(page: Page, kind: str, match_score: int, source_refs: list[str], stable_facts: list[str]) -> int: + status = normalize_search_text(str(page.meta.get("status") or "active")) + kind_bonus = QUERY_KIND_BONUS.get(kind, 0) + status_bonus = QUERY_STATUS_BONUS.get(status, 0) + quality_bonus = query_quality_bonus(page, source_refs, stable_facts) + locale_variant_penalty = query_locale_variant_penalty(page) + return match_score + kind_bonus + status_bonus + quality_bonus + locale_variant_penalty + + +def select_query_snippet(page: Page, terms: list[str]) -> str | None: + candidates: list[str] = [] + candidates.extend(extract_candidate_facts(page, 8)) + candidates.extend(page_heading_lines(page)) + candidates.extend(page_text_lines(page)) + + seen: set[str] = set() + for candidate in candidates: + compact = re.sub(r"\s+", " ", candidate).strip() + if not compact or compact in seen: + continue + seen.add(compact) + if matches_any_term(compact, terms): + return truncate_snippet(compact) + + for candidate in candidates: + compact = re.sub(r"\s+", " ", candidate).strip() + if compact: + return truncate_snippet(compact) + return None + + +def query_result_for_page(page: Page, terms: list[str], phrase: str) -> QueryResult | None: + kind = canonical_kind_for_path(page.path) + if kind is None: + return None + + title = str(page.meta.get("title") or page.path.stem) + title_text = normalize_search_text(title) + path_text = normalize_search_text(page.repo_rel) + headings = page_heading_lines(page) + heading_texts = [normalize_search_text(item) for item in headings] + stable_facts = extract_candidate_facts(page, 8) + stable_texts = [normalize_search_text(item) for item in stable_facts] + body_text = normalize_search_text(page.body) + combined_text = " ".join([title_text, path_text, *heading_texts, *stable_texts, body_text]).strip() + + match_score = 0 + matched_fields: list[str] = [] + + for term in terms: + if term in title_text: + match_score += 8 + matched_fields.append("title") + if term in path_text: + match_score += 6 + matched_fields.append("path") + if any(term in item for item in heading_texts): + match_score += 5 + matched_fields.append("heading") + if any(term in item for item in stable_texts): + match_score += 6 + matched_fields.append("stable-claim") + body_hits = body_text.count(term) + if body_hits: + match_score += min(body_hits, 3) + matched_fields.append("body") + + if phrase: + if phrase in title_text: + match_score += 6 + matched_fields.append("title-phrase") + elif phrase in combined_text: + match_score += 4 + matched_fields.append("phrase") + + if terms and all(term in combined_text for term in terms): + match_score += 3 + matched_fields.append("all-terms") + + if match_score == 0: + return None + + source_refs = resolve_meta_paths(page, "source_refs") + related = resolve_meta_paths(page, "related") + score = query_rank_score(page, kind, match_score, source_refs, stable_facts) + deduped_fields = list(dict.fromkeys(matched_fields)) + return QueryResult( + path=page.path, + kind=kind, + title=title, + match_score=match_score, + score=score, + matched_fields=deduped_fields, + snippet=select_query_snippet(page, terms), + source_refs=source_refs, + related=related, + suppressed_duplicates=[], + ) + + +def query_sort_key(result: QueryResult) -> tuple[int, int, str, str]: + return (result.score, result.match_score, result.kind, result.path.as_posix()) + + +def dedupe_query_results(results: list[QueryResult]) -> list[QueryResult]: + if len(results) < 2: + return results + + identities = {result.path: query_dedupe_identity(result) for result in results} + stem_groups: dict[tuple[str, str], list[QueryResult]] = {} + title_groups: dict[tuple[str, str], list[QueryResult]] = {} + + for result in results: + identity = identities[result.path] + stem_key = str(identity["stem_key"]).strip() + title_key = str(identity["title_key"]).strip() + if stem_key: + stem_groups.setdefault((result.kind, stem_key), []).append(result) + if title_key: + title_groups.setdefault((result.kind, title_key), []).append(result) + + active_stem_groups = { + key + for key, members in stem_groups.items() + if len(members) > 1 and any(bool(identities[item.path]["stem_variant"]) for item in members) + } + active_title_groups = { + key + for key, members in title_groups.items() + if len(members) > 1 and any(bool(identities[item.path]["title_variant"]) for item in members) + } + + deduped: list[QueryResult] = [] + winners: dict[tuple[str, str, str], QueryResult] = {} + for result in results: + identity = identities[result.path] + group_key: tuple[str, str, str] | None = None + stem_key = (result.kind, str(identity["stem_key"]).strip()) + title_key = (result.kind, str(identity["title_key"]).strip()) + if stem_key in active_stem_groups: + group_key = ("stem", stem_key[0], stem_key[1]) + elif title_key in active_title_groups: + group_key = ("title", title_key[0], title_key[1]) + + if group_key is None: + deduped.append(result) + continue + + winner = winners.get(group_key) + if winner is None: + winners[group_key] = result + deduped.append(result) + continue + + winner.suppressed_duplicates = dedupe_strings([*winner.suppressed_duplicates, repo_rel(result.path)]) + winner.source_refs = dedupe_strings([*winner.source_refs, *result.source_refs]) + winner.related = dedupe_strings([*winner.related, *result.related]) + if winner.snippet is None and result.snippet is not None: + winner.snippet = result.snippet + + return deduped + + +def query_pages(terms: list[str], phrase: str, kinds: set[str], limit: int, dedupe: bool = True) -> list[QueryResult]: + results: list[QueryResult] = [] + for path in canonical_pages(): + kind = canonical_kind_for_path(path) + if kind is None or kind not in kinds: + continue + result = query_result_for_page(load_page(path), terms, phrase) + if result is not None: + results.append(result) + ordered = sorted(results, key=lambda item: (-item.score, -item.match_score, item.kind, item.path.as_posix())) + if dedupe: + ordered = dedupe_query_results(ordered) + return ordered[:limit] + + +def render_query_results(query: str, results: list[QueryResult], as_json: bool) -> str: + if as_json: + payload = [ + { + "path": result.path.relative_to(REPO_ROOT).as_posix(), + "type": result.kind, + "title": result.title, + "score": result.score, + "match_score": result.match_score, + "matched_fields": result.matched_fields, + "snippet": result.snippet, + "source_refs": result.source_refs, + "related": result.related, + "suppressed_duplicates": result.suppressed_duplicates, + } + for result in results + ] + return json.dumps({"query": query, "results": payload}, ensure_ascii=False, indent=2) + + lines = [f"query: {query}", f"results: {len(results)}"] + if not results: + lines.append("- no matches") + return "\n".join(lines) + + for result in results: + lines.append(f"- [{result.kind}] {repo_rel(result.path)} (score: {result.score})") + lines.append(f" title: {result.title}") + if result.match_score != result.score: + lines.append(f" match_score: {result.match_score}") + lines.append(f" matched: {', '.join(result.matched_fields)}") + if result.snippet: + lines.append(f" snippet: {result.snippet}") + if result.source_refs: + lines.append(f" source_refs: {', '.join(result.source_refs)}") + if result.related: + lines.append(f" related: {', '.join(result.related)}") + if result.suppressed_duplicates: + lines.append(f" suppressed_duplicates: {', '.join(result.suppressed_duplicates)}") + return "\n".join(lines) + + +def cmd_query(args: argparse.Namespace) -> int: + query = " ".join(args.terms).strip() + terms = query_terms_from_parts(args.terms) + if not terms: + die("query needs at least one search term") + + kinds = set(args.type or []) + if not kinds: + kinds = set(CANONICAL_TYPES) + if not args.include_reports: + kinds.discard("report") + + phrase = normalize_search_text(query) + results = query_pages(terms, phrase, kinds, args.limit, dedupe=not args.no_dedupe) + print(render_query_results(query, results, args.json)) + return 0 + + def workspace_memory_home() -> tuple[str, Path]: index = json.loads(MEMORY_INDEX_PATH.read_text(encoding="utf-8")) key = index["workspaces"].get(str(REPO_ROOT).lower(), {}).get("key") @@ -883,6 +2074,15 @@ def build_parser() -> argparse.ArgumentParser: status_parser = subparsers.add_parser("status", help="Show a quick workspace snapshot") status_parser.set_defaults(func=cmd_status) + query_parser = subparsers.add_parser("query", aliases=["search", "ask"], help="Search canonical knowledge pages with lightweight provenance output") + query_parser.add_argument("terms", nargs="+", help="Search terms") + query_parser.add_argument("--type", choices=PAGE_LAYOUT.keys(), action="append", help="Limit search to one or more page types") + query_parser.add_argument("--include-reports", action="store_true", help="Include report pages in the result set") + query_parser.add_argument("--no-dedupe", action="store_true", help="Keep raw duplicate mirror results instead of collapsing them") + query_parser.add_argument("--limit", type=int, default=5, help="Max number of results to show") + query_parser.add_argument("--json", action="store_true", help="Render machine-readable JSON output") + query_parser.set_defaults(func=cmd_query) + add_parser = subparsers.add_parser("add", aliases=["new"], help="Add a new knowledge-base page scaffold") add_parser.add_argument("kind", choices=PAGE_LAYOUT.keys()) add_parser.add_argument("--slug", required=True) @@ -892,6 +2092,7 @@ def build_parser() -> argparse.ArgumentParser: add_parser.add_argument("--raw-ref", help="Relative repo path to a raw source file") add_parser.add_argument("--import-from", help="External file path to copy into raw/") add_parser.add_argument("--raw-dest", help="Repo-relative destination under raw/ when using --import-from") + add_parser.add_argument("--write-log", action="store_true", help="Also append a default structured log entry after creation") add_parser.add_argument("--dry-run", action="store_true") add_parser.set_defaults(func=cmd_add) @@ -904,9 +2105,16 @@ def build_parser() -> argparse.ArgumentParser: maintain_parser = subparsers.add_parser("maintain", aliases=["lint", "check"], help="Run lightweight maintenance checks") maintain_parser.add_argument("--write-report", action="store_true") + maintain_parser.add_argument("--json", action="store_true", help="Render machine-readable maintenance output") maintain_parser.add_argument("--dry-run", action="store_true") maintain_parser.set_defaults(func=cmd_maintain) + drift_parser = subparsers.add_parser("drift-review", aliases=["drift"], help="Review likely graph drift and stale guide/report signals") + drift_parser.add_argument("--write-report", action="store_true") + drift_parser.add_argument("--json", action="store_true", help="Render machine-readable drift review output") + drift_parser.add_argument("--dry-run", action="store_true") + drift_parser.set_defaults(func=cmd_drift_review) + reindex_parser = subparsers.add_parser("reindex", aliases=["update-index"], help="Check or sync wiki/index.md against canonical pages") reindex_parser.add_argument("--write", action="store_true", help="Insert missing canonical entries into wiki/index.md") reindex_parser.add_argument("--prune", action="store_true", help="Remove stale canonical entries from wiki/index.md") @@ -916,6 +2124,7 @@ def build_parser() -> argparse.ArgumentParser: delete_parser = subparsers.add_parser("delete", aliases=["remove"], help="Move a page to trash and remove its index entry") delete_parser.add_argument("path", help="Repo-relative path to the file to delete") delete_parser.add_argument("--with-raw", action="store_true", help="Also trash raw files referenced by source_refs") + delete_parser.add_argument("--write-log", action="store_true", help="Also append a default structured log entry after deletion") delete_parser.add_argument("--dry-run", action="store_true") delete_parser.set_defaults(func=cmd_delete) diff --git a/knowledge-base/raw/repos/codex-memory-kit/.gitignore b/knowledge-base/raw/repos/codex-memory-kit/.gitignore new file mode 100644 index 0000000..e02e4d0 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +.tmp/ +.omx/ +node_modules/ +.wx_article.html diff --git a/knowledge-base/raw/repos/codex-memory-kit/launchd/com.codex.mult-agent.feishu-bridge.plist.example b/knowledge-base/raw/repos/codex-memory-kit/launchd/com.codex.mult-agent.feishu-bridge.plist.example new file mode 100644 index 0000000..bb43c56 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/launchd/com.codex.mult-agent.feishu-bridge.plist.example @@ -0,0 +1,31 @@ + + + + + Label + com.codex.mult-agent.feishu-bridge + ProgramArguments + + /Users/wz/project/mult-agent/bin/feishu-codex-bridge-local.sh + + WorkingDirectory + /Users/wz/project/mult-agent + EnvironmentVariables + + HOME + /Users/wz + PATH + /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + CODEX_FEISHU_BRIDGE_ENV + /Users/wz/.config/mult-agent/feishu-bridge.env + + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/wz/project/mult-agent/.omx/logs/feishu-codex-bridge.out.log + StandardErrorPath + /Users/wz/project/mult-agent/.omx/logs/feishu-codex-bridge.err.log + + diff --git a/knowledge-base/raw/repos/codex-memory-kit/package-lock.json b/knowledge-base/raw/repos/codex-memory-kit/package-lock.json new file mode 100644 index 0000000..57fcd6c --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "codex-memory-kit", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codex-memory-kit", + "version": "0.1.0" + } + } +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/package.json b/knowledge-base/raw/repos/codex-memory-kit/package.json new file mode 100644 index 0000000..aab7ef4 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/package.json @@ -0,0 +1,10 @@ +{ + "name": "codex-memory-kit", + "private": true, + "version": "0.1.0", + "description": "Codex-native memory governance layer for formal memory, verification evidence, and promotion control.", + "type": "module", + "scripts": { + "test": "node --test test/*.test.js" + } +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/patches/0001-strict-formal-memory-adapter.patch b/knowledge-base/raw/repos/codex-memory-kit/patches/0001-strict-formal-memory-adapter.patch new file mode 100644 index 0000000..16ac3c2 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/patches/0001-strict-formal-memory-adapter.patch @@ -0,0 +1,992 @@ +From f9c84a558a91d75450cf6813f6d5d2a0e71e9d5d Mon Sep 17 00:00:00 2001 +From: wz +Date: Sat, 4 Apr 2026 19:09:41 +0800 +Subject: [PATCH] feat: integrate strict formal memory into memory server and + overlay + +--- + src/hooks/__tests__/agents-overlay.test.ts | 113 +++++++ + src/hooks/agents-overlay.ts | 15 +- + .../__tests__/formal-memory.test.ts | 141 +++++++++ + src/integration/formal-memory.ts | 278 ++++++++++++++++++ + .../memory-server-strict-mode.test.ts | 188 ++++++++++++ + src/mcp/memory-server.ts | 86 +++++- + 6 files changed, 808 insertions(+), 13 deletions(-) + create mode 100644 src/integration/__tests__/formal-memory.test.ts + create mode 100644 src/integration/formal-memory.ts + create mode 100644 src/mcp/__tests__/memory-server-strict-mode.test.ts + +diff --git a/src/hooks/__tests__/agents-overlay.test.ts b/src/hooks/__tests__/agents-overlay.test.ts +index 721c65a..bc968a9 100644 +--- a/src/hooks/__tests__/agents-overlay.test.ts ++++ b/src/hooks/__tests__/agents-overlay.test.ts +@@ -42,6 +42,77 @@ function setMockCodexHome(codexHomePath: string): () => void { + }; + } + ++function setEnv(name: string, value?: string): () => void { ++ const previous = process.env[name]; ++ if (typeof value === "string") process.env[name] = value; ++ else delete process.env[name]; ++ return () => { ++ if (typeof previous === "string") process.env[name] = previous; ++ else delete process.env[name]; ++ }; ++} ++ ++async function createFormalMemoryFixture(workspaceRoot: string): Promise { ++ const memoryRoot = await mkdtemp(join(tmpdir(), "omx-overlay-memory-")); ++ const workspaceKey = "workspace-123"; ++ const memoryHome = join(memoryRoot, "workspaces", workspaceKey); ++ ++ await mkdir(join(memoryRoot, "workspaces"), { recursive: true }); ++ await writeFile( ++ join(memoryRoot, "workspaces", "index.json"), ++ JSON.stringify( ++ { ++ version: 1, ++ workspaces: { ++ [workspaceRoot.toLowerCase()]: { ++ key: workspaceKey, ++ path: workspaceRoot, ++ }, ++ }, ++ }, ++ null, ++ 2, ++ ), ++ ); ++ await mkdir(join(memoryHome, "instructions", "repo"), { recursive: true }); ++ await mkdir(join(memoryHome, "memories"), { recursive: true }); ++ await mkdir(join(memoryHome, "runtime"), { recursive: true }); ++ await writeFile( ++ join(memoryHome, "instructions", "repo", "GUIDE.md"), ++ "# Repo Guide\nUse formal repo guidance.\n", ++ ); ++ await writeFile( ++ join(memoryHome, "memories", "MEMORY.md"), ++ "# Workspace Memory\nFormal durable truth.\n", ++ ); ++ await writeFile( ++ join(memoryHome, "runtime", "active_context.md"), ++ "# Active Context\nCurrent task context from formal memory.\n", ++ ); ++ ++ return memoryRoot; ++} ++ ++async function createSharedGuideOnlyMemoryFixture(): Promise { ++ const memoryRoot = await mkdtemp(join(tmpdir(), "omx-overlay-shared-memory-")); ++ await mkdir(join(memoryRoot, "instructions", "company"), { recursive: true }); ++ await mkdir(join(memoryRoot, "instructions", "user"), { recursive: true }); ++ await mkdir(join(memoryRoot, "instructions", "local"), { recursive: true }); ++ await writeFile( ++ join(memoryRoot, "instructions", "company", "GUIDE.md"), ++ "# Company Guide\nShared company guidance.\n", ++ ); ++ await writeFile( ++ join(memoryRoot, "instructions", "user", "GUIDE.md"), ++ "# User Guide\nShared user guidance.\n", ++ ); ++ await writeFile( ++ join(memoryRoot, "instructions", "local", "GUIDE.md"), ++ "# Local Guide\nShared local guidance.\n", ++ ); ++ return memoryRoot; ++} ++ + describe("generateOverlay", () => { + let tempDir: string; + before(async () => { +@@ -165,6 +236,48 @@ describe("generateOverlay", () => { + assert.ok(!overlay.includes("Low priority thing")); + }); + ++ it("prefers formal memory summary over local project-memory.json in strict mode", async () => { ++ const memoryRoot = await createFormalMemoryFixture(tempDir); ++ const restoreStrict = setEnv("OMX_STRICT_MEMORY_MODE", "1"); ++ const restoreMemoryRoot = setEnv("OMX_EXTERNAL_MEMORY_ROOT", memoryRoot); ++ try { ++ await writeFile( ++ join(tempDir, ".omx", "project-memory.json"), ++ JSON.stringify({ ++ techStack: "Legacy local summary", ++ directives: [{ directive: "Local directive", priority: "high" }], ++ }), ++ ); ++ ++ const overlay = await generateOverlay(tempDir, "strict-project-memory"); ++ assert.match(overlay, /Current task context from formal memory/); ++ assert.match(overlay, /Formal durable truth/); ++ assert.match(overlay, /Use formal repo guidance/); ++ assert.doesNotMatch(overlay, /Legacy local summary/); ++ assert.doesNotMatch(overlay, /Local directive/); ++ } finally { ++ restoreStrict(); ++ restoreMemoryRoot(); ++ await rm(memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it("falls back to shared formal guides when strict mode has no registered workspace", async () => { ++ const memoryRoot = await createSharedGuideOnlyMemoryFixture(); ++ const restoreStrict = setEnv("OMX_STRICT_MEMORY_MODE", "1"); ++ const restoreMemoryRoot = setEnv("OMX_EXTERNAL_MEMORY_ROOT", memoryRoot); ++ try { ++ const overlay = await generateOverlay(tempDir, "strict-shared-guides"); ++ assert.match(overlay, /Shared company guidance/); ++ assert.match(overlay, /Shared user guidance/); ++ assert.match(overlay, /Shared local guidance/); ++ } finally { ++ restoreStrict(); ++ restoreMemoryRoot(); ++ await rm(memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ + it("enforces size cap (overlay <= 3500 chars)", async () => { + const longText = "A".repeat(5000); + await writeFile( +diff --git a/src/hooks/agents-overlay.ts b/src/hooks/agents-overlay.ts +index 002727c..655c220 100644 +--- a/src/hooks/agents-overlay.ts ++++ b/src/hooks/agents-overlay.ts +@@ -24,6 +24,11 @@ import { + omxProjectMemoryPath, + packageRoot, + } from "../utils/paths.js"; ++import { ++ buildFormalProjectMemorySummary, ++ readFormalMemoryContext, ++ resolveStrictMemoryConfig, ++} from "../integration/formal-memory.js"; + import { + isPlanningComplete, + readPlanningArtifacts, +@@ -250,6 +255,12 @@ async function readNotepadPriority(cwd: string): Promise { + } + + async function readProjectMemorySummary(cwd: string): Promise { ++ const strictConfig = resolveStrictMemoryConfig(); ++ if (strictConfig.strictMode) { ++ const context = await readFormalMemoryContext(cwd); ++ return buildFormalProjectMemorySummary(context); ++ } ++ + const memPath = omxProjectMemoryPath(cwd); + if (!existsSync(memPath)) return ""; + +@@ -277,8 +288,8 @@ function getCompactionInstructions(): string { + return [ + "Before context compaction, preserve critical state:", + "1. Write progress checkpoint via state_write MCP tool", +- "2. Save key decisions to notepad via notepad_write_working", +- "3. If context is >80% full, proactively checkpoint state", ++ "2. Save run-local decisions to notepad via notepad_write_working", ++ "3. Promote durable memory only through the external refresh pipeline", + ].join("\n"); + } + +diff --git a/src/integration/__tests__/formal-memory.test.ts b/src/integration/__tests__/formal-memory.test.ts +new file mode 100644 +index 0000000..e85d63f +--- /dev/null ++++ b/src/integration/__tests__/formal-memory.test.ts +@@ -0,0 +1,141 @@ ++import { describe, it } from 'node:test'; ++import assert from 'node:assert/strict'; ++import { mkdtemp, mkdir, writeFile, readFile, rm } from 'fs/promises'; ++import { existsSync } from 'fs'; ++import { join } from 'path'; ++import { tmpdir } from 'os'; ++ ++import { ++ appendMemoryIntakeEntry, ++ buildFormalProjectMemorySummary, ++ buildFormalProjectMemoryView, ++ readFormalMemoryContext, ++ resolveStrictMemoryConfig, ++} from '../formal-memory.js'; ++ ++async function makeFixture() { ++ const workspaceRoot = await mkdtemp(join(tmpdir(), 'omx-formal-workspace-')); ++ const memoryRoot = await mkdtemp(join(tmpdir(), 'omx-formal-memory-')); ++ const workspaceKey = 'workspace-123'; ++ const memoryHome = join(memoryRoot, 'workspaces', workspaceKey); ++ ++ await mkdir(join(memoryRoot, 'workspaces'), { recursive: true }); ++ await writeFile( ++ join(memoryRoot, 'workspaces', 'index.json'), ++ JSON.stringify( ++ { ++ version: 1, ++ workspaces: { ++ [workspaceRoot.toLowerCase()]: { ++ key: workspaceKey, ++ path: workspaceRoot, ++ }, ++ }, ++ }, ++ null, ++ 2, ++ ), ++ ); ++ ++ await mkdir(join(memoryRoot, 'instructions', 'company'), { recursive: true }); ++ await mkdir(join(memoryRoot, 'instructions', 'user'), { recursive: true }); ++ await mkdir(join(memoryRoot, 'instructions', 'local'), { recursive: true }); ++ await mkdir(join(memoryHome, 'instructions', 'repo'), { recursive: true }); ++ await mkdir(join(memoryHome, 'memories'), { recursive: true }); ++ await mkdir(join(memoryHome, 'runtime'), { recursive: true }); ++ ++ await writeFile(join(memoryRoot, 'instructions', 'company', 'GUIDE.md'), '# Company\nCompany guide\n'); ++ await writeFile(join(memoryRoot, 'instructions', 'user', 'GUIDE.md'), '# User\nUser guide\n'); ++ await writeFile(join(memoryRoot, 'instructions', 'local', 'GUIDE.md'), '# Local\nLocal guide\n'); ++ await writeFile(join(memoryHome, 'instructions', 'repo', 'GUIDE.md'), '# Repo\nUse ESM modules.\n'); ++ await writeFile(join(memoryHome, 'memories', 'MEMORY.md'), '# Memory\nWorkspace durable truth.\n'); ++ await writeFile(join(memoryHome, 'runtime', 'active_context.md'), '# Active\nCurrent task context.\n'); ++ ++ return { ++ workspaceRoot, ++ memoryRoot, ++ }; ++} ++ ++describe('integration/formal-memory', () => { ++ it('resolves strict config from env', () => { ++ const config = resolveStrictMemoryConfig({ ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: '/tmp/custom-memory-root', ++ }); ++ ++ assert.equal(config.strictMode, true); ++ assert.equal(config.memoryRoot, '/tmp/custom-memory-root'); ++ }); ++ ++ it('reads formal memory context and summary for a registered workspace', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const context = await readFormalMemoryContext(fixture.workspaceRoot, { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }); ++ const summary = buildFormalProjectMemorySummary(context); ++ const view = buildFormalProjectMemoryView(context); ++ ++ assert.equal(context.workspace.registered, true); ++ assert.match(summary, /Current task context/); ++ assert.match(summary, /Workspace durable truth/); ++ assert.match(summary, /Use ESM modules/); ++ assert.equal(view.source, 'formal-memory'); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('falls back to shared guides when the workspace is not registered', async () => { ++ const workspaceRoot = await mkdtemp(join(tmpdir(), 'omx-formal-unregistered-')); ++ const memoryRoot = await mkdtemp(join(tmpdir(), 'omx-formal-shared-')); ++ ++ try { ++ await mkdir(join(memoryRoot, 'instructions', 'company'), { recursive: true }); ++ await mkdir(join(memoryRoot, 'instructions', 'user'), { recursive: true }); ++ await mkdir(join(memoryRoot, 'instructions', 'local'), { recursive: true }); ++ await writeFile(join(memoryRoot, 'instructions', 'company', 'GUIDE.md'), '# Company\nShared company guidance.\n'); ++ await writeFile(join(memoryRoot, 'instructions', 'user', 'GUIDE.md'), '# User\nShared user guidance.\n'); ++ await writeFile(join(memoryRoot, 'instructions', 'local', 'GUIDE.md'), '# Local\nShared local guidance.\n'); ++ ++ const context = await readFormalMemoryContext(workspaceRoot, { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: memoryRoot, ++ }); ++ const summary = buildFormalProjectMemorySummary(context); ++ ++ assert.equal(context.workspace.registered, false); ++ assert.match(summary, /Shared company guidance/); ++ assert.match(summary, /Shared user guidance/); ++ assert.match(summary, /Shared local guidance/); ++ } finally { ++ await rm(workspaceRoot, { recursive: true, force: true }); ++ await rm(memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('downgrades durable candidates into a run-local intake queue', async () => { ++ const workspaceRoot = await mkdtemp(join(tmpdir(), 'omx-formal-intake-')); ++ ++ try { ++ const result = await appendMemoryIntakeEntry({ ++ cwd: workspaceRoot, ++ kind: 'note', ++ content: 'Queue this observation.', ++ metadata: { category: 'architecture' }, ++ source: 'test', ++ }); ++ ++ assert.equal(existsSync(result.path), true); ++ const raw = await readFile(result.path, 'utf-8'); ++ assert.match(raw, /Queue this observation/); ++ assert.match(raw, /"kind":"note"/); ++ } finally { ++ await rm(workspaceRoot, { recursive: true, force: true }); ++ } ++ }); ++}); +diff --git a/src/integration/formal-memory.ts b/src/integration/formal-memory.ts +new file mode 100644 +index 0000000..3365262 +--- /dev/null ++++ b/src/integration/formal-memory.ts +@@ -0,0 +1,278 @@ ++import { createHash } from 'crypto'; ++import { existsSync } from 'fs'; ++import { appendFile, mkdir, readFile } from 'fs/promises'; ++import { join, resolve } from 'path'; ++ ++import { codexHome } from '../utils/paths.js'; ++ ++const WORKSPACE_INDEX_RELATIVE_PATH = join('workspaces', 'index.json'); ++const SHARED_GUIDE_RELATIVE_PATHS = [ ++ ['company', join('instructions', 'company', 'GUIDE.md')], ++ ['user', join('instructions', 'user', 'GUIDE.md')], ++ ['local', join('instructions', 'local', 'GUIDE.md')], ++] as const; ++ ++export interface StrictMemoryConfig { ++ strictMode: boolean; ++ memoryRoot: string; ++} ++ ++export interface FormalMemoryContext { ++ source: 'formal-memory'; ++ strictMode: boolean; ++ memoryRoot: string; ++ workspace: { ++ registered: boolean; ++ key?: string; ++ root?: string; ++ memoryHome?: string; ++ }; ++ repoGuide: string; ++ workspaceMemory: string; ++ activeContext: string; ++ sharedGuides: Record; ++} ++ ++function defaultExternalMemoryRoot(): string { ++ return join(codexHome(), 'memory'); ++} ++ ++function normalizeLookupPath(value: string): string { ++ return resolve(value).replace(/\\/g, '/').toLowerCase(); ++} ++ ++async function readTextIfExists(filePath: string): Promise { ++ try { ++ return await readFile(filePath, 'utf-8'); ++ } catch (error) { ++ if ((error as NodeJS.ErrnoException).code === 'ENOENT') { ++ return ''; ++ } ++ throw error; ++ } ++} ++ ++function summarizeSnippet(value: string, maxChars = 240): string { ++ const normalized = value ++ .replace(/^#+\s+/gm, '') ++ .replace(/\s+/g, ' ') ++ .trim(); ++ ++ if (!normalized) return ''; ++ if (normalized.length <= maxChars) return normalized; ++ return `${normalized.slice(0, maxChars - 3)}...`; ++} ++ ++export function parseBooleanFlag(value: unknown): boolean | undefined { ++ if (typeof value !== 'string') return undefined; ++ const normalized = value.trim().toLowerCase(); ++ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; ++ if (['0', 'false', 'no', 'off'].includes(normalized)) return false; ++ return undefined; ++} ++ ++export function resolveStrictMemoryConfig( ++ env: Record = process.env, ++): StrictMemoryConfig { ++ return { ++ strictMode: parseBooleanFlag(env.OMX_STRICT_MEMORY_MODE) ?? false, ++ memoryRoot: resolve(env.OMX_EXTERNAL_MEMORY_ROOT || defaultExternalMemoryRoot()), ++ }; ++} ++ ++export async function loadWorkspaceIndex(memoryRoot: string): Promise | null> { ++ const indexPath = join(memoryRoot, WORKSPACE_INDEX_RELATIVE_PATH); ++ if (!existsSync(indexPath)) return null; ++ return JSON.parse(await readFile(indexPath, 'utf-8')) as Record; ++} ++ ++export async function resolveFormalWorkspaceNode( ++ cwd: string, ++ memoryRoot: string, ++): Promise<{ key: string; root: string; memoryHome: string } | null> { ++ const index = await loadWorkspaceIndex(memoryRoot); ++ const workspaces = index?.workspaces as Record | undefined; ++ if (!workspaces || typeof workspaces !== 'object') return null; ++ ++ const lookupCwd = normalizeLookupPath(cwd); ++ let bestMatch: { key: string; root: string; memoryHome: string; lookupPath: string } | null = null; ++ ++ for (const [storedPath, entry] of Object.entries(workspaces)) { ++ if (!entry || typeof entry !== 'object' || typeof entry.key !== 'string') continue; ++ const registeredPath = typeof entry.path === 'string' ? entry.path : storedPath; ++ const candidatePath = normalizeLookupPath(registeredPath); ++ if (lookupCwd !== candidatePath && !lookupCwd.startsWith(`${candidatePath}/`)) { ++ continue; ++ } ++ ++ if (!bestMatch || candidatePath.length > bestMatch.lookupPath.length) { ++ bestMatch = { ++ key: entry.key, ++ root: resolve(registeredPath), ++ memoryHome: join(memoryRoot, 'workspaces', entry.key), ++ lookupPath: candidatePath, ++ }; ++ } ++ } ++ ++ if (!bestMatch) return null; ++ return { ++ key: bestMatch.key, ++ root: bestMatch.root, ++ memoryHome: bestMatch.memoryHome, ++ }; ++} ++ ++export async function readFormalMemoryContext( ++ cwd: string, ++ env: Record = process.env, ++): Promise { ++ const config = resolveStrictMemoryConfig(env); ++ const workspace = await resolveFormalWorkspaceNode(cwd, config.memoryRoot); ++ const sharedGuides = Object.fromEntries( ++ await Promise.all( ++ SHARED_GUIDE_RELATIVE_PATHS.map(async ([key, relativePath]) => [ ++ key, ++ await readTextIfExists(join(config.memoryRoot, relativePath)), ++ ]), ++ ), ++ ); ++ ++ if (!workspace) { ++ return { ++ source: 'formal-memory', ++ strictMode: config.strictMode, ++ memoryRoot: config.memoryRoot, ++ workspace: { ++ registered: false, ++ }, ++ repoGuide: '', ++ workspaceMemory: '', ++ activeContext: '', ++ sharedGuides, ++ }; ++ } ++ ++ const repoGuidePath = join(workspace.memoryHome, 'instructions', 'repo', 'GUIDE.md'); ++ const workspaceMemoryPath = join(workspace.memoryHome, 'memories', 'MEMORY.md'); ++ const activeContextPath = join(workspace.memoryHome, 'runtime', 'active_context.md'); ++ ++ const [repoGuide, workspaceMemory, activeContext] = await Promise.all([ ++ readTextIfExists(repoGuidePath), ++ readTextIfExists(workspaceMemoryPath), ++ readTextIfExists(activeContextPath), ++ ]); ++ ++ return { ++ source: 'formal-memory', ++ strictMode: config.strictMode, ++ memoryRoot: config.memoryRoot, ++ workspace: { ++ registered: true, ++ key: workspace.key, ++ root: workspace.root, ++ memoryHome: workspace.memoryHome, ++ }, ++ repoGuide, ++ workspaceMemory, ++ activeContext, ++ sharedGuides, ++ }; ++} ++ ++export function buildFormalProjectMemorySummary(context: FormalMemoryContext): string { ++ const parts: string[] = []; ++ ++ if (context.activeContext) { ++ parts.push(`- Active Context: ${summarizeSnippet(context.activeContext)}`); ++ } ++ if (context.workspaceMemory) { ++ parts.push(`- Workspace Memory: ${summarizeSnippet(context.workspaceMemory)}`); ++ } ++ if (context.repoGuide) { ++ parts.push(`- Repo Guide: ${summarizeSnippet(context.repoGuide)}`); ++ } ++ ++ if (parts.length === 0) { ++ for (const [key, value] of Object.entries(context.sharedGuides)) { ++ if (!value) continue; ++ parts.push(`- Shared Guide (${key}): ${summarizeSnippet(value)}`); ++ } ++ } ++ ++ return parts.join('\n'); ++} ++ ++export function buildFormalProjectMemoryView( ++ context: FormalMemoryContext, ++ section?: string, ++): Record { ++ const view = { ++ source: 'formal-memory', ++ strictMode: context.strictMode, ++ workspace: context.workspace, ++ summary: buildFormalProjectMemorySummary(context), ++ sections: { ++ activeContext: context.activeContext, ++ workspaceMemory: context.workspaceMemory, ++ repoGuide: context.repoGuide, ++ sharedGuides: context.sharedGuides, ++ }, ++ }; ++ ++ if (section && section !== 'all') { ++ return { ++ ...view, ++ requestedSection: section, ++ message: ++ 'Strict integration mode exposes a formal summary view instead of legacy .omx/project-memory.json sections.', ++ }; ++ } ++ ++ return view; ++} ++ ++function buildIntakeEntryId(payload: Record): string { ++ const hash = createHash('sha1') ++ .update(JSON.stringify(payload)) ++ .digest('hex') ++ .slice(0, 12); ++ return `intake-${hash}`; ++} ++ ++export async function appendMemoryIntakeEntry({ ++ cwd, ++ kind, ++ content, ++ metadata = {}, ++ source, ++}: { ++ cwd: string; ++ kind: string; ++ content: string; ++ metadata?: Record; ++ source: string; ++}): Promise<{ path: string; entry: Record }> { ++ const createdAt = new Date().toISOString(); ++ const entry = { ++ id: buildIntakeEntryId({ ++ kind, ++ content, ++ metadata, ++ source, ++ createdAt, ++ }), ++ kind, ++ content, ++ metadata, ++ source, ++ created_at: createdAt, ++ }; ++ const intakePath = join(cwd, '.omx', 'memory-intake.jsonl'); ++ await mkdir(join(cwd, '.omx'), { recursive: true }); ++ await appendFile(intakePath, `${JSON.stringify(entry)}\n`, 'utf-8'); ++ return { ++ path: intakePath, ++ entry, ++ }; ++} +diff --git a/src/mcp/__tests__/memory-server-strict-mode.test.ts b/src/mcp/__tests__/memory-server-strict-mode.test.ts +new file mode 100644 +index 0000000..6740654 +--- /dev/null ++++ b/src/mcp/__tests__/memory-server-strict-mode.test.ts +@@ -0,0 +1,188 @@ ++import { describe, it } from 'node:test'; ++import assert from 'node:assert/strict'; ++import { mkdtemp, mkdir, readFile, rm, writeFile } from 'fs/promises'; ++import { existsSync } from 'fs'; ++import { join } from 'path'; ++import { tmpdir } from 'os'; ++ ++async function makeFixture() { ++ const workspaceRoot = await mkdtemp(join(tmpdir(), 'omx-memory-server-workspace-')); ++ const memoryRoot = await mkdtemp(join(tmpdir(), 'omx-memory-server-memory-')); ++ const workspaceKey = 'workspace-123'; ++ const memoryHome = join(memoryRoot, 'workspaces', workspaceKey); ++ ++ await mkdir(join(memoryRoot, 'workspaces'), { recursive: true }); ++ await writeFile( ++ join(memoryRoot, 'workspaces', 'index.json'), ++ JSON.stringify( ++ { ++ version: 1, ++ workspaces: { ++ [workspaceRoot.toLowerCase()]: { ++ key: workspaceKey, ++ path: workspaceRoot, ++ }, ++ }, ++ }, ++ null, ++ 2, ++ ), ++ ); ++ ++ await mkdir(join(memoryHome, 'instructions', 'repo'), { recursive: true }); ++ await mkdir(join(memoryHome, 'memories'), { recursive: true }); ++ await mkdir(join(memoryHome, 'runtime'), { recursive: true }); ++ await writeFile(join(memoryHome, 'instructions', 'repo', 'GUIDE.md'), '# Repo\nUse pnpm.\n'); ++ await writeFile(join(memoryHome, 'memories', 'MEMORY.md'), '# Memory\nWorkspace durable truth.\n'); ++ await writeFile(join(memoryHome, 'runtime', 'active_context.md'), '# Active\nCurrent task context.\n'); ++ await mkdir(join(workspaceRoot, '.omx'), { recursive: true }); ++ await writeFile( ++ join(workspaceRoot, '.omx', 'project-memory.json'), ++ JSON.stringify({ techStack: 'Legacy local memory' }, null, 2), ++ ); ++ ++ return { ++ workspaceRoot, ++ memoryRoot, ++ }; ++} ++ ++async function loadMemoryServerModule() { ++ const previous = process.env.OMX_MEMORY_SERVER_DISABLE_AUTO_START; ++ process.env.OMX_MEMORY_SERVER_DISABLE_AUTO_START = '1'; ++ try { ++ return await import(`../memory-server.js?strict-memory-test=${Date.now()}-${Math.random()}`); ++ } finally { ++ if (typeof previous === 'string') process.env.OMX_MEMORY_SERVER_DISABLE_AUTO_START = previous; ++ else delete process.env.OMX_MEMORY_SERVER_DISABLE_AUTO_START; ++ } ++} ++ ++function parseToolPayload(result: { content: Array<{ text: string }>; isError?: boolean }) { ++ return { ++ ...result, ++ data: JSON.parse(result.content[0].text), ++ }; ++} ++ ++describe('mcp/memory-server strict mode behavior', () => { ++ it('reads formal memory instead of local project-memory.json in strict mode', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const mod = await loadMemoryServerModule(); ++ const result = parseToolPayload( ++ await mod.handleMemoryToolCall( ++ 'project_memory_read', ++ { ++ workingDirectory: fixture.workspaceRoot, ++ }, ++ { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ), ++ ); ++ ++ assert.equal(result.isError, undefined); ++ assert.equal(result.data.source, 'formal-memory'); ++ assert.match(result.data.summary, /Current task context/); ++ assert.doesNotMatch(JSON.stringify(result.data), /Legacy local memory/); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('denies direct project_memory_write in strict mode', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const mod = await loadMemoryServerModule(); ++ const result = parseToolPayload( ++ await mod.handleMemoryToolCall( ++ 'project_memory_write', ++ { ++ workingDirectory: fixture.workspaceRoot, ++ memory: { should: 'not persist' }, ++ }, ++ { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ), ++ ); ++ ++ assert.equal(result.isError, true); ++ assert.equal(result.data.decision, 'deny'); ++ ++ const localRaw = await readFile(join(fixture.workspaceRoot, '.omx', 'project-memory.json'), 'utf-8'); ++ assert.match(localRaw, /Legacy local memory/); ++ assert.doesNotMatch(localRaw, /should/); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('downgrades project_memory_add_note into memory-intake.jsonl in strict mode', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const mod = await loadMemoryServerModule(); ++ const result = parseToolPayload( ++ await mod.handleMemoryToolCall( ++ 'project_memory_add_note', ++ { ++ workingDirectory: fixture.workspaceRoot, ++ category: 'architecture', ++ content: 'Queue this note.', ++ }, ++ { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ), ++ ); ++ ++ assert.equal(result.data.decision, 'downgrade'); ++ assert.equal(existsSync(join(fixture.workspaceRoot, '.omx', 'memory-intake.jsonl')), true); ++ const intakeRaw = await readFile(join(fixture.workspaceRoot, '.omx', 'memory-intake.jsonl'), 'utf-8'); ++ assert.match(intakeRaw, /Queue this note/); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('downgrades project_memory_add_directive into memory-intake.jsonl in strict mode', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const mod = await loadMemoryServerModule(); ++ const result = parseToolPayload( ++ await mod.handleMemoryToolCall( ++ 'project_memory_add_directive', ++ { ++ workingDirectory: fixture.workspaceRoot, ++ directive: 'Keep promotion gated.', ++ priority: 'high', ++ context: 'review', ++ }, ++ { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ), ++ ); ++ ++ assert.equal(result.data.decision, 'downgrade'); ++ const intakeRaw = await readFile(join(fixture.workspaceRoot, '.omx', 'memory-intake.jsonl'), 'utf-8'); ++ assert.match(intakeRaw, /Keep promotion gated/); ++ assert.match(intakeRaw, /"kind":"directive"/); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++}); +diff --git a/src/mcp/memory-server.ts b/src/mcp/memory-server.ts +index db8a04b..036fda5 100644 +--- a/src/mcp/memory-server.ts ++++ b/src/mcp/memory-server.ts +@@ -15,6 +15,12 @@ import { existsSync } from 'fs'; + import { parseNotepadPruneDaysOld } from './memory-validation.js'; + import { autoStartStdioMcpServer } from './bootstrap.js'; + import { resolveWorkingDirectoryForState } from './state-paths.js'; ++import { ++ appendMemoryIntakeEntry, ++ buildFormalProjectMemoryView, ++ readFormalMemoryContext, ++ resolveStrictMemoryConfig, ++} from '../integration/formal-memory.js'; + + function getMemoryPath(wd: string): string { + return join(wd, '.omx', 'project-memory.json'); +@@ -43,7 +49,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + // Project Memory tools + { + name: 'project_memory_read', +- description: 'Read project memory. Can read full memory or a specific section.', ++ description: ++ 'Read project memory. In strict mode this returns a formal workspace memory summary instead of local .omx/project-memory.json.', + inputSchema: { + type: 'object', + properties: { +@@ -54,7 +61,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + }, + { + name: 'project_memory_write', +- description: 'Write/update project memory. Can replace entirely or merge.', ++ description: ++ 'Write/update local project memory. In strict mode this is denied so durable updates must flow through the external memory pipeline.', + inputSchema: { + type: 'object', + properties: { +@@ -67,7 +75,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + }, + { + name: 'project_memory_add_note', +- description: 'Add a categorized note to project memory.', ++ description: ++ 'Add a categorized note to project memory. In strict mode this downgrades into a run-local intake artifact.', + inputSchema: { + type: 'object', + properties: { +@@ -80,7 +89,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + }, + { + name: 'project_memory_add_directive', +- description: 'Add a persistent directive to project memory.', ++ description: ++ 'Add a persistent directive to project memory. In strict mode this downgrades into a run-local intake artifact.', + inputSchema: { + type: 'object', + properties: { +@@ -164,22 +174,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + ], + })); + +-server.setRequestHandler(CallToolRequestSchema, async (request) => { +- const { name, arguments: args } = request.params; +- const a = (args || {}) as Record; ++export async function handleMemoryToolCall( ++ name: string, ++ a: Record = {}, ++ env: Record = process.env, ++) { + let wd: string; + try { + wd = resolveWorkingDirectoryForState(a.workingDirectory as string | undefined); + } catch (error) { +- return { +- content: [{ type: 'text' as const, text: JSON.stringify({ error: (error as Error).message }) }], +- isError: true, +- }; ++ return errorText({ error: (error as Error).message }); + } ++ const strictConfig = resolveStrictMemoryConfig(env); + + switch (name) { + // === Project Memory === + case 'project_memory_read': { ++ if (strictConfig.strictMode) { ++ const context = await readFormalMemoryContext(wd, env); ++ return text(buildFormalProjectMemoryView(context, a.section as string | undefined)); ++ } + const memPath = getMemoryPath(wd); + if (!existsSync(memPath)) { + return text({ exists: false }); +@@ -198,6 +212,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { + } + + case 'project_memory_write': { ++ if (strictConfig.strictMode) { ++ return errorText({ ++ error: ++ 'Strict integration mode forbids direct project_memory_write. Use the formal memory pipeline or a run-local intake artifact instead.', ++ decision: 'deny', ++ }); ++ } + const memPath = getMemoryPath(wd); + await mkdir(join(wd, '.omx'), { recursive: true }); + const merge = a.merge as boolean; +@@ -218,6 +239,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { + } + + case 'project_memory_add_note': { ++ if (strictConfig.strictMode) { ++ const intake = await appendMemoryIntakeEntry({ ++ cwd: wd, ++ kind: 'note', ++ content: String(a.content as string), ++ metadata: { ++ category: a.category as string, ++ }, ++ source: 'project_memory_add_note', ++ }); ++ return text({ ++ success: true, ++ decision: 'downgrade', ++ downgradedTo: 'memory-intake-queue', ++ intake, ++ }); ++ } + const memPath = getMemoryPath(wd); + await mkdir(join(wd, '.omx'), { recursive: true }); + let data: ProjectMemory = {}; +@@ -239,6 +277,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { + } + + case 'project_memory_add_directive': { ++ if (strictConfig.strictMode) { ++ const intake = await appendMemoryIntakeEntry({ ++ cwd: wd, ++ kind: 'directive', ++ content: String(a.directive as string), ++ metadata: { ++ priority: (a.priority as string) || 'normal', ++ context: a.context as string | undefined, ++ }, ++ source: 'project_memory_add_directive', ++ }); ++ return text({ ++ success: true, ++ decision: 'downgrade', ++ downgradedTo: 'memory-intake-queue', ++ intake, ++ }); ++ } + const memPath = getMemoryPath(wd); + await mkdir(join(wd, '.omx'), { recursive: true }); + let data: ProjectMemory = {}; +@@ -412,12 +468,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { + default: + return { content: [{ type: 'text' as const, text: `Unknown tool: ${name}` }], isError: true }; + } ++} ++server.setRequestHandler(CallToolRequestSchema, async (request) => { ++ const { name, arguments: args } = request.params; ++ return handleMemoryToolCall(name, (args || {}) as Record); + }); + + function text(data: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] }; + } + ++function errorText(data: unknown) { ++ return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }], isError: true }; ++} ++ + function extractSection(content: string, section: string): string { + const header = `## ${section.toUpperCase()}`; + const idx = content.indexOf(header); +-- +2.50.1 (Apple Git-155) + diff --git a/knowledge-base/raw/repos/codex-memory-kit/patches/0002-strict-memory-refresh-bridge.patch b/knowledge-base/raw/repos/codex-memory-kit/patches/0002-strict-memory-refresh-bridge.patch new file mode 100644 index 0000000..8430237 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/patches/0002-strict-memory-refresh-bridge.patch @@ -0,0 +1,372 @@ +From 7e1b309f13108e80f293c04c761ea9753ba83042 Mon Sep 17 00:00:00 2001 +From: wz +Date: Sat, 4 Apr 2026 20:35:46 +0800 +Subject: [PATCH] feat: add strict memory refresh bridge on session exit + +--- + .../__tests__/formal-memory-refresh.test.ts | 155 ++++++++++++++++ + src/cli/index.ts | 172 +++++++++++++++++- + 2 files changed, 326 insertions(+), 1 deletion(-) + create mode 100644 src/cli/__tests__/formal-memory-refresh.test.ts + +diff --git a/src/cli/__tests__/formal-memory-refresh.test.ts b/src/cli/__tests__/formal-memory-refresh.test.ts +new file mode 100644 +index 0000000..03147f0 +--- /dev/null ++++ b/src/cli/__tests__/formal-memory-refresh.test.ts +@@ -0,0 +1,155 @@ ++import { describe, it } from "node:test"; ++import assert from "node:assert/strict"; ++import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; ++import { tmpdir } from "node:os"; ++import { join } from "node:path"; ++ ++import { ++ resolveFormalMemoryRefreshPlan, ++ resolveFormalMemoryRefreshScript, ++ scheduleFormalMemoryRefreshOnExit, ++} from "../index.js"; ++ ++async function createRefreshFixture(): Promise<{ ++ root: string; ++ memoryRoot: string; ++ scriptPath: string; ++}> { ++ const root = await mkdtemp(join(tmpdir(), "omx-formal-refresh-")); ++ const memoryRoot = join(root, "memory"); ++ const scriptPath = join(root, "scripts", "refresh_memory.py"); ++ await mkdir(memoryRoot, { recursive: true }); ++ await mkdir(join(root, "scripts"), { recursive: true }); ++ await writeFile(scriptPath, "#!/usr/bin/env python3\n", "utf-8"); ++ return { root, memoryRoot, scriptPath }; ++} ++ ++describe("formal memory refresh bridge", () => { ++ it("prefers explicit refresh script override", () => { ++ const env = { ++ OMX_EXTERNAL_MEMORY_REFRESH_SCRIPT: "/tmp/custom-refresh.py", ++ OMX_EXTERNAL_MEMORY_ROOT: "/tmp/ignored-memory-root", ++ } satisfies NodeJS.ProcessEnv; ++ assert.equal(resolveFormalMemoryRefreshScript(env), "/tmp/custom-refresh.py"); ++ }); ++ ++ it("infers the refresh script from the external memory root sibling scripts directory", async () => { ++ const fixture = await createRefreshFixture(); ++ try { ++ const env = { ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ } satisfies NodeJS.ProcessEnv; ++ assert.equal(resolveFormalMemoryRefreshScript(env), fixture.scriptPath); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++ ++ it("keeps refresh disabled until strict mode and refresh-on-exit are both enabled", () => { ++ const disabledStrict = resolveFormalMemoryRefreshPlan("/repo", "session-1", {}); ++ assert.equal(disabledStrict.enabled, false); ++ assert.equal(disabledStrict.reason, "strict_mode_disabled"); ++ ++ const disabledRefresh = resolveFormalMemoryRefreshPlan("/repo", "session-1", { ++ OMX_STRICT_MEMORY_MODE: "1", ++ }); ++ assert.equal(disabledRefresh.enabled, false); ++ assert.equal(disabledRefresh.reason, "refresh_on_exit_disabled"); ++ }); ++ ++ it("skips the refresh bridge for team worker processes", async () => { ++ const fixture = await createRefreshFixture(); ++ try { ++ const plan = resolveFormalMemoryRefreshPlan("/repo", "session-2", { ++ OMX_STRICT_MEMORY_MODE: "1", ++ OMX_STRICT_MEMORY_REFRESH_ON_EXIT: "1", ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ OMX_TEAM_WORKER: "alpha/worker-1", ++ }); ++ assert.equal(plan.enabled, false); ++ assert.equal(plan.reason, "team_worker_process"); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++ ++ it("spawns a detached refresh process for leader or standalone sessions", async () => { ++ const fixture = await createRefreshFixture(); ++ try { ++ let captured: ++ | { ++ command: string; ++ args: readonly string[]; ++ options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: "ignore"; ++ }; ++ } ++ | undefined; ++ let unrefCalled = false; ++ ++ const result = scheduleFormalMemoryRefreshOnExit( ++ "/repo", ++ "session-3", ++ { ++ OMX_STRICT_MEMORY_MODE: "1", ++ OMX_STRICT_MEMORY_REFRESH_ON_EXIT: "1", ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ OMX_EXTERNAL_MEMORY_REFRESH_PYTHON: "python3.12", ++ }, ++ ((command: string, args: readonly string[], options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: "ignore"; ++ }) => { ++ captured = { command, args, options }; ++ return { ++ unref() { ++ unrefCalled = true; ++ }, ++ }; ++ }) as never, ++ ); ++ ++ assert.equal(result.scheduled, true); ++ assert.equal(result.reason, "scheduled"); ++ assert.equal(captured?.command, "python3.12"); ++ assert.deepEqual(captured?.args, [fixture.scriptPath, "--workspace-root", "/repo"]); ++ assert.equal(captured?.options.cwd, "/repo"); ++ assert.equal(captured?.options.detached, true); ++ assert.equal(captured?.options.stdio, "ignore"); ++ assert.equal(captured?.options.env.OMX_EXTERNAL_MEMORY_REFRESH_SOURCE, "omx-postlaunch"); ++ assert.equal(captured?.options.env.OMX_EXTERNAL_MEMORY_REFRESH_SESSION_ID, "session-3"); ++ assert.equal(unrefCalled, true); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++ ++ it("reports spawn failures without throwing", async () => { ++ const fixture = await createRefreshFixture(); ++ try { ++ const result = scheduleFormalMemoryRefreshOnExit( ++ "/repo", ++ "session-4", ++ { ++ OMX_STRICT_MEMORY_MODE: "1", ++ OMX_STRICT_MEMORY_REFRESH_ON_EXIT: "1", ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ (() => { ++ throw new Error("boom"); ++ }) as never, ++ ); ++ ++ assert.equal(result.scheduled, false); ++ assert.match(result.reason, /^spawn_failed:boom$/); ++ assert.equal(result.plan.enabled, true); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++}); +diff --git a/src/cli/index.ts b/src/cli/index.ts +index 0a20389..576f1e6 100644 +--- a/src/cli/index.ts ++++ b/src/cli/index.ts +@@ -4,7 +4,7 @@ + */ + + import { execFileSync, spawn } from "child_process"; +-import { basename, dirname, join } from "path"; ++import { basename, dirname, join, resolve } from "path"; + import { existsSync, readFileSync } from "fs"; + import { constants as osConstants } from "os"; + import { setup, SETUP_SCOPES, type SetupScope } from "./setup.js"; +@@ -179,6 +179,161 @@ const REASONING_KEY = "model_reasoning_effort"; + const MODEL_INSTRUCTIONS_FILE_KEY = "model_instructions_file"; + const TEAM_WORKER_LAUNCH_ARGS_ENV = "OMX_TEAM_WORKER_LAUNCH_ARGS"; + const TEAM_INHERIT_LEADER_FLAGS_ENV = "OMX_TEAM_INHERIT_LEADER_FLAGS"; ++const STRICT_MEMORY_MODE_ENV = "OMX_STRICT_MEMORY_MODE"; ++const STRICT_MEMORY_REFRESH_ON_EXIT_ENV = "OMX_STRICT_MEMORY_REFRESH_ON_EXIT"; ++const EXTERNAL_MEMORY_ROOT_ENV = "OMX_EXTERNAL_MEMORY_ROOT"; ++const EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_SCRIPT"; ++const EXTERNAL_MEMORY_REFRESH_PYTHON_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_PYTHON"; ++const TEAM_WORKER_ENV = "OMX_TEAM_WORKER"; ++ ++export interface FormalMemoryRefreshPlan { ++ enabled: boolean; ++ strictMode: boolean; ++ reason: ++ | "strict_mode_disabled" ++ | "refresh_on_exit_disabled" ++ | "team_worker_process" ++ | "refresh_script_unavailable" ++ | "enabled"; ++ scriptPath?: string; ++ command?: string; ++ args?: string[]; ++ childEnv?: NodeJS.ProcessEnv; ++} ++ ++export interface FormalMemoryRefreshScheduleResult { ++ scheduled: boolean; ++ reason: string; ++ plan: FormalMemoryRefreshPlan; ++} ++ ++type FormalMemoryRefreshSpawn = ( ++ command: string, ++ args: readonly string[], ++ options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: "ignore"; ++ }, ++) => { ++ unref?: () => void; ++}; ++ ++function parseOptionalBooleanEnv(value: string | undefined): boolean | undefined { ++ if (typeof value !== "string") return undefined; ++ const normalized = value.trim().toLowerCase(); ++ if (["1", "true", "yes", "on"].includes(normalized)) return true; ++ if (["0", "false", "no", "off"].includes(normalized)) return false; ++ return undefined; ++} ++ ++export function resolveFormalMemoryRefreshScript( ++ env: NodeJS.ProcessEnv = process.env, ++): string | null { ++ const explicit = env[EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV]?.trim(); ++ if (explicit) return explicit; ++ ++ const memoryRoot = env[EXTERNAL_MEMORY_ROOT_ENV]?.trim(); ++ if (!memoryRoot) return null; ++ ++ const candidate = join(dirname(resolve(memoryRoot)), "scripts", "refresh_memory.py"); ++ return existsSync(candidate) ? candidate : null; ++} ++ ++export function resolveFormalMemoryRefreshPlan( ++ cwd: string, ++ sessionId: string, ++ env: NodeJS.ProcessEnv = process.env, ++): FormalMemoryRefreshPlan { ++ const strictMode = parseOptionalBooleanEnv(env[STRICT_MEMORY_MODE_ENV]) === true; ++ if (!strictMode) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "strict_mode_disabled", ++ }; ++ } ++ ++ if (parseOptionalBooleanEnv(env[STRICT_MEMORY_REFRESH_ON_EXIT_ENV]) !== true) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "refresh_on_exit_disabled", ++ }; ++ } ++ ++ if (typeof env[TEAM_WORKER_ENV] === "string" && env[TEAM_WORKER_ENV].trim() !== "") { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "team_worker_process", ++ }; ++ } ++ ++ const scriptPath = resolveFormalMemoryRefreshScript(env); ++ if (!scriptPath) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "refresh_script_unavailable", ++ }; ++ } ++ ++ const command = env[EXTERNAL_MEMORY_REFRESH_PYTHON_ENV]?.trim() || "python3"; ++ return { ++ enabled: true, ++ strictMode, ++ reason: "enabled", ++ scriptPath, ++ command, ++ args: [scriptPath, "--workspace-root", cwd], ++ childEnv: { ++ ...process.env, ++ ...env, ++ OMX_EXTERNAL_MEMORY_REFRESH_SOURCE: "omx-postlaunch", ++ OMX_EXTERNAL_MEMORY_REFRESH_SESSION_ID: sessionId, ++ }, ++ }; ++} ++ ++export function scheduleFormalMemoryRefreshOnExit( ++ cwd: string, ++ sessionId: string, ++ env: NodeJS.ProcessEnv = process.env, ++ spawnImpl: FormalMemoryRefreshSpawn = spawn, ++): FormalMemoryRefreshScheduleResult { ++ const plan = resolveFormalMemoryRefreshPlan(cwd, sessionId, env); ++ if (!plan.enabled || !plan.command || !plan.args || !plan.childEnv) { ++ return { ++ scheduled: false, ++ reason: plan.reason, ++ plan, ++ }; ++ } ++ ++ try { ++ const child = spawnImpl(plan.command, plan.args, { ++ cwd, ++ env: plan.childEnv, ++ detached: true, ++ stdio: "ignore", ++ }); ++ child.unref?.(); ++ return { ++ scheduled: true, ++ reason: "scheduled", ++ plan, ++ }; ++ } catch (error) { ++ const message = error instanceof Error ? error.message : String(error); ++ return { ++ scheduled: false, ++ reason: `spawn_failed:${message}`, ++ plan, ++ }; ++ } ++} + const OMX_BYPASS_DEFAULT_SYSTEM_PROMPT_ENV = "OMX_BYPASS_DEFAULT_SYSTEM_PROMPT"; + const OMX_MODEL_INSTRUCTIONS_FILE_ENV = "OMX_MODEL_INSTRUCTIONS_FILE"; + const OMX_RALPH_APPEND_INSTRUCTIONS_FILE_ENV = +@@ -2157,6 +2312,21 @@ async function postLaunch( + ); + } + ++ // 3.5 Trigger external formal-memory refresh on exit (strict mode opt-in, best effort). ++ try { ++ const refresh = scheduleFormalMemoryRefreshOnExit(cwd, sessionId); ++ if (!refresh.scheduled && ( ++ refresh.reason === "refresh_script_unavailable" ++ || refresh.reason.startsWith("spawn_failed:") ++ )) { ++ console.warn(`[omx] postLaunch: external formal-memory refresh skipped: ${refresh.reason}`); ++ } ++ } catch (err) { ++ console.error( ++ `[omx] postLaunch: external formal-memory refresh failed: ${err instanceof Error ? err.message : err}`, ++ ); ++ } ++ + // 4. Send session-end lifecycle notification (best effort) + try { + const { notifyLifecycle } = await import("../notifications/index.js"); +-- +2.50.1 (Apple Git-155) + diff --git a/knowledge-base/raw/repos/codex-memory-kit/patches/0003-team-complete-memory-refresh.patch b/knowledge-base/raw/repos/codex-memory-kit/patches/0003-team-complete-memory-refresh.patch new file mode 100644 index 0000000..2a2f9a1 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/patches/0003-team-complete-memory-refresh.patch @@ -0,0 +1,616 @@ +From 5586da08c85348a2ea9f52d97e4993187faeff37 Mon Sep 17 00:00:00 2001 +From: wz +Date: Sat, 4 Apr 2026 20:48:06 +0800 +Subject: [PATCH] feat: refresh strict memory when teams complete + +--- + src/cli/index.ts | 161 ++++---------------- + src/integration/formal-memory-refresh.ts | 180 +++++++++++++++++++++++ + src/team/__tests__/runtime-cli.test.ts | 113 +++++++++++++- + src/team/runtime-cli.ts | 37 +++++ + 4 files changed, 348 insertions(+), 143 deletions(-) + create mode 100644 src/integration/formal-memory-refresh.ts + +diff --git a/src/cli/index.ts b/src/cli/index.ts +index 576f1e6..7ae59d4 100644 +--- a/src/cli/index.ts ++++ b/src/cli/index.ts +@@ -4,7 +4,7 @@ + */ + + import { execFileSync, spawn } from "child_process"; +-import { basename, dirname, join, resolve } from "path"; ++import { basename, dirname, join } from "path"; + import { existsSync, readFileSync } from "fs"; + import { constants as osConstants } from "os"; + import { setup, SETUP_SCOPES, type SetupScope } from "./setup.js"; +@@ -94,6 +94,15 @@ import { + type NotifyTempContract, + type ParseNotifyTempContractResult, + } from "../notifications/temp-contract.js"; ++import { ++ scheduleFormalMemoryRefresh, ++ resolveFormalMemoryRefreshPlan as resolveSharedFormalMemoryRefreshPlan, ++ resolveFormalMemoryRefreshScript as resolveSharedFormalMemoryRefreshScript, ++ STRICT_MEMORY_REFRESH_ON_EXIT_ENV, ++ type FormalMemoryRefreshPlan, ++ type FormalMemoryRefreshScheduleResult, ++ type FormalMemoryRefreshSpawn, ++} from "../integration/formal-memory-refresh.js"; + + export function resolveNotifyFallbackWatcherScript(pkgRoot = getPackageRoot()): string { + return join(pkgRoot, "dist", "scripts", "notify-fallback-watcher.js"); +@@ -179,66 +188,11 @@ const REASONING_KEY = "model_reasoning_effort"; + const MODEL_INSTRUCTIONS_FILE_KEY = "model_instructions_file"; + const TEAM_WORKER_LAUNCH_ARGS_ENV = "OMX_TEAM_WORKER_LAUNCH_ARGS"; + const TEAM_INHERIT_LEADER_FLAGS_ENV = "OMX_TEAM_INHERIT_LEADER_FLAGS"; +-const STRICT_MEMORY_MODE_ENV = "OMX_STRICT_MEMORY_MODE"; +-const STRICT_MEMORY_REFRESH_ON_EXIT_ENV = "OMX_STRICT_MEMORY_REFRESH_ON_EXIT"; +-const EXTERNAL_MEMORY_ROOT_ENV = "OMX_EXTERNAL_MEMORY_ROOT"; +-const EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_SCRIPT"; +-const EXTERNAL_MEMORY_REFRESH_PYTHON_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_PYTHON"; +-const TEAM_WORKER_ENV = "OMX_TEAM_WORKER"; +- +-export interface FormalMemoryRefreshPlan { +- enabled: boolean; +- strictMode: boolean; +- reason: +- | "strict_mode_disabled" +- | "refresh_on_exit_disabled" +- | "team_worker_process" +- | "refresh_script_unavailable" +- | "enabled"; +- scriptPath?: string; +- command?: string; +- args?: string[]; +- childEnv?: NodeJS.ProcessEnv; +-} +- +-export interface FormalMemoryRefreshScheduleResult { +- scheduled: boolean; +- reason: string; +- plan: FormalMemoryRefreshPlan; +-} +- +-type FormalMemoryRefreshSpawn = ( +- command: string, +- args: readonly string[], +- options: { +- cwd: string; +- env: NodeJS.ProcessEnv; +- detached: boolean; +- stdio: "ignore"; +- }, +-) => { +- unref?: () => void; +-}; +- +-function parseOptionalBooleanEnv(value: string | undefined): boolean | undefined { +- if (typeof value !== "string") return undefined; +- const normalized = value.trim().toLowerCase(); +- if (["1", "true", "yes", "on"].includes(normalized)) return true; +- if (["0", "false", "no", "off"].includes(normalized)) return false; +- return undefined; +-} + + export function resolveFormalMemoryRefreshScript( + env: NodeJS.ProcessEnv = process.env, + ): string | null { +- const explicit = env[EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV]?.trim(); +- if (explicit) return explicit; +- +- const memoryRoot = env[EXTERNAL_MEMORY_ROOT_ENV]?.trim(); +- if (!memoryRoot) return null; +- +- const candidate = join(dirname(resolve(memoryRoot)), "scripts", "refresh_memory.py"); +- return existsSync(candidate) ? candidate : null; ++ return resolveSharedFormalMemoryRefreshScript(env); + } + + export function resolveFormalMemoryRefreshPlan( +@@ -246,55 +200,13 @@ export function resolveFormalMemoryRefreshPlan( + sessionId: string, + env: NodeJS.ProcessEnv = process.env, + ): FormalMemoryRefreshPlan { +- const strictMode = parseOptionalBooleanEnv(env[STRICT_MEMORY_MODE_ENV]) === true; +- if (!strictMode) { +- return { +- enabled: false, +- strictMode, +- reason: "strict_mode_disabled", +- }; +- } +- +- if (parseOptionalBooleanEnv(env[STRICT_MEMORY_REFRESH_ON_EXIT_ENV]) !== true) { +- return { +- enabled: false, +- strictMode, +- reason: "refresh_on_exit_disabled", +- }; +- } +- +- if (typeof env[TEAM_WORKER_ENV] === "string" && env[TEAM_WORKER_ENV].trim() !== "") { +- return { +- enabled: false, +- strictMode, +- reason: "team_worker_process", +- }; +- } +- +- const scriptPath = resolveFormalMemoryRefreshScript(env); +- if (!scriptPath) { +- return { +- enabled: false, +- strictMode, +- reason: "refresh_script_unavailable", +- }; +- } +- +- const command = env[EXTERNAL_MEMORY_REFRESH_PYTHON_ENV]?.trim() || "python3"; +- return { +- enabled: true, +- strictMode, +- reason: "enabled", +- scriptPath, +- command, +- args: [scriptPath, "--workspace-root", cwd], +- childEnv: { +- ...process.env, +- ...env, +- OMX_EXTERNAL_MEMORY_REFRESH_SOURCE: "omx-postlaunch", +- OMX_EXTERNAL_MEMORY_REFRESH_SESSION_ID: sessionId, +- }, +- }; ++ return resolveSharedFormalMemoryRefreshPlan({ ++ cwd, ++ source: "omx-postlaunch", ++ sessionId, ++ enableEnvKeys: [STRICT_MEMORY_REFRESH_ON_EXIT_ENV], ++ disabledReason: "refresh_on_exit_disabled", ++ }, env); + } + + export function scheduleFormalMemoryRefreshOnExit( +@@ -303,36 +215,13 @@ export function scheduleFormalMemoryRefreshOnExit( + env: NodeJS.ProcessEnv = process.env, + spawnImpl: FormalMemoryRefreshSpawn = spawn, + ): FormalMemoryRefreshScheduleResult { +- const plan = resolveFormalMemoryRefreshPlan(cwd, sessionId, env); +- if (!plan.enabled || !plan.command || !plan.args || !plan.childEnv) { +- return { +- scheduled: false, +- reason: plan.reason, +- plan, +- }; +- } +- +- try { +- const child = spawnImpl(plan.command, plan.args, { +- cwd, +- env: plan.childEnv, +- detached: true, +- stdio: "ignore", +- }); +- child.unref?.(); +- return { +- scheduled: true, +- reason: "scheduled", +- plan, +- }; +- } catch (error) { +- const message = error instanceof Error ? error.message : String(error); +- return { +- scheduled: false, +- reason: `spawn_failed:${message}`, +- plan, +- }; +- } ++ return scheduleFormalMemoryRefresh({ ++ cwd, ++ source: "omx-postlaunch", ++ sessionId, ++ enableEnvKeys: [STRICT_MEMORY_REFRESH_ON_EXIT_ENV], ++ disabledReason: "refresh_on_exit_disabled", ++ }, env, spawnImpl); + } + const OMX_BYPASS_DEFAULT_SYSTEM_PROMPT_ENV = "OMX_BYPASS_DEFAULT_SYSTEM_PROMPT"; + const OMX_MODEL_INSTRUCTIONS_FILE_ENV = "OMX_MODEL_INSTRUCTIONS_FILE"; +diff --git a/src/integration/formal-memory-refresh.ts b/src/integration/formal-memory-refresh.ts +new file mode 100644 +index 0000000..b765ea6 +--- /dev/null ++++ b/src/integration/formal-memory-refresh.ts +@@ -0,0 +1,180 @@ ++import { spawn } from "child_process"; ++import { existsSync } from "fs"; ++import { dirname, join, resolve } from "path"; ++ ++export const STRICT_MEMORY_MODE_ENV = "OMX_STRICT_MEMORY_MODE"; ++export const STRICT_MEMORY_REFRESH_ON_EXIT_ENV = "OMX_STRICT_MEMORY_REFRESH_ON_EXIT"; ++export const STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV = ++ "OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE"; ++export const EXTERNAL_MEMORY_ROOT_ENV = "OMX_EXTERNAL_MEMORY_ROOT"; ++export const EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_SCRIPT"; ++export const EXTERNAL_MEMORY_REFRESH_PYTHON_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_PYTHON"; ++export const TEAM_WORKER_ENV = "OMX_TEAM_WORKER"; ++ ++export interface FormalMemoryRefreshPlan { ++ enabled: boolean; ++ strictMode: boolean; ++ reason: string; ++ scriptPath?: string; ++ command?: string; ++ args?: string[]; ++ childEnv?: NodeJS.ProcessEnv; ++} ++ ++export interface FormalMemoryRefreshScheduleResult { ++ scheduled: boolean; ++ reason: string; ++ plan: FormalMemoryRefreshPlan; ++} ++ ++export interface FormalMemoryRefreshTarget { ++ cwd: string; ++ source: string; ++ enableEnvKeys: readonly string[]; ++ disabledReason: string; ++ sessionId?: string; ++ teamName?: string; ++ skipTeamWorker?: boolean; ++} ++ ++export type FormalMemoryRefreshSpawn = ( ++ command: string, ++ args: readonly string[], ++ options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: "ignore"; ++ }, ++) => { ++ unref?: () => void; ++}; ++ ++function parseOptionalBooleanEnv(value: string | undefined): boolean | undefined { ++ if (typeof value !== "string") return undefined; ++ const normalized = value.trim().toLowerCase(); ++ if (["1", "true", "yes", "on"].includes(normalized)) return true; ++ if (["0", "false", "no", "off"].includes(normalized)) return false; ++ return undefined; ++} ++ ++function isRefreshEnabled( ++ envKeys: readonly string[], ++ env: NodeJS.ProcessEnv, ++): boolean { ++ return envKeys.some((key) => parseOptionalBooleanEnv(env[key]) === true); ++} ++ ++export function resolveFormalMemoryRefreshScript( ++ env: NodeJS.ProcessEnv = process.env, ++): string | null { ++ const explicit = env[EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV]?.trim(); ++ if (explicit) return explicit; ++ ++ const memoryRoot = env[EXTERNAL_MEMORY_ROOT_ENV]?.trim(); ++ if (!memoryRoot) return null; ++ ++ const candidate = join(dirname(resolve(memoryRoot)), "scripts", "refresh_memory.py"); ++ return existsSync(candidate) ? candidate : null; ++} ++ ++export function resolveFormalMemoryRefreshPlan( ++ target: FormalMemoryRefreshTarget, ++ env: NodeJS.ProcessEnv = process.env, ++): FormalMemoryRefreshPlan { ++ const strictMode = parseOptionalBooleanEnv(env[STRICT_MEMORY_MODE_ENV]) === true; ++ if (!strictMode) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "strict_mode_disabled", ++ }; ++ } ++ ++ if (!isRefreshEnabled(target.enableEnvKeys, env)) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: target.disabledReason, ++ }; ++ } ++ ++ if ( ++ target.skipTeamWorker !== false ++ && typeof env[TEAM_WORKER_ENV] === "string" ++ && env[TEAM_WORKER_ENV].trim() !== "" ++ ) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "team_worker_process", ++ }; ++ } ++ ++ const scriptPath = resolveFormalMemoryRefreshScript(env); ++ if (!scriptPath) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "refresh_script_unavailable", ++ }; ++ } ++ ++ const command = env[EXTERNAL_MEMORY_REFRESH_PYTHON_ENV]?.trim() || "python3"; ++ return { ++ enabled: true, ++ strictMode, ++ reason: "enabled", ++ scriptPath, ++ command, ++ args: [scriptPath, "--workspace-root", target.cwd], ++ childEnv: { ++ ...process.env, ++ ...env, ++ OMX_EXTERNAL_MEMORY_REFRESH_SOURCE: target.source, ++ ...(target.sessionId ++ ? { OMX_EXTERNAL_MEMORY_REFRESH_SESSION_ID: target.sessionId } ++ : {}), ++ ...(target.teamName ++ ? { OMX_EXTERNAL_MEMORY_REFRESH_TEAM_NAME: target.teamName } ++ : {}), ++ }, ++ }; ++} ++ ++export function scheduleFormalMemoryRefresh( ++ target: FormalMemoryRefreshTarget, ++ env: NodeJS.ProcessEnv = process.env, ++ spawnImpl: FormalMemoryRefreshSpawn = spawn, ++): FormalMemoryRefreshScheduleResult { ++ const plan = resolveFormalMemoryRefreshPlan(target, env); ++ if (!plan.enabled || !plan.command || !plan.args || !plan.childEnv) { ++ return { ++ scheduled: false, ++ reason: plan.reason, ++ plan, ++ }; ++ } ++ ++ try { ++ const child = spawnImpl(plan.command, plan.args, { ++ cwd: target.cwd, ++ env: plan.childEnv, ++ detached: true, ++ stdio: "ignore", ++ }); ++ child.unref?.(); ++ return { ++ scheduled: true, ++ reason: "scheduled", ++ plan, ++ }; ++ } catch (error) { ++ const message = error instanceof Error ? error.message : String(error); ++ return { ++ scheduled: false, ++ reason: `spawn_failed:${message}`, ++ plan, ++ }; ++ } ++} +diff --git a/src/team/__tests__/runtime-cli.test.ts b/src/team/__tests__/runtime-cli.test.ts +index 77ed0cd..9b6849f 100644 +--- a/src/team/__tests__/runtime-cli.test.ts ++++ b/src/team/__tests__/runtime-cli.test.ts +@@ -1,9 +1,10 @@ + import { describe, it } from 'node:test'; + import assert from 'node:assert/strict'; +-import { mkdtemp, rm } from 'fs/promises'; + import { existsSync } from 'fs'; +-import { join } from 'path'; ++import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'; + import { tmpdir } from 'os'; ++import { join } from 'path'; ++ + import { initTeamState, createTask, readTeamConfig, saveTeamConfig } from '../state.js'; + + async function loadRuntimeCliModule() { +@@ -11,6 +12,20 @@ async function loadRuntimeCliModule() { + return await import('../runtime-cli.js'); + } + ++async function createRefreshFixture(): Promise<{ ++ root: string; ++ memoryRoot: string; ++ scriptPath: string; ++}> { ++ const root = await mkdtemp(join(tmpdir(), 'omx-runtime-cli-refresh-')); ++ const memoryRoot = join(root, 'memory'); ++ const scriptPath = join(root, 'scripts', 'refresh_memory.py'); ++ await mkdir(memoryRoot, { recursive: true }); ++ await mkdir(join(root, 'scripts'), { recursive: true }); ++ await writeFile(scriptPath, '#!/usr/bin/env python3\n', 'utf-8'); ++ return { root, memoryRoot, scriptPath }; ++} ++ + describe('runtime-cli helpers', () => { + it('normalizes per-worker providers and validates supported values', async () => { + const runtimeCli = await loadRuntimeCliModule(); +@@ -74,6 +89,14 @@ describe('runtime-cli helpers', () => { + assert.equal(liveBehavior.fixingWithNoWorkers, false); + }); + ++ it('does not treat leader pane as a worker pane for dead-worker detection', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); ++ ++ const result = runtimeCli.detectDeadWorkerFailure(1, 1, true, 'team-exec'); ++ assert.equal(result.deadWorkerFailure, true); ++ assert.equal(result.fixingWithNoWorkers, false); ++ }); ++ + it('gracefully shuts down only when the leader explicitly requests shutdown', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-cli-shutdown-')); + const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT; +@@ -128,13 +151,89 @@ describe('runtime-cli helpers', () => { + await rm(cwd, { recursive: true, force: true }); + } + }); +-}); + ++ it('keeps team-complete refresh disabled until strict mode and explicit team gate are enabled', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); + +- it('does not treat leader pane as a worker pane for dead-worker detection', async () => { ++ const disabledStrict = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', {}); ++ assert.equal(disabledStrict.scheduled, false); ++ assert.equal(disabledStrict.reason, 'strict_mode_disabled'); ++ ++ const disabledGate = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', { ++ OMX_STRICT_MEMORY_MODE: '1', ++ }); ++ assert.equal(disabledGate.scheduled, false); ++ assert.equal(disabledGate.reason, 'team_completion_refresh_disabled'); ++ }); ++ ++ it('schedules detached formal-memory refresh for completed leader teams', async () => { + const runtimeCli = await loadRuntimeCliModule(); ++ const fixture = await createRefreshFixture(); ++ try { ++ let captured: ++ | { ++ command: string; ++ args: readonly string[]; ++ options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: 'ignore'; ++ }; ++ } ++ | undefined; ++ let unrefCalled = false; ++ ++ const result = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete( ++ '/repo', ++ 'team-alpha', ++ { ++ OMX_STRICT_MEMORY_MODE: '1', ++ OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ OMX_EXTERNAL_MEMORY_REFRESH_PYTHON: 'python3.12', ++ }, ++ ((command: string, args: readonly string[], options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: 'ignore'; ++ }) => { ++ captured = { command, args, options }; ++ return { ++ unref() { ++ unrefCalled = true; ++ }, ++ }; ++ }) as never, ++ ); ++ ++ assert.equal(result.scheduled, true); ++ assert.equal(captured?.command, 'python3.12'); ++ assert.deepEqual(captured?.args, [fixture.scriptPath, '--workspace-root', '/repo']); ++ assert.equal(captured?.options.env.OMX_EXTERNAL_MEMORY_REFRESH_SOURCE, 'omx-team-runtime-complete'); ++ assert.equal(captured?.options.env.OMX_EXTERNAL_MEMORY_REFRESH_TEAM_NAME, 'team-alpha'); ++ assert.equal(captured?.options.detached, true); ++ assert.equal(unrefCalled, true); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); + +- const result = runtimeCli.detectDeadWorkerFailure(1, 1, true, 'team-exec'); +- assert.equal(result.deadWorkerFailure, true); +- assert.equal(result.fixingWithNoWorkers, false); ++ it('skips team-complete refresh when the process is marked as a team worker', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); ++ const fixture = await createRefreshFixture(); ++ try { ++ const result = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'team-worker', { ++ OMX_STRICT_MEMORY_MODE: '1', ++ OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ OMX_TEAM_WORKER: 'team-worker/worker-1', ++ }); ++ assert.equal(result.scheduled, false); ++ assert.equal(result.reason, 'team_worker_process'); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } + }); ++}); +diff --git a/src/team/runtime-cli.ts b/src/team/runtime-cli.ts +index 28fcf23..9379a4e 100644 +--- a/src/team/runtime-cli.ts ++++ b/src/team/runtime-cli.ts +@@ -9,6 +9,12 @@ + import { readdirSync, readFileSync } from 'fs'; + import { writeFile, rename } from 'fs/promises'; + import { join } from 'path'; ++import { ++ scheduleFormalMemoryRefresh, ++ STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV, ++ type FormalMemoryRefreshScheduleResult, ++ type FormalMemoryRefreshSpawn, ++} from '../integration/formal-memory-refresh.js'; + import { startTeam, monitorTeam, shutdownTeam } from './runtime.js'; + import type { TeamRuntime } from './runtime.js'; + import { teamReadConfig as readTeamConfig } from './team-ops.js'; +@@ -82,6 +88,21 @@ export async function shutdownWithForceFallback(teamName: string, cwd: string): + } + } + ++export function scheduleFormalMemoryRefreshOnTeamComplete( ++ cwd: string, ++ teamName: string, ++ env: NodeJS.ProcessEnv = process.env, ++ spawnImpl?: FormalMemoryRefreshSpawn, ++): FormalMemoryRefreshScheduleResult { ++ return scheduleFormalMemoryRefresh({ ++ cwd, ++ source: 'omx-team-runtime-complete', ++ teamName, ++ enableEnvKeys: [STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV], ++ disabledReason: 'team_completion_refresh_disabled', ++ }, env, spawnImpl); ++} ++ + export function detectDeadWorkerFailure( + deadWorkerCount: number, + liveWorkerPaneCount: number, +@@ -198,6 +219,22 @@ async function main(): Promise { + } + } + ++ if (status === 'completed') { ++ try { ++ const refresh = scheduleFormalMemoryRefreshOnTeamComplete(runtime?.cwd ?? cwd, teamName); ++ if (!refresh.scheduled && ( ++ refresh.reason === 'refresh_script_unavailable' ++ || refresh.reason.startsWith('spawn_failed:') ++ )) { ++ process.stderr.write( ++ `[runtime-cli] strict formal-memory refresh skipped: ${refresh.reason}\n`, ++ ); ++ } ++ } catch (err) { ++ process.stderr.write(`[runtime-cli] strict formal-memory refresh error: ${err}\n`); ++ } ++ } ++ + const duration = (Date.now() - startTime) / 1000; + const output: CliOutput = { + status: finalStatus, +-- +2.50.1 (Apple Git-155) + diff --git a/knowledge-base/raw/repos/codex-memory-kit/patches/0004-team-verification-evidence-gate.patch b/knowledge-base/raw/repos/codex-memory-kit/patches/0004-team-verification-evidence-gate.patch new file mode 100644 index 0000000..888af14 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/patches/0004-team-verification-evidence-gate.patch @@ -0,0 +1,489 @@ +From 8b89da5ef2da621e6742cf209132225210f01fbe Mon Sep 17 00:00:00 2001 +From: wz +Date: Sat, 4 Apr 2026 22:59:22 +0800 +Subject: [PATCH] feat: gate team refresh on verification evidence + +--- + src/team/__tests__/runtime-cli.test.ts | 101 +++++++++++++++++++++++-- + src/team/__tests__/runtime.test.ts | 10 +++ + src/team/runtime-cli.ts | 62 ++++++++++++++- + src/team/runtime.ts | 19 +++++ + src/team/state.ts | 69 +++++++++++++++++ + src/team/team-ops.ts | 3 + + 6 files changed, 256 insertions(+), 8 deletions(-) + +diff --git a/src/team/__tests__/runtime-cli.test.ts b/src/team/__tests__/runtime-cli.test.ts +index 9b6849f..7c71a4a 100644 +--- a/src/team/__tests__/runtime-cli.test.ts ++++ b/src/team/__tests__/runtime-cli.test.ts +@@ -5,7 +5,14 @@ import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'; + import { tmpdir } from 'os'; + import { join } from 'path'; + +-import { initTeamState, createTask, readTeamConfig, saveTeamConfig } from '../state.js'; ++import { ++ initTeamState, ++ createTask, ++ readTeamConfig, ++ saveTeamConfig, ++ writeTeamVerification, ++ type TeamVerificationState, ++} from '../state.js'; + + async function loadRuntimeCliModule() { + process.env.OMX_RUNTIME_CLI_DISABLE_AUTO_START = '1'; +@@ -26,6 +33,20 @@ async function createRefreshFixture(): Promise<{ + return { root, memoryRoot, scriptPath }; + } + ++function buildVerificationState( ++ overrides: Partial = {}, ++): TeamVerificationState { ++ return { ++ status: 'verified', ++ phase: 'complete', ++ completed_code_task_ids: ['task-1'], ++ verified_task_ids: ['task-1'], ++ pending_task_ids: [], ++ updated_at: new Date().toISOString(), ++ ...overrides, ++ }; ++} ++ + describe('runtime-cli helpers', () => { + it('normalizes per-worker providers and validates supported values', async () => { + const runtimeCli = await loadRuntimeCliModule(); +@@ -154,19 +175,48 @@ describe('runtime-cli helpers', () => { + + it('keeps team-complete refresh disabled until strict mode and explicit team gate are enabled', async () => { + const runtimeCli = await loadRuntimeCliModule(); ++ const verificationState = buildVerificationState(); + +- const disabledStrict = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', {}); ++ const disabledStrict = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', {}, undefined, verificationState); + assert.equal(disabledStrict.scheduled, false); + assert.equal(disabledStrict.reason, 'strict_mode_disabled'); + + const disabledGate = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', { + OMX_STRICT_MEMORY_MODE: '1', +- }); ++ }, undefined, verificationState); + assert.equal(disabledGate.scheduled, false); + assert.equal(disabledGate.reason, 'team_completion_refresh_disabled'); + }); + +- it('schedules detached formal-memory refresh for completed leader teams', async () => { ++ it('blocks team-complete refresh until verification evidence is marked complete and verified', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); ++ const fixture = await createRefreshFixture(); ++ try { ++ const baseEnv = { ++ OMX_STRICT_MEMORY_MODE: '1', ++ OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }; ++ ++ const missing = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'team-alpha', baseEnv); ++ assert.equal(missing.scheduled, false); ++ assert.equal(missing.reason, 'team_verification_state_missing'); ++ ++ const pending = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete( ++ '/repo', ++ 'team-alpha', ++ baseEnv, ++ undefined, ++ buildVerificationState({ status: 'pending', phase: 'team-verify', pending_task_ids: ['task-1'] }), ++ ); ++ assert.equal(pending.scheduled, false); ++ assert.equal(pending.reason, 'team_verification_phase_incomplete'); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('schedules detached formal-memory refresh for completed leader teams with verified evidence', async () => { + const runtimeCli = await loadRuntimeCliModule(); + const fixture = await createRefreshFixture(); + try { +@@ -206,6 +256,7 @@ describe('runtime-cli helpers', () => { + }, + }; + }) as never, ++ buildVerificationState(), + ); + + assert.equal(result.scheduled, true); +@@ -229,11 +280,51 @@ describe('runtime-cli helpers', () => { + OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', + OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, + OMX_TEAM_WORKER: 'team-worker/worker-1', +- }); ++ }, undefined, buildVerificationState()); + assert.equal(result.scheduled, false); + assert.equal(result.reason, 'team_worker_process'); + } finally { + await rm(fixture.root, { recursive: true, force: true }); + } + }); ++ ++ it('can pre-read verified team state before shutdown cleanup and still schedule refresh', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); ++ const fixture = await createRefreshFixture(); ++ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-cli-verify-state-')); ++ try { ++ await initTeamState('verified-refresh', 'task', 'executor', 1, cwd); ++ await writeTeamVerification('verified-refresh', buildVerificationState({ ++ completed_code_task_ids: ['task-42'], ++ verified_task_ids: ['task-42'], ++ }), cwd); ++ ++ let capturedCommand: string | undefined; ++ const verificationState = buildVerificationState({ ++ completed_code_task_ids: ['task-42'], ++ verified_task_ids: ['task-42'], ++ }); ++ ++ const result = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete( ++ cwd, ++ 'verified-refresh', ++ { ++ OMX_STRICT_MEMORY_MODE: '1', ++ OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ((command: string) => { ++ capturedCommand = command; ++ return { unref() {} }; ++ }) as never, ++ verificationState, ++ ); ++ ++ assert.equal(result.scheduled, true); ++ assert.equal(capturedCommand, 'python3'); ++ } finally { ++ await rm(cwd, { recursive: true, force: true }); ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); + }); +diff --git a/src/team/__tests__/runtime.test.ts b/src/team/__tests__/runtime.test.ts +index 4d3b4c3..a9e2e28 100644 +--- a/src/team/__tests__/runtime.test.ts ++++ b/src/team/__tests__/runtime.test.ts +@@ -18,6 +18,7 @@ import { + writeAtomic, + readTask, + readMonitorSnapshot, ++ readTeamVerification, + claimTask, + transitionTaskStatus, + writeWorkerStatus, +@@ -1793,6 +1794,10 @@ process.on('SIGTERM', () => { + first?.recommendations.some((r) => r.includes(`task-${task.id}`) && r.includes('Verification evidence missing')), + true, + ); ++ const firstVerification = await readTeamVerification('team-verify-gate', cwd); ++ assert.equal(firstVerification?.status, 'pending'); ++ assert.equal(firstVerification?.phase, 'team-verify'); ++ assert.deepEqual(firstVerification?.pending_task_ids, [task.id]); + + const taskPath = join(cwd, '.omx', 'state', 'team', 'team-verify-gate', 'tasks', `task-${task.id}.json`); + const fromDisk = JSON.parse(await readFile(taskPath, 'utf-8')) as Record; +@@ -1807,6 +1812,11 @@ process.on('SIGTERM', () => { + const second = await monitorTeam('team-verify-gate', cwd); + assert.ok(second); + assert.equal(second?.phase, 'complete'); ++ const secondVerification = await readTeamVerification('team-verify-gate', cwd); ++ assert.equal(secondVerification?.status, 'verified'); ++ assert.equal(secondVerification?.phase, 'complete'); ++ assert.deepEqual(secondVerification?.verified_task_ids, [task.id]); ++ assert.deepEqual(secondVerification?.pending_task_ids, []); + } finally { + if (typeof prevTeamStateRoot === 'string') process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot; + else delete process.env.OMX_TEAM_STATE_ROOT; +diff --git a/src/team/runtime-cli.ts b/src/team/runtime-cli.ts +index 9379a4e..7eb1064 100644 +--- a/src/team/runtime-cli.ts ++++ b/src/team/runtime-cli.ts +@@ -10,6 +10,7 @@ import { readdirSync, readFileSync } from 'fs'; + import { writeFile, rename } from 'fs/promises'; + import { join } from 'path'; + import { ++ resolveFormalMemoryRefreshPlan, + scheduleFormalMemoryRefresh, + STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV, + type FormalMemoryRefreshScheduleResult, +@@ -17,7 +18,11 @@ import { + } from '../integration/formal-memory-refresh.js'; + import { startTeam, monitorTeam, shutdownTeam } from './runtime.js'; + import type { TeamRuntime } from './runtime.js'; +-import { teamReadConfig as readTeamConfig } from './team-ops.js'; ++import { ++ teamReadConfig as readTeamConfig, ++ teamReadVerification as readTeamVerificationState, ++ type TeamVerificationState, ++} from './team-ops.js'; + + interface CliInput { + teamName: string; +@@ -88,18 +93,58 @@ export async function shutdownWithForceFallback(teamName: string, cwd: string): + } + } + ++export function evaluateTeamVerificationRefreshGate( ++ verificationState: TeamVerificationState | null | undefined, ++): { allowed: boolean; reason: string } { ++ if (!verificationState) { ++ return { allowed: false, reason: 'team_verification_state_missing' }; ++ } ++ if (verificationState.phase !== 'complete') { ++ return { allowed: false, reason: 'team_verification_phase_incomplete' }; ++ } ++ if (verificationState.status === 'verified') { ++ return { allowed: true, reason: 'team_verification_verified' }; ++ } ++ return { ++ allowed: false, ++ reason: verificationState.status === 'failed' ++ ? 'team_verification_failed' ++ : 'team_verification_pending', ++ }; ++} ++ + export function scheduleFormalMemoryRefreshOnTeamComplete( + cwd: string, + teamName: string, + env: NodeJS.ProcessEnv = process.env, + spawnImpl?: FormalMemoryRefreshSpawn, ++ verificationState?: TeamVerificationState | null, + ): FormalMemoryRefreshScheduleResult { +- return scheduleFormalMemoryRefresh({ ++ const refreshTarget = { + cwd, + source: 'omx-team-runtime-complete', + teamName, + enableEnvKeys: [STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV], + disabledReason: 'team_completion_refresh_disabled', ++ } as const; ++ const plan = resolveFormalMemoryRefreshPlan(refreshTarget, env); ++ if (!plan.enabled) { ++ return { ++ scheduled: false, ++ reason: plan.reason, ++ plan, ++ }; ++ } ++ const gate = evaluateTeamVerificationRefreshGate(verificationState); ++ if (!gate.allowed) { ++ return { ++ scheduled: false, ++ reason: gate.reason, ++ plan, ++ }; ++ } ++ return scheduleFormalMemoryRefresh({ ++ ...refreshTarget, + }, env, spawnImpl); + } + +@@ -201,9 +246,13 @@ async function main(): Promise { + async function doShutdown(status: 'completed' | 'failed'): Promise { + pollActive = false; + finalStatus = status; ++ const teamCwd = runtime?.cwd ?? cwd; + + // 1. Collect task results + const taskResults = collectTaskResults(stateRoot, teamName); ++ const verificationState = status === 'completed' ++ ? await readTeamVerificationState(teamName, teamCwd) ++ : null; + + // 2. Shutdown team + if (runtime) { +@@ -221,9 +270,16 @@ async function main(): Promise { + + if (status === 'completed') { + try { +- const refresh = scheduleFormalMemoryRefreshOnTeamComplete(runtime?.cwd ?? cwd, teamName); ++ const refresh = scheduleFormalMemoryRefreshOnTeamComplete( ++ teamCwd, ++ teamName, ++ process.env, ++ undefined, ++ verificationState, ++ ); + if (!refresh.scheduled && ( + refresh.reason === 'refresh_script_unavailable' ++ || refresh.reason.startsWith('team_verification_') + || refresh.reason.startsWith('spawn_failed:') + )) { + process.stderr.write( +diff --git a/src/team/runtime.ts b/src/team/runtime.ts +index 9697084..f52949a 100644 +--- a/src/team/runtime.ts ++++ b/src/team/runtime.ts +@@ -61,6 +61,7 @@ import { + teamWriteMonitorSnapshot as writeMonitorSnapshot, + teamReadPhase as readTeamPhaseState, + teamWritePhase as writeTeamPhaseState, ++ teamWriteVerification as writeTeamVerificationState, + type TeamConfig, + type WorkerInfo, + type WorkerHeartbeat, +@@ -68,6 +69,7 @@ import { + type TeamTask, + type TeamMonitorSnapshotState, + type TeamPhaseState, ++ type TeamVerificationState, + type TeamWorkerIntegrationState, + type TeamGovernance, + type TeamPolicy, +@@ -1942,6 +1944,23 @@ export async function monitorTeam(teamName: string, cwd: string): Promise task.status === 'completed' && task.requires_code_change === true, ++ ); ++ const verifiedCodeTasks = completedCodeTasks.filter((task) => hasStructuredVerificationEvidence(task.result)); ++ const verificationState: TeamVerificationState = { ++ status: phase === 'failed' ++ ? 'failed' ++ : allTasksTerminal && verificationPendingTasks.length === 0 ++ ? 'verified' ++ : 'pending', ++ phase, ++ completed_code_task_ids: completedCodeTasks.map((task) => task.id), ++ verified_task_ids: verifiedCodeTasks.map((task) => task.id), ++ pending_task_ids: verificationPendingTasks.map((task) => task.id), ++ updated_at: new Date().toISOString(), ++ }; ++ await writeTeamVerificationState(sanitized, verificationState, cwd); + await syncRootTeamModeStateOnTerminalPhase(sanitized, phase, cwd); + + if (deadWorkerStall) { +diff --git a/src/team/state.ts b/src/team/state.ts +index 735d654..ce052c5 100644 +--- a/src/team/state.ts ++++ b/src/team/state.ts +@@ -790,6 +790,18 @@ export async function initTeamState( + }, + cwd + ); ++ await writeTeamVerification( ++ teamName, ++ { ++ status: 'pending', ++ phase: 'team-exec', ++ completed_code_task_ids: [], ++ verified_task_ids: [], ++ pending_task_ids: [], ++ updated_at: new Date().toISOString(), ++ }, ++ cwd, ++ ); + await writeTeamManifestV2( + { + schema_version: 2, +@@ -1747,6 +1759,15 @@ export interface TeamPhaseState { + updated_at: string; + } + ++export interface TeamVerificationState { ++ status: 'pending' | 'verified' | 'failed'; ++ phase: TeamPhase | TerminalPhase; ++ completed_code_task_ids: string[]; ++ verified_task_ids: string[]; ++ pending_task_ids: string[]; ++ updated_at: string; ++} ++ + function teamPhasePath(teamName: string, cwd: string): string { + return join(teamDir(teamName, cwd), 'phase.json'); + } +@@ -1755,6 +1776,10 @@ function monitorSnapshotPath(teamName: string, cwd: string): string { + return join(teamDir(teamName, cwd), 'monitor-snapshot.json'); + } + ++function verificationStatePath(teamName: string, cwd: string): string { ++ return join(teamDir(teamName, cwd), 'verification-state.json'); ++} ++ + export async function readMonitorSnapshot( + teamName: string, + cwd: string, +@@ -1770,6 +1795,50 @@ export async function writeMonitorSnapshot( + await writeMonitorSnapshotImpl(teamName, snapshot, cwd, monitorSnapshotPath, writeAtomic); + } + ++export async function readTeamVerification( ++ teamName: string, ++ cwd: string, ++): Promise { ++ try { ++ const raw = await readFile(verificationStatePath(teamName, cwd), 'utf-8'); ++ const parsed = JSON.parse(raw) as TeamVerificationState; ++ if (parsed.status !== 'pending' && parsed.status !== 'verified' && parsed.status !== 'failed') { ++ return null; ++ } ++ if ( ++ parsed.phase !== 'team-exec' ++ && parsed.phase !== 'team-verify' ++ && parsed.phase !== 'team-fix' ++ && parsed.phase !== 'complete' ++ && parsed.phase !== 'failed' ++ ) { ++ return null; ++ } ++ return { ++ status: parsed.status, ++ phase: parsed.phase, ++ completed_code_task_ids: Array.isArray(parsed.completed_code_task_ids) ? parsed.completed_code_task_ids : [], ++ verified_task_ids: Array.isArray(parsed.verified_task_ids) ? parsed.verified_task_ids : [], ++ pending_task_ids: Array.isArray(parsed.pending_task_ids) ? parsed.pending_task_ids : [], ++ updated_at: typeof parsed.updated_at === 'string' ? parsed.updated_at : new Date().toISOString(), ++ }; ++ } catch (error) { ++ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; ++ return null; ++ } ++} ++ ++export async function writeTeamVerification( ++ teamName: string, ++ verificationState: TeamVerificationState, ++ cwd: string, ++): Promise { ++ await writeAtomic( ++ verificationStatePath(teamName, cwd), ++ JSON.stringify(verificationState, null, 2), ++ ); ++} ++ + export async function readTeamPhase( + teamName: string, + cwd: string, +diff --git a/src/team/team-ops.ts b/src/team/team-ops.ts +index 24fecd0..34995ad 100644 +--- a/src/team/team-ops.ts ++++ b/src/team/team-ops.ts +@@ -43,6 +43,7 @@ export type { + TeamMonitorSnapshotState, + TeamWorkerIntegrationState, + TeamPhaseState, ++ TeamVerificationState, + } from './state.js'; + + // === Constants === +@@ -109,6 +110,8 @@ export { readMonitorSnapshot as teamReadMonitorSnapshot } from './state.js'; + export { writeMonitorSnapshot as teamWriteMonitorSnapshot } from './state.js'; + export { readTeamPhase as teamReadPhase } from './state.js'; + export { writeTeamPhase as teamWritePhase } from './state.js'; ++export { readTeamVerification as teamReadVerification } from './state.js'; ++export { writeTeamVerification as teamWriteVerification } from './state.js'; + + // === Worker status write === + export { writeWorkerStatus as teamWriteWorkerStatus } from './state.js'; +-- +2.50.1 (Apple Git-155) + diff --git a/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-strict-formal-memory.patch b/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-strict-formal-memory.patch new file mode 100644 index 0000000..28415d1 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-strict-formal-memory.patch @@ -0,0 +1,971 @@ +diff --git a/src/hooks/__tests__/agents-overlay.test.ts b/src/hooks/__tests__/agents-overlay.test.ts +index 721c65a..bc968a9 100644 +--- a/src/hooks/__tests__/agents-overlay.test.ts ++++ b/src/hooks/__tests__/agents-overlay.test.ts +@@ -42,6 +42,77 @@ function setMockCodexHome(codexHomePath: string): () => void { + }; + } + ++function setEnv(name: string, value?: string): () => void { ++ const previous = process.env[name]; ++ if (typeof value === "string") process.env[name] = value; ++ else delete process.env[name]; ++ return () => { ++ if (typeof previous === "string") process.env[name] = previous; ++ else delete process.env[name]; ++ }; ++} ++ ++async function createFormalMemoryFixture(workspaceRoot: string): Promise { ++ const memoryRoot = await mkdtemp(join(tmpdir(), "omx-overlay-memory-")); ++ const workspaceKey = "workspace-123"; ++ const memoryHome = join(memoryRoot, "workspaces", workspaceKey); ++ ++ await mkdir(join(memoryRoot, "workspaces"), { recursive: true }); ++ await writeFile( ++ join(memoryRoot, "workspaces", "index.json"), ++ JSON.stringify( ++ { ++ version: 1, ++ workspaces: { ++ [workspaceRoot.toLowerCase()]: { ++ key: workspaceKey, ++ path: workspaceRoot, ++ }, ++ }, ++ }, ++ null, ++ 2, ++ ), ++ ); ++ await mkdir(join(memoryHome, "instructions", "repo"), { recursive: true }); ++ await mkdir(join(memoryHome, "memories"), { recursive: true }); ++ await mkdir(join(memoryHome, "runtime"), { recursive: true }); ++ await writeFile( ++ join(memoryHome, "instructions", "repo", "GUIDE.md"), ++ "# Repo Guide\nUse formal repo guidance.\n", ++ ); ++ await writeFile( ++ join(memoryHome, "memories", "MEMORY.md"), ++ "# Workspace Memory\nFormal durable truth.\n", ++ ); ++ await writeFile( ++ join(memoryHome, "runtime", "active_context.md"), ++ "# Active Context\nCurrent task context from formal memory.\n", ++ ); ++ ++ return memoryRoot; ++} ++ ++async function createSharedGuideOnlyMemoryFixture(): Promise { ++ const memoryRoot = await mkdtemp(join(tmpdir(), "omx-overlay-shared-memory-")); ++ await mkdir(join(memoryRoot, "instructions", "company"), { recursive: true }); ++ await mkdir(join(memoryRoot, "instructions", "user"), { recursive: true }); ++ await mkdir(join(memoryRoot, "instructions", "local"), { recursive: true }); ++ await writeFile( ++ join(memoryRoot, "instructions", "company", "GUIDE.md"), ++ "# Company Guide\nShared company guidance.\n", ++ ); ++ await writeFile( ++ join(memoryRoot, "instructions", "user", "GUIDE.md"), ++ "# User Guide\nShared user guidance.\n", ++ ); ++ await writeFile( ++ join(memoryRoot, "instructions", "local", "GUIDE.md"), ++ "# Local Guide\nShared local guidance.\n", ++ ); ++ return memoryRoot; ++} ++ + describe("generateOverlay", () => { + let tempDir: string; + before(async () => { +@@ -165,6 +236,48 @@ describe("generateOverlay", () => { + assert.ok(!overlay.includes("Low priority thing")); + }); + ++ it("prefers formal memory summary over local project-memory.json in strict mode", async () => { ++ const memoryRoot = await createFormalMemoryFixture(tempDir); ++ const restoreStrict = setEnv("OMX_STRICT_MEMORY_MODE", "1"); ++ const restoreMemoryRoot = setEnv("OMX_EXTERNAL_MEMORY_ROOT", memoryRoot); ++ try { ++ await writeFile( ++ join(tempDir, ".omx", "project-memory.json"), ++ JSON.stringify({ ++ techStack: "Legacy local summary", ++ directives: [{ directive: "Local directive", priority: "high" }], ++ }), ++ ); ++ ++ const overlay = await generateOverlay(tempDir, "strict-project-memory"); ++ assert.match(overlay, /Current task context from formal memory/); ++ assert.match(overlay, /Formal durable truth/); ++ assert.match(overlay, /Use formal repo guidance/); ++ assert.doesNotMatch(overlay, /Legacy local summary/); ++ assert.doesNotMatch(overlay, /Local directive/); ++ } finally { ++ restoreStrict(); ++ restoreMemoryRoot(); ++ await rm(memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it("falls back to shared formal guides when strict mode has no registered workspace", async () => { ++ const memoryRoot = await createSharedGuideOnlyMemoryFixture(); ++ const restoreStrict = setEnv("OMX_STRICT_MEMORY_MODE", "1"); ++ const restoreMemoryRoot = setEnv("OMX_EXTERNAL_MEMORY_ROOT", memoryRoot); ++ try { ++ const overlay = await generateOverlay(tempDir, "strict-shared-guides"); ++ assert.match(overlay, /Shared company guidance/); ++ assert.match(overlay, /Shared user guidance/); ++ assert.match(overlay, /Shared local guidance/); ++ } finally { ++ restoreStrict(); ++ restoreMemoryRoot(); ++ await rm(memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ + it("enforces size cap (overlay <= 3500 chars)", async () => { + const longText = "A".repeat(5000); + await writeFile( +diff --git a/src/hooks/agents-overlay.ts b/src/hooks/agents-overlay.ts +index 002727c..655c220 100644 +--- a/src/hooks/agents-overlay.ts ++++ b/src/hooks/agents-overlay.ts +@@ -24,6 +24,11 @@ import { + omxProjectMemoryPath, + packageRoot, + } from "../utils/paths.js"; ++import { ++ buildFormalProjectMemorySummary, ++ readFormalMemoryContext, ++ resolveStrictMemoryConfig, ++} from "../integration/formal-memory.js"; + import { + isPlanningComplete, + readPlanningArtifacts, +@@ -250,6 +255,12 @@ async function readNotepadPriority(cwd: string): Promise { + } + + async function readProjectMemorySummary(cwd: string): Promise { ++ const strictConfig = resolveStrictMemoryConfig(); ++ if (strictConfig.strictMode) { ++ const context = await readFormalMemoryContext(cwd); ++ return buildFormalProjectMemorySummary(context); ++ } ++ + const memPath = omxProjectMemoryPath(cwd); + if (!existsSync(memPath)) return ""; + +@@ -277,8 +288,8 @@ function getCompactionInstructions(): string { + return [ + "Before context compaction, preserve critical state:", + "1. Write progress checkpoint via state_write MCP tool", +- "2. Save key decisions to notepad via notepad_write_working", +- "3. If context is >80% full, proactively checkpoint state", ++ "2. Save run-local decisions to notepad via notepad_write_working", ++ "3. Promote durable memory only through the external refresh pipeline", + ].join("\n"); + } + +diff --git a/src/mcp/memory-server.ts b/src/mcp/memory-server.ts +index db8a04b..036fda5 100644 +--- a/src/mcp/memory-server.ts ++++ b/src/mcp/memory-server.ts +@@ -15,6 +15,12 @@ import { existsSync } from 'fs'; + import { parseNotepadPruneDaysOld } from './memory-validation.js'; + import { autoStartStdioMcpServer } from './bootstrap.js'; + import { resolveWorkingDirectoryForState } from './state-paths.js'; ++import { ++ appendMemoryIntakeEntry, ++ buildFormalProjectMemoryView, ++ readFormalMemoryContext, ++ resolveStrictMemoryConfig, ++} from '../integration/formal-memory.js'; + + function getMemoryPath(wd: string): string { + return join(wd, '.omx', 'project-memory.json'); +@@ -43,7 +49,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + // Project Memory tools + { + name: 'project_memory_read', +- description: 'Read project memory. Can read full memory or a specific section.', ++ description: ++ 'Read project memory. In strict mode this returns a formal workspace memory summary instead of local .omx/project-memory.json.', + inputSchema: { + type: 'object', + properties: { +@@ -54,7 +61,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + }, + { + name: 'project_memory_write', +- description: 'Write/update project memory. Can replace entirely or merge.', ++ description: ++ 'Write/update local project memory. In strict mode this is denied so durable updates must flow through the external memory pipeline.', + inputSchema: { + type: 'object', + properties: { +@@ -67,7 +75,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + }, + { + name: 'project_memory_add_note', +- description: 'Add a categorized note to project memory.', ++ description: ++ 'Add a categorized note to project memory. In strict mode this downgrades into a run-local intake artifact.', + inputSchema: { + type: 'object', + properties: { +@@ -80,7 +89,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + }, + { + name: 'project_memory_add_directive', +- description: 'Add a persistent directive to project memory.', ++ description: ++ 'Add a persistent directive to project memory. In strict mode this downgrades into a run-local intake artifact.', + inputSchema: { + type: 'object', + properties: { +@@ -164,22 +174,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ + ], + })); + +-server.setRequestHandler(CallToolRequestSchema, async (request) => { +- const { name, arguments: args } = request.params; +- const a = (args || {}) as Record; ++export async function handleMemoryToolCall( ++ name: string, ++ a: Record = {}, ++ env: Record = process.env, ++) { + let wd: string; + try { + wd = resolveWorkingDirectoryForState(a.workingDirectory as string | undefined); + } catch (error) { +- return { +- content: [{ type: 'text' as const, text: JSON.stringify({ error: (error as Error).message }) }], +- isError: true, +- }; ++ return errorText({ error: (error as Error).message }); + } ++ const strictConfig = resolveStrictMemoryConfig(env); + + switch (name) { + // === Project Memory === + case 'project_memory_read': { ++ if (strictConfig.strictMode) { ++ const context = await readFormalMemoryContext(wd, env); ++ return text(buildFormalProjectMemoryView(context, a.section as string | undefined)); ++ } + const memPath = getMemoryPath(wd); + if (!existsSync(memPath)) { + return text({ exists: false }); +@@ -198,6 +212,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { + } + + case 'project_memory_write': { ++ if (strictConfig.strictMode) { ++ return errorText({ ++ error: ++ 'Strict integration mode forbids direct project_memory_write. Use the formal memory pipeline or a run-local intake artifact instead.', ++ decision: 'deny', ++ }); ++ } + const memPath = getMemoryPath(wd); + await mkdir(join(wd, '.omx'), { recursive: true }); + const merge = a.merge as boolean; +@@ -218,6 +239,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { + } + + case 'project_memory_add_note': { ++ if (strictConfig.strictMode) { ++ const intake = await appendMemoryIntakeEntry({ ++ cwd: wd, ++ kind: 'note', ++ content: String(a.content as string), ++ metadata: { ++ category: a.category as string, ++ }, ++ source: 'project_memory_add_note', ++ }); ++ return text({ ++ success: true, ++ decision: 'downgrade', ++ downgradedTo: 'memory-intake-queue', ++ intake, ++ }); ++ } + const memPath = getMemoryPath(wd); + await mkdir(join(wd, '.omx'), { recursive: true }); + let data: ProjectMemory = {}; +@@ -239,6 +277,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { + } + + case 'project_memory_add_directive': { ++ if (strictConfig.strictMode) { ++ const intake = await appendMemoryIntakeEntry({ ++ cwd: wd, ++ kind: 'directive', ++ content: String(a.directive as string), ++ metadata: { ++ priority: (a.priority as string) || 'normal', ++ context: a.context as string | undefined, ++ }, ++ source: 'project_memory_add_directive', ++ }); ++ return text({ ++ success: true, ++ decision: 'downgrade', ++ downgradedTo: 'memory-intake-queue', ++ intake, ++ }); ++ } + const memPath = getMemoryPath(wd); + await mkdir(join(wd, '.omx'), { recursive: true }); + let data: ProjectMemory = {}; +@@ -412,12 +468,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { + default: + return { content: [{ type: 'text' as const, text: `Unknown tool: ${name}` }], isError: true }; + } ++} ++server.setRequestHandler(CallToolRequestSchema, async (request) => { ++ const { name, arguments: args } = request.params; ++ return handleMemoryToolCall(name, (args || {}) as Record); + }); + + function text(data: unknown) { + return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] }; + } + ++function errorText(data: unknown) { ++ return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }], isError: true }; ++} ++ + function extractSection(content: string, section: string): string { + const header = `## ${section.toUpperCase()}`; + const idx = content.indexOf(header); +diff --git a/src/integration/formal-memory.ts b/src/integration/formal-memory.ts +new file mode 100644 +index 0000000..3365262 +--- /dev/null ++++ b/src/integration/formal-memory.ts +@@ -0,0 +1,278 @@ ++import { createHash } from 'crypto'; ++import { existsSync } from 'fs'; ++import { appendFile, mkdir, readFile } from 'fs/promises'; ++import { join, resolve } from 'path'; ++ ++import { codexHome } from '../utils/paths.js'; ++ ++const WORKSPACE_INDEX_RELATIVE_PATH = join('workspaces', 'index.json'); ++const SHARED_GUIDE_RELATIVE_PATHS = [ ++ ['company', join('instructions', 'company', 'GUIDE.md')], ++ ['user', join('instructions', 'user', 'GUIDE.md')], ++ ['local', join('instructions', 'local', 'GUIDE.md')], ++] as const; ++ ++export interface StrictMemoryConfig { ++ strictMode: boolean; ++ memoryRoot: string; ++} ++ ++export interface FormalMemoryContext { ++ source: 'formal-memory'; ++ strictMode: boolean; ++ memoryRoot: string; ++ workspace: { ++ registered: boolean; ++ key?: string; ++ root?: string; ++ memoryHome?: string; ++ }; ++ repoGuide: string; ++ workspaceMemory: string; ++ activeContext: string; ++ sharedGuides: Record; ++} ++ ++function defaultExternalMemoryRoot(): string { ++ return join(codexHome(), 'memory'); ++} ++ ++function normalizeLookupPath(value: string): string { ++ return resolve(value).replace(/\\/g, '/').toLowerCase(); ++} ++ ++async function readTextIfExists(filePath: string): Promise { ++ try { ++ return await readFile(filePath, 'utf-8'); ++ } catch (error) { ++ if ((error as NodeJS.ErrnoException).code === 'ENOENT') { ++ return ''; ++ } ++ throw error; ++ } ++} ++ ++function summarizeSnippet(value: string, maxChars = 240): string { ++ const normalized = value ++ .replace(/^#+\s+/gm, '') ++ .replace(/\s+/g, ' ') ++ .trim(); ++ ++ if (!normalized) return ''; ++ if (normalized.length <= maxChars) return normalized; ++ return `${normalized.slice(0, maxChars - 3)}...`; ++} ++ ++export function parseBooleanFlag(value: unknown): boolean | undefined { ++ if (typeof value !== 'string') return undefined; ++ const normalized = value.trim().toLowerCase(); ++ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; ++ if (['0', 'false', 'no', 'off'].includes(normalized)) return false; ++ return undefined; ++} ++ ++export function resolveStrictMemoryConfig( ++ env: Record = process.env, ++): StrictMemoryConfig { ++ return { ++ strictMode: parseBooleanFlag(env.OMX_STRICT_MEMORY_MODE) ?? false, ++ memoryRoot: resolve(env.OMX_EXTERNAL_MEMORY_ROOT || defaultExternalMemoryRoot()), ++ }; ++} ++ ++export async function loadWorkspaceIndex(memoryRoot: string): Promise | null> { ++ const indexPath = join(memoryRoot, WORKSPACE_INDEX_RELATIVE_PATH); ++ if (!existsSync(indexPath)) return null; ++ return JSON.parse(await readFile(indexPath, 'utf-8')) as Record; ++} ++ ++export async function resolveFormalWorkspaceNode( ++ cwd: string, ++ memoryRoot: string, ++): Promise<{ key: string; root: string; memoryHome: string } | null> { ++ const index = await loadWorkspaceIndex(memoryRoot); ++ const workspaces = index?.workspaces as Record | undefined; ++ if (!workspaces || typeof workspaces !== 'object') return null; ++ ++ const lookupCwd = normalizeLookupPath(cwd); ++ let bestMatch: { key: string; root: string; memoryHome: string; lookupPath: string } | null = null; ++ ++ for (const [storedPath, entry] of Object.entries(workspaces)) { ++ if (!entry || typeof entry !== 'object' || typeof entry.key !== 'string') continue; ++ const registeredPath = typeof entry.path === 'string' ? entry.path : storedPath; ++ const candidatePath = normalizeLookupPath(registeredPath); ++ if (lookupCwd !== candidatePath && !lookupCwd.startsWith(`${candidatePath}/`)) { ++ continue; ++ } ++ ++ if (!bestMatch || candidatePath.length > bestMatch.lookupPath.length) { ++ bestMatch = { ++ key: entry.key, ++ root: resolve(registeredPath), ++ memoryHome: join(memoryRoot, 'workspaces', entry.key), ++ lookupPath: candidatePath, ++ }; ++ } ++ } ++ ++ if (!bestMatch) return null; ++ return { ++ key: bestMatch.key, ++ root: bestMatch.root, ++ memoryHome: bestMatch.memoryHome, ++ }; ++} ++ ++export async function readFormalMemoryContext( ++ cwd: string, ++ env: Record = process.env, ++): Promise { ++ const config = resolveStrictMemoryConfig(env); ++ const workspace = await resolveFormalWorkspaceNode(cwd, config.memoryRoot); ++ const sharedGuides = Object.fromEntries( ++ await Promise.all( ++ SHARED_GUIDE_RELATIVE_PATHS.map(async ([key, relativePath]) => [ ++ key, ++ await readTextIfExists(join(config.memoryRoot, relativePath)), ++ ]), ++ ), ++ ); ++ ++ if (!workspace) { ++ return { ++ source: 'formal-memory', ++ strictMode: config.strictMode, ++ memoryRoot: config.memoryRoot, ++ workspace: { ++ registered: false, ++ }, ++ repoGuide: '', ++ workspaceMemory: '', ++ activeContext: '', ++ sharedGuides, ++ }; ++ } ++ ++ const repoGuidePath = join(workspace.memoryHome, 'instructions', 'repo', 'GUIDE.md'); ++ const workspaceMemoryPath = join(workspace.memoryHome, 'memories', 'MEMORY.md'); ++ const activeContextPath = join(workspace.memoryHome, 'runtime', 'active_context.md'); ++ ++ const [repoGuide, workspaceMemory, activeContext] = await Promise.all([ ++ readTextIfExists(repoGuidePath), ++ readTextIfExists(workspaceMemoryPath), ++ readTextIfExists(activeContextPath), ++ ]); ++ ++ return { ++ source: 'formal-memory', ++ strictMode: config.strictMode, ++ memoryRoot: config.memoryRoot, ++ workspace: { ++ registered: true, ++ key: workspace.key, ++ root: workspace.root, ++ memoryHome: workspace.memoryHome, ++ }, ++ repoGuide, ++ workspaceMemory, ++ activeContext, ++ sharedGuides, ++ }; ++} ++ ++export function buildFormalProjectMemorySummary(context: FormalMemoryContext): string { ++ const parts: string[] = []; ++ ++ if (context.activeContext) { ++ parts.push(`- Active Context: ${summarizeSnippet(context.activeContext)}`); ++ } ++ if (context.workspaceMemory) { ++ parts.push(`- Workspace Memory: ${summarizeSnippet(context.workspaceMemory)}`); ++ } ++ if (context.repoGuide) { ++ parts.push(`- Repo Guide: ${summarizeSnippet(context.repoGuide)}`); ++ } ++ ++ if (parts.length === 0) { ++ for (const [key, value] of Object.entries(context.sharedGuides)) { ++ if (!value) continue; ++ parts.push(`- Shared Guide (${key}): ${summarizeSnippet(value)}`); ++ } ++ } ++ ++ return parts.join('\n'); ++} ++ ++export function buildFormalProjectMemoryView( ++ context: FormalMemoryContext, ++ section?: string, ++): Record { ++ const view = { ++ source: 'formal-memory', ++ strictMode: context.strictMode, ++ workspace: context.workspace, ++ summary: buildFormalProjectMemorySummary(context), ++ sections: { ++ activeContext: context.activeContext, ++ workspaceMemory: context.workspaceMemory, ++ repoGuide: context.repoGuide, ++ sharedGuides: context.sharedGuides, ++ }, ++ }; ++ ++ if (section && section !== 'all') { ++ return { ++ ...view, ++ requestedSection: section, ++ message: ++ 'Strict integration mode exposes a formal summary view instead of legacy .omx/project-memory.json sections.', ++ }; ++ } ++ ++ return view; ++} ++ ++function buildIntakeEntryId(payload: Record): string { ++ const hash = createHash('sha1') ++ .update(JSON.stringify(payload)) ++ .digest('hex') ++ .slice(0, 12); ++ return `intake-${hash}`; ++} ++ ++export async function appendMemoryIntakeEntry({ ++ cwd, ++ kind, ++ content, ++ metadata = {}, ++ source, ++}: { ++ cwd: string; ++ kind: string; ++ content: string; ++ metadata?: Record; ++ source: string; ++}): Promise<{ path: string; entry: Record }> { ++ const createdAt = new Date().toISOString(); ++ const entry = { ++ id: buildIntakeEntryId({ ++ kind, ++ content, ++ metadata, ++ source, ++ createdAt, ++ }), ++ kind, ++ content, ++ metadata, ++ source, ++ created_at: createdAt, ++ }; ++ const intakePath = join(cwd, '.omx', 'memory-intake.jsonl'); ++ await mkdir(join(cwd, '.omx'), { recursive: true }); ++ await appendFile(intakePath, `${JSON.stringify(entry)}\n`, 'utf-8'); ++ return { ++ path: intakePath, ++ entry, ++ }; ++} +diff --git a/src/integration/__tests__/formal-memory.test.ts b/src/integration/__tests__/formal-memory.test.ts +new file mode 100644 +index 0000000..e85d63f +--- /dev/null ++++ b/src/integration/__tests__/formal-memory.test.ts +@@ -0,0 +1,141 @@ ++import { describe, it } from 'node:test'; ++import assert from 'node:assert/strict'; ++import { mkdtemp, mkdir, writeFile, readFile, rm } from 'fs/promises'; ++import { existsSync } from 'fs'; ++import { join } from 'path'; ++import { tmpdir } from 'os'; ++ ++import { ++ appendMemoryIntakeEntry, ++ buildFormalProjectMemorySummary, ++ buildFormalProjectMemoryView, ++ readFormalMemoryContext, ++ resolveStrictMemoryConfig, ++} from '../formal-memory.js'; ++ ++async function makeFixture() { ++ const workspaceRoot = await mkdtemp(join(tmpdir(), 'omx-formal-workspace-')); ++ const memoryRoot = await mkdtemp(join(tmpdir(), 'omx-formal-memory-')); ++ const workspaceKey = 'workspace-123'; ++ const memoryHome = join(memoryRoot, 'workspaces', workspaceKey); ++ ++ await mkdir(join(memoryRoot, 'workspaces'), { recursive: true }); ++ await writeFile( ++ join(memoryRoot, 'workspaces', 'index.json'), ++ JSON.stringify( ++ { ++ version: 1, ++ workspaces: { ++ [workspaceRoot.toLowerCase()]: { ++ key: workspaceKey, ++ path: workspaceRoot, ++ }, ++ }, ++ }, ++ null, ++ 2, ++ ), ++ ); ++ ++ await mkdir(join(memoryRoot, 'instructions', 'company'), { recursive: true }); ++ await mkdir(join(memoryRoot, 'instructions', 'user'), { recursive: true }); ++ await mkdir(join(memoryRoot, 'instructions', 'local'), { recursive: true }); ++ await mkdir(join(memoryHome, 'instructions', 'repo'), { recursive: true }); ++ await mkdir(join(memoryHome, 'memories'), { recursive: true }); ++ await mkdir(join(memoryHome, 'runtime'), { recursive: true }); ++ ++ await writeFile(join(memoryRoot, 'instructions', 'company', 'GUIDE.md'), '# Company\nCompany guide\n'); ++ await writeFile(join(memoryRoot, 'instructions', 'user', 'GUIDE.md'), '# User\nUser guide\n'); ++ await writeFile(join(memoryRoot, 'instructions', 'local', 'GUIDE.md'), '# Local\nLocal guide\n'); ++ await writeFile(join(memoryHome, 'instructions', 'repo', 'GUIDE.md'), '# Repo\nUse ESM modules.\n'); ++ await writeFile(join(memoryHome, 'memories', 'MEMORY.md'), '# Memory\nWorkspace durable truth.\n'); ++ await writeFile(join(memoryHome, 'runtime', 'active_context.md'), '# Active\nCurrent task context.\n'); ++ ++ return { ++ workspaceRoot, ++ memoryRoot, ++ }; ++} ++ ++describe('integration/formal-memory', () => { ++ it('resolves strict config from env', () => { ++ const config = resolveStrictMemoryConfig({ ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: '/tmp/custom-memory-root', ++ }); ++ ++ assert.equal(config.strictMode, true); ++ assert.equal(config.memoryRoot, '/tmp/custom-memory-root'); ++ }); ++ ++ it('reads formal memory context and summary for a registered workspace', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const context = await readFormalMemoryContext(fixture.workspaceRoot, { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }); ++ const summary = buildFormalProjectMemorySummary(context); ++ const view = buildFormalProjectMemoryView(context); ++ ++ assert.equal(context.workspace.registered, true); ++ assert.match(summary, /Current task context/); ++ assert.match(summary, /Workspace durable truth/); ++ assert.match(summary, /Use ESM modules/); ++ assert.equal(view.source, 'formal-memory'); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('falls back to shared guides when the workspace is not registered', async () => { ++ const workspaceRoot = await mkdtemp(join(tmpdir(), 'omx-formal-unregistered-')); ++ const memoryRoot = await mkdtemp(join(tmpdir(), 'omx-formal-shared-')); ++ ++ try { ++ await mkdir(join(memoryRoot, 'instructions', 'company'), { recursive: true }); ++ await mkdir(join(memoryRoot, 'instructions', 'user'), { recursive: true }); ++ await mkdir(join(memoryRoot, 'instructions', 'local'), { recursive: true }); ++ await writeFile(join(memoryRoot, 'instructions', 'company', 'GUIDE.md'), '# Company\nShared company guidance.\n'); ++ await writeFile(join(memoryRoot, 'instructions', 'user', 'GUIDE.md'), '# User\nShared user guidance.\n'); ++ await writeFile(join(memoryRoot, 'instructions', 'local', 'GUIDE.md'), '# Local\nShared local guidance.\n'); ++ ++ const context = await readFormalMemoryContext(workspaceRoot, { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: memoryRoot, ++ }); ++ const summary = buildFormalProjectMemorySummary(context); ++ ++ assert.equal(context.workspace.registered, false); ++ assert.match(summary, /Shared company guidance/); ++ assert.match(summary, /Shared user guidance/); ++ assert.match(summary, /Shared local guidance/); ++ } finally { ++ await rm(workspaceRoot, { recursive: true, force: true }); ++ await rm(memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('downgrades durable candidates into a run-local intake queue', async () => { ++ const workspaceRoot = await mkdtemp(join(tmpdir(), 'omx-formal-intake-')); ++ ++ try { ++ const result = await appendMemoryIntakeEntry({ ++ cwd: workspaceRoot, ++ kind: 'note', ++ content: 'Queue this observation.', ++ metadata: { category: 'architecture' }, ++ source: 'test', ++ }); ++ ++ assert.equal(existsSync(result.path), true); ++ const raw = await readFile(result.path, 'utf-8'); ++ assert.match(raw, /Queue this observation/); ++ assert.match(raw, /"kind":"note"/); ++ } finally { ++ await rm(workspaceRoot, { recursive: true, force: true }); ++ } ++ }); ++}); +diff --git a/src/mcp/__tests__/memory-server-strict-mode.test.ts b/src/mcp/__tests__/memory-server-strict-mode.test.ts +new file mode 100644 +index 0000000..6740654 +--- /dev/null ++++ b/src/mcp/__tests__/memory-server-strict-mode.test.ts +@@ -0,0 +1,188 @@ ++import { describe, it } from 'node:test'; ++import assert from 'node:assert/strict'; ++import { mkdtemp, mkdir, readFile, rm, writeFile } from 'fs/promises'; ++import { existsSync } from 'fs'; ++import { join } from 'path'; ++import { tmpdir } from 'os'; ++ ++async function makeFixture() { ++ const workspaceRoot = await mkdtemp(join(tmpdir(), 'omx-memory-server-workspace-')); ++ const memoryRoot = await mkdtemp(join(tmpdir(), 'omx-memory-server-memory-')); ++ const workspaceKey = 'workspace-123'; ++ const memoryHome = join(memoryRoot, 'workspaces', workspaceKey); ++ ++ await mkdir(join(memoryRoot, 'workspaces'), { recursive: true }); ++ await writeFile( ++ join(memoryRoot, 'workspaces', 'index.json'), ++ JSON.stringify( ++ { ++ version: 1, ++ workspaces: { ++ [workspaceRoot.toLowerCase()]: { ++ key: workspaceKey, ++ path: workspaceRoot, ++ }, ++ }, ++ }, ++ null, ++ 2, ++ ), ++ ); ++ ++ await mkdir(join(memoryHome, 'instructions', 'repo'), { recursive: true }); ++ await mkdir(join(memoryHome, 'memories'), { recursive: true }); ++ await mkdir(join(memoryHome, 'runtime'), { recursive: true }); ++ await writeFile(join(memoryHome, 'instructions', 'repo', 'GUIDE.md'), '# Repo\nUse pnpm.\n'); ++ await writeFile(join(memoryHome, 'memories', 'MEMORY.md'), '# Memory\nWorkspace durable truth.\n'); ++ await writeFile(join(memoryHome, 'runtime', 'active_context.md'), '# Active\nCurrent task context.\n'); ++ await mkdir(join(workspaceRoot, '.omx'), { recursive: true }); ++ await writeFile( ++ join(workspaceRoot, '.omx', 'project-memory.json'), ++ JSON.stringify({ techStack: 'Legacy local memory' }, null, 2), ++ ); ++ ++ return { ++ workspaceRoot, ++ memoryRoot, ++ }; ++} ++ ++async function loadMemoryServerModule() { ++ const previous = process.env.OMX_MEMORY_SERVER_DISABLE_AUTO_START; ++ process.env.OMX_MEMORY_SERVER_DISABLE_AUTO_START = '1'; ++ try { ++ return await import(`../memory-server.js?strict-memory-test=${Date.now()}-${Math.random()}`); ++ } finally { ++ if (typeof previous === 'string') process.env.OMX_MEMORY_SERVER_DISABLE_AUTO_START = previous; ++ else delete process.env.OMX_MEMORY_SERVER_DISABLE_AUTO_START; ++ } ++} ++ ++function parseToolPayload(result: { content: Array<{ text: string }>; isError?: boolean }) { ++ return { ++ ...result, ++ data: JSON.parse(result.content[0].text), ++ }; ++} ++ ++describe('mcp/memory-server strict mode behavior', () => { ++ it('reads formal memory instead of local project-memory.json in strict mode', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const mod = await loadMemoryServerModule(); ++ const result = parseToolPayload( ++ await mod.handleMemoryToolCall( ++ 'project_memory_read', ++ { ++ workingDirectory: fixture.workspaceRoot, ++ }, ++ { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ), ++ ); ++ ++ assert.equal(result.isError, undefined); ++ assert.equal(result.data.source, 'formal-memory'); ++ assert.match(result.data.summary, /Current task context/); ++ assert.doesNotMatch(JSON.stringify(result.data), /Legacy local memory/); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('denies direct project_memory_write in strict mode', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const mod = await loadMemoryServerModule(); ++ const result = parseToolPayload( ++ await mod.handleMemoryToolCall( ++ 'project_memory_write', ++ { ++ workingDirectory: fixture.workspaceRoot, ++ memory: { should: 'not persist' }, ++ }, ++ { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ), ++ ); ++ ++ assert.equal(result.isError, true); ++ assert.equal(result.data.decision, 'deny'); ++ ++ const localRaw = await readFile(join(fixture.workspaceRoot, '.omx', 'project-memory.json'), 'utf-8'); ++ assert.match(localRaw, /Legacy local memory/); ++ assert.doesNotMatch(localRaw, /should/); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('downgrades project_memory_add_note into memory-intake.jsonl in strict mode', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const mod = await loadMemoryServerModule(); ++ const result = parseToolPayload( ++ await mod.handleMemoryToolCall( ++ 'project_memory_add_note', ++ { ++ workingDirectory: fixture.workspaceRoot, ++ category: 'architecture', ++ content: 'Queue this note.', ++ }, ++ { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ), ++ ); ++ ++ assert.equal(result.data.decision, 'downgrade'); ++ assert.equal(existsSync(join(fixture.workspaceRoot, '.omx', 'memory-intake.jsonl')), true); ++ const intakeRaw = await readFile(join(fixture.workspaceRoot, '.omx', 'memory-intake.jsonl'), 'utf-8'); ++ assert.match(intakeRaw, /Queue this note/); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('downgrades project_memory_add_directive into memory-intake.jsonl in strict mode', async () => { ++ const fixture = await makeFixture(); ++ ++ try { ++ const mod = await loadMemoryServerModule(); ++ const result = parseToolPayload( ++ await mod.handleMemoryToolCall( ++ 'project_memory_add_directive', ++ { ++ workingDirectory: fixture.workspaceRoot, ++ directive: 'Keep promotion gated.', ++ priority: 'high', ++ context: 'review', ++ }, ++ { ++ OMX_STRICT_MEMORY_MODE: 'true', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ), ++ ); ++ ++ assert.equal(result.data.decision, 'downgrade'); ++ const intakeRaw = await readFile(join(fixture.workspaceRoot, '.omx', 'memory-intake.jsonl'), 'utf-8'); ++ assert.match(intakeRaw, /Keep promotion gated/); ++ assert.match(intakeRaw, /"kind":"directive"/); ++ } finally { ++ await rm(fixture.workspaceRoot, { recursive: true, force: true }); ++ await rm(fixture.memoryRoot, { recursive: true, force: true }); ++ } ++ }); ++}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-strict-memory-refresh-bridge.patch b/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-strict-memory-refresh-bridge.patch new file mode 100644 index 0000000..6b70640 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-strict-memory-refresh-bridge.patch @@ -0,0 +1,358 @@ +diff --git a/src/cli/__tests__/formal-memory-refresh.test.ts b/src/cli/__tests__/formal-memory-refresh.test.ts +new file mode 100644 +index 0000000..03147f0 +--- /dev/null ++++ b/src/cli/__tests__/formal-memory-refresh.test.ts +@@ -0,0 +1,155 @@ ++import { describe, it } from "node:test"; ++import assert from "node:assert/strict"; ++import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; ++import { tmpdir } from "node:os"; ++import { join } from "node:path"; ++ ++import { ++ resolveFormalMemoryRefreshPlan, ++ resolveFormalMemoryRefreshScript, ++ scheduleFormalMemoryRefreshOnExit, ++} from "../index.js"; ++ ++async function createRefreshFixture(): Promise<{ ++ root: string; ++ memoryRoot: string; ++ scriptPath: string; ++}> { ++ const root = await mkdtemp(join(tmpdir(), "omx-formal-refresh-")); ++ const memoryRoot = join(root, "memory"); ++ const scriptPath = join(root, "scripts", "refresh_memory.py"); ++ await mkdir(memoryRoot, { recursive: true }); ++ await mkdir(join(root, "scripts"), { recursive: true }); ++ await writeFile(scriptPath, "#!/usr/bin/env python3\n", "utf-8"); ++ return { root, memoryRoot, scriptPath }; ++} ++ ++describe("formal memory refresh bridge", () => { ++ it("prefers explicit refresh script override", () => { ++ const env = { ++ OMX_EXTERNAL_MEMORY_REFRESH_SCRIPT: "/tmp/custom-refresh.py", ++ OMX_EXTERNAL_MEMORY_ROOT: "/tmp/ignored-memory-root", ++ } satisfies NodeJS.ProcessEnv; ++ assert.equal(resolveFormalMemoryRefreshScript(env), "/tmp/custom-refresh.py"); ++ }); ++ ++ it("infers the refresh script from the external memory root sibling scripts directory", async () => { ++ const fixture = await createRefreshFixture(); ++ try { ++ const env = { ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ } satisfies NodeJS.ProcessEnv; ++ assert.equal(resolveFormalMemoryRefreshScript(env), fixture.scriptPath); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++ ++ it("keeps refresh disabled until strict mode and refresh-on-exit are both enabled", () => { ++ const disabledStrict = resolveFormalMemoryRefreshPlan("/repo", "session-1", {}); ++ assert.equal(disabledStrict.enabled, false); ++ assert.equal(disabledStrict.reason, "strict_mode_disabled"); ++ ++ const disabledRefresh = resolveFormalMemoryRefreshPlan("/repo", "session-1", { ++ OMX_STRICT_MEMORY_MODE: "1", ++ }); ++ assert.equal(disabledRefresh.enabled, false); ++ assert.equal(disabledRefresh.reason, "refresh_on_exit_disabled"); ++ }); ++ ++ it("skips the refresh bridge for team worker processes", async () => { ++ const fixture = await createRefreshFixture(); ++ try { ++ const plan = resolveFormalMemoryRefreshPlan("/repo", "session-2", { ++ OMX_STRICT_MEMORY_MODE: "1", ++ OMX_STRICT_MEMORY_REFRESH_ON_EXIT: "1", ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ OMX_TEAM_WORKER: "alpha/worker-1", ++ }); ++ assert.equal(plan.enabled, false); ++ assert.equal(plan.reason, "team_worker_process"); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++ ++ it("spawns a detached refresh process for leader or standalone sessions", async () => { ++ const fixture = await createRefreshFixture(); ++ try { ++ let captured: ++ | { ++ command: string; ++ args: readonly string[]; ++ options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: "ignore"; ++ }; ++ } ++ | undefined; ++ let unrefCalled = false; ++ ++ const result = scheduleFormalMemoryRefreshOnExit( ++ "/repo", ++ "session-3", ++ { ++ OMX_STRICT_MEMORY_MODE: "1", ++ OMX_STRICT_MEMORY_REFRESH_ON_EXIT: "1", ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ OMX_EXTERNAL_MEMORY_REFRESH_PYTHON: "python3.12", ++ }, ++ ((command: string, args: readonly string[], options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: "ignore"; ++ }) => { ++ captured = { command, args, options }; ++ return { ++ unref() { ++ unrefCalled = true; ++ }, ++ }; ++ }) as never, ++ ); ++ ++ assert.equal(result.scheduled, true); ++ assert.equal(result.reason, "scheduled"); ++ assert.equal(captured?.command, "python3.12"); ++ assert.deepEqual(captured?.args, [fixture.scriptPath, "--workspace-root", "/repo"]); ++ assert.equal(captured?.options.cwd, "/repo"); ++ assert.equal(captured?.options.detached, true); ++ assert.equal(captured?.options.stdio, "ignore"); ++ assert.equal(captured?.options.env.OMX_EXTERNAL_MEMORY_REFRESH_SOURCE, "omx-postlaunch"); ++ assert.equal(captured?.options.env.OMX_EXTERNAL_MEMORY_REFRESH_SESSION_ID, "session-3"); ++ assert.equal(unrefCalled, true); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++ ++ it("reports spawn failures without throwing", async () => { ++ const fixture = await createRefreshFixture(); ++ try { ++ const result = scheduleFormalMemoryRefreshOnExit( ++ "/repo", ++ "session-4", ++ { ++ OMX_STRICT_MEMORY_MODE: "1", ++ OMX_STRICT_MEMORY_REFRESH_ON_EXIT: "1", ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ (() => { ++ throw new Error("boom"); ++ }) as never, ++ ); ++ ++ assert.equal(result.scheduled, false); ++ assert.match(result.reason, /^spawn_failed:boom$/); ++ assert.equal(result.plan.enabled, true); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++}); +diff --git a/src/cli/index.ts b/src/cli/index.ts +index 0a20389..576f1e6 100644 +--- a/src/cli/index.ts ++++ b/src/cli/index.ts +@@ -4,7 +4,7 @@ + */ + + import { execFileSync, spawn } from "child_process"; +-import { basename, dirname, join } from "path"; ++import { basename, dirname, join, resolve } from "path"; + import { existsSync, readFileSync } from "fs"; + import { constants as osConstants } from "os"; + import { setup, SETUP_SCOPES, type SetupScope } from "./setup.js"; +@@ -179,6 +179,161 @@ const REASONING_KEY = "model_reasoning_effort"; + const MODEL_INSTRUCTIONS_FILE_KEY = "model_instructions_file"; + const TEAM_WORKER_LAUNCH_ARGS_ENV = "OMX_TEAM_WORKER_LAUNCH_ARGS"; + const TEAM_INHERIT_LEADER_FLAGS_ENV = "OMX_TEAM_INHERIT_LEADER_FLAGS"; ++const STRICT_MEMORY_MODE_ENV = "OMX_STRICT_MEMORY_MODE"; ++const STRICT_MEMORY_REFRESH_ON_EXIT_ENV = "OMX_STRICT_MEMORY_REFRESH_ON_EXIT"; ++const EXTERNAL_MEMORY_ROOT_ENV = "OMX_EXTERNAL_MEMORY_ROOT"; ++const EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_SCRIPT"; ++const EXTERNAL_MEMORY_REFRESH_PYTHON_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_PYTHON"; ++const TEAM_WORKER_ENV = "OMX_TEAM_WORKER"; ++ ++export interface FormalMemoryRefreshPlan { ++ enabled: boolean; ++ strictMode: boolean; ++ reason: ++ | "strict_mode_disabled" ++ | "refresh_on_exit_disabled" ++ | "team_worker_process" ++ | "refresh_script_unavailable" ++ | "enabled"; ++ scriptPath?: string; ++ command?: string; ++ args?: string[]; ++ childEnv?: NodeJS.ProcessEnv; ++} ++ ++export interface FormalMemoryRefreshScheduleResult { ++ scheduled: boolean; ++ reason: string; ++ plan: FormalMemoryRefreshPlan; ++} ++ ++type FormalMemoryRefreshSpawn = ( ++ command: string, ++ args: readonly string[], ++ options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: "ignore"; ++ }, ++) => { ++ unref?: () => void; ++}; ++ ++function parseOptionalBooleanEnv(value: string | undefined): boolean | undefined { ++ if (typeof value !== "string") return undefined; ++ const normalized = value.trim().toLowerCase(); ++ if (["1", "true", "yes", "on"].includes(normalized)) return true; ++ if (["0", "false", "no", "off"].includes(normalized)) return false; ++ return undefined; ++} ++ ++export function resolveFormalMemoryRefreshScript( ++ env: NodeJS.ProcessEnv = process.env, ++): string | null { ++ const explicit = env[EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV]?.trim(); ++ if (explicit) return explicit; ++ ++ const memoryRoot = env[EXTERNAL_MEMORY_ROOT_ENV]?.trim(); ++ if (!memoryRoot) return null; ++ ++ const candidate = join(dirname(resolve(memoryRoot)), "scripts", "refresh_memory.py"); ++ return existsSync(candidate) ? candidate : null; ++} ++ ++export function resolveFormalMemoryRefreshPlan( ++ cwd: string, ++ sessionId: string, ++ env: NodeJS.ProcessEnv = process.env, ++): FormalMemoryRefreshPlan { ++ const strictMode = parseOptionalBooleanEnv(env[STRICT_MEMORY_MODE_ENV]) === true; ++ if (!strictMode) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "strict_mode_disabled", ++ }; ++ } ++ ++ if (parseOptionalBooleanEnv(env[STRICT_MEMORY_REFRESH_ON_EXIT_ENV]) !== true) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "refresh_on_exit_disabled", ++ }; ++ } ++ ++ if (typeof env[TEAM_WORKER_ENV] === "string" && env[TEAM_WORKER_ENV].trim() !== "") { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "team_worker_process", ++ }; ++ } ++ ++ const scriptPath = resolveFormalMemoryRefreshScript(env); ++ if (!scriptPath) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "refresh_script_unavailable", ++ }; ++ } ++ ++ const command = env[EXTERNAL_MEMORY_REFRESH_PYTHON_ENV]?.trim() || "python3"; ++ return { ++ enabled: true, ++ strictMode, ++ reason: "enabled", ++ scriptPath, ++ command, ++ args: [scriptPath, "--workspace-root", cwd], ++ childEnv: { ++ ...process.env, ++ ...env, ++ OMX_EXTERNAL_MEMORY_REFRESH_SOURCE: "omx-postlaunch", ++ OMX_EXTERNAL_MEMORY_REFRESH_SESSION_ID: sessionId, ++ }, ++ }; ++} ++ ++export function scheduleFormalMemoryRefreshOnExit( ++ cwd: string, ++ sessionId: string, ++ env: NodeJS.ProcessEnv = process.env, ++ spawnImpl: FormalMemoryRefreshSpawn = spawn, ++): FormalMemoryRefreshScheduleResult { ++ const plan = resolveFormalMemoryRefreshPlan(cwd, sessionId, env); ++ if (!plan.enabled || !plan.command || !plan.args || !plan.childEnv) { ++ return { ++ scheduled: false, ++ reason: plan.reason, ++ plan, ++ }; ++ } ++ ++ try { ++ const child = spawnImpl(plan.command, plan.args, { ++ cwd, ++ env: plan.childEnv, ++ detached: true, ++ stdio: "ignore", ++ }); ++ child.unref?.(); ++ return { ++ scheduled: true, ++ reason: "scheduled", ++ plan, ++ }; ++ } catch (error) { ++ const message = error instanceof Error ? error.message : String(error); ++ return { ++ scheduled: false, ++ reason: `spawn_failed:${message}`, ++ plan, ++ }; ++ } ++} + const OMX_BYPASS_DEFAULT_SYSTEM_PROMPT_ENV = "OMX_BYPASS_DEFAULT_SYSTEM_PROMPT"; + const OMX_MODEL_INSTRUCTIONS_FILE_ENV = "OMX_MODEL_INSTRUCTIONS_FILE"; + const OMX_RALPH_APPEND_INSTRUCTIONS_FILE_ENV = +@@ -2157,6 +2312,21 @@ async function postLaunch( + ); + } + ++ // 3.5 Trigger external formal-memory refresh on exit (strict mode opt-in, best effort). ++ try { ++ const refresh = scheduleFormalMemoryRefreshOnExit(cwd, sessionId); ++ if (!refresh.scheduled && ( ++ refresh.reason === "refresh_script_unavailable" ++ || refresh.reason.startsWith("spawn_failed:") ++ )) { ++ console.warn(`[omx] postLaunch: external formal-memory refresh skipped: ${refresh.reason}`); ++ } ++ } catch (err) { ++ console.error( ++ `[omx] postLaunch: external formal-memory refresh failed: ${err instanceof Error ? err.message : err}`, ++ ); ++ } ++ + // 4. Send session-end lifecycle notification (best effort) + try { + const { notifyLifecycle } = await import("../notifications/index.js"); diff --git a/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-team-complete-memory-refresh.patch b/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-team-complete-memory-refresh.patch new file mode 100644 index 0000000..fa38eed --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-team-complete-memory-refresh.patch @@ -0,0 +1,600 @@ +diff --git a/src/cli/index.ts b/src/cli/index.ts +index 576f1e6..7ae59d4 100644 +--- a/src/cli/index.ts ++++ b/src/cli/index.ts +@@ -4,7 +4,7 @@ + */ + + import { execFileSync, spawn } from "child_process"; +-import { basename, dirname, join, resolve } from "path"; ++import { basename, dirname, join } from "path"; + import { existsSync, readFileSync } from "fs"; + import { constants as osConstants } from "os"; + import { setup, SETUP_SCOPES, type SetupScope } from "./setup.js"; +@@ -94,6 +94,15 @@ import { + type NotifyTempContract, + type ParseNotifyTempContractResult, + } from "../notifications/temp-contract.js"; ++import { ++ scheduleFormalMemoryRefresh, ++ resolveFormalMemoryRefreshPlan as resolveSharedFormalMemoryRefreshPlan, ++ resolveFormalMemoryRefreshScript as resolveSharedFormalMemoryRefreshScript, ++ STRICT_MEMORY_REFRESH_ON_EXIT_ENV, ++ type FormalMemoryRefreshPlan, ++ type FormalMemoryRefreshScheduleResult, ++ type FormalMemoryRefreshSpawn, ++} from "../integration/formal-memory-refresh.js"; + + export function resolveNotifyFallbackWatcherScript(pkgRoot = getPackageRoot()): string { + return join(pkgRoot, "dist", "scripts", "notify-fallback-watcher.js"); +@@ -179,66 +188,11 @@ const REASONING_KEY = "model_reasoning_effort"; + const MODEL_INSTRUCTIONS_FILE_KEY = "model_instructions_file"; + const TEAM_WORKER_LAUNCH_ARGS_ENV = "OMX_TEAM_WORKER_LAUNCH_ARGS"; + const TEAM_INHERIT_LEADER_FLAGS_ENV = "OMX_TEAM_INHERIT_LEADER_FLAGS"; +-const STRICT_MEMORY_MODE_ENV = "OMX_STRICT_MEMORY_MODE"; +-const STRICT_MEMORY_REFRESH_ON_EXIT_ENV = "OMX_STRICT_MEMORY_REFRESH_ON_EXIT"; +-const EXTERNAL_MEMORY_ROOT_ENV = "OMX_EXTERNAL_MEMORY_ROOT"; +-const EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_SCRIPT"; +-const EXTERNAL_MEMORY_REFRESH_PYTHON_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_PYTHON"; +-const TEAM_WORKER_ENV = "OMX_TEAM_WORKER"; +- +-export interface FormalMemoryRefreshPlan { +- enabled: boolean; +- strictMode: boolean; +- reason: +- | "strict_mode_disabled" +- | "refresh_on_exit_disabled" +- | "team_worker_process" +- | "refresh_script_unavailable" +- | "enabled"; +- scriptPath?: string; +- command?: string; +- args?: string[]; +- childEnv?: NodeJS.ProcessEnv; +-} +- +-export interface FormalMemoryRefreshScheduleResult { +- scheduled: boolean; +- reason: string; +- plan: FormalMemoryRefreshPlan; +-} +- +-type FormalMemoryRefreshSpawn = ( +- command: string, +- args: readonly string[], +- options: { +- cwd: string; +- env: NodeJS.ProcessEnv; +- detached: boolean; +- stdio: "ignore"; +- }, +-) => { +- unref?: () => void; +-}; +- +-function parseOptionalBooleanEnv(value: string | undefined): boolean | undefined { +- if (typeof value !== "string") return undefined; +- const normalized = value.trim().toLowerCase(); +- if (["1", "true", "yes", "on"].includes(normalized)) return true; +- if (["0", "false", "no", "off"].includes(normalized)) return false; +- return undefined; +-} + + export function resolveFormalMemoryRefreshScript( + env: NodeJS.ProcessEnv = process.env, + ): string | null { +- const explicit = env[EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV]?.trim(); +- if (explicit) return explicit; +- +- const memoryRoot = env[EXTERNAL_MEMORY_ROOT_ENV]?.trim(); +- if (!memoryRoot) return null; +- +- const candidate = join(dirname(resolve(memoryRoot)), "scripts", "refresh_memory.py"); +- return existsSync(candidate) ? candidate : null; ++ return resolveSharedFormalMemoryRefreshScript(env); + } + + export function resolveFormalMemoryRefreshPlan( +@@ -246,55 +200,13 @@ export function resolveFormalMemoryRefreshPlan( + sessionId: string, + env: NodeJS.ProcessEnv = process.env, + ): FormalMemoryRefreshPlan { +- const strictMode = parseOptionalBooleanEnv(env[STRICT_MEMORY_MODE_ENV]) === true; +- if (!strictMode) { +- return { +- enabled: false, +- strictMode, +- reason: "strict_mode_disabled", +- }; +- } +- +- if (parseOptionalBooleanEnv(env[STRICT_MEMORY_REFRESH_ON_EXIT_ENV]) !== true) { +- return { +- enabled: false, +- strictMode, +- reason: "refresh_on_exit_disabled", +- }; +- } +- +- if (typeof env[TEAM_WORKER_ENV] === "string" && env[TEAM_WORKER_ENV].trim() !== "") { +- return { +- enabled: false, +- strictMode, +- reason: "team_worker_process", +- }; +- } +- +- const scriptPath = resolveFormalMemoryRefreshScript(env); +- if (!scriptPath) { +- return { +- enabled: false, +- strictMode, +- reason: "refresh_script_unavailable", +- }; +- } +- +- const command = env[EXTERNAL_MEMORY_REFRESH_PYTHON_ENV]?.trim() || "python3"; +- return { +- enabled: true, +- strictMode, +- reason: "enabled", +- scriptPath, +- command, +- args: [scriptPath, "--workspace-root", cwd], +- childEnv: { +- ...process.env, +- ...env, +- OMX_EXTERNAL_MEMORY_REFRESH_SOURCE: "omx-postlaunch", +- OMX_EXTERNAL_MEMORY_REFRESH_SESSION_ID: sessionId, +- }, +- }; ++ return resolveSharedFormalMemoryRefreshPlan({ ++ cwd, ++ source: "omx-postlaunch", ++ sessionId, ++ enableEnvKeys: [STRICT_MEMORY_REFRESH_ON_EXIT_ENV], ++ disabledReason: "refresh_on_exit_disabled", ++ }, env); + } + + export function scheduleFormalMemoryRefreshOnExit( +@@ -303,36 +215,13 @@ export function scheduleFormalMemoryRefreshOnExit( + env: NodeJS.ProcessEnv = process.env, + spawnImpl: FormalMemoryRefreshSpawn = spawn, + ): FormalMemoryRefreshScheduleResult { +- const plan = resolveFormalMemoryRefreshPlan(cwd, sessionId, env); +- if (!plan.enabled || !plan.command || !plan.args || !plan.childEnv) { +- return { +- scheduled: false, +- reason: plan.reason, +- plan, +- }; +- } +- +- try { +- const child = spawnImpl(plan.command, plan.args, { +- cwd, +- env: plan.childEnv, +- detached: true, +- stdio: "ignore", +- }); +- child.unref?.(); +- return { +- scheduled: true, +- reason: "scheduled", +- plan, +- }; +- } catch (error) { +- const message = error instanceof Error ? error.message : String(error); +- return { +- scheduled: false, +- reason: `spawn_failed:${message}`, +- plan, +- }; +- } ++ return scheduleFormalMemoryRefresh({ ++ cwd, ++ source: "omx-postlaunch", ++ sessionId, ++ enableEnvKeys: [STRICT_MEMORY_REFRESH_ON_EXIT_ENV], ++ disabledReason: "refresh_on_exit_disabled", ++ }, env, spawnImpl); + } + const OMX_BYPASS_DEFAULT_SYSTEM_PROMPT_ENV = "OMX_BYPASS_DEFAULT_SYSTEM_PROMPT"; + const OMX_MODEL_INSTRUCTIONS_FILE_ENV = "OMX_MODEL_INSTRUCTIONS_FILE"; +diff --git a/src/integration/formal-memory-refresh.ts b/src/integration/formal-memory-refresh.ts +new file mode 100644 +index 0000000..b765ea6 +--- /dev/null ++++ b/src/integration/formal-memory-refresh.ts +@@ -0,0 +1,180 @@ ++import { spawn } from "child_process"; ++import { existsSync } from "fs"; ++import { dirname, join, resolve } from "path"; ++ ++export const STRICT_MEMORY_MODE_ENV = "OMX_STRICT_MEMORY_MODE"; ++export const STRICT_MEMORY_REFRESH_ON_EXIT_ENV = "OMX_STRICT_MEMORY_REFRESH_ON_EXIT"; ++export const STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV = ++ "OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE"; ++export const EXTERNAL_MEMORY_ROOT_ENV = "OMX_EXTERNAL_MEMORY_ROOT"; ++export const EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_SCRIPT"; ++export const EXTERNAL_MEMORY_REFRESH_PYTHON_ENV = "OMX_EXTERNAL_MEMORY_REFRESH_PYTHON"; ++export const TEAM_WORKER_ENV = "OMX_TEAM_WORKER"; ++ ++export interface FormalMemoryRefreshPlan { ++ enabled: boolean; ++ strictMode: boolean; ++ reason: string; ++ scriptPath?: string; ++ command?: string; ++ args?: string[]; ++ childEnv?: NodeJS.ProcessEnv; ++} ++ ++export interface FormalMemoryRefreshScheduleResult { ++ scheduled: boolean; ++ reason: string; ++ plan: FormalMemoryRefreshPlan; ++} ++ ++export interface FormalMemoryRefreshTarget { ++ cwd: string; ++ source: string; ++ enableEnvKeys: readonly string[]; ++ disabledReason: string; ++ sessionId?: string; ++ teamName?: string; ++ skipTeamWorker?: boolean; ++} ++ ++export type FormalMemoryRefreshSpawn = ( ++ command: string, ++ args: readonly string[], ++ options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: "ignore"; ++ }, ++) => { ++ unref?: () => void; ++}; ++ ++function parseOptionalBooleanEnv(value: string | undefined): boolean | undefined { ++ if (typeof value !== "string") return undefined; ++ const normalized = value.trim().toLowerCase(); ++ if (["1", "true", "yes", "on"].includes(normalized)) return true; ++ if (["0", "false", "no", "off"].includes(normalized)) return false; ++ return undefined; ++} ++ ++function isRefreshEnabled( ++ envKeys: readonly string[], ++ env: NodeJS.ProcessEnv, ++): boolean { ++ return envKeys.some((key) => parseOptionalBooleanEnv(env[key]) === true); ++} ++ ++export function resolveFormalMemoryRefreshScript( ++ env: NodeJS.ProcessEnv = process.env, ++): string | null { ++ const explicit = env[EXTERNAL_MEMORY_REFRESH_SCRIPT_ENV]?.trim(); ++ if (explicit) return explicit; ++ ++ const memoryRoot = env[EXTERNAL_MEMORY_ROOT_ENV]?.trim(); ++ if (!memoryRoot) return null; ++ ++ const candidate = join(dirname(resolve(memoryRoot)), "scripts", "refresh_memory.py"); ++ return existsSync(candidate) ? candidate : null; ++} ++ ++export function resolveFormalMemoryRefreshPlan( ++ target: FormalMemoryRefreshTarget, ++ env: NodeJS.ProcessEnv = process.env, ++): FormalMemoryRefreshPlan { ++ const strictMode = parseOptionalBooleanEnv(env[STRICT_MEMORY_MODE_ENV]) === true; ++ if (!strictMode) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "strict_mode_disabled", ++ }; ++ } ++ ++ if (!isRefreshEnabled(target.enableEnvKeys, env)) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: target.disabledReason, ++ }; ++ } ++ ++ if ( ++ target.skipTeamWorker !== false ++ && typeof env[TEAM_WORKER_ENV] === "string" ++ && env[TEAM_WORKER_ENV].trim() !== "" ++ ) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "team_worker_process", ++ }; ++ } ++ ++ const scriptPath = resolveFormalMemoryRefreshScript(env); ++ if (!scriptPath) { ++ return { ++ enabled: false, ++ strictMode, ++ reason: "refresh_script_unavailable", ++ }; ++ } ++ ++ const command = env[EXTERNAL_MEMORY_REFRESH_PYTHON_ENV]?.trim() || "python3"; ++ return { ++ enabled: true, ++ strictMode, ++ reason: "enabled", ++ scriptPath, ++ command, ++ args: [scriptPath, "--workspace-root", target.cwd], ++ childEnv: { ++ ...process.env, ++ ...env, ++ OMX_EXTERNAL_MEMORY_REFRESH_SOURCE: target.source, ++ ...(target.sessionId ++ ? { OMX_EXTERNAL_MEMORY_REFRESH_SESSION_ID: target.sessionId } ++ : {}), ++ ...(target.teamName ++ ? { OMX_EXTERNAL_MEMORY_REFRESH_TEAM_NAME: target.teamName } ++ : {}), ++ }, ++ }; ++} ++ ++export function scheduleFormalMemoryRefresh( ++ target: FormalMemoryRefreshTarget, ++ env: NodeJS.ProcessEnv = process.env, ++ spawnImpl: FormalMemoryRefreshSpawn = spawn, ++): FormalMemoryRefreshScheduleResult { ++ const plan = resolveFormalMemoryRefreshPlan(target, env); ++ if (!plan.enabled || !plan.command || !plan.args || !plan.childEnv) { ++ return { ++ scheduled: false, ++ reason: plan.reason, ++ plan, ++ }; ++ } ++ ++ try { ++ const child = spawnImpl(plan.command, plan.args, { ++ cwd: target.cwd, ++ env: plan.childEnv, ++ detached: true, ++ stdio: "ignore", ++ }); ++ child.unref?.(); ++ return { ++ scheduled: true, ++ reason: "scheduled", ++ plan, ++ }; ++ } catch (error) { ++ const message = error instanceof Error ? error.message : String(error); ++ return { ++ scheduled: false, ++ reason: `spawn_failed:${message}`, ++ plan, ++ }; ++ } ++} +diff --git a/src/team/__tests__/runtime-cli.test.ts b/src/team/__tests__/runtime-cli.test.ts +index 77ed0cd..9b6849f 100644 +--- a/src/team/__tests__/runtime-cli.test.ts ++++ b/src/team/__tests__/runtime-cli.test.ts +@@ -1,9 +1,10 @@ + import { describe, it } from 'node:test'; + import assert from 'node:assert/strict'; +-import { mkdtemp, rm } from 'fs/promises'; + import { existsSync } from 'fs'; +-import { join } from 'path'; ++import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'; + import { tmpdir } from 'os'; ++import { join } from 'path'; ++ + import { initTeamState, createTask, readTeamConfig, saveTeamConfig } from '../state.js'; + + async function loadRuntimeCliModule() { +@@ -11,6 +12,20 @@ async function loadRuntimeCliModule() { + return await import('../runtime-cli.js'); + } + ++async function createRefreshFixture(): Promise<{ ++ root: string; ++ memoryRoot: string; ++ scriptPath: string; ++}> { ++ const root = await mkdtemp(join(tmpdir(), 'omx-runtime-cli-refresh-')); ++ const memoryRoot = join(root, 'memory'); ++ const scriptPath = join(root, 'scripts', 'refresh_memory.py'); ++ await mkdir(memoryRoot, { recursive: true }); ++ await mkdir(join(root, 'scripts'), { recursive: true }); ++ await writeFile(scriptPath, '#!/usr/bin/env python3\n', 'utf-8'); ++ return { root, memoryRoot, scriptPath }; ++} ++ + describe('runtime-cli helpers', () => { + it('normalizes per-worker providers and validates supported values', async () => { + const runtimeCli = await loadRuntimeCliModule(); +@@ -74,6 +89,14 @@ describe('runtime-cli helpers', () => { + assert.equal(liveBehavior.fixingWithNoWorkers, false); + }); + ++ it('does not treat leader pane as a worker pane for dead-worker detection', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); ++ ++ const result = runtimeCli.detectDeadWorkerFailure(1, 1, true, 'team-exec'); ++ assert.equal(result.deadWorkerFailure, true); ++ assert.equal(result.fixingWithNoWorkers, false); ++ }); ++ + it('gracefully shuts down only when the leader explicitly requests shutdown', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-cli-shutdown-')); + const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT; +@@ -128,13 +151,89 @@ describe('runtime-cli helpers', () => { + await rm(cwd, { recursive: true, force: true }); + } + }); +-}); + ++ it('keeps team-complete refresh disabled until strict mode and explicit team gate are enabled', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); + +- it('does not treat leader pane as a worker pane for dead-worker detection', async () => { ++ const disabledStrict = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', {}); ++ assert.equal(disabledStrict.scheduled, false); ++ assert.equal(disabledStrict.reason, 'strict_mode_disabled'); ++ ++ const disabledGate = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', { ++ OMX_STRICT_MEMORY_MODE: '1', ++ }); ++ assert.equal(disabledGate.scheduled, false); ++ assert.equal(disabledGate.reason, 'team_completion_refresh_disabled'); ++ }); ++ ++ it('schedules detached formal-memory refresh for completed leader teams', async () => { + const runtimeCli = await loadRuntimeCliModule(); ++ const fixture = await createRefreshFixture(); ++ try { ++ let captured: ++ | { ++ command: string; ++ args: readonly string[]; ++ options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: 'ignore'; ++ }; ++ } ++ | undefined; ++ let unrefCalled = false; ++ ++ const result = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete( ++ '/repo', ++ 'team-alpha', ++ { ++ OMX_STRICT_MEMORY_MODE: '1', ++ OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ OMX_EXTERNAL_MEMORY_REFRESH_PYTHON: 'python3.12', ++ }, ++ ((command: string, args: readonly string[], options: { ++ cwd: string; ++ env: NodeJS.ProcessEnv; ++ detached: boolean; ++ stdio: 'ignore'; ++ }) => { ++ captured = { command, args, options }; ++ return { ++ unref() { ++ unrefCalled = true; ++ }, ++ }; ++ }) as never, ++ ); ++ ++ assert.equal(result.scheduled, true); ++ assert.equal(captured?.command, 'python3.12'); ++ assert.deepEqual(captured?.args, [fixture.scriptPath, '--workspace-root', '/repo']); ++ assert.equal(captured?.options.env.OMX_EXTERNAL_MEMORY_REFRESH_SOURCE, 'omx-team-runtime-complete'); ++ assert.equal(captured?.options.env.OMX_EXTERNAL_MEMORY_REFRESH_TEAM_NAME, 'team-alpha'); ++ assert.equal(captured?.options.detached, true); ++ assert.equal(unrefCalled, true); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); + +- const result = runtimeCli.detectDeadWorkerFailure(1, 1, true, 'team-exec'); +- assert.equal(result.deadWorkerFailure, true); +- assert.equal(result.fixingWithNoWorkers, false); ++ it('skips team-complete refresh when the process is marked as a team worker', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); ++ const fixture = await createRefreshFixture(); ++ try { ++ const result = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'team-worker', { ++ OMX_STRICT_MEMORY_MODE: '1', ++ OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ OMX_TEAM_WORKER: 'team-worker/worker-1', ++ }); ++ assert.equal(result.scheduled, false); ++ assert.equal(result.reason, 'team_worker_process'); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } + }); ++}); +diff --git a/src/team/runtime-cli.ts b/src/team/runtime-cli.ts +index 28fcf23..9379a4e 100644 +--- a/src/team/runtime-cli.ts ++++ b/src/team/runtime-cli.ts +@@ -9,6 +9,12 @@ + import { readdirSync, readFileSync } from 'fs'; + import { writeFile, rename } from 'fs/promises'; + import { join } from 'path'; ++import { ++ scheduleFormalMemoryRefresh, ++ STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV, ++ type FormalMemoryRefreshScheduleResult, ++ type FormalMemoryRefreshSpawn, ++} from '../integration/formal-memory-refresh.js'; + import { startTeam, monitorTeam, shutdownTeam } from './runtime.js'; + import type { TeamRuntime } from './runtime.js'; + import { teamReadConfig as readTeamConfig } from './team-ops.js'; +@@ -82,6 +88,21 @@ export async function shutdownWithForceFallback(teamName: string, cwd: string): + } + } + ++export function scheduleFormalMemoryRefreshOnTeamComplete( ++ cwd: string, ++ teamName: string, ++ env: NodeJS.ProcessEnv = process.env, ++ spawnImpl?: FormalMemoryRefreshSpawn, ++): FormalMemoryRefreshScheduleResult { ++ return scheduleFormalMemoryRefresh({ ++ cwd, ++ source: 'omx-team-runtime-complete', ++ teamName, ++ enableEnvKeys: [STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV], ++ disabledReason: 'team_completion_refresh_disabled', ++ }, env, spawnImpl); ++} ++ + export function detectDeadWorkerFailure( + deadWorkerCount: number, + liveWorkerPaneCount: number, +@@ -198,6 +219,22 @@ async function main(): Promise { + } + } + ++ if (status === 'completed') { ++ try { ++ const refresh = scheduleFormalMemoryRefreshOnTeamComplete(runtime?.cwd ?? cwd, teamName); ++ if (!refresh.scheduled && ( ++ refresh.reason === 'refresh_script_unavailable' ++ || refresh.reason.startsWith('spawn_failed:') ++ )) { ++ process.stderr.write( ++ `[runtime-cli] strict formal-memory refresh skipped: ${refresh.reason}\n`, ++ ); ++ } ++ } catch (err) { ++ process.stderr.write(`[runtime-cli] strict formal-memory refresh error: ${err}\n`); ++ } ++ } ++ + const duration = (Date.now() - startTime) / 1000; + const output: CliOutput = { + status: finalStatus, diff --git a/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-team-verification-evidence-gate.patch b/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-team-verification-evidence-gate.patch new file mode 100644 index 0000000..950cc71 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/patches/oh-my-codex-upstream-team-verification-evidence-gate.patch @@ -0,0 +1,472 @@ +diff --git a/src/team/__tests__/runtime-cli.test.ts b/src/team/__tests__/runtime-cli.test.ts +index 9b6849f..7c71a4a 100644 +--- a/src/team/__tests__/runtime-cli.test.ts ++++ b/src/team/__tests__/runtime-cli.test.ts +@@ -5,7 +5,14 @@ import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'; + import { tmpdir } from 'os'; + import { join } from 'path'; + +-import { initTeamState, createTask, readTeamConfig, saveTeamConfig } from '../state.js'; ++import { ++ initTeamState, ++ createTask, ++ readTeamConfig, ++ saveTeamConfig, ++ writeTeamVerification, ++ type TeamVerificationState, ++} from '../state.js'; + + async function loadRuntimeCliModule() { + process.env.OMX_RUNTIME_CLI_DISABLE_AUTO_START = '1'; +@@ -26,6 +33,20 @@ async function createRefreshFixture(): Promise<{ + return { root, memoryRoot, scriptPath }; + } + ++function buildVerificationState( ++ overrides: Partial = {}, ++): TeamVerificationState { ++ return { ++ status: 'verified', ++ phase: 'complete', ++ completed_code_task_ids: ['task-1'], ++ verified_task_ids: ['task-1'], ++ pending_task_ids: [], ++ updated_at: new Date().toISOString(), ++ ...overrides, ++ }; ++} ++ + describe('runtime-cli helpers', () => { + it('normalizes per-worker providers and validates supported values', async () => { + const runtimeCli = await loadRuntimeCliModule(); +@@ -154,19 +175,48 @@ describe('runtime-cli helpers', () => { + + it('keeps team-complete refresh disabled until strict mode and explicit team gate are enabled', async () => { + const runtimeCli = await loadRuntimeCliModule(); ++ const verificationState = buildVerificationState(); + +- const disabledStrict = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', {}); ++ const disabledStrict = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', {}, undefined, verificationState); + assert.equal(disabledStrict.scheduled, false); + assert.equal(disabledStrict.reason, 'strict_mode_disabled'); + + const disabledGate = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'alpha', { + OMX_STRICT_MEMORY_MODE: '1', +- }); ++ }, undefined, verificationState); + assert.equal(disabledGate.scheduled, false); + assert.equal(disabledGate.reason, 'team_completion_refresh_disabled'); + }); + +- it('schedules detached formal-memory refresh for completed leader teams', async () => { ++ it('blocks team-complete refresh until verification evidence is marked complete and verified', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); ++ const fixture = await createRefreshFixture(); ++ try { ++ const baseEnv = { ++ OMX_STRICT_MEMORY_MODE: '1', ++ OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }; ++ ++ const missing = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete('/repo', 'team-alpha', baseEnv); ++ assert.equal(missing.scheduled, false); ++ assert.equal(missing.reason, 'team_verification_state_missing'); ++ ++ const pending = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete( ++ '/repo', ++ 'team-alpha', ++ baseEnv, ++ undefined, ++ buildVerificationState({ status: 'pending', phase: 'team-verify', pending_task_ids: ['task-1'] }), ++ ); ++ assert.equal(pending.scheduled, false); ++ assert.equal(pending.reason, 'team_verification_phase_incomplete'); ++ } finally { ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); ++ ++ it('schedules detached formal-memory refresh for completed leader teams with verified evidence', async () => { + const runtimeCli = await loadRuntimeCliModule(); + const fixture = await createRefreshFixture(); + try { +@@ -206,6 +256,7 @@ describe('runtime-cli helpers', () => { + }, + }; + }) as never, ++ buildVerificationState(), + ); + + assert.equal(result.scheduled, true); +@@ -229,11 +280,51 @@ describe('runtime-cli helpers', () => { + OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', + OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, + OMX_TEAM_WORKER: 'team-worker/worker-1', +- }); ++ }, undefined, buildVerificationState()); + assert.equal(result.scheduled, false); + assert.equal(result.reason, 'team_worker_process'); + } finally { + await rm(fixture.root, { recursive: true, force: true }); + } + }); ++ ++ it('can pre-read verified team state before shutdown cleanup and still schedule refresh', async () => { ++ const runtimeCli = await loadRuntimeCliModule(); ++ const fixture = await createRefreshFixture(); ++ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-cli-verify-state-')); ++ try { ++ await initTeamState('verified-refresh', 'task', 'executor', 1, cwd); ++ await writeTeamVerification('verified-refresh', buildVerificationState({ ++ completed_code_task_ids: ['task-42'], ++ verified_task_ids: ['task-42'], ++ }), cwd); ++ ++ let capturedCommand: string | undefined; ++ const verificationState = buildVerificationState({ ++ completed_code_task_ids: ['task-42'], ++ verified_task_ids: ['task-42'], ++ }); ++ ++ const result = runtimeCli.scheduleFormalMemoryRefreshOnTeamComplete( ++ cwd, ++ 'verified-refresh', ++ { ++ OMX_STRICT_MEMORY_MODE: '1', ++ OMX_STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE: '1', ++ OMX_EXTERNAL_MEMORY_ROOT: fixture.memoryRoot, ++ }, ++ ((command: string) => { ++ capturedCommand = command; ++ return { unref() {} }; ++ }) as never, ++ verificationState, ++ ); ++ ++ assert.equal(result.scheduled, true); ++ assert.equal(capturedCommand, 'python3'); ++ } finally { ++ await rm(cwd, { recursive: true, force: true }); ++ await rm(fixture.root, { recursive: true, force: true }); ++ } ++ }); + }); +diff --git a/src/team/__tests__/runtime.test.ts b/src/team/__tests__/runtime.test.ts +index 4d3b4c3..a9e2e28 100644 +--- a/src/team/__tests__/runtime.test.ts ++++ b/src/team/__tests__/runtime.test.ts +@@ -18,6 +18,7 @@ import { + writeAtomic, + readTask, + readMonitorSnapshot, ++ readTeamVerification, + claimTask, + transitionTaskStatus, + writeWorkerStatus, +@@ -1793,6 +1794,10 @@ process.on('SIGTERM', () => { + first?.recommendations.some((r) => r.includes(`task-${task.id}`) && r.includes('Verification evidence missing')), + true, + ); ++ const firstVerification = await readTeamVerification('team-verify-gate', cwd); ++ assert.equal(firstVerification?.status, 'pending'); ++ assert.equal(firstVerification?.phase, 'team-verify'); ++ assert.deepEqual(firstVerification?.pending_task_ids, [task.id]); + + const taskPath = join(cwd, '.omx', 'state', 'team', 'team-verify-gate', 'tasks', `task-${task.id}.json`); + const fromDisk = JSON.parse(await readFile(taskPath, 'utf-8')) as Record; +@@ -1807,6 +1812,11 @@ process.on('SIGTERM', () => { + const second = await monitorTeam('team-verify-gate', cwd); + assert.ok(second); + assert.equal(second?.phase, 'complete'); ++ const secondVerification = await readTeamVerification('team-verify-gate', cwd); ++ assert.equal(secondVerification?.status, 'verified'); ++ assert.equal(secondVerification?.phase, 'complete'); ++ assert.deepEqual(secondVerification?.verified_task_ids, [task.id]); ++ assert.deepEqual(secondVerification?.pending_task_ids, []); + } finally { + if (typeof prevTeamStateRoot === 'string') process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot; + else delete process.env.OMX_TEAM_STATE_ROOT; +diff --git a/src/team/runtime-cli.ts b/src/team/runtime-cli.ts +index 9379a4e..7eb1064 100644 +--- a/src/team/runtime-cli.ts ++++ b/src/team/runtime-cli.ts +@@ -10,6 +10,7 @@ import { readdirSync, readFileSync } from 'fs'; + import { writeFile, rename } from 'fs/promises'; + import { join } from 'path'; + import { ++ resolveFormalMemoryRefreshPlan, + scheduleFormalMemoryRefresh, + STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV, + type FormalMemoryRefreshScheduleResult, +@@ -17,7 +18,11 @@ import { + } from '../integration/formal-memory-refresh.js'; + import { startTeam, monitorTeam, shutdownTeam } from './runtime.js'; + import type { TeamRuntime } from './runtime.js'; +-import { teamReadConfig as readTeamConfig } from './team-ops.js'; ++import { ++ teamReadConfig as readTeamConfig, ++ teamReadVerification as readTeamVerificationState, ++ type TeamVerificationState, ++} from './team-ops.js'; + + interface CliInput { + teamName: string; +@@ -88,18 +93,58 @@ export async function shutdownWithForceFallback(teamName: string, cwd: string): + } + } + ++export function evaluateTeamVerificationRefreshGate( ++ verificationState: TeamVerificationState | null | undefined, ++): { allowed: boolean; reason: string } { ++ if (!verificationState) { ++ return { allowed: false, reason: 'team_verification_state_missing' }; ++ } ++ if (verificationState.phase !== 'complete') { ++ return { allowed: false, reason: 'team_verification_phase_incomplete' }; ++ } ++ if (verificationState.status === 'verified') { ++ return { allowed: true, reason: 'team_verification_verified' }; ++ } ++ return { ++ allowed: false, ++ reason: verificationState.status === 'failed' ++ ? 'team_verification_failed' ++ : 'team_verification_pending', ++ }; ++} ++ + export function scheduleFormalMemoryRefreshOnTeamComplete( + cwd: string, + teamName: string, + env: NodeJS.ProcessEnv = process.env, + spawnImpl?: FormalMemoryRefreshSpawn, ++ verificationState?: TeamVerificationState | null, + ): FormalMemoryRefreshScheduleResult { +- return scheduleFormalMemoryRefresh({ ++ const refreshTarget = { + cwd, + source: 'omx-team-runtime-complete', + teamName, + enableEnvKeys: [STRICT_MEMORY_REFRESH_ON_TEAM_COMPLETE_ENV], + disabledReason: 'team_completion_refresh_disabled', ++ } as const; ++ const plan = resolveFormalMemoryRefreshPlan(refreshTarget, env); ++ if (!plan.enabled) { ++ return { ++ scheduled: false, ++ reason: plan.reason, ++ plan, ++ }; ++ } ++ const gate = evaluateTeamVerificationRefreshGate(verificationState); ++ if (!gate.allowed) { ++ return { ++ scheduled: false, ++ reason: gate.reason, ++ plan, ++ }; ++ } ++ return scheduleFormalMemoryRefresh({ ++ ...refreshTarget, + }, env, spawnImpl); + } + +@@ -201,9 +246,13 @@ async function main(): Promise { + async function doShutdown(status: 'completed' | 'failed'): Promise { + pollActive = false; + finalStatus = status; ++ const teamCwd = runtime?.cwd ?? cwd; + + // 1. Collect task results + const taskResults = collectTaskResults(stateRoot, teamName); ++ const verificationState = status === 'completed' ++ ? await readTeamVerificationState(teamName, teamCwd) ++ : null; + + // 2. Shutdown team + if (runtime) { +@@ -221,9 +270,16 @@ async function main(): Promise { + + if (status === 'completed') { + try { +- const refresh = scheduleFormalMemoryRefreshOnTeamComplete(runtime?.cwd ?? cwd, teamName); ++ const refresh = scheduleFormalMemoryRefreshOnTeamComplete( ++ teamCwd, ++ teamName, ++ process.env, ++ undefined, ++ verificationState, ++ ); + if (!refresh.scheduled && ( + refresh.reason === 'refresh_script_unavailable' ++ || refresh.reason.startsWith('team_verification_') + || refresh.reason.startsWith('spawn_failed:') + )) { + process.stderr.write( +diff --git a/src/team/runtime.ts b/src/team/runtime.ts +index 9697084..f52949a 100644 +--- a/src/team/runtime.ts ++++ b/src/team/runtime.ts +@@ -61,6 +61,7 @@ import { + teamWriteMonitorSnapshot as writeMonitorSnapshot, + teamReadPhase as readTeamPhaseState, + teamWritePhase as writeTeamPhaseState, ++ teamWriteVerification as writeTeamVerificationState, + type TeamConfig, + type WorkerInfo, + type WorkerHeartbeat, +@@ -68,6 +69,7 @@ import { + type TeamTask, + type TeamMonitorSnapshotState, + type TeamPhaseState, ++ type TeamVerificationState, + type TeamWorkerIntegrationState, + type TeamGovernance, + type TeamPolicy, +@@ -1942,6 +1944,23 @@ export async function monitorTeam(teamName: string, cwd: string): Promise task.status === 'completed' && task.requires_code_change === true, ++ ); ++ const verifiedCodeTasks = completedCodeTasks.filter((task) => hasStructuredVerificationEvidence(task.result)); ++ const verificationState: TeamVerificationState = { ++ status: phase === 'failed' ++ ? 'failed' ++ : allTasksTerminal && verificationPendingTasks.length === 0 ++ ? 'verified' ++ : 'pending', ++ phase, ++ completed_code_task_ids: completedCodeTasks.map((task) => task.id), ++ verified_task_ids: verifiedCodeTasks.map((task) => task.id), ++ pending_task_ids: verificationPendingTasks.map((task) => task.id), ++ updated_at: new Date().toISOString(), ++ }; ++ await writeTeamVerificationState(sanitized, verificationState, cwd); + await syncRootTeamModeStateOnTerminalPhase(sanitized, phase, cwd); + + if (deadWorkerStall) { +diff --git a/src/team/state.ts b/src/team/state.ts +index 735d654..ce052c5 100644 +--- a/src/team/state.ts ++++ b/src/team/state.ts +@@ -790,6 +790,18 @@ export async function initTeamState( + }, + cwd + ); ++ await writeTeamVerification( ++ teamName, ++ { ++ status: 'pending', ++ phase: 'team-exec', ++ completed_code_task_ids: [], ++ verified_task_ids: [], ++ pending_task_ids: [], ++ updated_at: new Date().toISOString(), ++ }, ++ cwd, ++ ); + await writeTeamManifestV2( + { + schema_version: 2, +@@ -1747,6 +1759,15 @@ export interface TeamPhaseState { + updated_at: string; + } + ++export interface TeamVerificationState { ++ status: 'pending' | 'verified' | 'failed'; ++ phase: TeamPhase | TerminalPhase; ++ completed_code_task_ids: string[]; ++ verified_task_ids: string[]; ++ pending_task_ids: string[]; ++ updated_at: string; ++} ++ + function teamPhasePath(teamName: string, cwd: string): string { + return join(teamDir(teamName, cwd), 'phase.json'); + } +@@ -1755,6 +1776,10 @@ function monitorSnapshotPath(teamName: string, cwd: string): string { + return join(teamDir(teamName, cwd), 'monitor-snapshot.json'); + } + ++function verificationStatePath(teamName: string, cwd: string): string { ++ return join(teamDir(teamName, cwd), 'verification-state.json'); ++} ++ + export async function readMonitorSnapshot( + teamName: string, + cwd: string, +@@ -1770,6 +1795,50 @@ export async function writeMonitorSnapshot( + await writeMonitorSnapshotImpl(teamName, snapshot, cwd, monitorSnapshotPath, writeAtomic); + } + ++export async function readTeamVerification( ++ teamName: string, ++ cwd: string, ++): Promise { ++ try { ++ const raw = await readFile(verificationStatePath(teamName, cwd), 'utf-8'); ++ const parsed = JSON.parse(raw) as TeamVerificationState; ++ if (parsed.status !== 'pending' && parsed.status !== 'verified' && parsed.status !== 'failed') { ++ return null; ++ } ++ if ( ++ parsed.phase !== 'team-exec' ++ && parsed.phase !== 'team-verify' ++ && parsed.phase !== 'team-fix' ++ && parsed.phase !== 'complete' ++ && parsed.phase !== 'failed' ++ ) { ++ return null; ++ } ++ return { ++ status: parsed.status, ++ phase: parsed.phase, ++ completed_code_task_ids: Array.isArray(parsed.completed_code_task_ids) ? parsed.completed_code_task_ids : [], ++ verified_task_ids: Array.isArray(parsed.verified_task_ids) ? parsed.verified_task_ids : [], ++ pending_task_ids: Array.isArray(parsed.pending_task_ids) ? parsed.pending_task_ids : [], ++ updated_at: typeof parsed.updated_at === 'string' ? parsed.updated_at : new Date().toISOString(), ++ }; ++ } catch (error) { ++ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; ++ return null; ++ } ++} ++ ++export async function writeTeamVerification( ++ teamName: string, ++ verificationState: TeamVerificationState, ++ cwd: string, ++): Promise { ++ await writeAtomic( ++ verificationStatePath(teamName, cwd), ++ JSON.stringify(verificationState, null, 2), ++ ); ++} ++ + export async function readTeamPhase( + teamName: string, + cwd: string, +diff --git a/src/team/team-ops.ts b/src/team/team-ops.ts +index 24fecd0..34995ad 100644 +--- a/src/team/team-ops.ts ++++ b/src/team/team-ops.ts +@@ -43,6 +43,7 @@ export type { + TeamMonitorSnapshotState, + TeamWorkerIntegrationState, + TeamPhaseState, ++ TeamVerificationState, + } from './state.js'; + + // === Constants === +@@ -109,6 +110,8 @@ export { readMonitorSnapshot as teamReadMonitorSnapshot } from './state.js'; + export { writeMonitorSnapshot as teamWriteMonitorSnapshot } from './state.js'; + export { readTeamPhase as teamReadPhase } from './state.js'; + export { writeTeamPhase as teamWritePhase } from './state.js'; ++export { readTeamVerification as teamReadVerification } from './state.js'; ++export { writeTeamVerification as teamWriteVerification } from './state.js'; + + // === Worker status write === + export { writeWorkerStatus as teamWriteWorkerStatus } from './state.js'; diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/constants.js b/knowledge-base/raw/repos/codex-memory-kit/src/constants.js new file mode 100644 index 0000000..67edbf9 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/constants.js @@ -0,0 +1,24 @@ +import os from 'node:os'; +import path from 'node:path'; + +export const DEFAULT_MEMORY_ROOT = path.join(os.homedir(), '.codex', 'memory'); +export const STRICT_MODE_ENV = 'OMX_STRICT_MEMORY_MODE'; +export const EXTERNAL_MEMORY_ROOT_ENV = 'OMX_EXTERNAL_MEMORY_ROOT'; + +export const SHARED_GUIDE_RELATIVE_PATHS = [ + ['company', path.join('instructions', 'company', 'GUIDE.md')], + ['user', path.join('instructions', 'user', 'GUIDE.md')], + ['local', path.join('instructions', 'local', 'GUIDE.md')], +]; + +export function normalizeLookupPath(inputPath) { + return path.resolve(String(inputPath)).replace(/\\/g, '/'); +} + +export function workspaceMemoryHome(memoryRoot, key) { + if (!key || !String(key).trim()) { + throw new Error('workspaceMemoryHome requires a workspace key.'); + } + + return path.join(memoryRoot, 'workspaces', String(key)); +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/contracts/strict-integration-mode.js b/knowledge-base/raw/repos/codex-memory-kit/src/contracts/strict-integration-mode.js new file mode 100644 index 0000000..43f5eb6 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/contracts/strict-integration-mode.js @@ -0,0 +1,51 @@ +import path from 'node:path'; + +import { + DEFAULT_MEMORY_ROOT, + EXTERNAL_MEMORY_ROOT_ENV, + STRICT_MODE_ENV, +} from '../constants.js'; + +const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']); +const FALSE_VALUES = new Set(['0', 'false', 'no', 'off']); + +export function parseBooleanFlag(value) { + if (value == null) return null; + const normalized = String(value).trim().toLowerCase(); + if (TRUE_VALUES.has(normalized)) return true; + if (FALSE_VALUES.has(normalized)) return false; + return null; +} + +export function resolveStrictIntegrationConfig({ + cwd = process.cwd(), + env = process.env, + strictMode, + memoryRoot, +} = {}) { + const envStrictMode = parseBooleanFlag(env[STRICT_MODE_ENV]); + const resolvedStrictMode = strictMode ?? envStrictMode ?? false; + const resolvedMemoryRoot = path.resolve( + memoryRoot ?? env[EXTERNAL_MEMORY_ROOT_ENV] ?? DEFAULT_MEMORY_ROOT + ); + + return { + cwd: path.resolve(cwd), + strictMode: resolvedStrictMode, + memoryRoot: resolvedMemoryRoot, + envVarNames: { + strictMode: STRICT_MODE_ENV, + memoryRoot: EXTERNAL_MEMORY_ROOT_ENV, + }, + }; +} + +export function assertStrictIntegrationMode(config) { + if (!config.strictMode) { + throw new Error( + `Strict integration mode is disabled. Enable ${STRICT_MODE_ENV}=1 to activate the formal memory guards.` + ); + } + + return config; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/index.js b/knowledge-base/raw/repos/codex-memory-kit/src/index.js new file mode 100644 index 0000000..19ce9cd --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/index.js @@ -0,0 +1,113 @@ +export { + assertStrictIntegrationMode, + parseBooleanFlag, + resolveStrictIntegrationConfig, +} from './contracts/strict-integration-mode.js'; +export { buildFormalMemorySummary, readFormalMemoryContext } from './integration/external-memory.js'; +export { readProjectMemoryView } from './integration/project-memory-view.js'; +export { loadWorkspaceIndex, resolveWorkspaceNode } from './integration/workspace-resolver.js'; +export { + FormalMemoryWriteError, + RuntimeArtifactWriteError, + classifyArtifactPath, + guardWritePath, + isFormalMemoryPath, +} from './policy/path-guard.js'; +export { + HITL_DECISION_ALLOW, + HITL_DECISION_DENY, + HITL_DECISION_REVIEW, + evaluateHitlCheckpoints, +} from './policy/hitl-checkpoints.js'; +export { + PERMISSION_DECISION_ALLOW, + PERMISSION_DECISION_DENY, + PERMISSION_DECISION_REVIEW, + evaluatePermissionGate, +} from './policy/permission-gate.js'; +export { + RECOVERY_DECISION_COMPLETE, + RECOVERY_DECISION_ESCALATE, + RECOVERY_DECISION_FALLBACK, + RECOVERY_DECISION_HALT, + RECOVERY_DECISION_RETRY, + evaluateErrorRecovery, +} from './policy/error-recovery.js'; +export { + LEGACY_SOURCE_DECISION_ALLOW, + LEGACY_SOURCE_DECISION_BLOCK, + LEGACY_SOURCE_DECISION_SUPPLEMENT, + classifyLegacyMemorySource, + evaluateLegacyMemorySource, + evaluateLegacyMemorySources, +} from './policy/legacy-memory-bypass.js'; +export { buildOverlayContext, buildOverlaySections, renderOverlayContext } from './overlay/build-overlay-context.js'; +export { buildAgentStartupContext } from './runtime/agent-startup-context.js'; +export { runGuardedAction } from './runtime/guarded-action-runner.js'; +export { + REFRESH_DECISION_ALLOW, + REFRESH_DECISION_DENY, + REFRESH_DECISION_REVIEW, + buildRefreshCommand, + evaluateRefreshTrigger, + triggerFormalMemoryRefresh, +} from './runtime/leader-refresh-trigger.js'; +export { + MEMORY_INTAKE_FILENAME, + appendMemoryIntakeEntry, + readMemoryIntakeEntries, + summarizeMemoryIntake, +} from './runtime/memory-intake-queue.js'; +export { + VERIFICATION_STATE_FILENAME, + VERIFICATION_STATE_NAME, + VERIFICATION_STATUS_FAILED, + VERIFICATION_STATUS_PENDING, + VERIFICATION_STATUS_STALE, + VERIFICATION_STATUS_VERIFIED, + appendVerificationEvidence, + markVerificationFailed, + markVerificationPending, + markVerificationStale, + markVerified, + readVerificationState, + resolveVerificationStatus, +} from './runtime/verification-state.js'; +export { + PROMOTION_AUDIT_FILENAME, + appendPromotionAuditEvent, + readPromotionAuditEntries, + summarizePromotionAuditTrail, +} from './runtime/promotion-audit-trail.js'; +export { + PROMOTION_DECISION_ALLOW, + PROMOTION_DECISION_DENY, + PROMOTION_DECISION_REVIEW, + evaluatePromotionGate, + triggerFormalPromotion, +} from './runtime/promotion-gate.js'; +export { + PROJECT_MEMORY_DECISION_ALLOW, + PROJECT_MEMORY_DECISION_DENY, + PROJECT_MEMORY_DECISION_DOWNGRADE, + projectMemoryAddDirective, + projectMemoryAddNote, + projectMemoryRead, + projectMemoryWrite, +} from './runtime/project-memory-commands.js'; +export { createGovernanceFacade, createRuntimeFacade } from './runtime/runtime-facade.js'; +export { + stateClear, + stateGetStatus, + stateListActive, + stateRead, + stateWrite, +} from './runtime/state-store.js'; +export { + TEAM_ROLE_LEADER, + TEAM_ROLE_MAIN, + TEAM_ROLE_WORKER, + assertFormalMemoryRefreshAuthority, + assertTeamWriteAccess, + canTriggerFormalMemoryRefresh, +} from './team/team-contract.js'; diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/integration/external-memory.js b/knowledge-base/raw/repos/codex-memory-kit/src/integration/external-memory.js new file mode 100644 index 0000000..63e5a62 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/integration/external-memory.js @@ -0,0 +1,109 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { SHARED_GUIDE_RELATIVE_PATHS } from '../constants.js'; +import { resolveWorkspaceNode } from './workspace-resolver.js'; + +function readTextFileIfPresent(filePath) { + try { + return fs.readFileSync(filePath, 'utf8'); + } catch (error) { + if (error && error.code === 'ENOENT') return null; + throw error; + } +} + +function buildFileRecord(kind, filePath) { + const content = readTextFileIfPresent(filePath); + return { + kind, + path: filePath, + content, + exists: content != null, + }; +} + +export function readSharedGuides(memoryRoot) { + return SHARED_GUIDE_RELATIVE_PATHS.map(([kind, relativePath]) => + buildFileRecord(kind, path.join(memoryRoot, relativePath)) + ); +} + +export function readFormalMemoryContext({ cwd = process.cwd(), memoryRoot }) { + const sharedGuides = readSharedGuides(memoryRoot); + const diagnostics = []; + const workspace = resolveWorkspaceNode({ cwd, memoryRoot }); + + if (!workspace) { + diagnostics.push({ + kind: 'workspace-memory-unavailable', + message: + 'Workspace memory node could not be resolved. Falling back to shared guides and runtime-only context.', + }); + } + + if (!workspace) { + return { + workspace: null, + sharedGuides, + repoGuide: null, + memoryIndex: null, + activeContext: null, + diagnostics, + }; + } + + return { + workspace, + sharedGuides, + repoGuide: buildFileRecord('repo-guide', workspace.repoGuidePath), + memoryIndex: buildFileRecord('memory-index', workspace.memoryIndexPath), + activeContext: buildFileRecord('active-context', workspace.activeContextPath), + diagnostics, + }; +} + +export function buildFormalMemorySummary(context) { + const sections = []; + + if (context.activeContext?.content) { + sections.push({ + source: 'active-context', + path: context.activeContext.path, + content: context.activeContext.content, + }); + } + + if (context.memoryIndex?.content) { + sections.push({ + source: 'memory-index', + path: context.memoryIndex.path, + content: context.memoryIndex.content, + }); + } + + if (context.repoGuide?.content) { + sections.push({ + source: 'repo-guide', + path: context.repoGuide.path, + content: context.repoGuide.content, + }); + } + + for (const guide of context.sharedGuides ?? []) { + if (!guide.content) continue; + sections.push({ + source: `shared-guide:${guide.kind}`, + path: guide.path, + content: guide.content, + }); + } + + return { + workspace: context.workspace, + sections, + text: sections + .map((section) => `## ${section.source}\n\n${section.content.trim()}`) + .join('\n\n'), + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/integration/project-memory-view.js b/knowledge-base/raw/repos/codex-memory-kit/src/integration/project-memory-view.js new file mode 100644 index 0000000..3f127a6 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/integration/project-memory-view.js @@ -0,0 +1,78 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { buildFormalMemorySummary, readFormalMemoryContext } from './external-memory.js'; + +function readLocalProjectMemory(omxRoot) { + const projectMemoryPath = path.join(omxRoot, 'project-memory.json'); + try { + const raw = fs.readFileSync(projectMemoryPath, 'utf8'); + const parsed = JSON.parse(raw); + return { + path: projectMemoryPath, + exists: true, + raw, + parsed, + }; + } catch (error) { + if (error && error.code === 'ENOENT') { + return { + path: projectMemoryPath, + exists: false, + raw: null, + parsed: null, + }; + } + throw error; + } +} + +export function readProjectMemoryView({ + cwd = process.cwd(), + memoryRoot, + strictMode = false, + omxRoot = path.join(cwd, '.omx'), +} = {}) { + const formalContext = readFormalMemoryContext({ cwd, memoryRoot }); + const formalSummary = buildFormalMemorySummary(formalContext); + const localProjectMemory = readLocalProjectMemory(omxRoot); + + if (strictMode || !localProjectMemory.exists) { + return { + mode: strictMode ? 'strict-formal-memory' : 'formal-memory-fallback', + source: 'formal-memory', + workspace: formalSummary.workspace, + sections: formalSummary.sections, + text: formalSummary.text, + localProjectMemory, + diagnostics: [ + ...(formalContext.diagnostics ?? []), + ...(strictMode + ? [ + { + kind: 'local-project-memory-ignored', + message: + 'Strict integration mode ignores .omx/project-memory.json and uses the formal workspace memory summary.', + }, + ] + : []), + ], + }; + } + + return { + mode: 'local-project-memory', + source: 'local-project-memory', + workspace: formalSummary.workspace, + sections: [ + { + source: 'local-project-memory', + path: localProjectMemory.path, + content: JSON.stringify(localProjectMemory.parsed, null, 2), + }, + ], + text: JSON.stringify(localProjectMemory.parsed, null, 2), + localProjectMemory, + diagnostics: formalContext.diagnostics ?? [], + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/integration/workspace-resolver.js b/knowledge-base/raw/repos/codex-memory-kit/src/integration/workspace-resolver.js new file mode 100644 index 0000000..61441c8 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/integration/workspace-resolver.js @@ -0,0 +1,71 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { normalizeLookupPath, workspaceMemoryHome } from '../constants.js'; + +export function loadWorkspaceIndex(memoryRoot) { + const indexPath = path.join(memoryRoot, 'workspaces', 'index.json'); + let raw; + try { + raw = fs.readFileSync(indexPath, 'utf8'); + } catch (error) { + if (error && error.code === 'ENOENT') { + return null; + } + throw error; + } + const parsed = JSON.parse(raw); + + if (!parsed || typeof parsed !== 'object' || typeof parsed.workspaces !== 'object') { + throw new Error(`Invalid workspace index: ${indexPath}`); + } + + return { + indexPath, + version: parsed.version ?? null, + workspaces: parsed.workspaces, + }; +} + +function findBestWorkspaceMatch(workspaces, cwd) { + const lookupCwd = normalizeLookupPath(cwd); + let bestMatch = null; + + for (const [storedPath, entry] of Object.entries(workspaces)) { + if (!entry || typeof entry !== 'object') continue; + + const candidatePath = normalizeLookupPath(entry.path ?? storedPath); + if (lookupCwd !== candidatePath && !lookupCwd.startsWith(`${candidatePath}/`)) { + continue; + } + + if (!bestMatch || candidatePath.length > bestMatch.lookupPath.length) { + bestMatch = { + lookupPath: candidatePath, + registeredPath: entry.path ?? storedPath, + key: entry.key, + }; + } + } + + return bestMatch; +} + +export function resolveWorkspaceNode({ cwd = process.cwd(), memoryRoot }) { + const index = loadWorkspaceIndex(memoryRoot); + if (!index) return null; + const match = findBestWorkspaceMatch(index.workspaces, cwd); + if (!match) return null; + + const memoryHome = workspaceMemoryHome(memoryRoot, match.key); + + return { + key: match.key, + workspaceRoot: path.resolve(match.registeredPath), + memoryHome, + repoGuidePath: path.join(memoryHome, 'instructions', 'repo', 'GUIDE.md'), + memoryIndexPath: path.join(memoryHome, 'memories', 'MEMORY.md'), + activeContextPath: path.join(memoryHome, 'runtime', 'active_context.md'), + indexPath: index.indexPath, + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/overlay/build-overlay-context.js b/knowledge-base/raw/repos/codex-memory-kit/src/overlay/build-overlay-context.js new file mode 100644 index 0000000..26ccb92 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/overlay/build-overlay-context.js @@ -0,0 +1,112 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { + buildFormalMemorySummary, + readFormalMemoryContext, +} from '../integration/external-memory.js'; + +function readTextFileIfPresent(filePath) { + try { + return fs.readFileSync(filePath, 'utf8'); + } catch (error) { + if (error && error.code === 'ENOENT') return null; + throw error; + } +} + +function listStateFiles(omxRoot) { + const stateDir = path.join(omxRoot, 'state'); + try { + return fs + .readdirSync(stateDir, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('-state.json')) + .map((entry) => path.join(stateDir, entry.name)) + .sort(); + } catch (error) { + if (error && error.code === 'ENOENT') return []; + throw error; + } +} + +function extractMarkdownSection(markdown, headingName) { + if (!markdown) return null; + + const lines = markdown.split('\n'); + const normalizedHeading = headingName.trim().toLowerCase(); + let capture = false; + const collected = []; + + for (const line of lines) { + const headingMatch = line.match(/^(#{1,6})\s+(.*)$/); + if (headingMatch) { + const currentHeading = headingMatch[2].trim().toLowerCase(); + if (capture) break; + if (currentHeading === normalizedHeading) { + capture = true; + } + continue; + } + + if (capture) { + collected.push(line); + } + } + + const section = collected.join('\n').trim(); + return section || null; +} + +function readNotepadPriority(omxRoot) { + const notepadPath = path.join(omxRoot, 'notepad.md'); + const markdown = readTextFileIfPresent(notepadPath); + const content = extractMarkdownSection(markdown, 'PRIORITY'); + if (!content) return null; + + return { + source: 'notepad-priority', + path: notepadPath, + content, + }; +} + +export function buildOverlaySections({ + cwd = process.cwd(), + memoryRoot, + omxRoot = path.join(cwd, '.omx'), +}) { + const sections = []; + + for (const stateFile of listStateFiles(omxRoot)) { + const content = readTextFileIfPresent(stateFile); + if (!content) continue; + sections.push({ + source: 'omx-state', + path: stateFile, + content, + }); + } + + const formalMemoryContext = readFormalMemoryContext({ cwd, memoryRoot }); + const formalSummary = buildFormalMemorySummary(formalMemoryContext); + sections.push(...formalSummary.sections); + + const notepadPriority = readNotepadPriority(omxRoot); + if (notepadPriority) sections.push(notepadPriority); + + return sections; +} + +export function renderOverlayContext(sections) { + return sections + .map((section) => `## ${section.source}\n\n${section.content.trim()}`) + .join('\n\n'); +} + +export function buildOverlayContext(options) { + const sections = buildOverlaySections(options); + return { + sections, + text: renderOverlayContext(sections), + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/policy/error-recovery.js b/knowledge-base/raw/repos/codex-memory-kit/src/policy/error-recovery.js new file mode 100644 index 0000000..478ab23 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/policy/error-recovery.js @@ -0,0 +1,200 @@ +import { FormalMemoryWriteError, RuntimeArtifactWriteError } from './path-guard.js'; + +export const RECOVERY_DECISION_COMPLETE = 'complete'; +export const RECOVERY_DECISION_RETRY = 'retry'; +export const RECOVERY_DECISION_FALLBACK = 'fallback'; +export const RECOVERY_DECISION_ESCALATE = 'escalate'; +export const RECOVERY_DECISION_HALT = 'halt'; + +const TRANSIENT_ERROR_CODES = new Set([ + 'EAGAIN', + 'EBUSY', + 'ECONNRESET', + 'ECONNREFUSED', + 'EPIPE', + 'ETIMEDOUT', + 'EAI_AGAIN', +]); + +const PREREQUISITE_ERROR_CODES = new Set(['ENOENT', 'ENOTDIR']); +const INTERRUPT_SIGNALS = new Set(['SIGINT', 'SIGTERM']); + +function normalizeFailureFromError(error) { + if (!error) return null; + + const message = error.message ?? String(error); + let category = 'unknown'; + + if (error instanceof FormalMemoryWriteError || error instanceof RuntimeArtifactWriteError) { + category = 'policy'; + } else if (TRANSIENT_ERROR_CODES.has(error.code)) { + category = 'transient'; + } else if (PREREQUISITE_ERROR_CODES.has(error.code)) { + category = 'prerequisite'; + } else if (/requires .+ payload|requires non-empty|invalid/i.test(message)) { + category = 'validation'; + } else if (/forbidden|cannot trigger|requires explicit review|strict integration mode forbids/i.test(message)) { + category = 'policy'; + } + + return { + source: 'error', + category, + message, + code: error.code ?? null, + status: null, + signal: error.signal ?? null, + name: error.name ?? 'Error', + }; +} + +function categorizeProcessStatus(status, signal, stderr = '') { + if (signal && INTERRUPT_SIGNALS.has(signal)) { + return 'interrupt'; + } + + if (status === 124) { + return 'transient'; + } + + if (status === 126 || status === 127) { + return 'prerequisite'; + } + + if (/timed out|timeout|temporar/i.test(stderr)) { + return 'transient'; + } + + return 'unknown'; +} + +function normalizeFailureFromResult(result) { + if (!result) return null; + const status = typeof result.status === 'number' ? result.status : null; + + if (status == null || status === 0) { + return null; + } + + const stderr = result.stderr ? String(result.stderr) : ''; + return { + source: 'result', + category: categorizeProcessStatus(status, result.signal ?? null, stderr), + message: stderr || `Operation exited with status ${status}.`, + code: result.code ?? null, + status, + signal: result.signal ?? null, + name: 'ProcessResultError', + }; +} + +function normalizeFailure({ error, result } = {}) { + return normalizeFailureFromError(error) ?? normalizeFailureFromResult(result); +} + +function finalizeRecovery({ + operation, + decision, + reason, + attempt, + maxAttempts, + failure, + fallbackAvailable, +}) { + return { + operation, + decision, + complete: decision === RECOVERY_DECISION_COMPLETE, + retryable: decision === RECOVERY_DECISION_RETRY, + fallbackAvailable, + attempt, + maxAttempts, + nextAttempt: decision === RECOVERY_DECISION_RETRY ? attempt + 1 : null, + reason, + failure, + }; +} + +export function evaluateErrorRecovery({ + operation = 'operation', + error, + result, + attempt = 1, + maxAttempts = 2, + fallbackAvailable = false, +} = {}) { + const normalizedMaxAttempts = Math.max(1, Number(maxAttempts) || 1); + const failure = normalizeFailure({ error, result }); + + if (!failure) { + return finalizeRecovery({ + operation, + decision: RECOVERY_DECISION_COMPLETE, + reason: 'No recovery action is required.', + attempt, + maxAttempts: normalizedMaxAttempts, + failure: null, + fallbackAvailable, + }); + } + + if (failure.category === 'transient' && attempt < normalizedMaxAttempts) { + return finalizeRecovery({ + operation, + decision: RECOVERY_DECISION_RETRY, + reason: 'Transient failure detected; retrying the operation.', + attempt, + maxAttempts: normalizedMaxAttempts, + failure, + fallbackAvailable, + }); + } + + if ((failure.category === 'transient' || failure.category === 'prerequisite') && fallbackAvailable) { + return finalizeRecovery({ + operation, + decision: RECOVERY_DECISION_FALLBACK, + reason: 'Primary execution failed; falling back to the secondary path.', + attempt, + maxAttempts: normalizedMaxAttempts, + failure, + fallbackAvailable, + }); + } + + if (failure.category === 'policy') { + return finalizeRecovery({ + operation, + decision: RECOVERY_DECISION_ESCALATE, + reason: 'Policy or boundary enforcement blocked the operation and requires review.', + attempt, + maxAttempts: normalizedMaxAttempts, + failure, + fallbackAvailable, + }); + } + + if (failure.category === 'validation' || failure.category === 'interrupt') { + return finalizeRecovery({ + operation, + decision: RECOVERY_DECISION_HALT, + reason: 'The operation cannot be retried automatically and should stop here.', + attempt, + maxAttempts: normalizedMaxAttempts, + failure, + fallbackAvailable, + }); + } + + return finalizeRecovery({ + operation, + decision: fallbackAvailable ? RECOVERY_DECISION_FALLBACK : RECOVERY_DECISION_ESCALATE, + reason: fallbackAvailable + ? 'Primary execution failed with an unknown error; using fallback.' + : 'Primary execution failed with an unknown error and requires review.', + attempt, + maxAttempts: normalizedMaxAttempts, + failure, + fallbackAvailable, + }); +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/policy/hitl-checkpoints.js b/knowledge-base/raw/repos/codex-memory-kit/src/policy/hitl-checkpoints.js new file mode 100644 index 0000000..d19ba04 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/policy/hitl-checkpoints.js @@ -0,0 +1,166 @@ +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { + evaluatePermissionGate, + PERMISSION_DECISION_DENY, + PERMISSION_DECISION_REVIEW, +} from './permission-gate.js'; + +export const HITL_DECISION_ALLOW = 'allow'; +export const HITL_DECISION_REVIEW = 'review'; +export const HITL_DECISION_DENY = 'deny'; + +function appendCheckpoint(checkpoints, checkpoint) { + if (checkpoints.some((entry) => entry.id === checkpoint.id)) { + return checkpoints; + } + checkpoints.push(checkpoint); + return checkpoints; +} + +function buildCheckpoint(id, label, reason) { + return { + id, + label, + reason, + }; +} + +function mergeReasons(permissionReasons, checkpointReasons) { + const merged = [...permissionReasons, ...checkpointReasons]; + return [...new Set(merged)]; +} + +export function evaluateHitlCheckpoints({ + action, + role = 'main', + targetPath, + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + permission, + explicitUserIntent = false, + replacesRule = false, + sensitive = false, + externalSideEffect = false, + phase, + verificationStatus, +} = {}) { + const resolvedPermission = + permission ?? + evaluatePermissionGate({ + action, + role, + targetPath, + cwd, + memoryRoot, + replacesRule, + sensitive, + externalSideEffect, + }); + + const checkpoints = []; + + if (resolvedPermission.decision === PERMISSION_DECISION_REVIEW && action === 'write' && role === 'worker') { + appendCheckpoint( + checkpoints, + buildCheckpoint( + 'worker-outside-runtime', + 'Worker write outside runtime artifacts', + 'A worker is attempting to write outside the approved runtime artifact paths.' + ) + ); + } + + if (replacesRule) { + appendCheckpoint( + checkpoints, + buildCheckpoint( + 'rule-replacement', + 'Rule replacement', + 'Replacing an existing rule requires human confirmation.' + ) + ); + } + + if (sensitive) { + appendCheckpoint( + checkpoints, + buildCheckpoint( + 'sensitive-operation', + 'Sensitive operation', + 'Sensitive operations require human confirmation.' + ) + ); + } + + if (externalSideEffect) { + appendCheckpoint( + checkpoints, + buildCheckpoint( + 'external-side-effect', + 'External side effect', + 'External side effects require human confirmation.' + ) + ); + } + + if (action === 'formal-memory-refresh' && phase != null && phase !== 'terminal') { + appendCheckpoint( + checkpoints, + buildCheckpoint( + 'refresh-non-terminal', + 'Non-terminal refresh', + 'Formal memory refresh should normally wait until the terminal phase.' + ) + ); + } + + if ( + action === 'formal-memory-refresh' && + verificationStatus != null && + verificationStatus !== 'verified' + ) { + appendCheckpoint( + checkpoints, + buildCheckpoint( + 'refresh-unverified', + 'Unverified refresh', + 'Formal memory refresh should normally wait until verification is complete.' + ) + ); + } + + const checkpointReasons = checkpoints.map((checkpoint) => checkpoint.reason); + let decision = HITL_DECISION_ALLOW; + let satisfied = true; + + if (resolvedPermission.decision === PERMISSION_DECISION_DENY) { + decision = HITL_DECISION_DENY; + satisfied = false; + } else if (checkpoints.length === 0) { + decision = HITL_DECISION_ALLOW; + satisfied = true; + } else if (explicitUserIntent) { + decision = HITL_DECISION_ALLOW; + satisfied = true; + checkpointReasons.push('Explicit user intent satisfies the required human review checkpoints.'); + } else { + decision = HITL_DECISION_REVIEW; + satisfied = false; + checkpointReasons.push('Human review is required before continuing.'); + } + + return { + action, + role, + targetPath, + classification: resolvedPermission.classification, + decision, + allowed: decision === HITL_DECISION_ALLOW, + requiresHuman: checkpoints.length > 0, + satisfied, + explicitUserIntent, + checkpoints, + reasons: mergeReasons(resolvedPermission.reasons, checkpointReasons), + permission: resolvedPermission, + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/policy/legacy-memory-bypass.js b/knowledge-base/raw/repos/codex-memory-kit/src/policy/legacy-memory-bypass.js new file mode 100644 index 0000000..89026ad --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/policy/legacy-memory-bypass.js @@ -0,0 +1,91 @@ +import path from 'node:path'; + +import { classifyArtifactPath, isOmxPath } from './path-guard.js'; + +export const LEGACY_SOURCE_DECISION_ALLOW = 'allow'; +export const LEGACY_SOURCE_DECISION_SUPPLEMENT = 'supplement'; +export const LEGACY_SOURCE_DECISION_BLOCK = 'block'; + +function normalizeBasename(filePath) { + return path.basename(filePath || '').toLowerCase(); +} + +export function classifyLegacyMemorySource(sourcePath, { cwd = process.cwd(), memoryRoot } = {}) { + const artifactClass = classifyArtifactPath(sourcePath, { cwd, memoryRoot }); + const basename = normalizeBasename(sourcePath); + + if (artifactClass === 'telemetry') return 'telemetry'; + if (!isOmxPath(sourcePath, cwd)) return 'other'; + if (basename === 'project-memory.json') return 'local-project-memory'; + if (basename === 'notepad.md') return 'notepad'; + return 'runtime-artifact'; +} + +export function evaluateLegacyMemorySource({ + sourcePath, + intendedUse = 'primary', + strictMode = false, + cwd = process.cwd(), + memoryRoot, +} = {}) { + const sourceType = classifyLegacyMemorySource(sourcePath, { cwd, memoryRoot }); + + if (sourceType === 'telemetry') { + return { + decision: LEGACY_SOURCE_DECISION_BLOCK, + sourceType, + sourcePath, + intendedUse, + reason: 'Telemetry sources are excluded from startup memory context.', + }; + } + + if (sourceType === 'local-project-memory') { + if (strictMode) { + return { + decision: LEGACY_SOURCE_DECISION_BLOCK, + sourceType, + sourcePath, + intendedUse, + reason: + 'Strict integration mode blocks .omx/project-memory.json from acting as a primary memory source.', + }; + } + + return { + decision: LEGACY_SOURCE_DECISION_ALLOW, + sourceType, + sourcePath, + intendedUse, + reason: '.omx/project-memory.json is available only outside strict integration mode.', + }; + } + + if (sourceType === 'notepad') { + return { + decision: + intendedUse === 'supplement' + ? LEGACY_SOURCE_DECISION_SUPPLEMENT + : LEGACY_SOURCE_DECISION_BLOCK, + sourceType, + sourcePath, + intendedUse, + reason: + intendedUse === 'supplement' + ? '.omx/notepad.md may contribute temporary hot context as a supplement.' + : '.omx/notepad.md cannot replace formal memory as the primary startup source.', + }; + } + + return { + decision: LEGACY_SOURCE_DECISION_ALLOW, + sourceType, + sourcePath, + intendedUse, + reason: 'No legacy memory bypass detected for this source.', + }; +} + +export function evaluateLegacyMemorySources(sources, options) { + return sources.map((source) => evaluateLegacyMemorySource({ ...source, ...options })); +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/policy/path-guard.js b/knowledge-base/raw/repos/codex-memory-kit/src/policy/path-guard.js new file mode 100644 index 0000000..f6d65e5 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/policy/path-guard.js @@ -0,0 +1,85 @@ +import path from 'node:path'; + +import { DEFAULT_MEMORY_ROOT, normalizeLookupPath } from '../constants.js'; + +const TELEMETRY_HINTS = [ + '/logs/', + '/hud/', + '/metrics/', + 'buddypulse', + 'notification', + 'pane-capture', +]; + +export class FormalMemoryWriteError extends Error { + constructor(targetPath) { + super(`Direct writes to formal memory are forbidden in strict integration mode: ${targetPath}`); + this.name = 'FormalMemoryWriteError'; + this.targetPath = targetPath; + } +} + +export class RuntimeArtifactWriteError extends Error { + constructor(targetPath, role) { + super(`Role "${role}" may only write runtime artifacts under .omx/**: ${targetPath}`); + this.name = 'RuntimeArtifactWriteError'; + this.targetPath = targetPath; + this.role = role; + } +} + +export function isFormalMemoryPath(targetPath, memoryRoot = DEFAULT_MEMORY_ROOT) { + const normalizedTarget = normalizeLookupPath(targetPath); + const normalizedRoot = normalizeLookupPath(memoryRoot); + + if (!normalizedTarget.startsWith(`${normalizedRoot}/`)) return false; + + const relativePath = normalizedTarget.slice(normalizedRoot.length + 1); + const parts = relativePath.split('/').filter(Boolean); + + if (parts[0] === 'global' && parts[1] === 'memories') return true; + if (parts[0] === 'workspaces' && parts.length >= 3 && parts[2] === 'memories') return true; + + return false; +} + +export function isOmxPath(targetPath, cwd = process.cwd()) { + const normalizedTarget = normalizeLookupPath(targetPath); + const normalizedOmxRoot = normalizeLookupPath(path.join(cwd, '.omx')); + return normalizedTarget === normalizedOmxRoot || normalizedTarget.startsWith(`${normalizedOmxRoot}/`); +} + +export function classifyArtifactPath( + targetPath, + { cwd = process.cwd(), memoryRoot = DEFAULT_MEMORY_ROOT } = {} +) { + if (isFormalMemoryPath(targetPath, memoryRoot)) { + return 'formal-memory'; + } + + if (isOmxPath(targetPath, cwd)) { + const normalizedTarget = normalizeLookupPath(targetPath); + if (TELEMETRY_HINTS.some((hint) => normalizedTarget.includes(hint))) { + return 'telemetry'; + } + return 'worker-run'; + } + + return 'other'; +} + +export function guardWritePath( + targetPath, + { cwd = process.cwd(), memoryRoot = DEFAULT_MEMORY_ROOT } = {} +) { + const classification = classifyArtifactPath(targetPath, { cwd, memoryRoot }); + if (classification === 'formal-memory') { + throw new FormalMemoryWriteError(targetPath); + } + + return { + allowed: true, + classification, + targetPath, + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/policy/permission-gate.js b/knowledge-base/raw/repos/codex-memory-kit/src/policy/permission-gate.js new file mode 100644 index 0000000..c31647c --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/policy/permission-gate.js @@ -0,0 +1,104 @@ +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { classifyArtifactPath } from './path-guard.js'; +import { canTriggerFormalMemoryRefresh } from '../team/team-contract.js'; + +export const PERMISSION_DECISION_ALLOW = 'allow'; +export const PERMISSION_DECISION_REVIEW = 'review'; +export const PERMISSION_DECISION_DENY = 'deny'; + +function escalate(current, next) { + const order = { + [PERMISSION_DECISION_ALLOW]: 0, + [PERMISSION_DECISION_REVIEW]: 1, + [PERMISSION_DECISION_DENY]: 2, + }; + + return order[next] > order[current] ? next : current; +} + +export function evaluatePermissionGate({ + action, + role = 'main', + targetPath, + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + replacesRule = false, + sensitive = false, + externalSideEffect = false, +} = {}) { + let decision = PERMISSION_DECISION_ALLOW; + const reasons = []; + const classification = targetPath + ? classifyArtifactPath(targetPath, { cwd, memoryRoot }) + : null; + + if (action === 'formal-memory-refresh') { + if (!canTriggerFormalMemoryRefresh(role)) { + return { + decision: PERMISSION_DECISION_DENY, + allowed: false, + reasons: [`Role "${role}" cannot trigger formal memory refresh.`], + action, + role, + targetPath, + classification, + }; + } + + reasons.push('Formal memory refresh is allowed for this role.'); + } + + if (action === 'write' && classification === 'formal-memory') { + return { + decision: PERMISSION_DECISION_DENY, + allowed: false, + reasons: ['Direct writes to formal memory are forbidden.'], + action, + role, + targetPath, + classification, + }; + } + + if (action === 'write' && role === 'worker' && classification === 'other') { + decision = escalate(decision, PERMISSION_DECISION_REVIEW); + reasons.push('Workers writing outside runtime artifacts require explicit review.'); + } + + if (replacesRule) { + decision = escalate(decision, PERMISSION_DECISION_REVIEW); + reasons.push('Rule replacement requires human review.'); + } + + if (sensitive) { + decision = escalate(decision, PERMISSION_DECISION_REVIEW); + reasons.push('Sensitive operations require human review.'); + } + + if (externalSideEffect) { + decision = escalate(decision, PERMISSION_DECISION_REVIEW); + reasons.push('External side effects require human review.'); + } + + if (classification === 'worker-run') { + reasons.push('Runtime artifact write is allowed.'); + } else if (classification === 'telemetry') { + reasons.push('Telemetry write is allowed but remains excluded from formal memory.'); + } else if (classification === 'other' && action === 'write' && decision === PERMISSION_DECISION_ALLOW) { + reasons.push('Write path is outside formal memory.'); + } + + if (reasons.length === 0) { + reasons.push('No policy escalation required.'); + } + + return { + decision, + allowed: decision === PERMISSION_DECISION_ALLOW, + reasons, + action, + role, + targetPath, + classification, + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/agent-startup-context.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/agent-startup-context.js new file mode 100644 index 0000000..ba56e24 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/agent-startup-context.js @@ -0,0 +1,82 @@ +import path from 'node:path'; + +import { resolveStrictIntegrationConfig } from '../contracts/strict-integration-mode.js'; +import { readFormalMemoryContext } from '../integration/external-memory.js'; +import { readProjectMemoryView } from '../integration/project-memory-view.js'; +import { buildOverlayContext } from '../overlay/build-overlay-context.js'; +import { evaluateLegacyMemorySources } from '../policy/legacy-memory-bypass.js'; + +export function buildAgentStartupContext({ + cwd = process.cwd(), + env = process.env, + strictMode, + memoryRoot, + role = 'main', +} = {}) { + const config = resolveStrictIntegrationConfig({ + cwd, + env, + strictMode, + memoryRoot, + }); + + const formalContext = readFormalMemoryContext({ + cwd: config.cwd, + memoryRoot: config.memoryRoot, + }); + const projectMemoryView = readProjectMemoryView({ + cwd: config.cwd, + memoryRoot: config.memoryRoot, + strictMode: config.strictMode, + }); + const overlay = buildOverlayContext({ + cwd: config.cwd, + memoryRoot: config.memoryRoot, + }); + + const legacySourcePolicy = evaluateLegacyMemorySources( + [ + { + sourcePath: path.join(config.cwd, '.omx', 'project-memory.json'), + intendedUse: 'primary', + }, + { + sourcePath: path.join(config.cwd, '.omx', 'notepad.md'), + intendedUse: 'supplement', + }, + { + sourcePath: path.join(config.cwd, '.omx', 'logs', 'events.jsonl'), + intendedUse: 'primary', + }, + ], + { + strictMode: config.strictMode, + cwd: config.cwd, + memoryRoot: config.memoryRoot, + } + ); + + const diagnostics = [ + ...(formalContext.diagnostics ?? []), + ...(projectMemoryView.diagnostics ?? []), + ...legacySourcePolicy + .filter((entry) => entry.decision === 'block') + .map((entry) => ({ + kind: 'legacy-memory-source-blocked', + sourceType: entry.sourceType, + sourcePath: entry.sourcePath, + message: entry.reason, + })), + ]; + + return { + config, + role, + workspace: formalContext.workspace, + overlay, + projectMemoryView, + legacySourcePolicy, + diagnostics, + text: overlay.text, + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/guarded-action-runner.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/guarded-action-runner.js new file mode 100644 index 0000000..d841ad1 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/guarded-action-runner.js @@ -0,0 +1,271 @@ +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { evaluatePermissionGate, PERMISSION_DECISION_DENY } from '../policy/permission-gate.js'; +import { evaluateHitlCheckpoints, HITL_DECISION_ALLOW } from '../policy/hitl-checkpoints.js'; +import { + evaluateErrorRecovery, + RECOVERY_DECISION_COMPLETE, + RECOVERY_DECISION_FALLBACK, + RECOVERY_DECISION_RETRY, +} from '../policy/error-recovery.js'; + +function defaultSuccessEvaluator(result) { + if (result && typeof result.status === 'number') { + return result.status === 0; + } + return true; +} + +function finalizeBlocked(operation, permission, hitl) { + return { + operation, + status: 'blocked', + permitted: false, + executed: false, + success: false, + permission, + hitl, + attempts: [], + recovery: null, + result: null, + error: null, + usedFallback: false, + }; +} + +export function runGuardedAction({ + operation = 'operation', + action = 'write', + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + targetPath, + explicitUserIntent = false, + replacesRule = false, + sensitive = false, + externalSideEffect = false, + phase, + verificationStatus, + maxAttempts = 2, + perform, + fallback, + isSuccessfulResult = defaultSuccessEvaluator, + permission, + hitl, +} = {}) { + if (typeof perform !== 'function') { + throw new Error('runGuardedAction requires a perform function.'); + } + + const normalizedMaxAttempts = Math.max(1, Number(maxAttempts) || 1); + const resolvedPermission = + permission ?? + evaluatePermissionGate({ + action, + role, + targetPath, + cwd, + memoryRoot, + replacesRule, + sensitive, + externalSideEffect, + }); + const resolvedHitl = + hitl ?? + evaluateHitlCheckpoints({ + action, + role, + targetPath, + cwd, + memoryRoot, + permission: resolvedPermission, + explicitUserIntent, + replacesRule, + sensitive, + externalSideEffect, + phase, + verificationStatus, + }); + + if (resolvedPermission.decision === PERMISSION_DECISION_DENY || resolvedHitl.decision !== HITL_DECISION_ALLOW) { + return finalizeBlocked(operation, resolvedPermission, resolvedHitl); + } + + const attempts = []; + for (let attempt = 1; attempt <= normalizedMaxAttempts; attempt += 1) { + try { + const result = perform({ + attempt, + operation, + permission: resolvedPermission, + hitl: resolvedHitl, + }); + + if (isSuccessfulResult(result)) { + return { + operation, + status: 'success', + permitted: true, + executed: true, + success: true, + permission: resolvedPermission, + hitl: resolvedHitl, + attempts: [...attempts, { attempt, outcome: 'success' }], + recovery: evaluateErrorRecovery({ + operation, + attempt, + maxAttempts: normalizedMaxAttempts, + }), + result, + error: null, + usedFallback: false, + }; + } + + const recovery = evaluateErrorRecovery({ + operation, + result, + attempt, + maxAttempts: normalizedMaxAttempts, + fallbackAvailable: typeof fallback === 'function', + }); + attempts.push({ + attempt, + outcome: 'failure', + recovery: recovery.decision, + failure: recovery.failure, + }); + + if (recovery.decision === RECOVERY_DECISION_RETRY) { + continue; + } + + if (recovery.decision === RECOVERY_DECISION_FALLBACK) { + const fallbackResult = fallback({ + attempt, + operation, + permission: resolvedPermission, + hitl: resolvedHitl, + recovery, + result, + error: null, + }); + const fallbackSuccess = isSuccessfulResult(fallbackResult); + return { + operation, + status: fallbackSuccess ? 'fallback' : 'failed', + permitted: true, + executed: true, + success: fallbackSuccess, + permission: resolvedPermission, + hitl: resolvedHitl, + attempts, + recovery, + result: fallbackResult, + error: null, + usedFallback: true, + }; + } + + return { + operation, + status: 'failed', + permitted: true, + executed: true, + success: false, + permission: resolvedPermission, + hitl: resolvedHitl, + attempts, + recovery, + result, + error: null, + usedFallback: false, + }; + } catch (error) { + const recovery = evaluateErrorRecovery({ + operation, + error, + attempt, + maxAttempts: normalizedMaxAttempts, + fallbackAvailable: typeof fallback === 'function', + }); + attempts.push({ + attempt, + outcome: 'exception', + recovery: recovery.decision, + failure: recovery.failure, + }); + + if (recovery.decision === RECOVERY_DECISION_RETRY) { + continue; + } + + if (recovery.decision === RECOVERY_DECISION_FALLBACK) { + const fallbackResult = fallback({ + attempt, + operation, + permission: resolvedPermission, + hitl: resolvedHitl, + recovery, + result: null, + error, + }); + const fallbackSuccess = isSuccessfulResult(fallbackResult); + return { + operation, + status: fallbackSuccess ? 'fallback' : 'failed', + permitted: true, + executed: true, + success: fallbackSuccess, + permission: resolvedPermission, + hitl: resolvedHitl, + attempts, + recovery, + result: fallbackResult, + error, + usedFallback: true, + }; + } + + return { + operation, + status: 'failed', + permitted: true, + executed: true, + success: false, + permission: resolvedPermission, + hitl: resolvedHitl, + attempts, + recovery, + result: null, + error, + usedFallback: false, + }; + } + } + + return { + operation, + status: 'failed', + permitted: true, + executed: true, + success: false, + permission: resolvedPermission, + hitl: resolvedHitl, + attempts, + recovery: { + operation, + decision: RECOVERY_DECISION_COMPLETE, + complete: false, + retryable: false, + fallbackAvailable: typeof fallback === 'function', + attempt: normalizedMaxAttempts, + maxAttempts: normalizedMaxAttempts, + nextAttempt: null, + reason: 'Execution exhausted all attempts without a terminal recovery decision.', + failure: null, + }, + result: null, + error: null, + usedFallback: false, + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/leader-refresh-trigger.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/leader-refresh-trigger.js new file mode 100644 index 0000000..e2be49d --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/leader-refresh-trigger.js @@ -0,0 +1,182 @@ +import { spawnSync } from 'node:child_process'; + +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { assertFormalMemoryRefreshAuthority } from '../team/team-contract.js'; +import { evaluatePermissionGate, PERMISSION_DECISION_ALLOW } from '../policy/permission-gate.js'; +import { + evaluateHitlCheckpoints, + HITL_DECISION_ALLOW, + HITL_DECISION_DENY, + HITL_DECISION_REVIEW, +} from '../policy/hitl-checkpoints.js'; +import { runGuardedAction } from './guarded-action-runner.js'; +import { resolveVerificationStatus } from './verification-state.js'; + +export const REFRESH_DECISION_ALLOW = 'allow'; +export const REFRESH_DECISION_REVIEW = 'review'; +export const REFRESH_DECISION_DENY = 'deny'; + +const DEFAULT_TRIGGER_REASON = 'team-terminal'; + +export function evaluateRefreshTrigger({ + role, + workspaceRoot, + phase = 'terminal', + verificationStatus = 'verified', + verificationFilePath, + explicitUserIntent = false, + memoryRoot = DEFAULT_MEMORY_ROOT, +} = {}) { + try { + assertFormalMemoryRefreshAuthority(role); + } catch (error) { + return { + decision: REFRESH_DECISION_DENY, + allowed: false, + reasons: [error.message], + role, + workspaceRoot, + phase, + verificationStatus, + verification: null, + explicitUserIntent, + }; + } + + const verification = resolveVerificationStatus({ + cwd: workspaceRoot, + filePath: verificationFilePath, + verificationStatus, + }); + + const permission = evaluatePermissionGate({ + action: 'formal-memory-refresh', + role, + cwd: workspaceRoot, + memoryRoot, + }); + const hitl = evaluateHitlCheckpoints({ + action: 'formal-memory-refresh', + role, + cwd: workspaceRoot, + memoryRoot, + permission, + explicitUserIntent, + phase, + verificationStatus: verification.status, + }); + const reasons = [...new Set([...permission.reasons, ...hitl.reasons])]; + const decision = + permission.decision !== PERMISSION_DECISION_ALLOW || hitl.decision === HITL_DECISION_DENY + ? REFRESH_DECISION_DENY + : hitl.decision === HITL_DECISION_REVIEW + ? REFRESH_DECISION_REVIEW + : REFRESH_DECISION_ALLOW; + + return { + decision, + allowed: decision === REFRESH_DECISION_ALLOW, + reasons, + permission, + hitl, + verification, + role, + workspaceRoot, + phase, + verificationStatus: verification.status, + explicitUserIntent, + }; +} + +export function buildRefreshCommand({ workspaceRoot }) { + return [ + 'python3', + '/Users/wz/.codex/scripts/refresh_memory.py', + '--workspace-root', + workspaceRoot, + ]; +} + +function defaultRefreshRunner(command) { + return spawnSync(command[0], command.slice(1), { + encoding: 'utf8', + }); +} + +export function triggerFormalMemoryRefresh({ + role, + workspaceRoot, + phase = 'terminal', + verificationStatus = 'verified', + verificationFilePath, + explicitUserIntent = false, + reason = DEFAULT_TRIGGER_REASON, + execute = true, + memoryRoot = DEFAULT_MEMORY_ROOT, + runner = defaultRefreshRunner, + maxAttempts = 2, +} = {}) { + const evaluation = evaluateRefreshTrigger({ + role, + workspaceRoot, + phase, + verificationStatus, + verificationFilePath, + explicitUserIntent, + memoryRoot, + }); + + if (!evaluation.allowed) { + return { + ...evaluation, + executed: false, + reason, + command: buildRefreshCommand({ workspaceRoot }), + result: null, + }; + } + + const command = buildRefreshCommand({ workspaceRoot }); + if (!execute) { + return { + ...evaluation, + executed: false, + reason, + command, + result: null, + }; + } + const execution = runGuardedAction({ + operation: 'formal-memory-refresh', + action: 'formal-memory-refresh', + role, + cwd: workspaceRoot, + memoryRoot, + explicitUserIntent, + phase, + verificationStatus: evaluation.verification?.status ?? verificationStatus, + maxAttempts, + permission: evaluation.permission, + hitl: evaluation.hitl, + perform() { + return runner(command); + }, + isSuccessfulResult(result) { + return (result?.status ?? 0) === 0; + }, + }); + + return { + ...evaluation, + executed: execution.executed, + reason, + command, + result: execution.result, + success: execution.success, + attempts: execution.attempts, + recovery: execution.recovery, + usedFallback: execution.usedFallback, + status: execution.status, + error: execution.error, + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/memory-intake-queue.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/memory-intake-queue.js new file mode 100644 index 0000000..0024d55 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/memory-intake-queue.js @@ -0,0 +1,170 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; + +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { assertTeamWriteAccess } from '../team/team-contract.js'; + +export const MEMORY_INTAKE_FILENAME = 'memory-intake.jsonl'; + +function ensureDirectory(filePath) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); +} + +function intakePathFromOptions({ cwd = process.cwd(), filePath } = {}) { + return filePath ?? path.join(cwd, '.omx', MEMORY_INTAKE_FILENAME); +} + +function toJsonLine(value) { + return `${JSON.stringify(value)}\n`; +} + +function buildIntakeEntryId({ + kind, + role, + source, + content, + metadata, + createdAt, +}) { + const hash = crypto + .createHash('sha1') + .update( + JSON.stringify({ + kind, + role, + source, + content, + metadata, + createdAt, + }) + ) + .digest('hex') + .slice(0, 12); + + return `intake-${hash}`; +} + +function normalizeIntakeEntry(entry = {}) { + const kind = entry.kind ?? 'note'; + const role = entry.role ?? 'main'; + const source = entry.source ?? 'runtime'; + const content = entry.content ?? ''; + const metadata = entry.metadata ?? {}; + const createdAt = entry.created_at ?? new Date(0).toISOString(); + + return { + ...entry, + id: + entry.id ?? + buildIntakeEntryId({ + kind, + role, + source, + content, + metadata, + createdAt, + }), + kind, + role, + source, + content, + metadata, + created_at: createdAt, + }; +} + +export function appendMemoryIntakeEntry({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + filePath, + kind = 'note', + content, + source = 'runtime', + metadata = {}, + createdAt = new Date().toISOString(), + entryId, +} = {}) { + if (!content || !String(content).trim()) { + throw new Error('Memory intake entries require non-empty content.'); + } + + const targetPath = intakePathFromOptions({ cwd, filePath }); + assertTeamWriteAccess({ + role, + targetPath, + cwd, + memoryRoot, + }); + + const entry = { + id: + entryId ?? + buildIntakeEntryId({ + kind, + role, + source, + content: String(content), + metadata, + createdAt, + }), + kind, + role, + source, + content: String(content), + metadata, + created_at: createdAt, + }; + + ensureDirectory(targetPath); + fs.appendFileSync(targetPath, toJsonLine(entry), 'utf8'); + + return { + path: targetPath, + entry, + }; +} + +export function readMemoryIntakeEntries({ + cwd = process.cwd(), + filePath, +} = {}) { + const targetPath = intakePathFromOptions({ cwd, filePath }); + try { + const raw = fs.readFileSync(targetPath, 'utf8'); + const entries = raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => normalizeIntakeEntry(JSON.parse(line))); + + return { + path: targetPath, + exists: true, + entries, + }; + } catch (error) { + if (error && error.code === 'ENOENT') { + return { + path: targetPath, + exists: false, + entries: [], + }; + } + throw error; + } +} + +export function summarizeMemoryIntake({ + cwd = process.cwd(), + filePath, +} = {}) { + const queue = readMemoryIntakeEntries({ cwd, filePath }); + return { + ...queue, + count: queue.entries.length, + kinds: [...new Set(queue.entries.map((entry) => entry.kind))], + latest: queue.entries.at(-1) ?? null, + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/project-memory-commands.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/project-memory-commands.js new file mode 100644 index 0000000..5e239ea --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/project-memory-commands.js @@ -0,0 +1,233 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { readProjectMemoryView } from '../integration/project-memory-view.js'; +import { appendMemoryIntakeEntry } from './memory-intake-queue.js'; +import { assertTeamWriteAccess } from '../team/team-contract.js'; + +export const PROJECT_MEMORY_DECISION_ALLOW = 'allow'; +export const PROJECT_MEMORY_DECISION_DENY = 'deny'; +export const PROJECT_MEMORY_DECISION_DOWNGRADE = 'downgrade'; + +function projectMemoryFilePath({ cwd = process.cwd(), filePath } = {}) { + return filePath ?? path.join(cwd, '.omx', 'project-memory.json'); +} + +function readLocalProjectMemoryFile(targetPath) { + try { + return JSON.parse(fs.readFileSync(targetPath, 'utf8')); + } catch (error) { + if (error && error.code === 'ENOENT') { + return {}; + } + throw error; + } +} + +function writeLocalProjectMemoryFile(targetPath, payload) { + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +function appendListItem(payload, key, item) { + const next = { ...(payload ?? {}) }; + const currentList = Array.isArray(next[key]) ? next[key] : []; + next[key] = [...currentList, item]; + return next; +} + +export function projectMemoryRead({ + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + strictMode = false, +} = {}) { + const view = readProjectMemoryView({ + cwd, + memoryRoot, + strictMode, + }); + + return { + operation: 'project_memory_read', + decision: PROJECT_MEMORY_DECISION_ALLOW, + strictMode, + ...view, + }; +} + +export function projectMemoryWrite({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + strictMode = false, + payload, + filePath, +} = {}) { + if (payload == null || typeof payload !== 'object' || Array.isArray(payload)) { + throw new Error('project_memory_write requires an object payload.'); + } + + const targetPath = projectMemoryFilePath({ cwd, filePath }); + + if (strictMode) { + return { + operation: 'project_memory_write', + decision: PROJECT_MEMORY_DECISION_DENY, + strictMode, + path: targetPath, + message: + 'Strict integration mode forbids direct project_memory_write. Use the formal memory pipeline or intake queue instead.', + }; + } + + assertTeamWriteAccess({ + role, + targetPath, + cwd, + memoryRoot, + }); + writeLocalProjectMemoryFile(targetPath, payload); + + return { + operation: 'project_memory_write', + decision: PROJECT_MEMORY_DECISION_ALLOW, + strictMode, + path: targetPath, + payload, + }; +} + +function downgradeProjectMemoryAppend({ + operation, + role, + cwd, + memoryRoot, + kind, + content, + metadata = {}, +}) { + const appended = appendMemoryIntakeEntry({ + role, + cwd, + memoryRoot, + kind, + content, + source: operation, + metadata, + }); + + return { + operation, + decision: PROJECT_MEMORY_DECISION_DOWNGRADE, + downgraded_to: 'memory-intake-queue', + intake: appended, + }; +} + +function appendLocalProjectMemoryEntry({ + role, + cwd, + memoryRoot, + filePath, + listKey, + value, + operation, +}) { + const targetPath = projectMemoryFilePath({ cwd, filePath }); + assertTeamWriteAccess({ + role, + targetPath, + cwd, + memoryRoot, + }); + + const current = readLocalProjectMemoryFile(targetPath); + const next = appendListItem(current, listKey, value); + writeLocalProjectMemoryFile(targetPath, next); + + return { + operation, + decision: PROJECT_MEMORY_DECISION_ALLOW, + path: targetPath, + payload: next, + }; +} + +export function projectMemoryAddNote({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + strictMode = false, + note, + metadata = {}, + filePath, +} = {}) { + if (!note || !String(note).trim()) { + throw new Error('project_memory_add_note requires non-empty note content.'); + } + + if (strictMode) { + return downgradeProjectMemoryAppend({ + operation: 'project_memory_add_note', + role, + cwd, + memoryRoot, + kind: 'note', + content: String(note), + metadata, + }); + } + + return appendLocalProjectMemoryEntry({ + role, + cwd, + memoryRoot, + filePath, + listKey: 'notes', + value: { + note: String(note), + metadata, + }, + operation: 'project_memory_add_note', + }); +} + +export function projectMemoryAddDirective({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + strictMode = false, + directive, + metadata = {}, + filePath, +} = {}) { + if (!directive || !String(directive).trim()) { + throw new Error('project_memory_add_directive requires non-empty directive content.'); + } + + if (strictMode) { + return downgradeProjectMemoryAppend({ + operation: 'project_memory_add_directive', + role, + cwd, + memoryRoot, + kind: 'directive', + content: String(directive), + metadata, + }); + } + + return appendLocalProjectMemoryEntry({ + role, + cwd, + memoryRoot, + filePath, + listKey: 'directives', + value: { + directive: String(directive), + metadata, + }, + operation: 'project_memory_add_directive', + }); +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/promotion-audit-trail.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/promotion-audit-trail.js new file mode 100644 index 0000000..f87e1b9 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/promotion-audit-trail.js @@ -0,0 +1,123 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { assertTeamWriteAccess } from '../team/team-contract.js'; + +export const PROMOTION_AUDIT_FILENAME = 'promotion-audit.jsonl'; + +function ensureDirectory(filePath) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); +} + +function auditPathFromOptions({ cwd = process.cwd(), filePath } = {}) { + return filePath ?? path.join(cwd, '.omx', PROMOTION_AUDIT_FILENAME); +} + +function toJsonLine(value) { + return `${JSON.stringify(value)}\n`; +} + +function sanitizeSelectedEntries(selectedEntries = []) { + return selectedEntries.map((entry) => ({ + id: entry.id, + kind: entry.kind, + role: entry.role, + source: entry.source, + created_at: entry.created_at, + })); +} + +export function appendPromotionAuditEvent({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + filePath, + event, + decision = 'allow', + selectedEntries = [], + metadata = {}, + createdAt = new Date().toISOString(), + success, +} = {}) { + if (!event || !String(event).trim()) { + throw new Error('Promotion audit events require a non-empty event name.'); + } + + const targetPath = auditPathFromOptions({ cwd, filePath }); + assertTeamWriteAccess({ + role, + targetPath, + cwd, + memoryRoot, + }); + + const entry = { + event: String(event), + role, + decision, + success: typeof success === 'boolean' ? success : null, + entry_ids: sanitizeSelectedEntries(selectedEntries).map((item) => item.id).filter(Boolean), + selected_entries: sanitizeSelectedEntries(selectedEntries), + metadata, + created_at: createdAt, + }; + + ensureDirectory(targetPath); + fs.appendFileSync(targetPath, toJsonLine(entry), 'utf8'); + + return { + path: targetPath, + entry, + }; +} + +export function readPromotionAuditEntries({ + cwd = process.cwd(), + filePath, +} = {}) { + const targetPath = auditPathFromOptions({ cwd, filePath }); + try { + const raw = fs.readFileSync(targetPath, 'utf8'); + const entries = raw + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => JSON.parse(line)); + + return { + path: targetPath, + exists: true, + entries, + }; + } catch (error) { + if (error && error.code === 'ENOENT') { + return { + path: targetPath, + exists: false, + entries: [], + }; + } + throw error; + } +} + +export function summarizePromotionAuditTrail({ + cwd = process.cwd(), + filePath, +} = {}) { + const audit = readPromotionAuditEntries({ cwd, filePath }); + return { + ...audit, + count: audit.entries.length, + events: [...new Set(audit.entries.map((entry) => entry.event))], + latest: audit.entries.at(-1) ?? null, + promotedEntryIds: [ + ...new Set( + audit.entries + .filter((entry) => entry.event === 'promotion_refresh_completed' && entry.success === true) + .flatMap((entry) => entry.entry_ids ?? []) + ), + ], + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/promotion-gate.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/promotion-gate.js new file mode 100644 index 0000000..bbd5ee6 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/promotion-gate.js @@ -0,0 +1,305 @@ +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { TEAM_ROLE_LEADER, TEAM_ROLE_MAIN } from '../team/team-contract.js'; +import { evaluateRefreshTrigger, triggerFormalMemoryRefresh } from './leader-refresh-trigger.js'; +import { readMemoryIntakeEntries } from './memory-intake-queue.js'; +import { + appendPromotionAuditEvent, + readPromotionAuditEntries, + summarizePromotionAuditTrail, +} from './promotion-audit-trail.js'; + +export const PROMOTION_DECISION_ALLOW = 'allow'; +export const PROMOTION_DECISION_REVIEW = 'review'; +export const PROMOTION_DECISION_DENY = 'deny'; + +function isPromotionAuthority(role) { + return role === TEAM_ROLE_LEADER || role === TEAM_ROLE_MAIN; +} + +function mapRefreshDecisionToPromotionDecision(decision) { + if (decision === 'deny') return PROMOTION_DECISION_DENY; + if (decision === 'review') return PROMOTION_DECISION_REVIEW; + return PROMOTION_DECISION_ALLOW; +} + +function selectEntries(allEntries, entryIds = []) { + if (!Array.isArray(entryIds) || entryIds.length === 0) { + return { + selectedEntries: [...allEntries], + missingEntryIds: [], + }; + } + + const byId = new Map(allEntries.map((entry) => [entry.id, entry])); + const selectedEntries = []; + const missingEntryIds = []; + + for (const id of entryIds) { + if (byId.has(id)) { + selectedEntries.push(byId.get(id)); + } else { + missingEntryIds.push(id); + } + } + + return { + selectedEntries, + missingEntryIds, + }; +} + +export function evaluatePromotionGate({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + entryIds = [], + intakeFilePath, + auditFilePath, + phase = 'terminal', + verificationStatus = 'verified', + verificationFilePath, + explicitUserIntent = false, +} = {}) { + const queue = readMemoryIntakeEntries({ + cwd, + filePath: intakeFilePath, + }); + const audit = summarizePromotionAuditTrail({ + cwd, + filePath: auditFilePath, + }); + const { selectedEntries, missingEntryIds } = selectEntries(queue.entries, entryIds); + const alreadyPromotedIds = selectedEntries + .map((entry) => entry.id) + .filter((id) => audit.promotedEntryIds.includes(id)); + const reasons = []; + + if (!isPromotionAuthority(role)) { + reasons.push(`Role "${role}" cannot confirm promotion into the formal memory pipeline.`); + return { + decision: PROMOTION_DECISION_DENY, + allowed: false, + reasons, + role, + selectedEntries, + selectedEntryIds: selectedEntries.map((entry) => entry.id), + missingEntryIds, + alreadyPromotedIds, + queue, + audit, + refresh: null, + }; + } + + if (selectedEntries.length === 0) { + reasons.push('No pending intake entries were selected for promotion.'); + } + + if (missingEntryIds.length > 0) { + reasons.push('Some requested intake entries were not found in the queue.'); + } + + if (alreadyPromotedIds.length > 0 && !explicitUserIntent) { + reasons.push('Some selected intake entries were already promoted and require explicit confirmation to promote again.'); + } + + const refresh = evaluateRefreshTrigger({ + role, + workspaceRoot: cwd, + phase, + verificationStatus, + verificationFilePath, + explicitUserIntent, + memoryRoot, + }); + reasons.push(...refresh.reasons); + + let decision = mapRefreshDecisionToPromotionDecision(refresh.decision); + if (selectedEntries.length === 0 || missingEntryIds.length > 0 || alreadyPromotedIds.length > 0) { + decision = decision === PROMOTION_DECISION_DENY ? decision : PROMOTION_DECISION_REVIEW; + } + + return { + decision, + allowed: decision === PROMOTION_DECISION_ALLOW, + reasons: [...new Set(reasons)], + role, + selectedEntries, + selectedEntryIds: selectedEntries.map((entry) => entry.id), + missingEntryIds, + alreadyPromotedIds, + queue, + audit, + refresh, + }; +} + +function buildAuditMetadata({ + evaluation, + reason, + explicitUserIntent, + phase, + verificationStatus, + verificationFilePath, + refresh, +}) { + return { + reason, + explicit_user_intent: explicitUserIntent, + phase, + verification_status: refresh?.verification?.status ?? evaluation.refresh?.verification?.status ?? verificationStatus, + verification_source: refresh?.verification?.source ?? evaluation.refresh?.verification?.source ?? null, + verification_file_path: verificationFilePath ?? refresh?.verification?.path ?? evaluation.refresh?.verification?.path ?? null, + refresh_decision: refresh?.decision ?? evaluation.refresh?.decision ?? null, + refresh_allowed: refresh?.allowed ?? evaluation.refresh?.allowed ?? null, + missing_entry_ids: evaluation.missingEntryIds, + already_promoted_ids: evaluation.alreadyPromotedIds, + }; +} + +export function triggerFormalPromotion({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + entryIds = [], + intakeFilePath, + auditFilePath, + phase = 'terminal', + verificationStatus = 'verified', + verificationFilePath, + explicitUserIntent = false, + reason = 'promotion-gate', + execute = true, + runner, + maxAttempts = 2, +} = {}) { + const evaluation = evaluatePromotionGate({ + role, + cwd, + memoryRoot, + entryIds, + intakeFilePath, + auditFilePath, + phase, + verificationStatus, + verificationFilePath, + explicitUserIntent, + }); + + if (!evaluation.allowed) { + const blocked = appendPromotionAuditEvent({ + role, + cwd, + memoryRoot, + filePath: auditFilePath, + event: 'promotion_blocked', + decision: evaluation.decision, + selectedEntries: evaluation.selectedEntries, + metadata: buildAuditMetadata({ + evaluation, + reason, + explicitUserIntent, + phase, + verificationStatus, + verificationFilePath, + }), + success: false, + }); + + return { + ...evaluation, + executed: false, + success: false, + reason, + command: null, + refreshResult: null, + auditEvents: [blocked.entry], + auditPath: blocked.path, + }; + } + + const approved = appendPromotionAuditEvent({ + role, + cwd, + memoryRoot, + filePath: auditFilePath, + event: 'promotion_approved', + decision: evaluation.decision, + selectedEntries: evaluation.selectedEntries, + metadata: buildAuditMetadata({ + evaluation, + reason, + explicitUserIntent, + phase, + verificationStatus, + verificationFilePath, + }), + success: true, + }); + + if (!execute) { + return { + ...evaluation, + executed: false, + success: false, + reason, + command: null, + refreshResult: null, + auditEvents: [approved.entry], + auditPath: approved.path, + }; + } + + const refreshResult = triggerFormalMemoryRefresh({ + role, + workspaceRoot: cwd, + phase, + verificationStatus, + verificationFilePath, + explicitUserIntent, + reason, + execute, + memoryRoot, + runner, + maxAttempts, + }); + + const finalEvent = appendPromotionAuditEvent({ + role, + cwd, + memoryRoot, + filePath: auditFilePath, + event: refreshResult.success ? 'promotion_refresh_completed' : 'promotion_refresh_failed', + decision: refreshResult.decision, + selectedEntries: evaluation.selectedEntries, + metadata: { + ...buildAuditMetadata({ + evaluation, + reason, + explicitUserIntent, + phase, + verificationStatus, + verificationFilePath, + refresh: refreshResult, + }), + command: refreshResult.command, + attempts: refreshResult.attempts ?? [], + recovery_decision: refreshResult.recovery?.decision ?? null, + confirmed_by: role, + }, + success: refreshResult.success, + }); + + return { + ...evaluation, + executed: refreshResult.executed, + success: refreshResult.success, + reason, + command: refreshResult.command, + refreshResult, + auditEvents: [approved.entry, finalEvent.entry], + auditPath: approved.path, + }; +} + +export { readPromotionAuditEntries, summarizePromotionAuditTrail }; diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/runtime-facade.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/runtime-facade.js new file mode 100644 index 0000000..85e26fa --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/runtime-facade.js @@ -0,0 +1,423 @@ +import { resolveStrictIntegrationConfig } from '../contracts/strict-integration-mode.js'; +import { buildOverlayContext } from '../overlay/build-overlay-context.js'; +import { evaluateErrorRecovery } from '../policy/error-recovery.js'; +import { evaluateHitlCheckpoints } from '../policy/hitl-checkpoints.js'; +import { buildAgentStartupContext } from './agent-startup-context.js'; +import { runGuardedAction } from './guarded-action-runner.js'; +import { + buildRefreshCommand, + evaluateRefreshTrigger, + triggerFormalMemoryRefresh, +} from './leader-refresh-trigger.js'; +import { + appendMemoryIntakeEntry, + readMemoryIntakeEntries, + summarizeMemoryIntake, +} from './memory-intake-queue.js'; +import { + projectMemoryAddDirective, + projectMemoryAddNote, + projectMemoryRead, + projectMemoryWrite, +} from './project-memory-commands.js'; +import { + evaluatePromotionGate, + readPromotionAuditEntries, + summarizePromotionAuditTrail, + triggerFormalPromotion, +} from './promotion-gate.js'; +import { + stateClear, + stateGetStatus, + stateListActive, + stateRead, + stateWrite, +} from './state-store.js'; +import { + appendVerificationEvidence, + markVerificationFailed, + markVerificationPending, + markVerificationStale, + markVerified, + readVerificationState, + resolveVerificationStatus, +} from './verification-state.js'; + +function createConfigResolver(baseOptions = {}) { + const baseConfig = resolveStrictIntegrationConfig(baseOptions); + const baseEnv = baseOptions.env ?? process.env; + + function resolve(overrides = {}) { + return resolveStrictIntegrationConfig({ + cwd: overrides.cwd ?? baseConfig.cwd, + env: overrides.env ?? baseEnv, + strictMode: overrides.strictMode ?? baseConfig.strictMode, + memoryRoot: overrides.memoryRoot ?? baseConfig.memoryRoot, + }); + } + + return { baseConfig, resolve }; +} + +export function createGovernanceFacade(baseOptions = {}) { + const { baseConfig, resolve } = createConfigResolver(baseOptions); + + return { + config: baseConfig, + resolveConfig(overrides = {}) { + return resolve(overrides); + }, + startup: { + build(options = {}) { + const config = resolve(options); + return buildAgentStartupContext({ + ...config, + role: options.role ?? 'main', + }); + }, + }, + overlay: { + build(options = {}) { + const config = resolve(options); + return buildOverlayContext(config); + }, + }, + projectMemory: { + read(options = {}) { + const config = resolve(options); + return projectMemoryRead(config); + }, + write(options = {}) { + const config = resolve(options); + return projectMemoryWrite({ + ...config, + role: options.role ?? 'main', + payload: options.payload, + filePath: options.filePath, + }); + }, + addNote(options = {}) { + const config = resolve(options); + return projectMemoryAddNote({ + ...config, + role: options.role ?? 'main', + note: options.note, + metadata: options.metadata, + filePath: options.filePath, + }); + }, + addDirective(options = {}) { + const config = resolve(options); + return projectMemoryAddDirective({ + ...config, + role: options.role ?? 'main', + directive: options.directive, + metadata: options.metadata, + filePath: options.filePath, + }); + }, + }, + intake: { + append(options = {}) { + const config = resolve(options); + return appendMemoryIntakeEntry({ + ...config, + role: options.role ?? 'main', + kind: options.kind, + content: options.content, + source: options.source, + metadata: options.metadata, + createdAt: options.createdAt, + filePath: options.filePath, + }); + }, + read(options = {}) { + const config = resolve(options); + return readMemoryIntakeEntries({ + cwd: config.cwd, + filePath: options.filePath, + }); + }, + summarize(options = {}) { + const config = resolve(options); + return summarizeMemoryIntake({ + cwd: config.cwd, + filePath: options.filePath, + }); + }, + }, + promotion: { + evaluate(options = {}) { + const config = resolve(options); + return evaluatePromotionGate({ + ...config, + role: options.role ?? 'main', + entryIds: options.entryIds ?? [], + intakeFilePath: options.intakeFilePath, + auditFilePath: options.auditFilePath, + phase: options.phase, + verificationStatus: options.verificationStatus, + verificationFilePath: options.verificationFilePath, + explicitUserIntent: options.explicitUserIntent ?? false, + }); + }, + trigger(options = {}) { + const config = resolve(options); + return triggerFormalPromotion({ + ...config, + role: options.role ?? 'main', + entryIds: options.entryIds ?? [], + intakeFilePath: options.intakeFilePath, + auditFilePath: options.auditFilePath, + phase: options.phase, + verificationStatus: options.verificationStatus, + verificationFilePath: options.verificationFilePath, + explicitUserIntent: options.explicitUserIntent ?? false, + reason: options.reason, + execute: options.execute, + runner: options.runner, + maxAttempts: options.maxAttempts, + }); + }, + readAudit(options = {}) { + const config = resolve(options); + return readPromotionAuditEntries({ + cwd: config.cwd, + filePath: options.filePath ?? options.auditFilePath, + }); + }, + summarizeAudit(options = {}) { + const config = resolve(options); + return summarizePromotionAuditTrail({ + cwd: config.cwd, + filePath: options.filePath ?? options.auditFilePath, + }); + }, + }, + verification: { + read(options = {}) { + const config = resolve(options); + return readVerificationState({ + cwd: config.cwd, + dirPath: options.dirPath, + filePath: options.filePath ?? options.verificationFilePath, + }); + }, + resolveStatus(options = {}) { + const config = resolve(options); + return resolveVerificationStatus({ + cwd: config.cwd, + dirPath: options.dirPath, + filePath: options.filePath ?? options.verificationFilePath, + verificationStatus: options.verificationStatus, + }); + }, + appendEvidence(options = {}) { + const config = resolve(options); + return appendVerificationEvidence({ + ...config, + role: options.role ?? 'main', + dirPath: options.dirPath, + filePath: options.filePath ?? options.verificationFilePath, + evidence: options.evidence, + summary: options.summary, + kind: options.kind, + command: options.command, + metadata: options.metadata, + createdAt: options.createdAt, + }); + }, + markVerified(options = {}) { + const config = resolve(options); + return markVerified({ + ...config, + role: options.role ?? 'main', + dirPath: options.dirPath, + filePath: options.filePath ?? options.verificationFilePath, + scope: options.scope, + commands: options.commands, + evidence: options.evidence, + notes: options.notes, + updatedAt: options.updatedAt, + verifiedAt: options.verifiedAt, + }); + }, + markFailed(options = {}) { + const config = resolve(options); + return markVerificationFailed({ + ...config, + role: options.role ?? 'main', + dirPath: options.dirPath, + filePath: options.filePath ?? options.verificationFilePath, + scope: options.scope, + commands: options.commands, + evidence: options.evidence, + notes: options.notes, + updatedAt: options.updatedAt, + }); + }, + markPending(options = {}) { + const config = resolve(options); + return markVerificationPending({ + ...config, + role: options.role ?? 'main', + dirPath: options.dirPath, + filePath: options.filePath ?? options.verificationFilePath, + scope: options.scope, + commands: options.commands, + evidence: options.evidence, + notes: options.notes, + updatedAt: options.updatedAt, + }); + }, + markStale(options = {}) { + const config = resolve(options); + return markVerificationStale({ + ...config, + role: options.role ?? 'main', + dirPath: options.dirPath, + filePath: options.filePath ?? options.verificationFilePath, + scope: options.scope, + commands: options.commands, + evidence: options.evidence, + notes: options.notes, + updatedAt: options.updatedAt, + }); + }, + }, + review: { + evaluate(options = {}) { + const config = resolve(options); + return evaluateHitlCheckpoints({ + ...config, + action: options.action, + role: options.role ?? 'main', + targetPath: options.targetPath, + permission: options.permission, + explicitUserIntent: options.explicitUserIntent ?? false, + replacesRule: options.replacesRule ?? false, + sensitive: options.sensitive ?? false, + externalSideEffect: options.externalSideEffect ?? false, + phase: options.phase, + verificationStatus: options.verificationStatus, + }); + }, + }, + recovery: { + evaluate(options = {}) { + return evaluateErrorRecovery({ + operation: options.operation, + error: options.error, + result: options.result, + attempt: options.attempt, + maxAttempts: options.maxAttempts, + fallbackAvailable: options.fallbackAvailable, + }); + }, + run(options = {}) { + const config = resolve(options); + return runGuardedAction({ + ...config, + operation: options.operation, + action: options.action, + role: options.role ?? 'main', + targetPath: options.targetPath, + explicitUserIntent: options.explicitUserIntent ?? false, + replacesRule: options.replacesRule ?? false, + sensitive: options.sensitive ?? false, + externalSideEffect: options.externalSideEffect ?? false, + phase: options.phase, + verificationStatus: options.verificationStatus, + maxAttempts: options.maxAttempts, + perform: options.perform, + fallback: options.fallback, + isSuccessfulResult: options.isSuccessfulResult, + permission: options.permission, + hitl: options.hitl, + }); + }, + }, + state: { + read(options = {}) { + const config = resolve(options); + return stateRead({ + cwd: config.cwd, + dirPath: options.dirPath, + name: options.name, + }); + }, + write(options = {}) { + const config = resolve(options); + return stateWrite({ + ...config, + role: options.role ?? 'main', + dirPath: options.dirPath, + name: options.name, + data: options.data, + }); + }, + clear(options = {}) { + const config = resolve(options); + return stateClear({ + ...config, + role: options.role ?? 'main', + dirPath: options.dirPath, + name: options.name, + }); + }, + listActive(options = {}) { + const config = resolve(options); + return stateListActive({ + cwd: config.cwd, + dirPath: options.dirPath, + }); + }, + getStatus(options = {}) { + const config = resolve(options); + return stateGetStatus({ + cwd: config.cwd, + dirPath: options.dirPath, + name: options.name, + }); + }, + }, + refresh: { + buildCommand(options = {}) { + const config = resolve(options); + return buildRefreshCommand({ + workspaceRoot: options.workspaceRoot ?? config.cwd, + }); + }, + evaluate(options = {}) { + const config = resolve(options); + return evaluateRefreshTrigger({ + ...config, + role: options.role ?? 'main', + workspaceRoot: options.workspaceRoot ?? config.cwd, + phase: options.phase, + verificationStatus: options.verificationStatus, + verificationFilePath: options.verificationFilePath, + explicitUserIntent: options.explicitUserIntent, + }); + }, + trigger(options = {}) { + const config = resolve(options); + return triggerFormalMemoryRefresh({ + ...config, + role: options.role ?? 'main', + workspaceRoot: options.workspaceRoot ?? config.cwd, + phase: options.phase, + verificationStatus: options.verificationStatus, + verificationFilePath: options.verificationFilePath, + explicitUserIntent: options.explicitUserIntent, + reason: options.reason, + execute: options.execute, + runner: options.runner, + maxAttempts: options.maxAttempts, + }); + }, + }, + }; +} + +export const createRuntimeFacade = createGovernanceFacade; diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/state-store.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/state-store.js new file mode 100644 index 0000000..5ef8d94 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/state-store.js @@ -0,0 +1,179 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { assertTeamWriteAccess, canTriggerFormalMemoryRefresh } from '../team/team-contract.js'; + +const RESERVED_VERIFICATION_STATE = 'verification'; + +function stateDirFromOptions({ cwd = process.cwd(), dirPath } = {}) { + return dirPath ?? path.join(cwd, '.omx', 'state'); +} + +function normalizeStateName(name) { + if (!name || !String(name).trim()) { + throw new Error('State operations require a non-empty state name.'); + } + return String(name).trim().replace(/\.json$/i, '').replace(/-state$/i, ''); +} + +function stateFilePath({ cwd = process.cwd(), dirPath, name } = {}) { + const stateName = normalizeStateName(name); + return path.join(stateDirFromOptions({ cwd, dirPath }), `${stateName}-state.json`); +} + +function readJsonFile(filePath) { + try { + const raw = fs.readFileSync(filePath, 'utf8'); + return { + exists: true, + raw, + data: JSON.parse(raw), + }; + } catch (error) { + if (error && error.code === 'ENOENT') { + return { + exists: false, + raw: '', + data: null, + }; + } + throw error; + } +} + +export function stateRead({ + cwd = process.cwd(), + dirPath, + name, +} = {}) { + const targetPath = stateFilePath({ cwd, dirPath, name }); + const stateName = normalizeStateName(name); + const current = readJsonFile(targetPath); + + return { + name: stateName, + path: targetPath, + exists: current.exists, + active: current.exists, + data: current.data, + }; +} + +export function stateWrite({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + dirPath, + name, + data, +} = {}) { + if (data == null || typeof data !== 'object' || Array.isArray(data)) { + throw new Error('state_write requires an object payload.'); + } + + const targetPath = stateFilePath({ cwd, dirPath, name }); + const stateName = normalizeStateName(name); + if (stateName === RESERVED_VERIFICATION_STATE && !canTriggerFormalMemoryRefresh(role)) { + throw new Error('Workers must not modify reserved verification state via raw state_write.'); + } + assertTeamWriteAccess({ + role, + targetPath, + cwd, + memoryRoot, + }); + + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8'); + + return { + name: stateName, + path: targetPath, + exists: true, + active: true, + data, + }; +} + +export function stateClear({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + dirPath, + name, +} = {}) { + const targetPath = stateFilePath({ cwd, dirPath, name }); + const stateName = normalizeStateName(name); + if (stateName === RESERVED_VERIFICATION_STATE && !canTriggerFormalMemoryRefresh(role)) { + throw new Error('Workers must not clear reserved verification state via raw state_clear.'); + } + assertTeamWriteAccess({ + role, + targetPath, + cwd, + memoryRoot, + }); + + try { + fs.unlinkSync(targetPath); + return { + name: stateName, + path: targetPath, + removed: true, + }; + } catch (error) { + if (error && error.code === 'ENOENT') { + return { + name: stateName, + path: targetPath, + removed: false, + }; + } + throw error; + } +} + +export function stateListActive({ + cwd = process.cwd(), + dirPath, +} = {}) { + const targetDir = stateDirFromOptions({ cwd, dirPath }); + try { + const states = fs + .readdirSync(targetDir, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('-state.json')) + .map((entry) => normalizeStateName(entry.name)) + .sort(); + + return { + dir: targetDir, + states, + count: states.length, + }; + } catch (error) { + if (error && error.code === 'ENOENT') { + return { + dir: targetDir, + states: [], + count: 0, + }; + } + throw error; + } +} + +export function stateGetStatus({ + cwd = process.cwd(), + dirPath, + name, +} = {}) { + const current = stateRead({ cwd, dirPath, name }); + return { + name: current.name, + path: current.path, + active: current.active, + exists: current.exists, + keys: current.data ? Object.keys(current.data).sort() : [], + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/runtime/verification-state.js b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/verification-state.js new file mode 100644 index 0000000..62d8729 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/runtime/verification-state.js @@ -0,0 +1,325 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { canTriggerFormalMemoryRefresh } from '../team/team-contract.js'; +import { assertTeamWriteAccess } from '../team/team-contract.js'; + +export const VERIFICATION_STATE_FILENAME = 'verification-state.json'; +export const VERIFICATION_STATE_NAME = 'verification'; + +export const VERIFICATION_STATUS_PENDING = 'pending'; +export const VERIFICATION_STATUS_VERIFIED = 'verified'; +export const VERIFICATION_STATUS_FAILED = 'failed'; +export const VERIFICATION_STATUS_STALE = 'stale'; + +const ALLOWED_STATUSES = new Set([ + VERIFICATION_STATUS_PENDING, + VERIFICATION_STATUS_VERIFIED, + VERIFICATION_STATUS_FAILED, + VERIFICATION_STATUS_STALE, +]); + +function verificationFilePath({ cwd = process.cwd(), dirPath, filePath } = {}) { + return filePath ?? path.join(dirPath ?? path.join(cwd, '.omx', 'state'), VERIFICATION_STATE_FILENAME); +} + +function normalizeStringArray(value) { + if (!Array.isArray(value)) return []; + return [...new Set(value.map((item) => String(item ?? '').trim()).filter(Boolean))]; +} + +function normalizeStatus(status, fallback = VERIFICATION_STATUS_PENDING) { + const candidate = String(status ?? fallback).trim().toLowerCase(); + if (!ALLOWED_STATUSES.has(candidate)) { + throw new Error(`Unsupported verification status: "${status}".`); + } + return candidate; +} + +function normalizeEvidenceEntry(entry, defaults = {}) { + if (entry == null) { + throw new Error('Verification evidence entry is required.'); + } + + if (typeof entry === 'string') { + return { + kind: 'note', + summary: entry.trim(), + command: null, + metadata: {}, + observed_by: defaults.observedBy ?? 'main', + created_at: defaults.createdAt ?? new Date().toISOString(), + }; + } + + if (typeof entry !== 'object' || Array.isArray(entry)) { + throw new Error('Verification evidence entry must be a string or object.'); + } + + const summary = String(entry.summary ?? entry.message ?? entry.text ?? '').trim(); + if (!summary) { + throw new Error('Verification evidence entry requires a non-empty summary.'); + } + + const metadata = + entry.metadata && typeof entry.metadata === 'object' && !Array.isArray(entry.metadata) + ? entry.metadata + : {}; + + return { + kind: String(entry.kind ?? 'note').trim() || 'note', + summary, + command: entry.command != null ? String(entry.command) : null, + metadata, + observed_by: String(entry.observed_by ?? entry.observedBy ?? defaults.observedBy ?? 'main'), + created_at: entry.created_at ?? entry.createdAt ?? defaults.createdAt ?? new Date().toISOString(), + }; +} + +function normalizeEvidenceList(value, defaults = {}) { + if (!Array.isArray(value)) return []; + return value.map((entry) => normalizeEvidenceEntry(entry, defaults)); +} + +function buildDefaultVerificationState(targetPath) { + return { + path: targetPath, + exists: false, + active: false, + status: VERIFICATION_STATUS_PENDING, + scope: [], + commands: [], + evidence: [], + notes: '', + verified_by: null, + verified_at: null, + updated_at: null, + source: 'default', + }; +} + +function normalizeStateData(data, { targetPath, exists }) { + const normalized = buildDefaultVerificationState(targetPath); + normalized.exists = exists; + normalized.active = exists; + normalized.source = exists ? 'artifact' : 'default'; + + if (!data || typeof data !== 'object' || Array.isArray(data)) { + return normalized; + } + + normalized.status = normalizeStatus(data.status, normalized.status); + normalized.scope = normalizeStringArray(data.scope); + normalized.commands = normalizeStringArray(data.commands); + normalized.evidence = normalizeEvidenceList(data.evidence); + normalized.notes = data.notes != null ? String(data.notes) : ''; + normalized.verified_by = data.verified_by != null ? String(data.verified_by) : null; + normalized.verified_at = data.verified_at != null ? String(data.verified_at) : null; + normalized.updated_at = data.updated_at != null ? String(data.updated_at) : null; + + if (normalized.status !== VERIFICATION_STATUS_VERIFIED) { + normalized.verified_by = null; + normalized.verified_at = null; + } + + return normalized; +} + +function readJsonFile(targetPath) { + try { + const raw = fs.readFileSync(targetPath, 'utf8'); + return { + exists: true, + data: JSON.parse(raw), + }; + } catch (error) { + if (error && error.code === 'ENOENT') { + return { + exists: false, + data: null, + }; + } + throw error; + } +} + +function writeStateFile(targetPath, data) { + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8'); +} + +function assertVerificationAuthority(role, status) { + if (status === VERIFICATION_STATUS_VERIFIED && !canTriggerFormalMemoryRefresh(role)) { + throw new Error(`Role "${role}" cannot mark verification as verified.`); + } +} + +export function readVerificationState({ + cwd = process.cwd(), + dirPath, + filePath, +} = {}) { + const targetPath = verificationFilePath({ cwd, dirPath, filePath }); + const current = readJsonFile(targetPath); + return normalizeStateData(current.data, { + targetPath, + exists: current.exists, + }); +} + +export function appendVerificationEvidence({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + dirPath, + filePath, + evidence, + summary, + kind, + command, + metadata, + createdAt, +} = {}) { + const targetPath = verificationFilePath({ cwd, dirPath, filePath }); + assertTeamWriteAccess({ + role, + targetPath, + cwd, + memoryRoot, + }); + + const current = readVerificationState({ cwd, dirPath, filePath }); + const entry = normalizeEvidenceEntry( + evidence ?? { + kind, + summary, + command, + metadata, + observed_by: role, + created_at: createdAt, + }, + { + observedBy: role, + createdAt, + } + ); + + const next = { + status: current.status, + scope: current.scope, + commands: entry.command ? normalizeStringArray([...current.commands, entry.command]) : current.commands, + evidence: [...current.evidence, entry], + notes: current.notes, + verified_by: current.verified_by, + verified_at: current.verified_at, + updated_at: createdAt ?? new Date().toISOString(), + }; + + writeStateFile(targetPath, next); + + return readVerificationState({ cwd, dirPath, filePath }); +} + +export function markVerificationStatus({ + role = 'main', + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, + dirPath, + filePath, + status, + scope, + commands, + evidence, + notes, + updatedAt, + verifiedAt, +} = {}) { + const targetPath = verificationFilePath({ cwd, dirPath, filePath }); + assertTeamWriteAccess({ + role, + targetPath, + cwd, + memoryRoot, + }); + + const nextStatus = normalizeStatus(status); + assertVerificationAuthority(role, nextStatus); + + const current = readVerificationState({ cwd, dirPath, filePath }); + const nextUpdatedAt = updatedAt ?? new Date().toISOString(); + const nextEvidence = evidence != null + ? [...current.evidence, ...normalizeEvidenceList(evidence, { observedBy: role, createdAt: nextUpdatedAt })] + : current.evidence; + + const next = { + status: nextStatus, + scope: scope != null ? normalizeStringArray(scope) : current.scope, + commands: commands != null ? normalizeStringArray(commands) : current.commands, + evidence: nextEvidence, + notes: notes != null ? String(notes) : current.notes, + verified_by: nextStatus === VERIFICATION_STATUS_VERIFIED ? role : null, + verified_at: nextStatus === VERIFICATION_STATUS_VERIFIED ? (verifiedAt ?? nextUpdatedAt) : null, + updated_at: nextUpdatedAt, + }; + + writeStateFile(targetPath, next); + + return readVerificationState({ cwd, dirPath, filePath }); +} + +export function resolveVerificationStatus({ + cwd = process.cwd(), + dirPath, + filePath, + verificationStatus, +} = {}) { + const state = readVerificationState({ cwd, dirPath, filePath }); + if (state.exists) { + return { + status: state.status, + source: 'artifact', + state, + path: state.path, + }; + } + + const status = verificationStatus != null + ? normalizeStatus(verificationStatus, VERIFICATION_STATUS_PENDING) + : VERIFICATION_STATUS_VERIFIED; + + return { + status, + source: verificationStatus != null ? 'parameter' : 'default', + state, + path: state.path, + }; +} + +export function markVerified(options = {}) { + return markVerificationStatus({ + ...options, + status: VERIFICATION_STATUS_VERIFIED, + }); +} + +export function markVerificationPending(options = {}) { + return markVerificationStatus({ + ...options, + status: VERIFICATION_STATUS_PENDING, + }); +} + +export function markVerificationFailed(options = {}) { + return markVerificationStatus({ + ...options, + status: VERIFICATION_STATUS_FAILED, + }); +} + +export function markVerificationStale(options = {}) { + return markVerificationStatus({ + ...options, + status: VERIFICATION_STATUS_STALE, + }); +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/src/team/team-contract.js b/knowledge-base/raw/repos/codex-memory-kit/src/team/team-contract.js new file mode 100644 index 0000000..2829189 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/src/team/team-contract.js @@ -0,0 +1,41 @@ +import { DEFAULT_MEMORY_ROOT } from '../constants.js'; +import { + RuntimeArtifactWriteError, + guardWritePath, +} from '../policy/path-guard.js'; + +export const TEAM_ROLE_LEADER = 'leader'; +export const TEAM_ROLE_WORKER = 'worker'; +export const TEAM_ROLE_MAIN = 'main'; + +export function canTriggerFormalMemoryRefresh(role) { + return role === TEAM_ROLE_LEADER || role === TEAM_ROLE_MAIN; +} + +export function assertFormalMemoryRefreshAuthority(role) { + if (!canTriggerFormalMemoryRefresh(role)) { + throw new Error(`Role "${role}" cannot trigger formal memory refresh.`); + } +} + +export function assertTeamWriteAccess({ + role, + targetPath, + cwd = process.cwd(), + memoryRoot = DEFAULT_MEMORY_ROOT, +}) { + if (!role) { + throw new Error('A team role is required to validate write access.'); + } + + const result = { + role, + ...guardWritePath(targetPath, { cwd, memoryRoot }), + }; + + if (role === TEAM_ROLE_WORKER && !['worker-run', 'telemetry'].includes(result.classification)) { + throw new RuntimeArtifactWriteError(targetPath, role); + } + + return result; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/agent-startup-context.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/agent-startup-context.test.js new file mode 100644 index 0000000..6bbdbce --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/agent-startup-context.test.js @@ -0,0 +1,50 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { buildAgentStartupContext } from '../src/runtime/agent-startup-context.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('strict startup context uses formal overlay text and blocks legacy primary memory sources', () => { + const fixture = createWorkspaceFixture(); + + const startup = buildAgentStartupContext({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + role: 'worker', + }); + + assert.equal(startup.config.strictMode, true); + assert.equal(startup.projectMemoryView.mode, 'strict-formal-memory'); + assert.ok(startup.text.includes('Current task context')); + assert.ok(startup.text.includes('Workspace memory')); + assert.ok(!startup.text.includes('"should": "be ignored"')); + + const localProjectMemoryPolicy = startup.legacySourcePolicy.find( + (entry) => entry.sourceType === 'local-project-memory' + ); + const notepadPolicy = startup.legacySourcePolicy.find( + (entry) => entry.sourceType === 'notepad' + ); + + assert.equal(localProjectMemoryPolicy.decision, 'block'); + assert.equal(notepadPolicy.decision, 'supplement'); + assert.ok( + startup.diagnostics.some((entry) => entry.kind === 'legacy-memory-source-blocked') + ); +}); + +test('non-strict startup context still exposes local project memory view', () => { + const fixture = createWorkspaceFixture(); + + const startup = buildAgentStartupContext({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: false, + role: 'main', + }); + + assert.equal(startup.projectMemoryView.mode, 'local-project-memory'); + assert.ok(startup.projectMemoryView.text.includes('"should": "be ignored"')); + assert.ok(startup.text.includes('Current task context')); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/error-recovery.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/error-recovery.test.js new file mode 100644 index 0000000..9f29a8e --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/error-recovery.test.js @@ -0,0 +1,56 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + RECOVERY_DECISION_ESCALATE, + RECOVERY_DECISION_FALLBACK, + RECOVERY_DECISION_HALT, + RECOVERY_DECISION_RETRY, + evaluateErrorRecovery, +} from '../src/policy/error-recovery.js'; +import { FormalMemoryWriteError } from '../src/policy/path-guard.js'; + +test('transient failures retry before the max attempt is exhausted', () => { + const error = Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' }); + const result = evaluateErrorRecovery({ + operation: 'refresh', + error, + attempt: 1, + maxAttempts: 2, + }); + + assert.equal(result.decision, RECOVERY_DECISION_RETRY); + assert.equal(result.nextAttempt, 2); + assert.equal(result.failure.category, 'transient'); +}); + +test('fallback is selected when retry budget is exhausted and a fallback exists', () => { + const result = evaluateErrorRecovery({ + operation: 'refresh', + result: { status: 124, stderr: 'timed out' }, + attempt: 2, + maxAttempts: 2, + fallbackAvailable: true, + }); + + assert.equal(result.decision, RECOVERY_DECISION_FALLBACK); + assert.equal(result.failure.category, 'transient'); +}); + +test('policy and validation failures do not auto-retry', () => { + const policy = evaluateErrorRecovery({ + operation: 'write', + error: new FormalMemoryWriteError('Blocked by policy.'), + attempt: 1, + maxAttempts: 3, + }); + assert.equal(policy.decision, RECOVERY_DECISION_ESCALATE); + + const validation = evaluateErrorRecovery({ + operation: 'write', + error: new Error('project_memory_add_note requires non-empty note content.'), + attempt: 1, + maxAttempts: 3, + }); + assert.equal(validation.decision, RECOVERY_DECISION_HALT); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/external-memory.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/external-memory.test.js new file mode 100644 index 0000000..c148f29 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/external-memory.test.js @@ -0,0 +1,48 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { + buildFormalMemorySummary, + readFormalMemoryContext, +} from '../src/integration/external-memory.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('readFormalMemoryContext falls back cleanly when the workspace index is missing', () => { + const fixture = createWorkspaceFixture(); + fs.rmSync(path.join(fixture.memoryRoot, 'workspaces', 'index.json')); + + const context = readFormalMemoryContext({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + + assert.equal(context.workspace, null); + assert.equal(context.repoGuide, null); + assert.equal(context.memoryIndex, null); + assert.equal(context.activeContext, null); + assert.equal(context.sharedGuides.length, 3); + assert.equal(context.diagnostics[0]?.kind, 'workspace-memory-unavailable'); +}); + +test('buildFormalMemorySummary still returns shared guides during fallback', () => { + const fixture = createWorkspaceFixture(); + fs.rmSync(path.join(fixture.memoryRoot, 'workspaces', 'index.json')); + + const summary = buildFormalMemorySummary( + readFormalMemoryContext({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }) + ); + + assert.equal(summary.workspace, null); + assert.deepEqual( + summary.sections.map((section) => section.source), + ['shared-guide:company', 'shared-guide:user', 'shared-guide:local'] + ); + assert.ok(summary.text.includes('Company rules')); + assert.ok(summary.text.includes('User rules')); + assert.ok(summary.text.includes('Local rules')); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/guarded-action-runner.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/guarded-action-runner.test.js new file mode 100644 index 0000000..da5f4a1 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/guarded-action-runner.test.js @@ -0,0 +1,111 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { runGuardedAction } from '../src/runtime/guarded-action-runner.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('guarded actions stop at HITL checkpoints until explicit user intent is provided', () => { + const fixture = createWorkspaceFixture(); + const targetPath = path.join(fixture.workspaceRoot, '.omx', 'plans', 'plan.md'); + + const blocked = runGuardedAction({ + operation: 'rule-sync', + action: 'write', + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + targetPath, + replacesRule: true, + perform() { + return { status: 0 }; + }, + isSuccessfulResult(result) { + return result.status === 0; + }, + }); + + assert.equal(blocked.status, 'blocked'); + assert.equal(blocked.executed, false); + + const allowed = runGuardedAction({ + operation: 'rule-sync', + action: 'write', + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + targetPath, + replacesRule: true, + explicitUserIntent: true, + perform() { + return { status: 0 }; + }, + isSuccessfulResult(result) { + return result.status === 0; + }, + }); + + assert.equal(allowed.status, 'success'); + assert.equal(allowed.success, true); + assert.equal(allowed.hitl.satisfied, true); +}); + +test('guarded actions retry transient failures and then succeed', () => { + const fixture = createWorkspaceFixture(); + const targetPath = path.join(fixture.workspaceRoot, '.omx', 'plans', 'plan.md'); + let calls = 0; + + const result = runGuardedAction({ + operation: 'refresh-plan', + action: 'write', + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + targetPath, + perform() { + calls += 1; + if (calls === 1) { + throw Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' }); + } + return { status: 0 }; + }, + isSuccessfulResult(outcome) { + return outcome.status === 0; + }, + maxAttempts: 2, + }); + + assert.equal(result.status, 'success'); + assert.equal(result.success, true); + assert.equal(calls, 2); + assert.equal(result.attempts.length, 2); + assert.equal(result.attempts[0].recovery, 'retry'); +}); + +test('guarded actions can fall back after a prerequisite failure', () => { + const fixture = createWorkspaceFixture(); + let fallbackCalled = 0; + + const result = runGuardedAction({ + operation: 'external-read', + action: 'read', + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + perform() { + throw Object.assign(new Error('missing binary'), { code: 'ENOENT' }); + }, + fallback() { + fallbackCalled += 1; + return { status: 0, source: 'fallback' }; + }, + isSuccessfulResult(outcome) { + return outcome.status === 0; + }, + }); + + assert.equal(result.status, 'fallback'); + assert.equal(result.success, true); + assert.equal(result.usedFallback, true); + assert.equal(fallbackCalled, 1); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/helpers/fixtures.js b/knowledge-base/raw/repos/codex-memory-kit/test/helpers/fixtures.js new file mode 100644 index 0000000..7ff6ae9 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/helpers/fixtures.js @@ -0,0 +1,104 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +export function writeText(filePath, content) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf8'); + return filePath; +} + +function runGit(cwd, args) { + const result = spawnSync('git', args, { + cwd, + encoding: 'utf8', + }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error(result.stderr || result.stdout || `git ${args.join(' ')} failed`); + } + return result; +} + +export function createWorkspaceFixture() { + const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mult-agent-workspace-')); + const memoryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mult-agent-memory-')); + const workspaceKey = 'workspace-123'; + const workspaceMemoryHome = path.join(memoryRoot, 'workspaces', workspaceKey); + + writeText( + path.join(memoryRoot, 'workspaces', 'index.json'), + JSON.stringify( + { + version: 1, + workspaces: { + [workspaceRoot.toLowerCase()]: { + key: workspaceKey, + path: workspaceRoot, + }, + }, + }, + null, + 2 + ) + ); + + writeText(path.join(memoryRoot, 'instructions', 'company', 'GUIDE.md'), '# Company Guide\n\nCompany rules'); + writeText(path.join(memoryRoot, 'instructions', 'user', 'GUIDE.md'), '# User Guide\n\nUser rules'); + writeText(path.join(memoryRoot, 'instructions', 'local', 'GUIDE.md'), '# Local Guide\n\nLocal rules'); + + writeText(path.join(workspaceMemoryHome, 'instructions', 'repo', 'GUIDE.md'), '# Repo Guide\n\nRepo rules'); + writeText(path.join(workspaceMemoryHome, 'memories', 'MEMORY.md'), '# MEMORY\n\nWorkspace memory'); + writeText( + path.join(workspaceMemoryHome, 'runtime', 'active_context.md'), + '# Compressed Context\n\nCurrent task context' + ); + + writeText(path.join(workspaceRoot, '.omx', 'state', 'ralph-state.json'), '{"mode":"team"}\n'); + writeText( + path.join(workspaceRoot, '.omx', 'notepad.md'), + '# Notepad\n\n## PRIORITY\n\nKeep the hottest context here.\n\n## WORKING MEMORY\n\nScratch.\n' + ); + writeText(path.join(workspaceRoot, '.omx', 'project-memory.json'), '{"should":"be ignored"}\n'); + writeText(path.join(workspaceRoot, '.omx', 'logs', 'events.jsonl'), '{"kind":"telemetry"}\n'); + + return { + memoryRoot, + workspaceRoot, + workspaceKey, + workspaceMemoryHome, + }; +} + +export function createSkillsFixture() { + const skillsRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mult-agent-skills-')); + writeText( + path.join(skillsRoot, 'planner', 'SKILL.md'), + '# Planner\n\nPlan the work before implementation.\n' + ); + writeText( + path.join(skillsRoot, '.system', 'reviewer', 'SKILL.md'), + '# Reviewer\n\nReview changes for bugs and regressions.\n' + ); + + return { + skillsRoot, + }; +} + +export function createGitRepoFixture() { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mult-agent-git-repo-')); + writeText(path.join(repoRoot, 'README.md'), '# Test Repo\n'); + runGit(repoRoot, ['init']); + runGit(repoRoot, ['config', 'user.name', 'Test User']); + runGit(repoRoot, ['config', 'user.email', 'test@example.com']); + runGit(repoRoot, ['add', '.']); + runGit(repoRoot, ['commit', '-m', 'Initial commit']); + + return { + repoRoot, + }; +} diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/hitl-checkpoints.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/hitl-checkpoints.test.js new file mode 100644 index 0000000..95a40f5 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/hitl-checkpoints.test.js @@ -0,0 +1,82 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { + HITL_DECISION_ALLOW, + HITL_DECISION_DENY, + HITL_DECISION_REVIEW, + evaluateHitlCheckpoints, +} from '../src/policy/hitl-checkpoints.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('high-risk writes require HITL review until explicit user intent is present', () => { + const fixture = createWorkspaceFixture(); + const targetPath = path.join(fixture.workspaceRoot, '.omx', 'plans', 'plan.md'); + + const review = evaluateHitlCheckpoints({ + action: 'write', + role: 'leader', + targetPath, + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + replacesRule: true, + externalSideEffect: true, + }); + + assert.equal(review.decision, HITL_DECISION_REVIEW); + assert.equal(review.requiresHuman, true); + assert.deepEqual( + review.checkpoints.map((checkpoint) => checkpoint.id).sort(), + ['external-side-effect', 'rule-replacement'] + ); + + const allowed = evaluateHitlCheckpoints({ + action: 'write', + role: 'leader', + targetPath, + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + replacesRule: true, + externalSideEffect: true, + explicitUserIntent: true, + }); + + assert.equal(allowed.decision, HITL_DECISION_ALLOW); + assert.equal(allowed.satisfied, true); +}); + +test('refresh preconditions become HITL checkpoints when verification or phase is incomplete', () => { + const fixture = createWorkspaceFixture(); + + const result = evaluateHitlCheckpoints({ + action: 'formal-memory-refresh', + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + phase: 'active', + verificationStatus: 'pending', + }); + + assert.equal(result.decision, HITL_DECISION_REVIEW); + assert.deepEqual( + result.checkpoints.map((checkpoint) => checkpoint.id).sort(), + ['refresh-non-terminal', 'refresh-unverified'] + ); +}); + +test('permission denial remains denied even before HITL satisfaction is considered', () => { + const fixture = createWorkspaceFixture(); + const targetPath = path.join(fixture.workspaceMemoryHome, 'memories', 'project', 'truth.md'); + + const result = evaluateHitlCheckpoints({ + action: 'write', + role: 'worker', + targetPath, + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + + assert.equal(result.decision, HITL_DECISION_DENY); + assert.equal(result.allowed, false); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/leader-refresh-trigger.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/leader-refresh-trigger.test.js new file mode 100644 index 0000000..8bb9262 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/leader-refresh-trigger.test.js @@ -0,0 +1,183 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + REFRESH_DECISION_ALLOW, + REFRESH_DECISION_DENY, + REFRESH_DECISION_REVIEW, + buildRefreshCommand, + evaluateRefreshTrigger, + triggerFormalMemoryRefresh, +} from '../src/runtime/leader-refresh-trigger.js'; +import { markVerificationPending, markVerified } from '../src/runtime/verification-state.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('buildRefreshCommand targets the shared refresh script', () => { + const fixture = createWorkspaceFixture(); + assert.deepEqual(buildRefreshCommand({ workspaceRoot: fixture.workspaceRoot }), [ + 'python3', + '/Users/wz/.codex/scripts/refresh_memory.py', + '--workspace-root', + fixture.workspaceRoot, + ]); +}); + +test('leader terminal verified refresh is allowed, worker is denied', () => { + const fixture = createWorkspaceFixture(); + + const allowed = evaluateRefreshTrigger({ + role: 'leader', + workspaceRoot: fixture.workspaceRoot, + phase: 'terminal', + verificationStatus: 'verified', + memoryRoot: fixture.memoryRoot, + }); + assert.equal(allowed.decision, REFRESH_DECISION_ALLOW); + assert.equal(allowed.allowed, true); + + const denied = evaluateRefreshTrigger({ + role: 'worker', + workspaceRoot: fixture.workspaceRoot, + phase: 'terminal', + verificationStatus: 'verified', + memoryRoot: fixture.memoryRoot, + }); + assert.equal(denied.decision, REFRESH_DECISION_DENY); + assert.equal(denied.allowed, false); +}); + +test('refresh outside terminal or before verification requires review unless explicitly requested', () => { + const fixture = createWorkspaceFixture(); + + const early = evaluateRefreshTrigger({ + role: 'leader', + workspaceRoot: fixture.workspaceRoot, + phase: 'active', + verificationStatus: 'verified', + memoryRoot: fixture.memoryRoot, + }); + assert.equal(early.decision, REFRESH_DECISION_REVIEW); + + const unverified = evaluateRefreshTrigger({ + role: 'leader', + workspaceRoot: fixture.workspaceRoot, + phase: 'terminal', + verificationStatus: 'pending', + memoryRoot: fixture.memoryRoot, + }); + assert.equal(unverified.decision, REFRESH_DECISION_REVIEW); + + const explicit = evaluateRefreshTrigger({ + role: 'leader', + workspaceRoot: fixture.workspaceRoot, + phase: 'active', + verificationStatus: 'pending', + explicitUserIntent: true, + memoryRoot: fixture.memoryRoot, + }); + assert.equal(explicit.decision, REFRESH_DECISION_ALLOW); +}); + +test('refresh evaluation prefers verification artifact status over the caller parameter', () => { + const fixture = createWorkspaceFixture(); + + markVerificationPending({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + notes: 'Verification still pending.', + }); + + const pending = evaluateRefreshTrigger({ + role: 'leader', + workspaceRoot: fixture.workspaceRoot, + phase: 'terminal', + verificationStatus: 'verified', + memoryRoot: fixture.memoryRoot, + }); + assert.equal(pending.decision, REFRESH_DECISION_REVIEW); + assert.equal(pending.verification.source, 'artifact'); + assert.equal(pending.verification.status, 'pending'); + + markVerified({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + scope: ['tests'], + commands: ['npm test'], + }); + + const verified = evaluateRefreshTrigger({ + role: 'leader', + workspaceRoot: fixture.workspaceRoot, + phase: 'terminal', + verificationStatus: 'pending', + memoryRoot: fixture.memoryRoot, + }); + assert.equal(verified.decision, REFRESH_DECISION_ALLOW); + assert.equal(verified.verification.source, 'artifact'); + assert.equal(verified.verification.status, 'verified'); +}); + +test('triggerFormalMemoryRefresh executes via the injected runner only when allowed', () => { + const fixture = createWorkspaceFixture(); + const calls = []; + + const success = triggerFormalMemoryRefresh({ + role: 'leader', + workspaceRoot: fixture.workspaceRoot, + phase: 'terminal', + verificationStatus: 'verified', + memoryRoot: fixture.memoryRoot, + runner(command) { + calls.push(command); + return { status: 0, stdout: 'ok', stderr: '' }; + }, + }); + + assert.equal(success.executed, true); + assert.equal(success.success, true); + assert.equal(calls.length, 1); + + const blocked = triggerFormalMemoryRefresh({ + role: 'worker', + workspaceRoot: fixture.workspaceRoot, + phase: 'terminal', + verificationStatus: 'verified', + memoryRoot: fixture.memoryRoot, + runner(command) { + calls.push(command); + return { status: 0 }; + }, + }); + + assert.equal(blocked.executed, false); + assert.equal(calls.length, 1); +}); + +test('triggerFormalMemoryRefresh retries transient runner failures before succeeding', () => { + const fixture = createWorkspaceFixture(); + let calls = 0; + + const result = triggerFormalMemoryRefresh({ + role: 'leader', + workspaceRoot: fixture.workspaceRoot, + phase: 'terminal', + verificationStatus: 'verified', + memoryRoot: fixture.memoryRoot, + maxAttempts: 2, + runner() { + calls += 1; + if (calls === 1) { + throw Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' }); + } + return { status: 0, stdout: 'ok', stderr: '' }; + }, + }); + + assert.equal(result.success, true); + assert.equal(result.executed, true); + assert.equal(calls, 2); + assert.equal(result.attempts.length, 2); + assert.equal(result.recovery.complete, true); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/legacy-memory-bypass.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/legacy-memory-bypass.test.js new file mode 100644 index 0000000..754c7c4 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/legacy-memory-bypass.test.js @@ -0,0 +1,85 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { + LEGACY_SOURCE_DECISION_ALLOW, + LEGACY_SOURCE_DECISION_BLOCK, + LEGACY_SOURCE_DECISION_SUPPLEMENT, + classifyLegacyMemorySource, + evaluateLegacyMemorySource, +} from '../src/policy/legacy-memory-bypass.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('legacy source classification distinguishes local memory, notepad, and telemetry', () => { + const fixture = createWorkspaceFixture(); + + assert.equal( + classifyLegacyMemorySource(path.join(fixture.workspaceRoot, '.omx', 'project-memory.json'), { + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }), + 'local-project-memory' + ); + + assert.equal( + classifyLegacyMemorySource(path.join(fixture.workspaceRoot, '.omx', 'notepad.md'), { + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }), + 'notepad' + ); + + assert.equal( + classifyLegacyMemorySource(path.join(fixture.workspaceRoot, '.omx', 'logs', 'events.jsonl'), { + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }), + 'telemetry' + ); +}); + +test('strict mode blocks local project memory as a primary source', () => { + const fixture = createWorkspaceFixture(); + + const result = evaluateLegacyMemorySource({ + sourcePath: path.join(fixture.workspaceRoot, '.omx', 'project-memory.json'), + intendedUse: 'primary', + strictMode: true, + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + + assert.equal(result.decision, LEGACY_SOURCE_DECISION_BLOCK); +}); + +test('notepad is allowed only as a supplement and telemetry is always blocked', () => { + const fixture = createWorkspaceFixture(); + + const supplement = evaluateLegacyMemorySource({ + sourcePath: path.join(fixture.workspaceRoot, '.omx', 'notepad.md'), + intendedUse: 'supplement', + strictMode: true, + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + assert.equal(supplement.decision, LEGACY_SOURCE_DECISION_SUPPLEMENT); + + const telemetry = evaluateLegacyMemorySource({ + sourcePath: path.join(fixture.workspaceRoot, '.omx', 'logs', 'events.jsonl'), + intendedUse: 'primary', + strictMode: false, + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + assert.equal(telemetry.decision, LEGACY_SOURCE_DECISION_BLOCK); + + const localNonStrict = evaluateLegacyMemorySource({ + sourcePath: path.join(fixture.workspaceRoot, '.omx', 'project-memory.json'), + intendedUse: 'primary', + strictMode: false, + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + assert.equal(localNonStrict.decision, LEGACY_SOURCE_DECISION_ALLOW); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/memory-intake-queue.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/memory-intake-queue.test.js new file mode 100644 index 0000000..8b0e3d1 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/memory-intake-queue.test.js @@ -0,0 +1,111 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { + MEMORY_INTAKE_FILENAME, + appendMemoryIntakeEntry, + readMemoryIntakeEntries, + summarizeMemoryIntake, +} from '../src/runtime/memory-intake-queue.js'; +import { RuntimeArtifactWriteError } from '../src/policy/path-guard.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('memory intake queue appends and reads entries from .omx/memory-intake.jsonl', () => { + const fixture = createWorkspaceFixture(); + + const appended = appendMemoryIntakeEntry({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + kind: 'observation', + content: 'Potential durable rule candidate.', + metadata: { source_file: 'plan.md' }, + createdAt: '2026-04-04T10:00:00Z', + }); + + assert.match(appended.path, new RegExp(`\\.omx/${MEMORY_INTAKE_FILENAME.replace('.', '\\.')}$`)); + + const queue = readMemoryIntakeEntries({ + cwd: fixture.workspaceRoot, + }); + + assert.equal(queue.exists, true); + assert.equal(queue.entries.length, 1); + assert.match(queue.entries[0].id, /^intake-/); + assert.equal(queue.entries[0].kind, 'observation'); + assert.equal(queue.entries[0].role, 'worker'); + assert.equal(queue.entries[0].metadata.source_file, 'plan.md'); +}); + +test('memory intake summary reports kinds and latest entry', () => { + const fixture = createWorkspaceFixture(); + + appendMemoryIntakeEntry({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + kind: 'observation', + content: 'First note.', + createdAt: '2026-04-04T10:00:00Z', + }); + appendMemoryIntakeEntry({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + kind: 'directive', + content: 'Second note.', + createdAt: '2026-04-04T10:05:00Z', + }); + + const summary = summarizeMemoryIntake({ + cwd: fixture.workspaceRoot, + }); + + assert.equal(summary.count, 2); + assert.deepEqual(summary.kinds.sort(), ['directive', 'observation']); + assert.equal(summary.latest.content, 'Second note.'); +}); + +test('workers cannot place the intake queue outside runtime artifacts', () => { + const fixture = createWorkspaceFixture(); + + assert.throws( + () => + appendMemoryIntakeEntry({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + filePath: path.join(fixture.workspaceRoot, 'notes', 'memory-intake.jsonl'), + kind: 'observation', + content: 'This should fail.', + }), + RuntimeArtifactWriteError + ); +}); + +test('legacy intake entries without ids are normalized on read', () => { + const fixture = createWorkspaceFixture(); + const legacyPath = path.join(fixture.workspaceRoot, '.omx', MEMORY_INTAKE_FILENAME); + fs.writeFileSync( + legacyPath, + `${JSON.stringify({ + kind: 'note', + role: 'worker', + source: 'legacy', + content: 'Old queue entry', + metadata: {}, + created_at: '2026-04-04T10:00:00Z', + })}\n`, + 'utf8' + ); + + const queue = readMemoryIntakeEntries({ + cwd: fixture.workspaceRoot, + }); + + assert.equal(queue.entries.length, 1); + assert.match(queue.entries[0].id, /^intake-/); + assert.equal(queue.entries[0].source, 'legacy'); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/overlay-context.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/overlay-context.test.js new file mode 100644 index 0000000..b9d9fbb --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/overlay-context.test.js @@ -0,0 +1,32 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { buildOverlayContext } from '../src/overlay/build-overlay-context.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('buildOverlayContext follows the formal memory first read order', () => { + const fixture = createWorkspaceFixture(); + + const overlay = buildOverlayContext({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + + assert.deepEqual( + overlay.sections.map((section) => section.source), + [ + 'omx-state', + 'active-context', + 'memory-index', + 'repo-guide', + 'shared-guide:company', + 'shared-guide:user', + 'shared-guide:local', + 'notepad-priority', + ] + ); + + assert.ok(overlay.text.includes('Current task context')); + assert.ok(overlay.text.includes('Workspace memory')); + assert.ok(!overlay.text.includes('should":"be ignored')); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/path-guard.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/path-guard.test.js new file mode 100644 index 0000000..19bb598 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/path-guard.test.js @@ -0,0 +1,57 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { + FormalMemoryWriteError, + classifyArtifactPath, + guardWritePath, +} from '../src/policy/path-guard.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('guardWritePath rejects formal memory writes', () => { + const fixture = createWorkspaceFixture(); + const formalMemoryPath = path.join( + fixture.workspaceMemoryHome, + 'memories', + 'project', + 'truth.md' + ); + + assert.throws( + () => + guardWritePath(formalMemoryPath, { + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }), + FormalMemoryWriteError + ); +}); + +test('classifyArtifactPath distinguishes worker-run, telemetry, and other paths', () => { + const fixture = createWorkspaceFixture(); + + assert.equal( + classifyArtifactPath(path.join(fixture.workspaceRoot, '.omx', 'state', 'ralph-state.json'), { + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }), + 'worker-run' + ); + + assert.equal( + classifyArtifactPath(path.join(fixture.workspaceRoot, '.omx', 'logs', 'events.jsonl'), { + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }), + 'telemetry' + ); + + assert.equal( + classifyArtifactPath(path.join(fixture.workspaceRoot, 'src', 'app.js'), { + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }), + 'other' + ); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/permission-gate.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/permission-gate.test.js new file mode 100644 index 0000000..57f692f --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/permission-gate.test.js @@ -0,0 +1,74 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { + PERMISSION_DECISION_ALLOW, + PERMISSION_DECISION_DENY, + PERMISSION_DECISION_REVIEW, + evaluatePermissionGate, +} from '../src/policy/permission-gate.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('formal memory writes are denied', () => { + const fixture = createWorkspaceFixture(); + const result = evaluatePermissionGate({ + action: 'write', + role: 'worker', + targetPath: path.join(fixture.workspaceMemoryHome, 'memories', 'project', 'truth.md'), + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + + assert.equal(result.decision, PERMISSION_DECISION_DENY); + assert.equal(result.allowed, false); +}); + +test('worker writes outside runtime artifacts require review', () => { + const fixture = createWorkspaceFixture(); + const result = evaluatePermissionGate({ + action: 'write', + role: 'worker', + targetPath: path.join(fixture.workspaceRoot, 'src', 'feature.js'), + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + + assert.equal(result.decision, PERMISSION_DECISION_REVIEW); +}); + +test('external side effects and rule replacements escalate to review', () => { + const fixture = createWorkspaceFixture(); + const result = evaluatePermissionGate({ + action: 'write', + role: 'leader', + targetPath: path.join(fixture.workspaceRoot, '.omx', 'plans', 'plan.md'), + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + replacesRule: true, + externalSideEffect: true, + }); + + assert.equal(result.decision, PERMISSION_DECISION_REVIEW); + assert.match(result.reasons.join(' '), /review/i); +}); + +test('leader can trigger formal memory refresh but worker cannot', () => { + const fixture = createWorkspaceFixture(); + + const allowed = evaluatePermissionGate({ + action: 'formal-memory-refresh', + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + assert.equal(allowed.decision, PERMISSION_DECISION_ALLOW); + + const denied = evaluatePermissionGate({ + action: 'formal-memory-refresh', + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + assert.equal(denied.decision, PERMISSION_DECISION_DENY); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/project-memory-commands.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/project-memory-commands.test.js new file mode 100644 index 0000000..9831e84 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/project-memory-commands.test.js @@ -0,0 +1,119 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { + PROJECT_MEMORY_DECISION_ALLOW, + PROJECT_MEMORY_DECISION_DENY, + PROJECT_MEMORY_DECISION_DOWNGRADE, + projectMemoryAddDirective, + projectMemoryAddNote, + projectMemoryRead, + projectMemoryWrite, +} from '../src/runtime/project-memory-commands.js'; +import { readMemoryIntakeEntries } from '../src/runtime/memory-intake-queue.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('projectMemoryRead follows the formal memory view in strict mode', () => { + const fixture = createWorkspaceFixture(); + + const result = projectMemoryRead({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + }); + + assert.equal(result.decision, PROJECT_MEMORY_DECISION_ALLOW); + assert.equal(result.source, 'formal-memory'); + assert.ok(result.text.includes('Current task context')); + assert.ok(!result.text.includes('"should": "be ignored"')); +}); + +test('projectMemoryWrite is explicitly denied in strict mode', () => { + const fixture = createWorkspaceFixture(); + + const result = projectMemoryWrite({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + payload: { should: 'not write' }, + }); + + assert.equal(result.decision, PROJECT_MEMORY_DECISION_DENY); + + const raw = fs.readFileSync(path.join(fixture.workspaceRoot, '.omx', 'project-memory.json'), 'utf8'); + assert.ok(raw.includes('be ignored')); + assert.ok(!raw.includes('not write')); +}); + +test('strict project_memory_add_note and add_directive downgrade into intake queue', () => { + const fixture = createWorkspaceFixture(); + + const note = projectMemoryAddNote({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + note: 'Candidate note', + }); + assert.equal(note.decision, PROJECT_MEMORY_DECISION_DOWNGRADE); + + const directive = projectMemoryAddDirective({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + directive: 'Candidate directive', + }); + assert.equal(directive.decision, PROJECT_MEMORY_DECISION_DOWNGRADE); + + const queue = readMemoryIntakeEntries({ + cwd: fixture.workspaceRoot, + }); + + assert.equal(queue.entries.length, 2); + assert.match(queue.entries[0].id, /^intake-/); + assert.equal(queue.entries[0].source, 'project_memory_add_note'); + assert.equal(queue.entries[1].source, 'project_memory_add_directive'); +}); + +test('non-strict project_memory_write and add_* update local project-memory.json', () => { + const fixture = createWorkspaceFixture(); + const projectMemoryPath = path.join(fixture.workspaceRoot, '.omx', 'project-memory.json'); + + const write = projectMemoryWrite({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: false, + payload: { mode: 'local', notes: [] }, + }); + assert.equal(write.decision, PROJECT_MEMORY_DECISION_ALLOW); + + const note = projectMemoryAddNote({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: false, + note: 'Saved locally', + }); + assert.equal(note.decision, PROJECT_MEMORY_DECISION_ALLOW); + + const directive = projectMemoryAddDirective({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: false, + directive: 'Do this next', + }); + assert.equal(directive.decision, PROJECT_MEMORY_DECISION_ALLOW); + + const parsed = JSON.parse(fs.readFileSync(projectMemoryPath, 'utf8')); + assert.equal(parsed.mode, 'local'); + assert.equal(parsed.notes.length, 1); + assert.equal(parsed.notes[0].note, 'Saved locally'); + assert.equal(parsed.directives.length, 1); + assert.equal(parsed.directives[0].directive, 'Do this next'); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/project-memory-view.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/project-memory-view.test.js new file mode 100644 index 0000000..1e35b3c --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/project-memory-view.test.js @@ -0,0 +1,37 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { readProjectMemoryView } from '../src/integration/project-memory-view.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('strict mode project memory view ignores local .omx/project-memory.json', () => { + const fixture = createWorkspaceFixture(); + + const view = readProjectMemoryView({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + }); + + assert.equal(view.mode, 'strict-formal-memory'); + assert.equal(view.source, 'formal-memory'); + assert.ok(view.text.includes('Current task context')); + assert.ok(view.text.includes('Workspace memory')); + assert.ok(!view.text.includes('should')); + assert.equal(view.localProjectMemory.exists, true); + assert.equal(view.diagnostics.at(-1)?.kind, 'local-project-memory-ignored'); +}); + +test('non-strict mode project memory view can read local .omx/project-memory.json', () => { + const fixture = createWorkspaceFixture(); + + const view = readProjectMemoryView({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: false, + }); + + assert.equal(view.mode, 'local-project-memory'); + assert.equal(view.source, 'local-project-memory'); + assert.ok(view.text.includes('"should": "be ignored"')); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/promotion-audit-trail.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/promotion-audit-trail.test.js new file mode 100644 index 0000000..5ec134a --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/promotion-audit-trail.test.js @@ -0,0 +1,65 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + appendPromotionAuditEvent, + PROMOTION_AUDIT_FILENAME, + readPromotionAuditEntries, + summarizePromotionAuditTrail, +} from '../src/runtime/promotion-audit-trail.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('promotion audit trail appends and reads audit events under .omx', () => { + const fixture = createWorkspaceFixture(); + + const appended = appendPromotionAuditEvent({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + event: 'promotion_approved', + selectedEntries: [{ id: 'intake-1', kind: 'note', role: 'worker', source: 'queue', created_at: '2026-04-04T10:00:00Z' }], + metadata: { reason: 'reviewed' }, + createdAt: '2026-04-04T10:10:00Z', + success: true, + }); + + assert.match(appended.path, new RegExp(`\\.omx/${PROMOTION_AUDIT_FILENAME.replace('.', '\\.')}$`)); + + const audit = readPromotionAuditEntries({ + cwd: fixture.workspaceRoot, + }); + + assert.equal(audit.exists, true); + assert.equal(audit.entries.length, 1); + assert.equal(audit.entries[0].event, 'promotion_approved'); + assert.equal(audit.entries[0].entry_ids[0], 'intake-1'); +}); + +test('promotion audit summary reports completed promoted entry ids', () => { + const fixture = createWorkspaceFixture(); + + appendPromotionAuditEvent({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + event: 'promotion_approved', + selectedEntries: [{ id: 'intake-1', kind: 'note', role: 'worker', source: 'queue', created_at: '2026-04-04T10:00:00Z' }], + success: true, + }); + appendPromotionAuditEvent({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + event: 'promotion_refresh_completed', + selectedEntries: [{ id: 'intake-1', kind: 'note', role: 'worker', source: 'queue', created_at: '2026-04-04T10:00:00Z' }], + success: true, + }); + + const summary = summarizePromotionAuditTrail({ + cwd: fixture.workspaceRoot, + }); + + assert.equal(summary.count, 2); + assert.ok(summary.events.includes('promotion_refresh_completed')); + assert.deepEqual(summary.promotedEntryIds, ['intake-1']); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/promotion-gate.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/promotion-gate.test.js new file mode 100644 index 0000000..3033bae --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/promotion-gate.test.js @@ -0,0 +1,152 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { appendMemoryIntakeEntry, readMemoryIntakeEntries } from '../src/runtime/memory-intake-queue.js'; +import { readPromotionAuditEntries } from '../src/runtime/promotion-audit-trail.js'; +import { + PROMOTION_DECISION_ALLOW, + PROMOTION_DECISION_DENY, + PROMOTION_DECISION_REVIEW, + evaluatePromotionGate, + triggerFormalPromotion, +} from '../src/runtime/promotion-gate.js'; +import { markVerificationPending } from '../src/runtime/verification-state.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('leader can promote selected intake entries and records the audit trail', () => { + const fixture = createWorkspaceFixture(); + const appended = appendMemoryIntakeEntry({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + kind: 'note', + content: 'Promote this note.', + source: 'project_memory_add_note', + createdAt: '2026-04-04T10:00:00Z', + }); + + const evaluation = evaluatePromotionGate({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + entryIds: [appended.entry.id], + phase: 'terminal', + verificationStatus: 'verified', + }); + assert.equal(evaluation.decision, PROMOTION_DECISION_ALLOW); + + const calls = []; + const result = triggerFormalPromotion({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + entryIds: [appended.entry.id], + phase: 'terminal', + verificationStatus: 'verified', + runner(command) { + calls.push(command); + return { status: 0, stdout: 'ok', stderr: '' }; + }, + }); + + assert.equal(result.success, true); + assert.equal(result.executed, true); + assert.equal(calls.length, 1); + assert.equal(result.auditEvents.length, 2); + assert.equal(result.auditEvents[0].event, 'promotion_approved'); + assert.equal(result.auditEvents[1].event, 'promotion_refresh_completed'); + + const audit = readPromotionAuditEntries({ + cwd: fixture.workspaceRoot, + }); + assert.equal(audit.entries.length, 2); + assert.equal(audit.entries[1].entry_ids[0], appended.entry.id); +}); + +test('worker cannot confirm promotion and the blocked attempt is audited', () => { + const fixture = createWorkspaceFixture(); + const appended = appendMemoryIntakeEntry({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + kind: 'note', + content: 'Blocked promotion.', + }); + + const result = triggerFormalPromotion({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + entryIds: [appended.entry.id], + }); + + assert.equal(result.decision, PROMOTION_DECISION_DENY); + assert.equal(result.executed, false); + assert.equal(result.auditEvents[0].event, 'promotion_blocked'); +}); + +test('already promoted entries require review before re-promotion', () => { + const fixture = createWorkspaceFixture(); + const appended = appendMemoryIntakeEntry({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + kind: 'directive', + content: 'Already promoted once.', + }); + + const first = triggerFormalPromotion({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + entryIds: [appended.entry.id], + phase: 'terminal', + verificationStatus: 'verified', + runner() { + return { status: 0, stdout: 'ok', stderr: '' }; + }, + }); + assert.equal(first.success, true); + + const second = evaluatePromotionGate({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + entryIds: [appended.entry.id], + phase: 'terminal', + verificationStatus: 'verified', + }); + assert.equal(second.decision, PROMOTION_DECISION_REVIEW); + assert.deepEqual(second.alreadyPromotedIds, [appended.entry.id]); +}); + +test('promotion gate prefers verification artifact status over the caller parameter', () => { + const fixture = createWorkspaceFixture(); + const appended = appendMemoryIntakeEntry({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + kind: 'note', + content: 'Pending verification should block auto-promotion.', + }); + + markVerificationPending({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + notes: 'Still waiting on checks.', + }); + + const evaluation = evaluatePromotionGate({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + entryIds: [appended.entry.id], + phase: 'terminal', + verificationStatus: 'verified', + }); + + assert.equal(evaluation.decision, PROMOTION_DECISION_REVIEW); + assert.equal(evaluation.refresh.verification.status, 'pending'); + assert.equal(evaluation.refresh.verification.source, 'artifact'); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/runtime-facade.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/runtime-facade.test.js new file mode 100644 index 0000000..4bc1192 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/runtime-facade.test.js @@ -0,0 +1,214 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; +import fs from 'node:fs'; + +import { createGovernanceFacade, createRuntimeFacade } from '../src/runtime/runtime-facade.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('strict governance facade uses formal memory for startup and project memory reads', () => { + const fixture = createWorkspaceFixture(); + const facade = createGovernanceFacade({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + }); + + const startup = facade.startup.build({ role: 'worker' }); + const projectMemory = facade.projectMemory.read(); + const overlay = facade.overlay.build(); + + assert.equal(facade.config.strictMode, true); + assert.ok(startup.text.includes('Current task context')); + assert.equal(projectMemory.source, 'formal-memory'); + assert.ok(!projectMemory.text.includes('"should": "be ignored"')); + assert.ok(overlay.text.includes('Workspace memory')); +}); + +test('strict governance facade rejects writes and downgrades addNote into intake queue', () => { + const fixture = createWorkspaceFixture(); + const facade = createGovernanceFacade({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + }); + + const denied = facade.projectMemory.write({ + role: 'leader', + payload: { should: 'not persist' }, + }); + assert.equal(denied.decision, 'deny'); + + const downgraded = facade.projectMemory.addNote({ + role: 'worker', + note: 'Queue this note', + }); + assert.equal(downgraded.decision, 'downgrade'); + + const queue = facade.intake.read(); + assert.equal(queue.entries.length, 1); + assert.equal(queue.entries[0].content, 'Queue this note'); +}); + +test('non-strict governance facade can update local project memory and delegate refresh execution', () => { + const fixture = createWorkspaceFixture(); + const facade = createGovernanceFacade({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: false, + }); + + const write = facade.projectMemory.write({ + role: 'leader', + payload: { mode: 'local' }, + }); + assert.equal(write.decision, 'allow'); + + const note = facade.projectMemory.addDirective({ + role: 'leader', + directive: 'Persist locally', + }); + assert.equal(note.decision, 'allow'); + + const parsed = JSON.parse( + fs.readFileSync(path.join(fixture.workspaceRoot, '.omx', 'project-memory.json'), 'utf8') + ); + assert.equal(parsed.mode, 'local'); + assert.equal(parsed.directives[0].directive, 'Persist locally'); + + const calls = []; + const refresh = facade.refresh.trigger({ + role: 'leader', + phase: 'terminal', + verificationStatus: 'verified', + runner(command) { + calls.push(command); + return { status: 0, stdout: 'ok', stderr: '' }; + }, + }); + + assert.equal(refresh.executed, true); + assert.equal(refresh.success, true); + assert.equal(calls.length, 1); +}); + +test('governance facade exposes review and guarded recovery helpers', () => { + const fixture = createWorkspaceFixture(); + const facade = createGovernanceFacade({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + }); + + const review = facade.review.evaluate({ + action: 'write', + role: 'leader', + targetPath: path.join(fixture.workspaceRoot, '.omx', 'plans', 'plan.md'), + externalSideEffect: true, + }); + assert.equal(review.decision, 'review'); + + let calls = 0; + const recovery = facade.recovery.run({ + operation: 'runtime-check', + action: 'write', + role: 'leader', + targetPath: path.join(fixture.workspaceRoot, '.omx', 'plans', 'plan.md'), + maxAttempts: 2, + perform() { + calls += 1; + if (calls === 1) { + throw Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' }); + } + return { status: 0 }; + }, + isSuccessfulResult(result) { + return result.status === 0; + }, + }); + + assert.equal(recovery.status, 'success'); + assert.equal(recovery.success, true); + assert.equal(calls, 2); +}); + +test('governance facade exposes promotion and verification helpers', () => { + const fixture = createWorkspaceFixture(); + const facade = createGovernanceFacade({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + }); + + const queued = facade.intake.append({ + role: 'worker', + kind: 'note', + content: 'Promote through facade.', + source: 'runtime-facade-test', + }); + + const evaluation = facade.promotion.evaluate({ + role: 'leader', + entryIds: [queued.entry.id], + phase: 'terminal', + verificationStatus: 'verified', + }); + assert.equal(evaluation.decision, 'allow'); + + facade.verification.appendEvidence({ + role: 'worker', + summary: 'npm test passed', + kind: 'test', + command: 'npm test', + }); + assert.equal(facade.verification.read().status, 'pending'); + + facade.verification.markVerified({ + role: 'leader', + scope: ['tests'], + commands: ['npm test'], + notes: 'Checks passed.', + }); + + const trigger = facade.promotion.trigger({ + role: 'leader', + entryIds: [queued.entry.id], + phase: 'terminal', + verificationStatus: 'pending', + runner() { + return { status: 0, stdout: 'ok', stderr: '' }; + }, + }); + assert.equal(trigger.success, true); + + const audit = facade.promotion.summarizeAudit(); + assert.ok(audit.promotedEntryIds.includes(queued.entry.id)); +}); + +test('governance facade does not expose native-covered helpers at top level', () => { + const fixture = createWorkspaceFixture(); + const facade = createGovernanceFacade({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + }); + + assert.equal('skills' in facade, false); + assert.equal('subagents' in facade, false); + assert.equal('observability' in facade, false); + assert.equal('worktrees' in facade, false); +}); + +test('createRuntimeFacade is an alias of the kept governance surface', () => { + const fixture = createWorkspaceFixture(); + const facade = createRuntimeFacade({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + strictMode: true, + }); + + assert.equal(typeof facade.projectMemory.read, 'function'); + assert.equal(typeof facade.refresh.trigger, 'function'); + assert.equal('skills' in facade, false); + assert.equal('worktrees' in facade, false); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/state-store.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/state-store.test.js new file mode 100644 index 0000000..4a3c2fa --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/state-store.test.js @@ -0,0 +1,101 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + stateClear, + stateGetStatus, + stateListActive, + stateRead, + stateWrite, +} from '../src/runtime/state-store.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('stateWrite/stateRead/stateGetStatus round-trip state objects', () => { + const fixture = createWorkspaceFixture(); + + const written = stateWrite({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + name: 'plan', + data: { step: 1, status: 'active' }, + }); + assert.equal(written.active, true); + + const read = stateRead({ + cwd: fixture.workspaceRoot, + name: 'plan', + }); + assert.equal(read.exists, true); + assert.equal(read.data.step, 1); + + const status = stateGetStatus({ + cwd: fixture.workspaceRoot, + name: 'plan', + }); + assert.deepEqual(status.keys, ['status', 'step']); +}); + +test('stateListActive and stateClear manage state lifecycle', () => { + const fixture = createWorkspaceFixture(); + + stateWrite({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + name: 'alpha', + data: { mode: 'a' }, + }); + stateWrite({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + name: 'beta', + data: { mode: 'b' }, + }); + + const listed = stateListActive({ + cwd: fixture.workspaceRoot, + }); + assert.deepEqual(listed.states, ['alpha', 'beta', 'ralph']); + + const cleared = stateClear({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + name: 'alpha', + }); + assert.equal(cleared.removed, true); + + const after = stateListActive({ + cwd: fixture.workspaceRoot, + }); + assert.deepEqual(after.states, ['beta', 'ralph']); +}); + +test('workers cannot modify reserved verification state through raw state tools', () => { + const fixture = createWorkspaceFixture(); + + assert.throws( + () => + stateWrite({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + name: 'verification', + data: { status: 'verified' }, + }), + /reserved verification state/i + ); + + assert.throws( + () => + stateClear({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + name: 'verification', + }), + /reserved verification state/i + ); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/strict-integration-mode.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/strict-integration-mode.test.js new file mode 100644 index 0000000..227b439 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/strict-integration-mode.test.js @@ -0,0 +1,29 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + parseBooleanFlag, + resolveStrictIntegrationConfig, +} from '../src/contracts/strict-integration-mode.js'; + +test('parseBooleanFlag parses supported truthy and falsy values', () => { + assert.equal(parseBooleanFlag('1'), true); + assert.equal(parseBooleanFlag('TRUE'), true); + assert.equal(parseBooleanFlag('0'), false); + assert.equal(parseBooleanFlag('off'), false); + assert.equal(parseBooleanFlag('maybe'), null); +}); + +test('resolveStrictIntegrationConfig respects env overrides', () => { + const config = resolveStrictIntegrationConfig({ + cwd: '/tmp/example', + env: { + OMX_STRICT_MEMORY_MODE: '1', + OMX_EXTERNAL_MEMORY_ROOT: '/tmp/memory-root', + }, + }); + + assert.equal(config.strictMode, true); + assert.equal(config.cwd, '/tmp/example'); + assert.equal(config.memoryRoot, '/tmp/memory-root'); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/team-contract.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/team-contract.test.js new file mode 100644 index 0000000..b3073b4 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/team-contract.test.js @@ -0,0 +1,81 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { + TEAM_ROLE_LEADER, + TEAM_ROLE_MAIN, + TEAM_ROLE_WORKER, + assertFormalMemoryRefreshAuthority, + assertTeamWriteAccess, + canTriggerFormalMemoryRefresh, +} from '../src/team/team-contract.js'; +import { + FormalMemoryWriteError, + RuntimeArtifactWriteError, +} from '../src/policy/path-guard.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('only leader and main execution owner can trigger formal memory refresh', () => { + assert.equal(canTriggerFormalMemoryRefresh(TEAM_ROLE_LEADER), true); + assert.equal(canTriggerFormalMemoryRefresh(TEAM_ROLE_MAIN), true); + assert.equal(canTriggerFormalMemoryRefresh(TEAM_ROLE_WORKER), false); + + assert.doesNotThrow(() => assertFormalMemoryRefreshAuthority(TEAM_ROLE_LEADER)); + assert.throws(() => assertFormalMemoryRefreshAuthority(TEAM_ROLE_WORKER), /cannot trigger formal memory refresh/i); +}); + +test('team write access still blocks formal memory writes', () => { + const fixture = createWorkspaceFixture(); + const forbiddenPath = path.join( + fixture.workspaceMemoryHome, + 'memories', + 'feedback', + 'rule.md' + ); + + assert.throws( + () => + assertTeamWriteAccess({ + role: TEAM_ROLE_WORKER, + targetPath: forbiddenPath, + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }), + FormalMemoryWriteError + ); + + const allowed = assertTeamWriteAccess({ + role: TEAM_ROLE_WORKER, + targetPath: path.join(fixture.workspaceRoot, '.omx', 'context', 'task.md'), + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + + assert.equal(allowed.allowed, true); + assert.equal(allowed.classification, 'worker-run'); +}); + +test('team workers cannot write arbitrary non-runtime paths', () => { + const fixture = createWorkspaceFixture(); + + assert.throws( + () => + assertTeamWriteAccess({ + role: TEAM_ROLE_WORKER, + targetPath: path.join(fixture.workspaceRoot, 'src', 'feature.js'), + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }), + RuntimeArtifactWriteError + ); + + assert.doesNotThrow(() => + assertTeamWriteAccess({ + role: TEAM_ROLE_LEADER, + targetPath: path.join(fixture.workspaceRoot, 'src', 'feature.js'), + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }) + ); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/verification-state.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/verification-state.test.js new file mode 100644 index 0000000..11e7b22 --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/verification-state.test.js @@ -0,0 +1,87 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + VERIFICATION_STATUS_PENDING, + VERIFICATION_STATUS_VERIFIED, + appendVerificationEvidence, + markVerified, + readVerificationState, + resolveVerificationStatus, +} from '../src/runtime/verification-state.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('verification state defaults to pending when no artifact exists', () => { + const fixture = createWorkspaceFixture(); + + const state = readVerificationState({ + cwd: fixture.workspaceRoot, + }); + + assert.equal(state.exists, false); + assert.equal(state.status, VERIFICATION_STATUS_PENDING); + assert.deepEqual(state.evidence, []); +}); + +test('workers can append evidence but only leader can mark verified', () => { + const fixture = createWorkspaceFixture(); + + const appended = appendVerificationEvidence({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + summary: '91/91 passed', + kind: 'test', + command: 'npm test', + createdAt: '2026-04-04T12:00:00Z', + }); + assert.equal(appended.exists, true); + assert.equal(appended.evidence.length, 1); + assert.equal(appended.evidence[0].observed_by, 'worker'); + + assert.throws( + () => + markVerified({ + role: 'worker', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + scope: ['tests'], + commands: ['npm test'], + }), + /cannot mark verification as verified/i + ); + + const verified = markVerified({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + scope: ['tests'], + commands: ['npm test'], + notes: 'No blocking findings.', + verifiedAt: '2026-04-04T12:05:00Z', + }); + assert.equal(verified.status, VERIFICATION_STATUS_VERIFIED); + assert.equal(verified.verified_by, 'leader'); + assert.equal(verified.verified_at, '2026-04-04T12:05:00Z'); + assert.equal(verified.evidence.length, 1); +}); + +test('resolveVerificationStatus prefers the artifact over a caller-supplied parameter', () => { + const fixture = createWorkspaceFixture(); + + markVerified({ + role: 'leader', + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + scope: ['tests'], + commands: ['npm test'], + }); + + const resolved = resolveVerificationStatus({ + cwd: fixture.workspaceRoot, + verificationStatus: 'pending', + }); + + assert.equal(resolved.status, VERIFICATION_STATUS_VERIFIED); + assert.equal(resolved.source, 'artifact'); +}); diff --git a/knowledge-base/raw/repos/codex-memory-kit/test/workspace-resolver.test.js b/knowledge-base/raw/repos/codex-memory-kit/test/workspace-resolver.test.js new file mode 100644 index 0000000..12fb3cc --- /dev/null +++ b/knowledge-base/raw/repos/codex-memory-kit/test/workspace-resolver.test.js @@ -0,0 +1,45 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { resolveWorkspaceNode } from '../src/integration/workspace-resolver.js'; +import { createWorkspaceFixture } from './helpers/fixtures.js'; + +test('resolveWorkspaceNode finds the registered workspace for nested cwd', () => { + const fixture = createWorkspaceFixture(); + const nestedCwd = path.join(fixture.workspaceRoot, 'src', 'components'); + + const resolved = resolveWorkspaceNode({ + cwd: nestedCwd, + memoryRoot: fixture.memoryRoot, + }); + + assert.equal(resolved.key, fixture.workspaceKey); + assert.equal(resolved.workspaceRoot, fixture.workspaceRoot); + assert.equal(resolved.memoryHome, fixture.workspaceMemoryHome); + assert.match(resolved.repoGuidePath, /instructions\/repo\/GUIDE\.md$/); +}); + +test('resolveWorkspaceNode returns null when the workspace is not registered', () => { + const fixture = createWorkspaceFixture(); + + const resolved = resolveWorkspaceNode({ + cwd: '/tmp/unregistered-workspace', + memoryRoot: fixture.memoryRoot, + }); + + assert.equal(resolved, null); +}); + +test('resolveWorkspaceNode returns null when the workspace index is missing', () => { + const fixture = createWorkspaceFixture(); + fs.rmSync(path.join(fixture.memoryRoot, 'workspaces', 'index.json')); + + const resolved = resolveWorkspaceNode({ + cwd: fixture.workspaceRoot, + memoryRoot: fixture.memoryRoot, + }); + + assert.equal(resolved, null); +}); diff --git a/knowledge-base/tests/test_kb_query.py b/knowledge-base/tests/test_kb_query.py new file mode 100644 index 0000000..98abac4 --- /dev/null +++ b/knowledge-base/tests/test_kb_query.py @@ -0,0 +1,1004 @@ +from __future__ import annotations + +import argparse +import contextlib +import importlib.machinery +import importlib.util +import io +import json +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_PATH = Path("/Users/wz/project/codex-enhanced-system/knowledge-base/kb") + + +def load_module(): + loader = importlib.machinery.SourceFileLoader("kb_module", str(SCRIPT_PATH)) + spec = importlib.util.spec_from_loader(loader.name, loader) + assert spec is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + loader.exec_module(module) + return module + + +def write_text(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +class KBQueryTests(unittest.TestCase): + def setUp(self): + self.module = load_module() + self.tempdir = tempfile.TemporaryDirectory() + self.repo_root = Path(self.tempdir.name).resolve() + self.wiki_root = self.repo_root / "wiki" + self.raw_root = self.repo_root / "raw" + self.output_root = self.repo_root / "output" + self.wiki_root.mkdir() + self.raw_root.mkdir() + self.output_root.mkdir() + + self.module.REPO_ROOT = self.repo_root + self.module.WIKI_ROOT = self.wiki_root + self.module.RAW_ROOT = self.raw_root + self.module.OUTPUT_ROOT = self.output_root + self.module.INDEX_PATH = self.wiki_root / "index.md" + self.module.LOG_PATH = self.wiki_root / "log.md" + self.module.PAGE_LAYOUT = { + "source": { + "dir": self.wiki_root / "sources", + "prefix": "source-", + "index_heading": "## Sources", + }, + "entity": { + "dir": self.wiki_root / "entities", + "prefix": "entity-", + "index_heading": "## Entities", + }, + "concept": { + "dir": self.wiki_root / "concepts", + "prefix": "concept-", + "index_heading": "## Concepts", + }, + "synthesis": { + "dir": self.wiki_root / "syntheses", + "prefix": "synthesis-", + "index_heading": "## Syntheses", + }, + "domain": { + "dir": self.wiki_root / "domains", + "prefix": "domain-", + "index_heading": "## Domains", + }, + "report": { + "dir": self.wiki_root / "reports", + "prefix": "report-", + "index_heading": "## Reports", + }, + } + + for spec in self.module.PAGE_LAYOUT.values(): + spec["dir"].mkdir(parents=True, exist_ok=True) + + write_text(self.raw_root / "inbox" / "alpha.md", "# Alpha Raw\n") + write_text( + self.wiki_root / "domains" / "domain-memory-governance.md", + "\n".join( + [ + "---", + 'title: "Domain: Memory Governance"', + "type: domain", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs: []", + "related:", + " - ../overview", + " - ../hot", + " - ../syntheses/synthesis-memory-baseline", + "---", + "", + "# Domain: Memory Governance", + "", + "## Main Entry", + "", + "- [Memory Baseline](../syntheses/synthesis-memory-baseline.md)", + "", + ] + ) + + "\n", + ) + + write_text( + self.wiki_root / "sources" / "source-alpha.md", + "\n".join( + [ + "---", + 'title: "Alpha Source"', + "type: source", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs:", + " - ../../raw/inbox/alpha.md", + "related: []", + "---", + "", + "# Alpha Source", + "", + "## Key Claims", + "", + "- Formal memory authority belongs to the global memory system.", + "", + ] + ) + + "\n", + ) + write_text( + self.wiki_root / "syntheses" / "synthesis-memory-baseline.md", + "\n".join( + [ + "---", + 'title: "Memory Baseline"', + "type: synthesis", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs:", + " - ../sources/source-alpha", + "related: []", + "---", + "", + "# Memory Baseline", + "", + "## Stable Conclusions", + "", + "- Formal memory authority remains centralized.", + "", + ] + ) + + "\n", + ) + write_text( + self.wiki_root / "concepts" / "concept-formal-memory-authority.md", + "\n".join( + [ + "---", + 'title: "Formal Memory Authority"', + "type: concept", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs:", + " - ../sources/source-alpha", + "related:", + " - ../syntheses/synthesis-memory-baseline", + "---", + "", + "# Formal Memory Authority", + "", + "## Summary", + "", + "- Formal memory authority belongs to the shared global memory system.", + "", + "## Stable Claims", + "", + "- The workspace should not invent a second long-term memory authority.", + "", + ] + ) + + "\n", + ) + write_text( + self.wiki_root / "concepts" / "concept-formal-memory-authority-zh.md", + "\n".join( + [ + "---", + 'title: "Formal Memory Authority (中文)"', + "type: concept", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs:", + " - ../sources/source-alpha", + "related:", + " - ../syntheses/synthesis-memory-baseline", + "---", + "", + "# Formal Memory Authority (中文)", + "", + "## Summary", + "", + "- Shared memory authority stays centralized.", + "", + "## Stable Claims", + "", + "- The workspace should not invent a second long-term memory authority.", + "", + ] + ) + + "\n", + ) + write_text( + self.wiki_root / "reports" / "report-memory-readiness-en.md", + "\n".join( + [ + "---", + 'title: "Memory Readiness (English)"', + "type: report", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs: []", + "related:", + " - ../concepts/concept-formal-memory-authority", + "---", + "", + "# Memory Readiness (English)", + "", + "## Findings", + "", + "- Formal memory authority remains centralized.", + "", + ] + ) + + "\n", + ) + write_text( + self.wiki_root / "overview.md", + "\n".join( + [ + "---", + 'title: "Knowledge Base Overview"', + "type: guide", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs: []", + "related:", + " - index", + " - hot", + " - log", + " - domains/domain-memory-governance", + " - reports/report-memory-readiness-en", + "---", + "", + "# Knowledge Base Overview", + "", + "## Current Scope", + "", + "- Canonical knowledge remains traceable back to explicit source pages.", + "", + ] + ) + + "\n", + ) + write_text( + self.wiki_root / "hot.md", + "\n".join( + [ + "---", + 'title: "Hot Path"', + "type: guide", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs: []", + "related:", + " - index", + " - overview", + " - log", + " - domains/domain-memory-governance", + " - reports/report-memory-readiness-en", + " - syntheses/synthesis-memory-baseline", + "---", + "", + "# Hot Path", + "", + "## Start Here", + "", + "- [Domain](./domains/domain-memory-governance.md)", + "- [Synthesis](./syntheses/synthesis-memory-baseline.md)", + "- [Report](./reports/report-memory-readiness-en.md)", + "", + ] + ) + + "\n", + ) + write_text( + self.module.LOG_PATH, + "\n".join( + [ + "---", + 'title: "Knowledge Base Log"', + "type: report", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs: []", + "related:", + " - index", + " - overview", + "---", + "", + "# Knowledge Base Log", + "", + "## [2026-04-11] scaffold | test fixture", + "", + "- Added the minimal fixture pages for query and maintenance tests.", + "", + ] + ) + + "\n", + ) + write_text( + self.module.INDEX_PATH, + "\n".join( + [ + "---", + 'title: "Wiki Index"', + "type: guide", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs: []", + "related:", + " - overview", + " - hot", + " - log", + "---", + "", + "# Wiki Index", + "", + "## Overview", + "", + "- [overview](./overview.md)", + "- [hot](./hot.md)", + "- [log](./log.md)", + "", + "## Domains", + "", + "- [domain-memory-governance](./domains/domain-memory-governance.md)", + "", + "## Reports", + "", + "- [report-memory-readiness-en](./reports/report-memory-readiness-en.md)", + "", + "## Sources", + "", + "- [source-alpha](./sources/source-alpha.md)", + "", + "## Concepts", + "", + "- [concept-formal-memory-authority](./concepts/concept-formal-memory-authority.md)", + "- [concept-formal-memory-authority-zh](./concepts/concept-formal-memory-authority-zh.md)", + "", + "## Syntheses", + "", + "- [synthesis-memory-baseline](./syntheses/synthesis-memory-baseline.md)", + "", + ] + ) + + "\n", + ) + + def tearDown(self): + self.tempdir.cleanup() + + def test_extract_frontmatter_links_resolves_md_shorthand(self): + page = self.module.load_page(self.wiki_root / "concepts" / "concept-formal-memory-authority.md") + + links = self.module.extract_frontmatter_links(page) + resolved = {path.relative_to(self.repo_root).as_posix() for _, path in links} + + self.assertEqual( + resolved, + { + "wiki/sources/source-alpha.md", + "wiki/syntheses/synthesis-memory-baseline.md", + }, + ) + + def test_add_auto_registers_new_page_in_index(self): + exit_code = self.module.cmd_add( + argparse.Namespace( + kind="concept", + slug="memory-latency", + title="Memory Latency", + source_ref=["wiki/sources/source-alpha.md"], + related=["wiki/syntheses/synthesis-memory-baseline.md"], + raw_ref=None, + import_from=None, + raw_dest=None, + write_log=False, + dry_run=False, + ) + ) + + self.assertEqual(exit_code, 0) + created_path = self.wiki_root / "concepts" / "concept-memory-latency.md" + self.assertTrue(created_path.exists()) + index_text = self.module.load_text(self.module.INDEX_PATH) + self.assertIn("./concepts/concept-memory-latency.md", index_text) + + def test_add_can_optionally_write_log(self): + exit_code = self.module.cmd_add( + argparse.Namespace( + kind="concept", + slug="memory-cadence", + title="Memory Cadence", + source_ref=["wiki/sources/source-alpha.md"], + related=["wiki/syntheses/synthesis-memory-baseline.md"], + raw_ref=None, + import_from=None, + raw_dest=None, + write_log=True, + dry_run=False, + ) + ) + + self.assertEqual(exit_code, 0) + log_text = self.module.load_text(self.module.LOG_PATH) + self.assertIn("## [2026-04-11] ingest | add concept-memory-cadence", log_text) + self.assertIn("Added wiki/concepts/concept-memory-cadence.md", log_text) + + def test_query_pages_returns_scored_result_with_provenance(self): + results = self.module.query_pages( + ["formal", "memory"], + "formal memory", + {"concept", "source", "synthesis", "entity", "domain"}, + limit=5, + ) + + self.assertEqual(results[0].path.name, "concept-formal-memory-authority.md") + self.assertIn("title", results[0].matched_fields) + self.assertEqual(results[0].source_refs, ["wiki/sources/source-alpha.md"]) + self.assertEqual(results[0].related, ["wiki/syntheses/synthesis-memory-baseline.md"]) + self.assertIsNotNone(results[0].snippet) + self.assertGreater(results[0].score, results[0].match_score) + + def test_render_query_results_json_contains_provenance(self): + results = self.module.query_pages( + ["authority"], + "authority", + {"concept", "source", "synthesis", "entity", "domain"}, + limit=5, + ) + + payload = json.loads(self.module.render_query_results("authority", results, as_json=True)) + + self.assertEqual(payload["query"], "authority") + self.assertEqual(payload["results"][0]["path"], "wiki/concepts/concept-formal-memory-authority.md") + self.assertEqual(payload["results"][0]["source_refs"], ["wiki/sources/source-alpha.md"]) + self.assertIn("match_score", payload["results"][0]) + self.assertEqual(payload["results"][0]["suppressed_duplicates"], ["wiki/concepts/concept-formal-memory-authority-zh.md"]) + + def test_check_maintenance_accepts_shorthand_source_refs(self): + _, issues = self.module.check_maintenance() + messages = [issue.message for issue in issues] + + self.assertFalse(any("source_refs should resolve" in message for message in messages)) + self.assertFalse(any("broken frontmatter ref" in message for message in messages)) + self.assertFalse(any("missing health link" in message for message in messages)) + + def test_query_pages_dedupes_locale_mirror_results_by_default(self): + results = self.module.query_pages( + ["formal", "memory", "authority"], + "formal memory authority", + {"concept"}, + limit=5, + ) + + self.assertEqual([result.path.name for result in results], ["concept-formal-memory-authority.md"]) + self.assertEqual(results[0].suppressed_duplicates, ["wiki/concepts/concept-formal-memory-authority-zh.md"]) + + def test_query_pages_can_keep_raw_duplicates_when_requested(self): + results = self.module.query_pages( + ["formal", "memory", "authority"], + "formal memory authority", + {"concept"}, + limit=5, + dedupe=False, + ) + + self.assertEqual( + [result.path.name for result in results], + [ + "concept-formal-memory-authority.md", + "concept-formal-memory-authority-zh.md", + ], + ) + + def test_report_pages_are_downranked_against_answer_like_pages(self): + results = self.module.query_pages( + ["memory"], + "memory", + {"concept", "report"}, + limit=5, + ) + + self.assertEqual([result.kind for result in results[:2]], ["concept", "report"]) + self.assertGreater(results[0].score, results[1].score) + + def test_check_maintenance_warns_when_answer_page_lacks_canonical_source_support(self): + write_text(self.raw_root / "inbox" / "raw-only.md", "# Raw Only\n") + write_text( + self.wiki_root / "concepts" / "concept-raw-only.md", + "\n".join( + [ + "---", + 'title: "Raw Only Concept"', + "type: concept", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs:", + " - ../../raw/inbox/raw-only.md", + "related: []", + "---", + "", + "# Raw Only Concept", + "", + "## Summary", + "", + "- This page points only to raw support.", + "", + ] + ) + + "\n", + ) + write_text( + self.module.INDEX_PATH, + self.module.load_text(self.module.INDEX_PATH) + "- [concept-raw-only](./concepts/concept-raw-only.md)\n", + ) + + _, issues = self.module.check_maintenance() + messages = [issue.message for issue in issues] + + self.assertIn( + "wiki/concepts/concept-raw-only.md: source_refs should include at least one wiki/sources page", + messages, + ) + + def test_check_maintenance_warns_when_hot_path_loses_report_link(self): + write_text( + self.wiki_root / "hot.md", + "\n".join( + [ + "---", + 'title: "Hot Path"', + "type: guide", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs: []", + "related:", + " - index", + " - overview", + " - log", + " - domains/domain-memory-governance", + " - syntheses/synthesis-memory-baseline", + "---", + "", + "# Hot Path", + "", + "## Start Here", + "", + "- [Domain](./domains/domain-memory-governance.md)", + "- [Synthesis](./syntheses/synthesis-memory-baseline.md)", + "", + ] + ) + + "\n", + ) + + _, issues = self.module.check_maintenance() + messages = [issue.message for issue in issues] + + self.assertIn("wiki/hot.md: missing health link for `report` pages", messages) + + def test_build_maintenance_payload_includes_issue_counts(self): + write_text(self.raw_root / "inbox" / "raw-only.md", "# Raw Only\n") + write_text( + self.wiki_root / "concepts" / "concept-raw-only.md", + "\n".join( + [ + "---", + 'title: "Raw Only Concept"', + "type: concept", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs:", + " - ../../raw/inbox/raw-only.md", + "related: []", + "---", + "", + "# Raw Only Concept", + "", + "## Summary", + "", + "- This page points only to raw support.", + "", + ] + ) + + "\n", + ) + write_text( + self.module.INDEX_PATH, + self.module.load_text(self.module.INDEX_PATH) + "- [concept-raw-only](./concepts/concept-raw-only.md)\n", + ) + + counts, issues = self.module.check_maintenance() + payload = self.module.build_maintenance_payload(counts, issues) + + self.assertEqual(payload["status"], "ok") + self.assertEqual(payload["health_verdict"], "needs-attention") + self.assertEqual(payload["issue_counts"], {"warn": 1}) + self.assertEqual(payload["issue_groups"], {"provenance": [{"level": "warn", "message": "wiki/concepts/concept-raw-only.md: source_refs should include at least one wiki/sources page"}]}) + self.assertEqual(payload["issues"][0]["level"], "warn") + self.assertEqual(payload["issues"][0]["category"], "provenance") + + def test_render_maintenance_summary_json_includes_report_metadata(self): + counts, issues = self.module.check_maintenance() + report_path = self.wiki_root / "reports" / "report-maintenance-2026-04-11.md" + + payload = json.loads( + self.module.render_maintenance_summary( + counts, + issues, + as_json=True, + report_path=report_path, + report_action="would_write", + ) + ) + + self.assertEqual(payload["status"], "ok") + self.assertEqual(payload["health_verdict"], "healthy") + self.assertEqual(payload["report_path"], "wiki/reports/report-maintenance-2026-04-11.md") + self.assertEqual(payload["report_action"], "would_write") + + def test_maintain_write_report_syncs_index_and_keeps_workspace_healthy(self): + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + exit_code = self.module.cmd_maintain( + argparse.Namespace( + write_report=True, + dry_run=False, + json=False, + ) + ) + + self.assertEqual(exit_code, 0) + summary = stdout.getvalue() + self.assertIn("- health_verdict: healthy", summary) + self.assertIn("- reports: 2", summary) + self.assertIn("wiki/reports/report-maintenance-2026-04-11.md", summary) + + counts, issues = self.module.check_maintenance() + self.assertEqual(counts["report"], 2) + self.assertEqual(issues, []) + self.assertIn("./reports/report-maintenance-2026-04-11.md", self.module.load_text(self.module.INDEX_PATH)) + + def test_render_maintenance_summary_text_includes_health_verdict_and_groups(self): + write_text(self.raw_root / "inbox" / "raw-only.md", "# Raw Only\n") + write_text( + self.wiki_root / "concepts" / "concept-raw-only.md", + "\n".join( + [ + "---", + 'title: "Raw Only Concept"', + "type: concept", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs:", + " - ../../raw/inbox/raw-only.md", + "related: []", + "---", + "", + "# Raw Only Concept", + "", + "## Summary", + "", + "- This page points only to raw support.", + "", + ] + ) + + "\n", + ) + write_text( + self.module.INDEX_PATH, + self.module.load_text(self.module.INDEX_PATH) + "- [concept-raw-only](./concepts/concept-raw-only.md)\n", + ) + + counts, issues = self.module.check_maintenance() + summary = self.module.render_maintenance_summary(counts, issues, as_json=False) + + self.assertIn("- health_verdict: needs-attention", summary) + self.assertIn("- issue_groups: provenance=1", summary) + self.assertIn("[warn][provenance] wiki/concepts/concept-raw-only.md: source_refs should include at least one wiki/sources page", summary) + + def test_render_maintenance_report_includes_verdict_groups_and_recommendations(self): + write_text(self.raw_root / "inbox" / "raw-only.md", "# Raw Only\n") + write_text( + self.wiki_root / "concepts" / "concept-raw-only.md", + "\n".join( + [ + "---", + 'title: "Raw Only Concept"', + "type: concept", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs:", + " - ../../raw/inbox/raw-only.md", + "related: []", + "---", + "", + "# Raw Only Concept", + "", + "## Summary", + "", + "- This page points only to raw support.", + "", + ] + ) + + "\n", + ) + write_text( + self.module.INDEX_PATH, + self.module.load_text(self.module.INDEX_PATH) + "- [concept-raw-only](./concepts/concept-raw-only.md)\n", + ) + + counts, issues = self.module.check_maintenance() + report = self.module.render_maintenance_report(counts, issues) + + self.assertIn("- Health verdict: `needs-attention`", report) + self.assertIn("## Findings By Category", report) + self.assertIn("### provenance", report) + self.assertIn("Repair weak or duplicated provenance so answer-like pages keep clear canonical support.", report) + + def test_reindex_write_updates_index_updated_at_when_it_adds_missing_entry(self): + write_text( + self.wiki_root / "reports" / "report-new-health.md", + "\n".join( + [ + "---", + 'title: "New Health Report"', + "type: report", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-11", + "source_refs: []", + "related:", + " - ../overview.md", + "---", + "", + "# New Health Report", + "", + ] + ) + + "\n", + ) + write_text( + self.module.INDEX_PATH, + self.module.load_text(self.module.INDEX_PATH).replace("updated_at: 2026-04-11", "updated_at: 2026-04-10", 1), + ) + + exit_code = self.module.cmd_reindex( + argparse.Namespace( + write=True, + prune=False, + dry_run=False, + ) + ) + + self.assertEqual(exit_code, 0) + index_text = self.module.load_text(self.module.INDEX_PATH) + self.assertIn("./reports/report-new-health.md", index_text) + self.assertIn("updated_at: 2026-04-11", index_text) + + def test_remove_index_lines_for_path_updates_index_updated_at(self): + write_text( + self.module.INDEX_PATH, + self.module.load_text(self.module.INDEX_PATH).replace("updated_at: 2026-04-11", "updated_at: 2026-04-10", 1), + ) + + removed = self.module.remove_index_lines_for_path(self.wiki_root / "reports" / "report-memory-readiness-en.md", dry_run=False) + + self.assertEqual(removed, 1) + index_text = self.module.load_text(self.module.INDEX_PATH) + self.assertNotIn("./reports/report-memory-readiness-en.md", index_text) + self.assertIn("updated_at: 2026-04-11", index_text) + + def test_log_write_updates_log_updated_at(self): + write_text( + self.module.LOG_PATH, + self.module.load_text(self.module.LOG_PATH).replace("updated_at: 2026-04-11", "updated_at: 2026-04-10", 1), + ) + + exit_code = self.module.cmd_log( + argparse.Namespace( + action="maintenance", + summary="refresh metadata", + note=["Updated the log metadata semantics."], + dry_run=False, + ) + ) + + self.assertEqual(exit_code, 0) + log_text = self.module.load_text(self.module.LOG_PATH) + self.assertIn("updated_at: 2026-04-11", log_text) + self.assertIn("## [2026-04-11] maintenance | refresh metadata", log_text) + + def test_delete_can_optionally_write_log(self): + target = self.wiki_root / "reports" / "report-memory-readiness-en.md" + + exit_code = self.module.cmd_delete( + argparse.Namespace( + path=str(target), + with_raw=False, + write_log=True, + dry_run=False, + ) + ) + + self.assertEqual(exit_code, 0) + self.assertFalse(target.exists()) + log_text = self.module.load_text(self.module.LOG_PATH) + self.assertIn("## [2026-04-11] maintenance | delete report-memory-readiness-en", log_text) + self.assertIn("Deleted wiki/reports/report-memory-readiness-en.md", log_text) + + def test_drift_review_is_stable_for_aligned_fixture(self): + signals = self.module.drift_review_signals() + payload = self.module.build_drift_review_payload(signals) + + self.assertEqual(signals, []) + self.assertEqual(payload["status"], "ok") + self.assertEqual(payload["drift_verdict"], "stable") + + def test_drift_review_detects_source_lag(self): + write_text( + self.wiki_root / "sources" / "source-alpha.md", + "\n".join( + [ + "---", + 'title: "Alpha Source"', + "type: source", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-12", + "source_refs:", + " - ../../raw/inbox/alpha.md", + "related: []", + "---", + "", + "# Alpha Source", + "", + "## Key Claims", + "", + "- Formal memory authority belongs to the global memory system.", + "", + ] + ) + + "\n", + ) + + signals = self.module.drift_review_signals() + source_lag_pages = {(signal.category, signal.page) for signal in signals} + + self.assertIn(("source-lag", "wiki/concepts/concept-formal-memory-authority.md"), source_lag_pages) + self.assertIn(("source-lag", "wiki/syntheses/synthesis-memory-baseline.md"), source_lag_pages) + + def test_drift_review_detects_log_metadata_lag(self): + write_text( + self.module.LOG_PATH, + "\n".join( + [ + "---", + 'title: "Knowledge Base Log"', + "type: report", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-10", + "source_refs: []", + "related:", + " - index", + " - overview", + "---", + "", + "# Knowledge Base Log", + "", + "## [2026-04-11] scaffold | test fixture", + "", + "- Added the minimal fixture pages for query and maintenance tests.", + "", + ] + ) + + "\n", + ) + + signals = self.module.drift_review_signals() + metadata_lag_pages = {(signal.category, signal.page) for signal in signals} + + self.assertIn(("metadata-lag", "wiki/log.md"), metadata_lag_pages) + + def test_render_drift_review_summary_json_includes_report_metadata(self): + signals = self.module.drift_review_signals() + report_path = self.wiki_root / "reports" / "report-drift-review-2026-04-11.md" + payload = json.loads( + self.module.render_drift_review_summary( + signals, + as_json=True, + report_path=report_path, + report_action="would_write", + ) + ) + + self.assertEqual(payload["status"], "ok") + self.assertEqual(payload["drift_verdict"], "stable") + self.assertEqual(payload["report_path"], "wiki/reports/report-drift-review-2026-04-11.md") + self.assertEqual(payload["report_action"], "would_write") + + def test_drift_review_write_report_renders_post_write_stable_state(self): + write_text( + self.wiki_root / "reports" / "report-memory-readiness-en.md", + "\n".join( + [ + "---", + 'title: "Memory Readiness (English)"', + "type: report", + "status: active", + "created_at: 2026-04-11", + "updated_at: 2026-04-10", + "source_refs: []", + "related:", + " - ../concepts/concept-formal-memory-authority", + "---", + "", + "# Memory Readiness (English)", + "", + "## Findings", + "", + "- Formal memory authority remains centralized.", + "", + ] + ) + + "\n", + ) + + signals = self.module.drift_review_signals() + self.assertEqual([(signal.category, signal.page) for signal in signals], [("report-lag", "wiki/reports")]) + + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + exit_code = self.module.cmd_drift_review( + argparse.Namespace( + json=False, + write_report=True, + dry_run=False, + ) + ) + + self.assertEqual(exit_code, 0) + self.assertIn("- drift_verdict: stable", stdout.getvalue()) + + report_path = self.wiki_root / "reports" / "report-drift-review-2026-04-11.md" + report_content = self.module.load_text(report_path) + + self.assertIn("- Drift verdict: `stable`", report_content) + self.assertIn("- signal counts: none", report_content) + self.assertNotIn("report-lag", report_content) + self.assertIn("./reports/report-drift-review-2026-04-11.md", self.module.load_text(self.module.INDEX_PATH)) + self.assertEqual(self.module.drift_review_signals(), []) + _, issues = self.module.check_maintenance() + self.assertEqual(issues, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/knowledge-base/wiki/concepts/concept-codex-native-memory-governance.md b/knowledge-base/wiki/concepts/concept-codex-native-memory-governance.md index a847988..748310b 100644 --- a/knowledge-base/wiki/concepts/concept-codex-native-memory-governance.md +++ b/knowledge-base/wiki/concepts/concept-codex-native-memory-governance.md @@ -3,7 +3,7 @@ title: "Codex-native memory governance" type: concept status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../sources/source-codex-memory-kit-readme.md - ../sources/source-oh-my-codex-memory-integration-executive-summary.md @@ -21,6 +21,7 @@ related: - This concept positions the repository as a governance layer that fills long-term memory gaps around authority, promotion, and verification - It assumes Codex App already owns native execution/runtime capabilities and should remain the default for those surfaces +- The merged workspace now keeps the archived repository snapshot as a traceable artifact home for governance docs, retained modules, tests, and patch sets ## Stable Claims @@ -28,11 +29,13 @@ related: - Formal memory needs explicit scope, authority, and promotion controls - Governance is most valuable at the boundary between runtime artifacts and durable truth - Upstream rollout should preserve that narrow scope instead of reopening a wider custom runtime surface +- Preserved implementation modules and patch artifacts are reference surfaces for governance and review, not a signal to revive the deleted local runtime ## Why It Exists - Native workflow execution alone does not define a durable memory authority model - Without governance, runtime artifacts and local project-memory replicas can drift into second-truth systems +- The merged repository needs one place where durable governance rules and archived implementation artifacts stay legible together ## Related Pages diff --git a/knowledge-base/wiki/concepts/concept-verification-evidence-gate.md b/knowledge-base/wiki/concepts/concept-verification-evidence-gate.md index 3ba70ee..2d8f069 100644 --- a/knowledge-base/wiki/concepts/concept-verification-evidence-gate.md +++ b/knowledge-base/wiki/concepts/concept-verification-evidence-gate.md @@ -3,7 +3,7 @@ title: "Verification evidence gate" type: concept status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../sources/source-oh-my-codex-verification-evidence-gate-design.md - ../sources/source-oh-my-codex-upstream-review-summary.md @@ -26,12 +26,14 @@ related: - Workers may contribute evidence, but final `verified` authority belongs to leader or main execution ownership - Refresh and promotion should prefer structured verification state over bare caller parameters - This gate narrows runtime behavior without broadening the durable-memory authority model +- The evidence gate is already part of the staged upstream integration wave, not just a local design sketch ## Practical Meaning - `.omx/state/verification-state.json` is treated as evidence, not truth - The evidence gate lets the system become more auditable before introducing a dedicated verify team - It is a better fit for the governance model than inventing a second verification memory surface +- Status and rollout material now make the gate reviewable as an active upstream concern rather than only a future refinement ## Related Pages diff --git a/knowledge-base/wiki/entities/entity-codex-memory-kit.md b/knowledge-base/wiki/entities/entity-codex-memory-kit.md index faf7d74..de7b424 100644 --- a/knowledge-base/wiki/entities/entity-codex-memory-kit.md +++ b/knowledge-base/wiki/entities/entity-codex-memory-kit.md @@ -3,7 +3,7 @@ title: "codex-memory-kit" type: entity status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../sources/source-codex-memory-kit-readme.md - ../sources/source-raiden-workshop-repository-map.md @@ -27,11 +27,13 @@ related: - Adds governance around refresh, promotion, verification, and workspace resolution - Carries the local planning and reviewer packet that stages the same governance model into upstream `oh-my-codex` patches - Serves as the current main public repository inside the wider planned `raiden-workshop` repo layout +- Preserves the archived raw snapshot, retained modules, tests, and patch artifacts that reviewers or implementers still need after consolidation ## Boundaries - Should not duplicate Codex App native thread/worktree/harness capabilities - Should not turn workflow telemetry or `.omx/**` artifacts into automatic durable truth +- Should not treat the preserved raw snapshot as permission to reopen the deleted broader local runtime surface ## Related Pages diff --git a/knowledge-base/wiki/hot.md b/knowledge-base/wiki/hot.md index 2f3f669..8ca97d7 100644 --- a/knowledge-base/wiki/hot.md +++ b/knowledge-base/wiki/hot.md @@ -3,12 +3,13 @@ title: "Hot Path" type: guide status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: [] related: - domains/domain-codex-native-memory-governance - index - overview + - reports/report-drift-review-2026-04-11 - reports/report-domain-expansion-readiness-2026-04-09 - reports/report-lint-2026-04-09 - syntheses/synthesis-codex-native-memory-governance-baseline @@ -40,6 +41,7 @@ related: - If the question is "How should upstream PRs be reviewed?": read reviewer-packet synthesis, then [source-oh-my-codex-upstream-review-summary](./sources/source-oh-my-codex-upstream-review-summary.md) - If the question is "How are patches applied and verified?": read reviewer-packet synthesis, then [source-oh-my-codex-upstream-first-integration-apply](./sources/source-oh-my-codex-upstream-first-integration-apply.md) - If the question is "Why is verification evidence important?": read [concept-verification-evidence-gate](./concepts/concept-verification-evidence-gate.md), then [source-oh-my-codex-verification-evidence-gate-design](./sources/source-oh-my-codex-verification-evidence-gate-design.md) +- If the question is "Which pages may need refresh after recent source changes?": read [report-drift-review-2026-04-11](./reports/report-drift-review-2026-04-11.md) - If the question is "Should this become a new domain?": read [reports/report-domain-expansion-readiness-2026-04-09](./reports/report-domain-expansion-readiness-2026-04-09.md) ## Full Maps @@ -47,6 +49,7 @@ related: - [reports/report-lint-2026-04-09](./reports/report-lint-2026-04-09.md) - [reports/report-source-priority-2026-04-09](./reports/report-source-priority-2026-04-09.md) - [reports/report-domain-expansion-readiness-2026-04-09](./reports/report-domain-expansion-readiness-2026-04-09.md) +- [reports/report-drift-review-2026-04-11](./reports/report-drift-review-2026-04-11.md) - [domains/domain-codex-native-memory-governance](./domains/domain-codex-native-memory-governance.md) - [index](./index.md) - [overview](./overview.md) diff --git a/knowledge-base/wiki/index.md b/knowledge-base/wiki/index.md index 4d9e124..68fbee7 100644 --- a/knowledge-base/wiki/index.md +++ b/knowledge-base/wiki/index.md @@ -3,7 +3,7 @@ title: "Wiki Index" type: guide status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: [] related: - domains/domain-codex-native-memory-governance @@ -24,6 +24,7 @@ related: - Seed baseline migrated from `/Users/wz/project/mult-agent/knowledge-base` on `2026-04-09` - Canonical knowledge lives under `wiki/` - Source materials live under `raw/` +- `raw/repos/codex-memory-kit/` now preserves the archived full repository snapshot, including implementation, tests, and exported patch artifacts ## Overview @@ -40,6 +41,7 @@ related: - [report-lint-2026-04-09](./reports/report-lint-2026-04-09.md): First manual health snapshot after seeding baseline, rollout, and reviewer-support layers - [report-source-priority-2026-04-09](./reports/report-source-priority-2026-04-09.md): Priority order for the next candidate sources that might enter the canonical graph - [report-domain-expansion-readiness-2026-04-09](./reports/report-domain-expansion-readiness-2026-04-09.md): Gate for when the independent workspace should admit a second domain +- [report-drift-review-2026-04-11](./reports/report-drift-review-2026-04-11.md): First dedicated drift review snapshot after query, maintenance, and health-check tooling landed ## Log @@ -47,7 +49,7 @@ related: ## Sources -- [source-codex-memory-kit-readme](./sources/source-codex-memory-kit-readme.md): Repository-level statement that the project is a Codex-native memory governance layer +- [source-codex-memory-kit-readme](./sources/source-codex-memory-kit-readme.md): Repository-level statement that the project is a Codex-native memory governance layer and entry point to the archived raw snapshot - [source-codex-memory-kit-docs-index](./sources/source-codex-memory-kit-docs-index.md): Docs-level navigation layer that groups canonical reading, upstream integration, and repo-planning references - [source-raiden-workshop-repository-map](./sources/source-raiden-workshop-repository-map.md): Support source for how `codex-memory-kit` fits into the wider planned repo layout - [source-raiden-workshop-naming-convention](./sources/source-raiden-workshop-naming-convention.md): Support source for organization and repository naming rules in the wider planned repo ecosystem @@ -55,13 +57,13 @@ related: - [source-adr-001-oh-my-codex-memory-integration](./sources/source-adr-001-oh-my-codex-memory-integration.md): Accepted authority-boundary decision for strict integration mode - [source-oh-my-codex-memory-integration-spec](./sources/source-oh-my-codex-memory-integration-spec.md): Normative contract for authority, writes, overlay, and team behavior - [source-oh-my-codex-memory-integration-design](./sources/source-oh-my-codex-memory-integration-design.md): Detailed rationale and system-layer conflict analysis -- [source-oh-my-codex-memory-integration-development](./sources/source-oh-my-codex-memory-integration-development.md): Kept implementation surface, deleted local runtime surface, and recommended upstream sequence +- [source-oh-my-codex-memory-integration-development](./sources/source-oh-my-codex-memory-integration-development.md): Kept implementation surface, deleted local runtime surface, recommended upstream sequence, and archived retained modules - [source-oh-my-codex-upstream-review-summary](./sources/source-oh-my-codex-upstream-review-summary.md): Reviewer-facing order and intent across the six draft PRs - [source-oh-my-codex-verification-evidence-gate-design](./sources/source-oh-my-codex-verification-evidence-gate-design.md): Evidence-based verification artifact model for refresh and promotion gates -- [source-oh-my-codex-upstream-first-integration-status](./sources/source-oh-my-codex-upstream-first-integration-status.md): Implementation-status report for the first six upstream integration patches +- [source-oh-my-codex-upstream-first-integration-status](./sources/source-oh-my-codex-upstream-first-integration-status.md): Implementation-status report for the first six upstream integration patches and their preserved artifact set - [source-oh-my-codex-memory-integration-review-checklist](./sources/source-oh-my-codex-memory-integration-review-checklist.md): Acceptance checklist for authority, overlay, tool, team, and telemetry boundaries -- [source-oh-my-codex-upstream-first-integration-apply](./sources/source-oh-my-codex-upstream-first-integration-apply.md): Patch application order, artifact locations, and validation commands -- [source-oh-my-codex-upstream-review-notes](./sources/source-oh-my-codex-upstream-review-notes.md): PR-by-PR reviewer focus, ignore list, and patch-reading guidance +- [source-oh-my-codex-upstream-first-integration-apply](./sources/source-oh-my-codex-upstream-first-integration-apply.md): Patch application order, archived artifact locations, and validation commands +- [source-oh-my-codex-upstream-review-notes](./sources/source-oh-my-codex-upstream-review-notes.md): PR-by-PR reviewer focus, ignore list, and archived patch-reading guidance ## Entities diff --git a/knowledge-base/wiki/log.md b/knowledge-base/wiki/log.md index 41d8292..c374c34 100644 --- a/knowledge-base/wiki/log.md +++ b/knowledge-base/wiki/log.md @@ -3,7 +3,7 @@ title: "Knowledge Base Log" type: report status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: [] related: - index @@ -102,3 +102,16 @@ related: - Rewrote `WORKER_HANDOFF.md` from migration-history-heavy notes into a concise execution handoff - Updated `AGENTS.md`, `README.md`, and `CONTRIBUTING.md` so the worker read order now starts from `START_HERE.md` - Clarified that `/Users/wz/project/mult-agent/knowledge-base` is only historical seed origin, not an active workspace + +## [2026-04-11] ingest | full codex-memory-kit raw snapshot + +- Synced the remaining archived repository files into `raw/repos/codex-memory-kit/` +- Preserved the retained local implementation surface under `raw/repos/codex-memory-kit/src/` and `raw/repos/codex-memory-kit/test/` +- Preserved the exported upstream patch artifacts under `raw/repos/codex-memory-kit/patches/` +- Updated source pages, `wiki/index.md`, and `wiki/overview.md` so workers can resolve the merged-repo artifact locations without depending on the retired standalone repo path + +## [2026-04-11] maintenance | drift review refresh + +- Reviewed canonical pages flagged by `kb drift-review` against newer supporting source pages +- Refreshed affected concept, entity, and synthesis pages so archived artifact-home details and rollout status remain aligned +- Updated `wiki/hot.md`, `wiki/index.md`, `wiki/overview.md`, and generated a dedicated drift review report for the new maintenance surface diff --git a/knowledge-base/wiki/overview.md b/knowledge-base/wiki/overview.md index db71bb3..e071c07 100644 --- a/knowledge-base/wiki/overview.md +++ b/knowledge-base/wiki/overview.md @@ -3,12 +3,13 @@ title: "Knowledge Base Overview" type: guide status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: [] related: - domains/domain-codex-native-memory-governance - hot - index + - reports/report-drift-review-2026-04-11 - reports/report-domain-expansion-readiness-2026-04-09 - reports/report-lint-2026-04-09 - log @@ -41,6 +42,9 @@ related: - A naming-convention support source now sits inside the graph for naming-policy questions, again without changing the main domain thesis - A second lightweight report now ranks the next candidate sources so future ingest can follow an explicit priority order - A third lightweight report now defines when the cross-project shell is actually ready to admit a second domain +- The full archived `codex-memory-kit` repository now lives under `raw/repos/codex-memory-kit/`, including the retained JS reference modules, tests, and exported upstream patch artifacts +- Manual `kb maintain` and `kb drift-review` commands now provide a lightweight health-and-drift review surface without introducing heavier automation +- A dedicated drift-review report can now capture which canonical pages may need refresh after source changes ## What Counts As Canonical @@ -54,15 +58,17 @@ related: - The cross-project shell still has only one domain; future domains need explicit registry pages and should be added only when recurring usage justifies them - A second domain backlog exists only as report-layer guidance; no candidate is admitted yet -- No periodic lint or stale-review workflow exists yet +- No periodic lint or stale-review automation exists yet, even though manual `maintain` and `drift-review` checks now exist - No `candidates/` or `state/` helpers exist yet; only a minimal `reports/` layer exists for manual health checks - There is no remaining high-priority canonical expansion item right now; future ingest should be triggered by actual recurring questions +- The raw source snapshot still contains historical standalone-workspace paths inside some copied source documents; use the merged-repo raw snapshot itself as the current local artifact location ## Next Step - Use this workspace in real cross-project queries before adding a second domain - Add more concepts only when the current seed pages are no longer enough to answer recurring questions - Use `wiki/reports/` to record future lint, drift, or stale-review results before introducing heavier automation +- Use the latest drift-review report before refreshing canonical pages whose support sources changed recently - Use the source-priority report before expanding the canonical graph with new support docs - Use the domain-expansion-readiness report before adding a second domain - Prefer freezing or reviewing this baseline before adding more adjacent support sources diff --git a/knowledge-base/wiki/reports/report-drift-review-2026-04-11.md b/knowledge-base/wiki/reports/report-drift-review-2026-04-11.md new file mode 100644 index 0000000..e556e35 --- /dev/null +++ b/knowledge-base/wiki/reports/report-drift-review-2026-04-11.md @@ -0,0 +1,33 @@ +--- +title: "Drift Review 2026-04-11" +type: report +status: active +created_at: 2026-04-11 +updated_at: 2026-04-11 +source_refs: [] +related: + - ../index.md + - ../overview.md + - ../hot.md + - ../log.md +--- + +# Drift Review 2026-04-11 + +## Scope + +- Report type: `command-generated drift review` +- Checked on: `2026-04-11` +- Drift verdict: `stable` + +## Signal Summary + +- signal counts: none + +## Signals By Category + +- No drift signals remain after the canonical refresh and report write-back pass + +## Recommendations + +- No drift signals detected; rerun `kb drift-review` after future graph changes. diff --git a/knowledge-base/wiki/sources/source-codex-memory-kit-readme.md b/knowledge-base/wiki/sources/source-codex-memory-kit-readme.md index 6659637..6ffc6fd 100644 --- a/knowledge-base/wiki/sources/source-codex-memory-kit-readme.md +++ b/knowledge-base/wiki/sources/source-codex-memory-kit-readme.md @@ -3,7 +3,7 @@ title: "codex-memory-kit README" type: source status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../../raw/repos/codex-memory-kit/README.md related: @@ -18,12 +18,14 @@ related: - Source kind: `repo-readme` - Raw copy: [README](../../raw/repos/codex-memory-kit/README.md) +- Archived repo snapshot: [raw repo root](../../raw/repos/codex-memory-kit/) with `docs/`, `src/`, `test/`, and `patches/` preserved - Repository focus: the project now keeps one core responsibility, a Codex-native memory governance layer ## Why It Matters - This source defines the repository's top-level self-description - It narrows the domain away from "build another runtime" and toward governance, authority, and promotion control +- It now doubles as the entry point to the archived full-repository snapshot inside the merged workspace ## Key Claims diff --git a/knowledge-base/wiki/sources/source-oh-my-codex-memory-integration-development.md b/knowledge-base/wiki/sources/source-oh-my-codex-memory-integration-development.md index b1e0d44..a8e9979 100644 --- a/knowledge-base/wiki/sources/source-oh-my-codex-memory-integration-development.md +++ b/knowledge-base/wiki/sources/source-oh-my-codex-memory-integration-development.md @@ -3,7 +3,7 @@ title: "oh-my-codex Memory Integration Development" type: source status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../../raw/repos/codex-memory-kit/docs/oh-my-codex-memory-integration-development.md related: @@ -18,12 +18,14 @@ related: - Source kind: `development-plan` - Raw copy: [integration-development](../../raw/repos/codex-memory-kit/docs/oh-my-codex-memory-integration-development.md) +- Archived retained modules: [src](../../raw/repos/codex-memory-kit/src) and [test](../../raw/repos/codex-memory-kit/test) - Last updated in source: `2026-04-07` ## Why It Matters - This source links the governance model to actual retained modules, deleted runtime surface, and recommended next execution order - It is the best bridge between abstract architecture and the code or PR work that remains active +- The merged workspace now preserves the exact local implementation surface that this source describes as the kept governance layer ## Key Claims diff --git a/knowledge-base/wiki/sources/source-oh-my-codex-upstream-first-integration-apply.md b/knowledge-base/wiki/sources/source-oh-my-codex-upstream-first-integration-apply.md index 12d93d6..4c86b0e 100644 --- a/knowledge-base/wiki/sources/source-oh-my-codex-upstream-first-integration-apply.md +++ b/knowledge-base/wiki/sources/source-oh-my-codex-upstream-first-integration-apply.md @@ -3,7 +3,7 @@ title: "oh-my-codex Upstream First Integration Apply Guide" type: source status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../../raw/repos/codex-memory-kit/docs/oh-my-codex-upstream-first-integration-apply.md related: @@ -17,12 +17,14 @@ related: - Source kind: `apply-guide` - Raw copy: [upstream-first-integration-apply](../../raw/repos/codex-memory-kit/docs/oh-my-codex-upstream-first-integration-apply.md) +- Archived patch set: [patches](../../raw/repos/codex-memory-kit/patches) - Last updated in source: `2026-04-04` ## Why It Matters - This source explains how the upstream patch set is actually applied, in which order, and with which validation commands - It is the most operational source for moving from review to implementation +- The merged workspace now preserves the patch artifacts that this guide expects implementers to apply ## Key Claims diff --git a/knowledge-base/wiki/sources/source-oh-my-codex-upstream-first-integration-status.md b/knowledge-base/wiki/sources/source-oh-my-codex-upstream-first-integration-status.md index d7281ef..e599aea 100644 --- a/knowledge-base/wiki/sources/source-oh-my-codex-upstream-first-integration-status.md +++ b/knowledge-base/wiki/sources/source-oh-my-codex-upstream-first-integration-status.md @@ -3,7 +3,7 @@ title: "oh-my-codex Upstream First Integration Status" type: source status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../../raw/repos/codex-memory-kit/docs/oh-my-codex-upstream-first-integration-status.md related: @@ -17,12 +17,14 @@ related: - Source kind: `status-report` - Raw copy: [upstream-first-integration-status](../../raw/repos/codex-memory-kit/docs/oh-my-codex-upstream-first-integration-status.md) +- Archived patch set: [patches](../../raw/repos/codex-memory-kit/patches) - Last updated in source: `2026-04-04` ## Why It Matters - This source records what has already been implemented, verified, published, and split into upstream draft PRs - It is the strongest source for the current rollout state, not just the intended plan +- It now has a merged-repo artifact home for the patch set that the status report describes ## Key Claims diff --git a/knowledge-base/wiki/sources/source-oh-my-codex-upstream-review-notes.md b/knowledge-base/wiki/sources/source-oh-my-codex-upstream-review-notes.md index 7bc0e12..9137226 100644 --- a/knowledge-base/wiki/sources/source-oh-my-codex-upstream-review-notes.md +++ b/knowledge-base/wiki/sources/source-oh-my-codex-upstream-review-notes.md @@ -3,7 +3,7 @@ title: "oh-my-codex Upstream Review Notes" type: source status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../../raw/repos/codex-memory-kit/docs/oh-my-codex-upstream-review-notes.md related: @@ -17,12 +17,14 @@ related: - Source kind: `review-notes` - Raw copy: [upstream-review-notes](../../raw/repos/codex-memory-kit/docs/oh-my-codex-upstream-review-notes.md) +- Archived patch set: [patches](../../raw/repos/codex-memory-kit/patches) - Last updated in source: `2026-04-04` ## Why It Matters - This source gives the most detailed PR-by-PR reviewer focus, file pointers, and ignore guidance - It is the best source for how to inspect the stacked PR chain without conflating unrelated failures +- It remains actionable after consolidation because the referenced patch artifacts are preserved inside the merged repo snapshot ## Key Claims diff --git a/knowledge-base/wiki/syntheses/synthesis-codex-native-memory-governance-baseline.md b/knowledge-base/wiki/syntheses/synthesis-codex-native-memory-governance-baseline.md index 174f462..d317765 100644 --- a/knowledge-base/wiki/syntheses/synthesis-codex-native-memory-governance-baseline.md +++ b/knowledge-base/wiki/syntheses/synthesis-codex-native-memory-governance-baseline.md @@ -3,7 +3,7 @@ title: "Codex-native memory governance baseline" type: synthesis status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../sources/source-codex-memory-kit-readme.md - ../sources/source-oh-my-codex-memory-integration-executive-summary.md @@ -34,6 +34,7 @@ related: - The ADR converts that positioning into an accepted architectural decision - The specification turns the decision into normative constraints on writes, overlay behavior, and team execution - The design document explains the conflict model that makes those constraints necessary +- The merged workspace now preserves the archived repo snapshot, retained JS reference modules, tests, and patch artifacts as traceable governance support material ## Stable Baseline Conclusions @@ -41,12 +42,13 @@ related: - Promotion into formal memory must be explicit and gated - Overlay order matters because reading a local replica as truth recreates a second authority even without direct writes - The domain is about governance boundaries, not about rebuilding a whole alternative agent runtime +- Archived implementation and patch artifacts may remain as reviewable support material without changing the governance-first scope ## Open Questions - How much of the verification evidence gate should remain local versus move upstream - Which review and implementation documents should be treated as next-tier canonical sources -- When this workspace grows, which `v1.5` governance helpers should be introduced first +- When this workspace grows further, which helper should come after the current `hot.md` and `reports/` layer ## Related Pages diff --git a/knowledge-base/wiki/syntheses/synthesis-upstream-integration-rollout.md b/knowledge-base/wiki/syntheses/synthesis-upstream-integration-rollout.md index 26837be..cee3458 100644 --- a/knowledge-base/wiki/syntheses/synthesis-upstream-integration-rollout.md +++ b/knowledge-base/wiki/syntheses/synthesis-upstream-integration-rollout.md @@ -3,7 +3,7 @@ title: "Upstream integration rollout" type: synthesis status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../sources/source-oh-my-codex-memory-integration-development.md - ../sources/source-oh-my-codex-upstream-review-summary.md @@ -33,6 +33,7 @@ related: - The upstream review sequence starts from the strict formal-memory adapter, then stabilizes notify or tmux test behavior, then aligns prompt or docs surfaces - Only after those read-path and surface changes does the rollout open runtime refresh paths, first at session end, then at leader-side team completion - Verification evidence gate is the final tightening layer so refresh depends on evidence rather than a fragile caller flag +- The merged workspace keeps the retained governance modules, tests, and patch artifacts needed to inspect this rollout without reviving the deleted local runtime ## Stable Conclusions @@ -40,18 +41,19 @@ related: - Stacked PR ordering matters because `#1236` depends on `#1235`, and `#1238` depends on `#1236` - Notify or tmux stability belongs in a separate patch line so it does not blur the semantics of the core memory adapter - Verification artifact design strengthens refresh and promotion governance while keeping evidence in runtime space +- The preserved patch set and retained local modules make the rollout reviewable from the merged workspace itself ## Current Status - The first integration wave already exists as six draft PRs and reviewer materials, not just as private local planning - Implementation status, review packet documents, checklist, apply guidance, and review notes now provide enough material for canonical knowledge about rollout shape and review procedure -- The workspace still has room to ingest auxiliary repo-planning or docs-index material if those start answering recurring questions +- The workspace now also preserves the exact patch artifacts and retained local modules that those rollout materials reference ## Open Questions - Which of the remaining reviewer-support documents deserve canonical source pages next - How far verification evidence gating should expand beyond the current team-complete refresh path -- When this workspace should add `v1.5` helpers such as `hot.md` or report pages to support repeated querying +- Which additional helper or report, if any, should come after the current `hot.md` and reports layer ## Related Pages diff --git a/knowledge-base/wiki/syntheses/synthesis-upstream-reviewer-packet.md b/knowledge-base/wiki/syntheses/synthesis-upstream-reviewer-packet.md index 71d980a..30588ec 100644 --- a/knowledge-base/wiki/syntheses/synthesis-upstream-reviewer-packet.md +++ b/knowledge-base/wiki/syntheses/synthesis-upstream-reviewer-packet.md @@ -3,7 +3,7 @@ title: "Upstream reviewer packet" type: synthesis status: active created_at: 2026-04-09 -updated_at: 2026-04-09 +updated_at: 2026-04-11 source_refs: - ../sources/source-codex-memory-kit-docs-index.md - ../sources/source-oh-my-codex-upstream-review-summary.md @@ -32,6 +32,7 @@ related: - Review notes give the deeper PR-by-PR focus, file pointers, and ignore lists - Apply guide explains patch artifacts, stacked dependencies, and stage-specific validation commands - Review checklist defines the acceptance bar in terms of authority, overlay, write-path, team, and telemetry invariants +- The merged workspace now preserves the patch artifacts and retained reference modules that these packet documents expect reviewers to inspect ## Stable Conclusions @@ -39,6 +40,7 @@ related: - Narrow targeted validation is preferred over noisy wider-suite signals when deciding whether a patch preserves the strict-memory contract - Acceptance requires more than code success; it also requires the invariants and docs to stay aligned - Reviewer support material is part of the governance system because it prevents scope drift and mis-review +- Archived patch artifacts remain a legitimate review surface after consolidation when GitHub diff context is not enough ## Recommended Read Path diff --git a/memory-system/AGENTS.md b/memory-system/AGENTS.md index 0034a2e..4efd646 100644 --- a/memory-system/AGENTS.md +++ b/memory-system/AGENTS.md @@ -34,14 +34,14 @@ python3 /Users/wz/.codex/scripts/refresh_memory.py --workspace-root "$(git rev-p ## Method Forge Autonomous Coding -- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”“进入实现阶段”等实现意图时,默认进入 `method-forge` 的实现流程,并优先在当前线程使用 `method-forge-execute` 推进,不隐式创建后台 automation。 -- 只有当用户明确要求自动续跑、后台继续、autonomous/heartbeat 模式,或当前任务本来就在恢复一个已存在的 autonomous run 时,才进入 `method-forge` autonomous mode。 -- autonomous mode 使用 Codex 原生 heartbeat / background automation 作为监听者,不重做第二套调度器,也不替代原生 automation 创建入口。 +- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”“进入实现阶段”等实现意图时,默认进入 `method-forge` autonomous mode,除非用户明确要求不要自动、先别写代码或只做方案。 +- autonomous mode 使用 Codex 原生 heartbeat / background automation 作为监听者,不重做第二套调度器。 - autonomous mode 的内层默认执行引擎是 `method-forge-execute`;在 autonomous 已经启动后,后续续跑周期不需要用户再次指定“按 method-forge 执行”。 - 若当前任务还没有标准变更包,先建立 `docs/specs//`、`package-index.md` 和 `runtime/run-state.md`。 - 若已有需求或设计草稿,先由 `method-forge-execute` 归一化为 `intake/spec/plan/tasks`,再进入实现。 - 实现完成后必须产出 `verify.md`;高风险改动可在 verify 前追加 `code-review.md`。 - 若 `verify.md` 认为存在稳定可复用结论,只能提出 `memory-candidate.md`,不得直接写长期 memory。 - autonomous mode 必须执行 loop guard:同一步最多重试 3 次,同一错误签名最多重复 2 次,连续无进展最多 2 轮,单任务最多 12 个周期。 +- 不要把单个微切片或一次 `verify` 通过误记为 `completed`;若同一用户目标下下一安全切片已明确,应保持 `running` 并自动推进。 / Do not mark the run `completed` just because a single micro-slice or one `verify` pass finished; if the next safe slice inside the same user goal is already known, keep it `running` and continue automatically. - 触发 `blocked`、`waiting-human`、`waiting-external` 或 `completed` 后必须停止自动推进,并在 `run-state.md` 写明 `stop_reason` 与 `next_action`。 - 若本文件已有更具体的 memory system 规则,以更具体规则为准,但不得取消 `verify` 或 loop guard。 diff --git a/memory-system/codex-global-memory-kit/templates/codex/memory/instructions/user/GUIDE.md b/memory-system/codex-global-memory-kit/templates/codex/memory/instructions/user/GUIDE.md index 03d0a13..85c55d1 100644 --- a/memory-system/codex-global-memory-kit/templates/codex/memory/instructions/user/GUIDE.md +++ b/memory-system/codex-global-memory-kit/templates/codex/memory/instructions/user/GUIDE.md @@ -2,4 +2,4 @@ - Use Codex App as the primary AI workspace and avoid unnecessary tool switching. - Default to the simplest stable path that works. -- When the user says phrases like “开始落地代码”, “开始实现”, “开始写代码”, “继续写代码”, or “进入实现阶段”, default to the `method-forge` implementation flow when the `method-forge-execute` skill is available; stay in the current thread by default, and enter `method-forge` autonomous / heartbeat mode only when the user explicitly asks for automation or background continuation, or when an existing autonomous run is already being resumed. Keep loop guards plus `verify` mandatory. +- When the user says phrases like “开始落地代码”, “开始实现”, “开始写代码”, “继续写代码”, or “进入实现阶段”, default to `method-forge` autonomous mode when the relevant skills are available, unless the user explicitly says not to automate, not to write code yet, or to stay at the planning stage. Use native Codex heartbeat/background automation as the listener and `method-forge-execute` as the default inner engine. Keep loop guards plus `verify` mandatory. diff --git a/method-forge/AGENTS.md b/method-forge/AGENTS.md index 1d67fca..b9b0d02 100644 --- a/method-forge/AGENTS.md +++ b/method-forge/AGENTS.md @@ -14,13 +14,13 @@ ## Method Forge Autonomous Coding -- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”“进入实现阶段”等实现意图时,默认进入 `method-forge` 的实现流程,并优先在当前线程使用 `method-forge-execute` 推进,不隐式创建后台 automation。 -- 只有当用户明确要求自动续跑、后台继续、autonomous/heartbeat 模式,或当前任务本来就在恢复一个已存在的 autonomous run 时,才进入 `method-forge` autonomous mode。 -- autonomous mode 使用 Codex 原生 heartbeat / background automation 作为监听者,不重做第二套调度器,也不替代原生 automation 创建入口。 +- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”“进入实现阶段”等实现意图时,默认进入 `method-forge` autonomous mode,除非用户明确要求不要自动、先别写代码或只做方案。 +- autonomous mode 使用 Codex 原生 heartbeat / background automation 作为监听者,不重做第二套调度器。 - autonomous mode 的内层默认执行引擎是 `method-forge-execute`;在 autonomous 已经启动后,后续续跑周期不需要用户再次指定“按 method-forge 执行”。 - 若当前任务还没有标准变更包,先建立 `docs/specs//`、`package-index.md` 和 `runtime/run-state.md`。 - 若已有需求或设计草稿,先由 `method-forge-execute` 归一化为 `intake/spec/plan/tasks`,再进入实现。 - 实现完成后必须产出 `verify.md`;高风险改动可在 verify 前追加 `code-review.md`。 - 若 `verify.md` 认为存在稳定可复用结论,只能提出 `memory-candidate.md`,不得直接写长期 memory。 - autonomous mode 必须执行 loop guard:同一步最多重试 3 次,同一错误签名最多重复 2 次,连续无进展最多 2 轮,单任务最多 12 个周期。 +- 不要把单个微切片或一次 `verify` 通过误记为 `completed`;若同一用户目标下下一安全切片已明确,应保持 `running` 并自动推进。 / Do not mark the run `completed` just because a single micro-slice or one `verify` pass finished; if the next safe slice inside the same user goal is already known, keep it `running` and continue automatically. - 触发 `blocked`、`waiting-human`、`waiting-external` 或 `completed` 后必须停止自动推进,并在 `run-state.md` 写明 `stop_reason` 与 `next_action`。 diff --git a/method-forge/README.md b/method-forge/README.md index 388ae1a..6eb4d42 100644 --- a/method-forge/README.md +++ b/method-forge/README.md @@ -85,6 +85,7 @@ route-request - `按method-forge方式执行这个需求:...` - `这里有一份设计草稿,按method-forge方式执行并补齐缺的文档、实现和 verify` +- `开始落地代码,让 method-forge 自动跑到本轮任务结束` - `启动 autonomous mode,用 method-forge 自动跑到本轮任务结束` ## 核心文档 @@ -139,7 +140,7 @@ route-request - resume / loop guard 规则 - 默认自动调用 `method-forge-execute` -如果你希望在每个 worker 里支持显式 autonomous 请求或恢复既有 autonomous run,还需要: +如果你希望在每个 worker 里支持“开始落地代码 / 开始实现 / 继续写代码”等实现意图默认进入 autonomous mode,或恢复既有 autonomous run,还需要: - 让入口 skill 全局可见 - 采用 [activation-rules.md](docs/method/activation-rules.md) 中的触发规则 diff --git a/method-forge/docs/method/activation-rules.md b/method-forge/docs/method/activation-rules.md index 699f6cb..371a70c 100644 --- a/method-forge/docs/method/activation-rules.md +++ b/method-forge/docs/method/activation-rules.md @@ -7,8 +7,8 @@ activation rules 用于把用户的自然语言意图自动映射到 `method-for 特别是当用户说“开始落地代码”这类话时,系统应默认理解为: - 进入实现阶段 -- 先在当前线程进入 `method-forge-execute` -- 只有在用户明确要求 automation / autonomous / heartbeat 续跑时,才进入原生 automation 驱动的 autonomous mode +- 默认进入 `method-forge` autonomous mode +- `method-forge-execute` 作为 autonomous 的默认内层执行引擎 ## 2. 触发优先级 @@ -43,7 +43,7 @@ activation rules 用于把用户的自然语言意图自动映射到 `method-for - 已有变更包 - 已有 `spec.md` / `plan.md` / `tasks.md` -则默认触发 **`method-forge-execute`**,先在当前线程推进实现。 +则默认触发 **`method-forge-autonomous-execution`**。 ### 2.3 明确要求自动续跑,或恢复既有 autonomous run @@ -61,18 +61,15 @@ activation rules 用于把用户的自然语言意图自动映射到 `method-for ## 3. 为什么默认进 autonomous -这一步现在默认不直接进 autonomous。 +这一步恢复为默认进入 autonomous。 -原因是 Codex App 的 automation / heartbeat 属于原生后台能力,应该显式创建、显式恢复,而不是被普通实现意图隐式触发。 +原因是用户原本想要的行为就是:当已经表达明确实现意图时,不只是在当前线程里开始写一点代码,而是让 `method-forge` 尽量自动把缺失文档、实现、`verify` 和续跑一起推进。 -普通“开始实现”最符合预期的行为通常是: +这并不意味着重做第二套调度器: -- 不再只停在设计 -- 先在当前线程归一化缺失文档 -- 进入实现 -- 补 `verify` - -而不是悄悄创建新的后台 automation。 +- 监听者仍然是 Codex App 原生 heartbeat / background automation +- `method-forge-execute` 仍然是默认内层执行引擎 +- 只有当用户明确说不要自动、先别写代码、只做方案,或当前风险过高必须人工确认时,才降级出 autonomous ## 4. 默认行为 @@ -81,14 +78,16 @@ activation rules 用于把用户的自然语言意图自动映射到 `method-for 1. 先检查当前变更包是否已存在。 2. 若不存在,则创建 `docs/specs/-/` 和 `package-index.md`。 3. 若缺 `intake/spec/plan/tasks`,则由 `method-forge-execute` 自动补齐。 -4. 若已具备实现上下文,则直接开始实现。 -5. 实现完成后补 `verify.md`。 +4. 启用 autonomous 运行态,写入 `runtime/run-state.md`。 +5. 通过 Codex App 原生 automation 入口创建或恢复监听。 +6. 若已具备实现上下文,则直接开始实现。 +7. 实现完成后补 `verify.md`。 +8. 后续 heartbeat 周期默认继续调用 `method-forge-execute`。 -若用户明确要求 autonomous / heartbeat: +补充: -6. 启用 autonomous 运行态,写入 `runtime/run-state.md`。 -7. 通过 Codex App 原生 automation 入口创建或恢复监听。 -8. 后续 heartbeat 周期默认继续调用 `method-forge-execute`。 +- 若同一用户目标下已经存在明确且安全的下一实现切片,不应在某个微切片刚完成后就退出到等待用户再次说“继续”。 +- If the same user goal already has a clear and safe next implementation slice, the autonomous run should not drop back to waiting for the user to say “continue” again after just one micro-slice completes. ## 5. 退出或降级条件 @@ -99,7 +98,6 @@ activation rules 用于把用户的自然语言意图自动映射到 `method-for - 用户只想做方案,不想实现 - 当前缺关键 repo / workspace 上下文 - 任务风险过高且必须先人工确认 -- 用户只是表达实现意图,但没有明确要求后台 automation / autonomous / heartbeat ## 6. 在每个 worker 生效的前提 @@ -122,17 +120,17 @@ activation rules 用于把用户的自然语言意图自动映射到 `method-for 至少要在消费方 `AGENTS.md` 或共享规则里写入: -- “开始落地代码 / 开始实现 / 开始写代码” 默认进入 `method-forge-execute` -- 显式 autonomous 请求或既有 autonomous run 恢复时,才进入原生 heartbeat automation +- “开始落地代码 / 开始实现 / 开始写代码” 默认进入 `method-forge` autonomous mode +- autonomous 的监听者使用 Codex 原生 heartbeat automation - autonomous 的默认执行引擎为 `method-forge-execute` +- “不要自动 / 先别写代码 / 只做方案” 等短语可以降级出 autonomous ## 7. 推荐写法 消费方可以在 `AGENTS.md` 使用类似规则: ```text -当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”等实现意图时,默认进入 `method-forge-execute` 驱动的实现流程,并优先留在当前线程。 -只有当用户明确要求自动续跑、后台继续、autonomous/heartbeat 模式,或当前任务本来就在恢复一个 autonomous run 时,才进入 autonomous mode。 +当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”等实现意图时,默认进入 `method-forge` autonomous mode,除非用户明确要求不要自动、先别写代码或只做方案。 autonomous mode 的监听者使用 Codex 原生 heartbeat automation,内层默认执行引擎为 `method-forge-execute`。 ``` diff --git a/method-forge/docs/method/autonomous-execution.md b/method-forge/docs/method/autonomous-execution.md index 302c6d3..46593a8 100644 --- a/method-forge/docs/method/autonomous-execution.md +++ b/method-forge/docs/method/autonomous-execution.md @@ -28,9 +28,9 @@ 补充边界: -- autonomous / heartbeat 的创建必须是显式动作 -- 普通“开始实现”默认只进入当前线程里的 `method-forge-execute` -- 只有显式要求自动续跑,或恢复既有 autonomous run 时,才启用这层扩展 +- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”等实现意图时,默认启用这层扩展 +- 若用户明确说不要自动、先别写代码或只做方案,可降级为当前线程里的 `method-forge-execute` +- 恢复既有 autonomous run 时,也继续启用这层扩展 ## 3. 是否还要重复指定 `method-forge` @@ -45,7 +45,8 @@ 也就是说: -- **启动时** 你可以说一次“按method-forge方式执行”并显式要求自动续跑 +- **启动时** 你可以直接说“开始落地代码”“开始实现”“继续写代码”,系统就默认进入 autonomous mode +- **显式启动时** 你也可以直接说“启动 autonomous mode” - **后续自动续跑时** 不需要你每轮再重复指定 ## 4. 推荐运行时结构 @@ -91,7 +92,12 @@ autonomous 周期恢复时应按以下顺序读取: 7. 更新 `package-index.md` 8. 写一份新的 cycle report -这套周期行为的前提是:该 automation 已经由 Codex App 原生入口显式创建,或当前 run 明确处于恢复状态。 +这套周期行为的前提是:该 automation 已经由实现意图触发、显式 autonomous 请求触发,或当前 run 明确处于恢复状态。 + +补充一条关键约束: + +- 不要把“某个微切片刚完成 `verify`”误当成整个任务已经 `completed`。若同一用户目标下的下一安全切片已经明确,状态应保持 `running`,并把 `current_step` / `next_action` 推进到下一切片继续自动执行。 +- Do not treat “one micro-slice just passed `verify`” as the whole task being `completed`. If the next safe slice within the same user goal is already known, keep the status `running` and advance `current_step` / `next_action` so execution continues automatically. ## 7. 允许的停止状态 @@ -110,6 +116,8 @@ autonomous 周期恢复时应按以下顺序读取: - `waiting-human` 表示必须人工确认 - `waiting-external` 表示依赖外部系统、权限或输入 - `completed` 表示本轮任务已结束 +- `completed` 只表示当前顶层任务或当前变更包目标已经结束,不表示单个微切片刚刚结束 +- `completed` only means the current top-level task or change-package goal is finished; it does not mean a single micro-slice has just finished ## 8. 自动推进与人工确认边界 diff --git a/method-forge/docs/method/consumer-adoption.md b/method-forge/docs/method/consumer-adoption.md index 6b2b2bf..f6fa38f 100644 --- a/method-forge/docs/method/consumer-adoption.md +++ b/method-forge/docs/method/consumer-adoption.md @@ -59,9 +59,9 @@ 4. 复杂任务按 `intake -> spec -> plan -> plan-review -> tasks -> verify` 走。 5. 高风险任务按需追加 `code-review` 和 `memory-candidate`。 6. 定期用 `workflow-health-report.md` 做人工健康检查。 -7. 若要自动续跑,再启用 `runtime/run-state.md` 和 Codex App 原生 heartbeat automation。 +7. 若采用默认行为,则在用户表达“开始落地代码”“开始实现”“继续写代码”等实现意图时启用 `runtime/run-state.md` 和 Codex App 原生 heartbeat automation。 -若你希望支持显式 autonomous 请求或恢复既有 autonomous run,还需要: +若你希望支持默认 implementation-intent -> autonomous 触发,或恢复既有 autonomous run,还需要: - 让入口 skill 在所有 worker 中可见 - 在消费方 `AGENTS.md` 中写入触发短语规则 @@ -103,7 +103,7 @@ - `memory-candidate.md` 只整理候选,不直接写 memory - 会话内流程继续统一叫 `orchestrations` - autonomous 模式下,heartbeat 应默认使用 `method-forge-execute` -- 不把普通实现意图隐式升级成后台 automation +- 普通实现意图默认可以进入后台 automation;若消费方不希望自动,必须显式声明降级短语 ## 7. 入口文件 diff --git a/method-forge/docs/method/resume-rules.md b/method-forge/docs/method/resume-rules.md index b569803..f1a50c6 100644 --- a/method-forge/docs/method/resume-rules.md +++ b/method-forge/docs/method/resume-rules.md @@ -19,6 +19,8 @@ resume 规则用于让 autonomous 执行在暂停、中断或报错后,能够 - 不再继续实现 - 只允许做轻量确认或收尾状态同步 +- 这里的 `completed` 只应用在顶层任务真正完成时;若只是一个微切片完成且下一安全切片已知,状态应保持 `running` +- `completed` here is reserved for a truly finished top-level task; if only one micro-slice is done and the next safe slice is already known, the status should stay `running` ### 3.2 如果 `status=waiting-human` diff --git a/method-forge/docs/method/skill-contracts.md b/method-forge/docs/method/skill-contracts.md index ee3d233..f79f679 100644 --- a/method-forge/docs/method/skill-contracts.md +++ b/method-forge/docs/method/skill-contracts.md @@ -88,7 +88,7 @@ ### 3.6.6 `method-forge-autonomous-execution` - 必须使用 Codex 原生 automation 作为监听器,而不是重做第二套调度器 -- 只能在用户显式要求 automation / autonomous / heartbeat 续跑,或恢复既有 autonomous run 时启用 +- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”等实现意图时默认启用;显式要求 automation / autonomous / heartbeat 续跑或恢复既有 autonomous run 时也启用 - 必须把 `method-forge-execute` 设为默认内层执行引擎 - 必须维护 `run-state.md` 并执行 loop guard diff --git a/method-forge/docs/presets/minimal-change-package/README.md b/method-forge/docs/presets/minimal-change-package/README.md index 986b557..d538c1f 100644 --- a/method-forge/docs/presets/minimal-change-package/README.md +++ b/method-forge/docs/presets/minimal-change-package/README.md @@ -66,7 +66,8 @@ docs/specs// ## Notes - 若消费方希望一句话触发整条流程,可以直接使用 [method-forge-execute](../../../skills/method-forge-execute/SKILL.md) 的自然语言入口。 -- 若消费方希望任务自动续跑到本轮结束,可再叠加 [method-forge-autonomous-execution](../../../skills/method-forge-autonomous-execution/SKILL.md) 和 heartbeat automation prompt。 +- 若消费方希望“开始落地代码”“开始实现”“继续写代码”等实现意图默认自动续跑到本轮结束,应同时接入 [method-forge-autonomous-execution](../../../skills/method-forge-autonomous-execution/SKILL.md) 和 heartbeat automation prompt。 +- 若消费方想手动显式启动 autonomous,也仍可直接使用 [method-forge-autonomous-execution](../../../skills/method-forge-autonomous-execution/SKILL.md)。 - 消费方可以先把 [consumer-agents-rules-template.md](../../templates/consumer-agents-rules-template.md) 贴进自己的 `AGENTS.md` 再细化。 - `package-index.md` 是导航页,不是第二份 spec。 - 若消费方只做轻任务,不必强制补齐所有文件。 diff --git a/method-forge/docs/templates/autonomous-heartbeat-prompt-template.md b/method-forge/docs/templates/autonomous-heartbeat-prompt-template.md index f663e33..d69f78f 100644 --- a/method-forge/docs/templates/autonomous-heartbeat-prompt-template.md +++ b/method-forge/docs/templates/autonomous-heartbeat-prompt-template.md @@ -18,9 +18,10 @@ Execution rules: - Continue automatically when the task is still safely actionable. - Do not ask the user to restate "use method-forge" again. - Use `method-forge-execute` to normalize rough inputs, continue the correct workflow phase, and complete implementation/verify when context is sufficient. +- Do not stop just because one micro-slice passed `verify`; if the next safe slice inside the same user goal is already known, keep the run `running` and continue on the next cycle. - If progress is made, update `run-state.md`, `package-index.md`, and write a new cycle report under `runtime/reports/`. - If no safe progress can be made, set status to `blocked`, `waiting-human`, or `waiting-external` with a concrete `stop_reason`. -- If the task is finished, set status to `completed` and stop. +- Set status to `completed` only when the top-level task is actually finished, not when a single sub-slice happens to complete. Loop guard: - Max step retries: 3 diff --git a/method-forge/docs/templates/consumer-agents-rules-template.md b/method-forge/docs/templates/consumer-agents-rules-template.md index ebcf721..ce1ea5a 100644 --- a/method-forge/docs/templates/consumer-agents-rules-template.md +++ b/method-forge/docs/templates/consumer-agents-rules-template.md @@ -6,8 +6,9 @@ - 高风险改动可在 `verify` 前追加 `code-review.md`。 - `verify.md` 可以提出 `memory-candidate.md`,但不得直接写长期 memory。 - 失败回退时优先修订原真相源,不新增平行的 `final-v2` 式文档分支。 -- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”等实现意图时,默认进入 `method-forge-execute` 驱动的实现流程,并优先留在当前线程。 -- 只有当用户明确要求自动续跑、后台继续、autonomous/heartbeat 模式,或当前任务本来就在恢复一个 autonomous run 时,才进入 autonomous mode;其内层默认执行引擎为 `method-forge-execute`。 +- 当用户表达“开始落地代码”“开始实现”“开始写代码”“继续写代码”等实现意图时,默认进入 `method-forge` autonomous mode,除非用户明确要求不要自动、先别写代码或只做方案。 +- autonomous mode 使用 Codex 原生 heartbeat automation 作为监听者,其内层默认执行引擎为 `method-forge-execute`。 +- 不要把单个微切片或一次 `verify` 通过误记为 `completed`;若同一用户目标下下一安全切片已明确,应保持 `running` 并自动推进。 / Do not mark the run `completed` just because a single micro-slice or one `verify` pass finished; if the next safe slice inside the same user goal is already known, keep it `running` and continue automatically. - research 和 knowledge-base 只作为输入,不替代 `spec.md`、`plan.md` 或 `tasks.md`。 - 会话内流程编排统一叫 `orchestrations`;`automations` 只指 Codex App 原生后台任务。 - 不重复实现 Codex 原生的 multi-agent、worktrees、diff review、git/PR、background automations、sandbox/approvals、skill loading。 diff --git a/method-forge/skills/method-forge-autonomous-execution/SKILL.md b/method-forge/skills/method-forge-autonomous-execution/SKILL.md index 2223905..60f57da 100644 --- a/method-forge/skills/method-forge-autonomous-execution/SKILL.md +++ b/method-forge/skills/method-forge-autonomous-execution/SKILL.md @@ -1,6 +1,6 @@ --- name: method-forge-autonomous-execution -description: Use when the user explicitly asks for autonomous execution, background continuation, heartbeat-based continuation, pause or resume of an existing autonomous run, or asks whether method-forge should keep running without restating it every cycle. This skill uses native Codex heartbeat automation as the listener and method-forge-execute as the default inner engine. +description: Use when the user says phrases like "开始落地代码", "开始实现", "开始写代码", "继续写代码", or explicitly asks for autonomous execution, background continuation, heartbeat-based continuation, pause or resume of an existing autonomous run, or asks whether method-forge should keep running without restating it every cycle. This skill uses native Codex heartbeat automation as the listener and method-forge-execute as the default inner engine. --- # method-forge-autonomous-execution @@ -18,6 +18,10 @@ description: Use when the user explicitly asks for autonomous execution, backgro ## Use When +- 用户说“开始落地代码” +- 用户说“开始实现” +- 用户说“开始写代码” +- 用户说“继续写代码” - 用户说“自动执行直到结束” - 用户说“后台继续跑” - 用户说“启动 autonomous mode” @@ -38,15 +42,16 @@ description: Use when the user explicitly asks for autonomous execution, backgro 1. 建立或更新变更包。 2. 建立 `runtime/run-state.md`。 -3. 通过 Codex App 原生 automation 入口显式创建或恢复 heartbeat 监听;若当前环境只需要说明文档,则至少准备 heartbeat automation prompt,明确默认使用 `method-forge-execute`。 +3. 通过 Codex App 原生 automation 入口创建或恢复 heartbeat 监听;当实现意图命中时默认应创建,除非用户明确说不要自动、先别写代码或只做方案。若当前环境只需要说明文档,则至少准备 heartbeat automation prompt,明确默认使用 `method-forge-execute`。 4. 每一轮恢复时先读 `run-state.md`、`package-index.md` 和当前阶段文档。 5. 只有在状态允许时才继续推进。 6. 每轮结束更新 `run-state.md` 并写 cycle report。 7. 若触发 loop guard,则转为 `blocked` 或 `waiting-human`,而不是盲重试。 +8. 不要把单个微切片或一次 `verify` 通过误记为 `completed`;若同一用户目标下下一安全切片已明确,应保持 `running` 并继续自动推进。 / Do not mark a run `completed` just because a single micro-slice or one `verify` pass finished; if the next safe slice inside the same user goal is already known, keep the run `running` and continue automatically. ## Default Rule -一旦 autonomous 模式已经启动,后续 heartbeat 周期不需要用户再次指定“按method-forge执行”。 +当用户用实现意图短语触发本 skill,或 autonomous 模式已经启动后,后续 heartbeat 周期都不需要用户再次指定“按method-forge执行”。 默认就应当使用 `method-forge-execute`,除非任务已经: @@ -54,6 +59,8 @@ description: Use when the user explicitly asks for autonomous execution, backgro - `blocked` - `waiting-human` +这里的 `completed` 指顶层任务完成,不是某个微切片刚结束。 / Here `completed` means the top-level task is done, not that one micro-slice just ended. + ## Outputs - `runtime/run-state.md`