@betterdanlins/ai-memory 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -3
- package/README.zh-CN.md +53 -3
- package/bin/cli.js +97 -2
- package/package.json +1 -1
- package/src/framework.js +5 -4
- package/src/model-routing.js +108 -0
- package/src/workflow.js +185 -0
- package/templates/claude/.claude/agents/economy-test-worker.md +8 -0
- package/templates/claude/.claude/agents/premium-implementer.md +8 -0
- package/templates/claude/.claude/agents/premium-planner.md +8 -0
- package/templates/claude/.claude/agents/premium-reviewer.md +8 -0
- package/templates/claude/.claude/agents/standard-implementer.md +8 -0
- package/templates/claude/.claude/agents/standard-test-worker.md +8 -0
- package/templates/claude/.claude/skills/model-routing/SKILL.md +6 -0
- package/templates/claude/CLAUDE.md +1 -0
- package/templates/codex/.agents/skills/model-routing/SKILL.md +6 -0
- package/templates/codex/.codex/agents/economy_test_worker.toml +6 -0
- package/templates/codex/.codex/agents/premium_implementer.toml +6 -0
- package/templates/codex/.codex/agents/premium_planner.toml +6 -0
- package/templates/codex/.codex/agents/premium_reviewer.toml +6 -0
- package/templates/codex/.codex/agents/standard_implementer.toml +6 -0
- package/templates/codex/.codex/agents/standard_test_worker.toml +6 -0
- package/templates/codex/AGENTS.md +1 -0
- package/templates/common/.ai/README.md +4 -1
- package/templates/common/.ai/config/model-routing.json +5 -0
- package/templates/common/.ai/runs/.gitignore +2 -0
- package/templates/common/.ai/skills/critic.md +1 -0
- package/templates/common/.ai/skills/delivery-readiness.md +2 -0
- package/templates/common/.ai/skills/feature-design.md +2 -0
- package/templates/common/.ai/skills/model-routing.md +39 -0
- package/templates/common/.ai/skills/requirements-flow.md +2 -0
package/README.md
CHANGED
|
@@ -21,6 +21,9 @@ npx @betterdanlins/ai-memory init
|
|
|
21
21
|
# Non-interactive
|
|
22
22
|
npx @betterdanlins/ai-memory init --name demo --stack "PHP + Vue" --tools claude,codex --yes
|
|
23
23
|
|
|
24
|
+
# Opt in to staged model routing
|
|
25
|
+
npx @betterdanlins/ai-memory init --model-profile balanced --yes
|
|
26
|
+
|
|
24
27
|
# Import user-level memory (profile/feedback) from an existing project
|
|
25
28
|
npx @betterdanlins/ai-memory init --import /path/to/other-project
|
|
26
29
|
|
|
@@ -29,6 +32,9 @@ npx @betterdanlins/ai-memory update --dry-run
|
|
|
29
32
|
|
|
30
33
|
# Apply only conflict-free safe updates; abort entirely on user changes
|
|
31
34
|
npx @betterdanlins/ai-memory update --yes
|
|
35
|
+
|
|
36
|
+
# Switch an initialized project from inherited models to staged routing
|
|
37
|
+
npx @betterdanlins/ai-memory models configure --profile balanced
|
|
32
38
|
```
|
|
33
39
|
|
|
34
40
|
Nothing is ever overwritten silently: existing files are asked about one by one (interactive) or skipped (`--yes`).
|
|
@@ -36,12 +42,45 @@ An invalid, non-directory, or unreadable `--import` project path fails explicitl
|
|
|
36
42
|
Before writing, the CLI checks for duplicate template destinations, path traversal, and symbolic links, then reads and renders every planned file. If an actual filesystem write fails midway, the error summary lists both written and not-yet-written files.
|
|
37
43
|
An initialized project cannot be initialized again. A newer CLI first uses `update --dry-run` to identify versions and user modifications. `update --yes` applies only additions and baseline-matching safe updates, aborting before any write when merge or review items exist.
|
|
38
44
|
|
|
45
|
+
## Versions and compatibility
|
|
46
|
+
|
|
47
|
+
### v0.4.0 — staged model routing and verified handoffs
|
|
48
|
+
|
|
49
|
+
- Adds opt-in `inherit`, `balanced`, and `quality` profiles. New and upgraded projects default to `inherit`, so installing v0.4.0 does not silently increase model cost or change v0.3.0 workflow depth.
|
|
50
|
+
- Adds native Claude Code and Codex agents for premium planning/review, standard or premium implementation, and economy or standard test authoring. Model routing selects an executor only after the existing S/M/L workflow decides which stages are required.
|
|
51
|
+
- Adds `models show/configure` plus `workflow prepare/verify/complete`. Cross-model handoffs reference formal requirements, designs, and plans by path and SHA-256; stale inputs, unresolved decisions, route changes, unsafe paths, and incomplete acceptance receipts fail closed.
|
|
52
|
+
- Keeps `.ai/config/model-routing.json` user-owned and `.ai/runs/` local-only. Exact account entitlements are not auto-discovered: Claude uses native model aliases, while Codex agents use native reasoning-effort settings.
|
|
53
|
+
|
|
54
|
+
### v0.3.0 — safe framework upgrades and engineering workflow
|
|
55
|
+
|
|
56
|
+
- Introduced `.ai/ai-memory.json` framework/schema metadata, file ownership, generated baseline hashes, legacy v0.1 detection, and explicit migration planning.
|
|
57
|
+
- Added `update --dry-run` and conflict-free `update --yes`. User memory, requirements, architecture, and design assets are preserved; changed framework or mixed files require review instead of being overwritten.
|
|
58
|
+
- Added duplicate-destination checks, path traversal and symlink/junction protection, three-phase scaffolding, and diagnostic partial-write summaries.
|
|
59
|
+
- Added the language-agnostic `project-inception`, `feature-design`, and `delivery-readiness` workflows covering data modeling, technical contracts, deployment, performance, observability, scalability, reliability, security, and cost.
|
|
60
|
+
|
|
61
|
+
### Upgrade from v0.3.0 to v0.4.0
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Preview only; this must not modify the project
|
|
65
|
+
npx @betterdanlins/ai-memory@0.4.0 update --dry-run
|
|
66
|
+
|
|
67
|
+
# Apply only when the preview has no merge/review blockers
|
|
68
|
+
npx @betterdanlins/ai-memory@0.4.0 update --yes
|
|
69
|
+
|
|
70
|
+
# Optional: explicitly enable staged routing after the safe update
|
|
71
|
+
npx @betterdanlins/ai-memory@0.4.0 models configure --profile balanced
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
After the update, the profile remains `inherit` until explicitly changed. Do not re-run `init` on an existing v0.3.0 project. If dry-run reports merge or review items, merge them with the generated v0.4.0 files as references, then run dry-run again.
|
|
75
|
+
|
|
39
76
|
## What it generates
|
|
40
77
|
|
|
41
78
|
```
|
|
42
79
|
.ai/ # single source of truth, shared by all agents
|
|
43
80
|
├── README.md # session entry protocol
|
|
44
81
|
├── ai-memory.json # version, schema, tools, ownership, baseline hashes
|
|
82
|
+
├── config/
|
|
83
|
+
│ └── model-routing.json # inherit/balanced/quality stage routing; user-owned
|
|
45
84
|
├── memory/
|
|
46
85
|
│ ├── MEMORY.md # index — agents read this first
|
|
47
86
|
│ ├── project-state.md # stack, current version, requirement progress
|
|
@@ -49,8 +88,9 @@ An initialized project cannot be initialized again. A newer CLI first uses `upda
|
|
|
49
88
|
│ ├── user-profile.md # background, preferences, communication style
|
|
50
89
|
│ ├── feedback.md # behavioral norms distilled from feedback
|
|
51
90
|
│ └── features/ # per-feature dossiers
|
|
91
|
+
├── runs/ # local handoffs and stage receipts; ignored by Git
|
|
52
92
|
└── skills/ # methodology: requirements-flow, architecture,
|
|
53
|
-
#
|
|
93
|
+
# feature-design, model-routing, delivery-readiness, etc.
|
|
54
94
|
|
|
55
95
|
docs/architecture/ # engineering baseline for 0-to-1 projects
|
|
56
96
|
├── system-context.md # boundaries, actors, modules, and key flows
|
|
@@ -66,7 +106,7 @@ docs/design/vX.Y.Z/ # M/L-risk feature how, technical contracts, delta
|
|
|
66
106
|
|
|
67
107
|
# Thin adapters, generated only for the tools you enable:
|
|
68
108
|
CLAUDE.md + .claude/ # Claude Code: commands, skills, agents, settings
|
|
69
|
-
AGENTS.md + .agents/
|
|
109
|
+
AGENTS.md + .agents/ + .codex/# Codex: skills and model-specific custom agents
|
|
70
110
|
```
|
|
71
111
|
|
|
72
112
|
## CLI commands
|
|
@@ -76,6 +116,11 @@ AGENTS.md + .agents/ # Codex: skills
|
|
|
76
116
|
| `init` | Initialize a new project; refuses to run over an existing ai-memory installation |
|
|
77
117
|
| `update --dry-run` | Read-only analysis of the upgrade plan from an existing project to this CLI version |
|
|
78
118
|
| `update --yes` | Apply conflict-free safe updates; refuse user changes, mixed-file conflicts, or missing migration paths |
|
|
119
|
+
| `models show` | Show the selected profile and resolved tier for every workflow stage |
|
|
120
|
+
| `models configure --profile <name>` | Select `inherit`, `balanced`, or `quality` without changing requirements, designs, or code |
|
|
121
|
+
| `workflow prepare` | Create a local handoff manifest with hashes of formal input documents |
|
|
122
|
+
| `workflow verify` | Reject missing, stale, unresolved, or route-mismatched handoffs before execution |
|
|
123
|
+
| `workflow complete` | Validate and store a structured stage receipt for downstream review |
|
|
79
124
|
|
|
80
125
|
### `init` options
|
|
81
126
|
|
|
@@ -84,17 +129,20 @@ AGENTS.md + .agents/ # Codex: skills
|
|
|
84
129
|
| `--name <name>` | Project name (defaults to the current directory name) |
|
|
85
130
|
| `--stack <desc>` | Tech-stack description |
|
|
86
131
|
| `--tools <list>` | Comma-separated adapters to enable: `claude`, `codex` |
|
|
132
|
+
| `--model-profile <profile>` | `inherit` (default), `balanced`, or `quality`; `--yes` never opts into costly routing implicitly |
|
|
87
133
|
| `--import <path>` | Import `user-profile` / `feedback` from an existing project |
|
|
88
134
|
| `--yes` | Non-interactive: fill defaults, skip on any conflict |
|
|
89
135
|
|
|
90
136
|
## How it works
|
|
91
137
|
|
|
92
|
-
The generator is a small set of
|
|
138
|
+
The generator is a small set of focused modules:
|
|
93
139
|
|
|
94
140
|
- **manifest** — walks the `common` / `claude` / `codex` template groups, keeping only the groups whose tools are enabled, and produces a flat `{src, dest}` list; duplicate destinations fail before writing and list every source.
|
|
95
141
|
- **render** — substitutes `{{variable}}` placeholders and throws on any undefined variable, so a broken template fails loudly instead of shipping `{{...}}`.
|
|
96
142
|
- **scaffold** — validates safe destinations and symbolic links, resolves conflicts, and reads/renders the complete plan in memory before writing; it also validates `--import` and pulls `user-profile` / `feedback` from it (falling back to the template with an explicit report when an individual source file is missing).
|
|
97
143
|
- **framework metadata** — a fresh init records framework/schema versions, ownership, and generated hashes; update uses them to distinguish safe updates from user changes. Metadata-free v0.1 projects are planned conservatively as legacy installations.
|
|
144
|
+
- **model routing** — optional profiles map existing workflow stages to premium, standard, economy, inherited, or no-model execution. Claude and Codex adapters use native custom-agent configuration; routing never creates extra S/M/L stages.
|
|
145
|
+
- **workflow handoff** — a local manifest references formal requirements/design/plan files by path and SHA-256. Verification fails closed on stale inputs, unresolved decisions, changed routing, path escape, or symbolic links.
|
|
98
146
|
|
|
99
147
|
## Design principles
|
|
100
148
|
|
|
@@ -103,6 +151,8 @@ The generator is a small set of pure functions:
|
|
|
103
151
|
- **Dual requirement directories** — human draft (`draft/`) → AI final (`final/`) behind a mandatory critic gate.
|
|
104
152
|
- **Project engineering baseline** — 0-to-1 projects establish language-agnostic system, data, quality-attribute, and deployment baselines once; ordinary features record only deltas.
|
|
105
153
|
- **Risk-tiered design** — S-risk requirements proceed directly to implementation; M/L-risk work uses feature-design for how, technical contracts, and engineering impact, with Superpowers selected only when justified by risk.
|
|
154
|
+
- **Optional cost routing** — `inherit` preserves prior behavior; explicit profiles can use stronger models for planning/review and cheaper workers for bounded test work while deterministic test execution uses no model.
|
|
155
|
+
- **Artifact-based handoff** — cross-model stages read formal artifacts through a verified handoff rather than relying on conversational summaries; implementation receipts expose coverage, deviations, and unresolved risk to the final reviewer.
|
|
106
156
|
- **Evidence-based delivery** — before release, verify applicable contracts, tests, migration, rollback, performance, observability, and operations, producing an explicit readiness verdict.
|
|
107
157
|
- **Low-noise memory** — persist independently verifiable engineering nodes, durable decisions, status changes, and session handoffs instead of every small coding step.
|
|
108
158
|
- **Idempotent** — existing files are asked about one by one; `--yes` always skips. Never a silent overwrite.
|
package/README.zh-CN.md
CHANGED
|
@@ -21,6 +21,9 @@ npx @betterdanlins/ai-memory init
|
|
|
21
21
|
# 非交互式
|
|
22
22
|
npx @betterdanlins/ai-memory init --name demo --stack "PHP + Vue" --tools claude,codex --yes
|
|
23
23
|
|
|
24
|
+
# 显式启用分阶段模型路由
|
|
25
|
+
npx @betterdanlins/ai-memory init --model-profile balanced --yes
|
|
26
|
+
|
|
24
27
|
# 从已有项目导入用户级记忆(profile/feedback)
|
|
25
28
|
npx @betterdanlins/ai-memory init --import /path/to/other-project
|
|
26
29
|
|
|
@@ -29,6 +32,9 @@ npx @betterdanlins/ai-memory update --dry-run
|
|
|
29
32
|
|
|
30
33
|
# 只应用无冲突的安全更新;有用户修改时整体拒绝
|
|
31
34
|
npx @betterdanlins/ai-memory update --yes
|
|
35
|
+
|
|
36
|
+
# 已初始化项目从继承模型切换到分层路由
|
|
37
|
+
npx @betterdanlins/ai-memory models configure --profile balanced
|
|
32
38
|
```
|
|
33
39
|
|
|
34
40
|
绝不静默覆盖:已存在的文件会逐个询问(交互式),或直接跳过(`--yes`)。
|
|
@@ -36,12 +42,45 @@ npx @betterdanlins/ai-memory update --yes
|
|
|
36
42
|
生成前会检查重复模板目标、路径越界和符号链接;模板读取与渲染全部通过后才开始写入。若实际磁盘写入中途失败,错误摘要会列出已经写入和尚未写入的文件。
|
|
37
43
|
已初始化项目不能重新运行 `init`;新版 CLI 先通过 `update --dry-run` 识别版本和用户修改。`update --yes` 只应用新增和基线哈希匹配的安全更新,存在合并/审查项时在写入前整体拒绝。
|
|
38
44
|
|
|
45
|
+
## 版本与兼容性
|
|
46
|
+
|
|
47
|
+
### v0.4.0 —— 分阶段模型路由与可靠交接
|
|
48
|
+
|
|
49
|
+
- 新增可选的 `inherit`、`balanced`、`quality` profile。新项目和升级项目默认保持 `inherit`,安装 v0.4.0 不会静默增加模型成本,也不会改变 v0.3.0 的工作流深度。
|
|
50
|
+
- 为 Claude Code 和 Codex 新增原生分层 agents,覆盖高级规划/审查、中等或高级实现、低成本或中等测试编写。仍先由原有 S/M/L 流程决定是否需要某个阶段,模型路由只选择执行者。
|
|
51
|
+
- 新增 `models show/configure` 和 `workflow prepare/verify/complete`。跨模型交接通过路径和 SHA-256 引用正式需求、设计与计划;输入过期、未决问题、路由变化、不安全路径和验收回执缺失都会关闭式失败。
|
|
52
|
+
- `.ai/config/model-routing.json` 属于用户配置,`.ai/runs/` 只保存本地运行状态。框架不会自动探测账号模型权限:Claude 使用原生模型别名,Codex agents 使用原生推理等级。
|
|
53
|
+
|
|
54
|
+
### v0.3.0 —— 安全升级与工程工作流
|
|
55
|
+
|
|
56
|
+
- 引入 `.ai/ai-memory.json` 框架/Schema 元数据、文件所有权、生成基线哈希、v0.1 legacy 检测和显式迁移规划。
|
|
57
|
+
- 新增 `update --dry-run` 与无冲突 `update --yes`。用户记忆、需求、架构和设计资产始终保留;修改过的框架文件或混合文件进入人工审查,不会被直接覆盖。
|
|
58
|
+
- 增加重复目标、路径穿越、符号链接/junction 防护、三阶段生成和可诊断的部分写入摘要。
|
|
59
|
+
- 增加语言无关的 `project-inception`、`feature-design`、`delivery-readiness`,覆盖数据建模、技术接口、部署、性能、可观测性、扩展性、可靠性、安全和成本。
|
|
60
|
+
|
|
61
|
+
### 从 v0.3.0 升级到 v0.4.0
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 只读预览,不得修改项目
|
|
65
|
+
npx @betterdanlins/ai-memory@0.4.0 update --dry-run
|
|
66
|
+
|
|
67
|
+
# 仅在预览没有 merge/review 阻塞项时应用
|
|
68
|
+
npx @betterdanlins/ai-memory@0.4.0 update --yes
|
|
69
|
+
|
|
70
|
+
# 可选:安全升级后显式启用分阶段模型路由
|
|
71
|
+
npx @betterdanlins/ai-memory@0.4.0 models configure --profile balanced
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
升级完成后 profile 仍为 `inherit`,只有显式配置才会改变。不要在已有 v0.3.0 项目重新运行 `init`。若 dry-run 出现 merge/review 项,应以新生成的 v0.4.0 文件为参考人工合并,然后重新执行 dry-run。
|
|
75
|
+
|
|
39
76
|
## 生成什么
|
|
40
77
|
|
|
41
78
|
```
|
|
42
79
|
.ai/ # 单一源,所有 agent 共享
|
|
43
80
|
├── README.md # 会话进场协议
|
|
44
81
|
├── ai-memory.json # 版本、Schema、工具、所有权和生成基线哈希
|
|
82
|
+
├── config/
|
|
83
|
+
│ └── model-routing.json # inherit/balanced/quality 阶段路由;属于用户配置
|
|
45
84
|
├── memory/
|
|
46
85
|
│ ├── MEMORY.md # 索引 —— agent 进场先读
|
|
47
86
|
│ ├── project-state.md # 技术栈、当前版本、需求进度
|
|
@@ -49,8 +88,9 @@ npx @betterdanlins/ai-memory update --yes
|
|
|
49
88
|
│ ├── user-profile.md # 背景、偏好、沟通方式
|
|
50
89
|
│ ├── feedback.md # 从反馈提炼的行为规范
|
|
51
90
|
│ └── features/ # 功能档案
|
|
91
|
+
├── runs/ # 本地交接与阶段回执;默认不进入 Git
|
|
52
92
|
└── skills/ # 方法论:requirements-flow、architecture、
|
|
53
|
-
#
|
|
93
|
+
# feature-design、model-routing、delivery-readiness 等
|
|
54
94
|
|
|
55
95
|
docs/architecture/ # 0→1 项目工程基线
|
|
56
96
|
├── system-context.md # 系统边界、参与者、模块和关键流程
|
|
@@ -66,7 +106,7 @@ docs/design/vX.Y.Z/ # M/L 级功能 how、技术接口与工程增量
|
|
|
66
106
|
|
|
67
107
|
# 薄适配层,仅为你启用的工具生成:
|
|
68
108
|
CLAUDE.md + .claude/ # Claude Code:commands、skills、agents、settings
|
|
69
|
-
AGENTS.md + .agents/
|
|
109
|
+
AGENTS.md + .agents/ + .codex/# Codex:skills 与分层自定义 agents
|
|
70
110
|
```
|
|
71
111
|
|
|
72
112
|
## CLI 命令
|
|
@@ -76,6 +116,11 @@ AGENTS.md + .agents/ # Codex:skills
|
|
|
76
116
|
| `init` | 初始化全新项目;检测到已有 ai-memory 安装时拒绝执行 |
|
|
77
117
|
| `update --dry-run` | 只读分析旧项目到当前 CLI 版本的升级计划 |
|
|
78
118
|
| `update --yes` | 应用无冲突安全更新;用户修改、混合文件冲突或缺失迁移路径会拒绝执行 |
|
|
119
|
+
| `models show` | 显示当前 profile 和每个阶段解析后的模型等级 |
|
|
120
|
+
| `models configure --profile <name>` | 选择 `inherit`、`balanced` 或 `quality`,不修改需求、设计或代码 |
|
|
121
|
+
| `workflow prepare` | 用正式输入文档的哈希创建本地交接清单 |
|
|
122
|
+
| `workflow verify` | 执行前拒绝缺失、过期、有未决问题或路由不匹配的交接 |
|
|
123
|
+
| `workflow complete` | 校验并保存供后续审查使用的结构化阶段回执 |
|
|
79
124
|
|
|
80
125
|
### `init` 选项
|
|
81
126
|
|
|
@@ -84,17 +129,20 @@ AGENTS.md + .agents/ # Codex:skills
|
|
|
84
129
|
| `--name <name>` | 项目名(默认取当前目录名) |
|
|
85
130
|
| `--stack <desc>` | 技术栈描述 |
|
|
86
131
|
| `--tools <list>` | 逗号分隔的适配层:`claude`、`codex` |
|
|
132
|
+
| `--model-profile <profile>` | `inherit`(默认)、`balanced` 或 `quality`;`--yes` 不会静默启用高成本路由 |
|
|
87
133
|
| `--import <path>` | 从已有项目导入 `user-profile` / `feedback` |
|
|
88
134
|
| `--yes` | 非交互模式:缺参用默认值,冲突一律跳过 |
|
|
89
135
|
|
|
90
136
|
## 工作原理
|
|
91
137
|
|
|
92
|
-
|
|
138
|
+
生成器由一组职责明确的小模块构成:
|
|
93
139
|
|
|
94
140
|
- **manifest** —— 遍历 `common` / `claude` / `codex` 模板组,只保留已启用工具对应的组,产出一份扁平的 `{src, dest}` 清单;重复目标会在写入前报错并列出来源。
|
|
95
141
|
- **render** —— 替换 `{{变量}}` 占位符,遇到任何未定义变量即抛错,让坏掉的模板大声失败,而不是把 `{{...}}` 发出去。
|
|
96
142
|
- **scaffold** —— 预检安全目标路径和符号链接,处理冲突并在内存中完成全部读取/渲染后再逐个写入;提供 `--import` 时先校验导入目录,再从中拉取 `user-profile` / `feedback`(单个源文件缺失则回退到模板并明确报告)。
|
|
97
143
|
- **framework metadata** —— 全新 init 记录 framework/schema 版本、文件所有权和生成哈希;update 据此区分安全更新与用户修改。无元数据的 v0.1 项目按 legacy 保守规划。
|
|
144
|
+
- **model routing** —— 可选 profile 把现有阶段映射为 premium、standard、economy、inherit 或无模型执行;Claude/Codex 使用原生自定义 agent 配置,路由不会增加 S/M/L 阶段。
|
|
145
|
+
- **workflow handoff** —— 本地清单只用路径和 SHA-256 引用正式需求/设计/计划;输入过期、未决问题、路由变化、路径越界或符号链接都会关闭式失败。
|
|
98
146
|
|
|
99
147
|
## 设计原则
|
|
100
148
|
|
|
@@ -103,6 +151,8 @@ AGENTS.md + .agents/ # Codex:skills
|
|
|
103
151
|
- **需求双目录** —— 人工粗稿(`draft/`)→ AI 定稿(`final/`),中间隔一道强制 critic 门。
|
|
104
152
|
- **项目工程基线** —— 0→1 项目先建立语言无关的系统、数据、质量属性和部署基线;普通功能只记录增量,不重复设计全局架构。
|
|
105
153
|
- **风险分级设计** —— S 级需求直接实现;M/L 级用 feature-design 补齐 how、技术接口和工程影响,Superpowers 只按风险选择使用。
|
|
154
|
+
- **可选成本路由** —— `inherit` 保持旧行为;显式 profile 才用高级模型规划/审查和低成本 worker 做有边界的测试工作,确定性测试执行不调用模型。
|
|
155
|
+
- **工程产物交接** —— 跨模型阶段通过校验后的正式文档交接,不依赖对话摘要;阶段回执向最终 reviewer 暴露验收覆盖、设计偏差和未决风险。
|
|
106
156
|
- **证据化交付** —— 发布前按风险检查契约、测试、迁移、回滚、性能、可观测性和运行保障,输出明确 ready 状态。
|
|
107
157
|
- **低噪声记忆** —— 只在可验收工程节点、关键决策、状态变化和会话切换时落盘,不记录每个细碎代码步骤。
|
|
108
158
|
- **幂等** —— 已存在的文件逐个询问;`--yes` 一律跳过。绝不静默覆盖。
|
package/bin/cli.js
CHANGED
|
@@ -4,7 +4,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { applyFrameworkUpdate, detectInstallation, METADATA_DEST, planFrameworkUpdate, writeFrameworkMetadata } from '../src/framework.js';
|
|
7
|
+
import {
|
|
8
|
+
createModelRoutingConfig, MODEL_PROFILES, readModelRoutingConfig,
|
|
9
|
+
resolveModelRouting, writeModelRoutingConfig,
|
|
10
|
+
} from '../src/model-routing.js';
|
|
7
11
|
import { ScaffoldError, scaffold } from '../src/scaffold.js';
|
|
12
|
+
import { prepareHandoff, recordStageResult, verifyHandoff } from '../src/workflow.js';
|
|
8
13
|
|
|
9
14
|
const TEMPLATES = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates');
|
|
10
15
|
const PACKAGE = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -18,6 +23,7 @@ program
|
|
|
18
23
|
.option('--name <name>', '项目名')
|
|
19
24
|
.option('--stack <desc>', '技术栈描述')
|
|
20
25
|
.option('--tools <list>', '启用工具,逗号分隔:claude,codex')
|
|
26
|
+
.option('--model-profile <profile>', '模型路由策略:inherit,balanced,quality', 'inherit')
|
|
21
27
|
.option('--import <path>', '从已有项目导入 user-profile/feedback')
|
|
22
28
|
.option('--yes', '非交互模式:缺参用默认值,冲突一律跳过')
|
|
23
29
|
.action(async (opts) => {
|
|
@@ -30,6 +36,9 @@ program
|
|
|
30
36
|
throw new Error(`${label};不要重新运行 init,请先执行 ai-memory update --dry-run`);
|
|
31
37
|
}
|
|
32
38
|
let { name, stack, tools: toolsRaw } = opts;
|
|
39
|
+
if (!MODEL_PROFILES.includes(opts.modelProfile)) {
|
|
40
|
+
throw new Error(`model profile 仅支持 ${MODEL_PROFILES.join('、')},收到: ${opts.modelProfile}`);
|
|
41
|
+
}
|
|
33
42
|
|
|
34
43
|
// Validate CLI-supplied --tools before entering interactive prompts
|
|
35
44
|
if (toolsRaw !== undefined) {
|
|
@@ -43,7 +52,7 @@ program
|
|
|
43
52
|
|
|
44
53
|
let importFrom = opts.import;
|
|
45
54
|
if (!opts.yes) {
|
|
46
|
-
const { input, checkbox } = await import('@inquirer/prompts');
|
|
55
|
+
const { input, checkbox, select } = await import('@inquirer/prompts');
|
|
47
56
|
name ??= await input({ message: '项目名:', default: path.basename(targetDir) });
|
|
48
57
|
stack ??= await input({ message: '技术栈描述:', default: '(待补充)' });
|
|
49
58
|
toolsRaw ??= (await checkbox({
|
|
@@ -56,6 +65,17 @@ program
|
|
|
56
65
|
importFrom ??= (await input({
|
|
57
66
|
message: '从已有项目导入 user-profile/feedback?(输入项目路径,留空跳过)', default: '',
|
|
58
67
|
})) || undefined;
|
|
68
|
+
if (opts.modelProfile === 'inherit') {
|
|
69
|
+
opts.modelProfile = await select({
|
|
70
|
+
message: '模型路由策略:',
|
|
71
|
+
choices: [
|
|
72
|
+
{ name: 'inherit - 继承当前会话模型,兼容旧行为', value: 'inherit' },
|
|
73
|
+
{ name: 'balanced - 高级规划/中等实现/便宜测试/高级审查', value: 'balanced' },
|
|
74
|
+
{ name: 'quality - 关键实现与诊断也使用高级模型', value: 'quality' },
|
|
75
|
+
],
|
|
76
|
+
default: 'inherit',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
59
79
|
}
|
|
60
80
|
name ??= path.basename(targetDir);
|
|
61
81
|
stack ??= '(待补充)';
|
|
@@ -74,7 +94,9 @@ program
|
|
|
74
94
|
};
|
|
75
95
|
|
|
76
96
|
const date = new Date().toISOString().slice(0, 10);
|
|
77
|
-
const vars = {
|
|
97
|
+
const vars = {
|
|
98
|
+
projectName: name, techStack: stack, date, modelProfile: opts.modelProfile, frameworkVersion: PACKAGE.version,
|
|
99
|
+
};
|
|
78
100
|
const r = await scaffold({ templatesRoot: TEMPLATES, targetDir, vars, tools, onConflict, importFrom });
|
|
79
101
|
if (r.skipped.length === 0) {
|
|
80
102
|
try {
|
|
@@ -93,12 +115,81 @@ program
|
|
|
93
115
|
console.log('警告:存在冲突跳过项,未创建框架元数据;当前结果不能自动升级');
|
|
94
116
|
}
|
|
95
117
|
console.log(`完成:写入 ${r.written.length},跳过 ${r.skipped.length}`);
|
|
118
|
+
console.log(`模型路由:${opts.modelProfile}`);
|
|
96
119
|
if (r.imported.length) for (const d of r.imported) console.log(` 已导入 ${d}`);
|
|
97
120
|
if (r.fallback.length) for (const d of r.fallback) console.log(` 导入源缺失,使用模板 ${d}`);
|
|
98
121
|
if (r.skipped.length) for (const d of r.skipped) console.log(` 跳过 ${d}`);
|
|
99
122
|
console.log('下一步:打开项目让 agent 读 .ai/memory/MEMORY.md;把项目命令补进 CLAUDE.md/AGENTS.md。');
|
|
100
123
|
});
|
|
101
124
|
|
|
125
|
+
const models = program.command('models').description('查看或配置阶段模型路由');
|
|
126
|
+
|
|
127
|
+
models
|
|
128
|
+
.command('show')
|
|
129
|
+
.description('显示当前模型路由配置与阶段映射')
|
|
130
|
+
.action(async () => {
|
|
131
|
+
const config = await readModelRoutingConfig(process.cwd());
|
|
132
|
+
console.log(`模型路由:${config.profile}`);
|
|
133
|
+
for (const [stage, tier] of Object.entries(resolveModelRouting(config))) console.log(` ${stage}: ${tier}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
models
|
|
137
|
+
.command('configure')
|
|
138
|
+
.description('切换模型路由预设;不修改需求、设计或代码')
|
|
139
|
+
.requiredOption('--profile <profile>', 'inherit,balanced,quality')
|
|
140
|
+
.action(async (opts) => {
|
|
141
|
+
const installation = await detectInstallation(process.cwd());
|
|
142
|
+
if (installation.kind === 'none') throw new Error('当前目录不是 ai-memory 项目,请先运行 init');
|
|
143
|
+
const current = await readModelRoutingConfig(process.cwd());
|
|
144
|
+
const next = createModelRoutingConfig(opts.profile);
|
|
145
|
+
next.overrides = current.overrides;
|
|
146
|
+
await writeModelRoutingConfig(process.cwd(), next);
|
|
147
|
+
console.log(`模型路由已切换:${current.profile} → ${next.profile}`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const workflow = program.command('workflow').description('创建并校验跨模型阶段交接');
|
|
151
|
+
|
|
152
|
+
workflow
|
|
153
|
+
.command('prepare')
|
|
154
|
+
.description('根据正式文档生成带哈希的本地交接清单')
|
|
155
|
+
.requiredOption('--feature <id>', '功能标识,不得包含路径分隔符')
|
|
156
|
+
.requiredOption('--stage <stage>', '目标工作流阶段')
|
|
157
|
+
.requiredOption('--risk <level>', 'S,M,L')
|
|
158
|
+
.option('--input <path>', '正式输入文件,可重复', collect, [])
|
|
159
|
+
.option('--acceptance <id>', '验收标准编号,可重复', collect, [])
|
|
160
|
+
.option('--scope <path>', '允许变更范围,可重复', collect, [])
|
|
161
|
+
.option('--unresolved <text>', '未决问题,可重复;存在时 verify 会阻止进入阶段', collect, [])
|
|
162
|
+
.action(async (opts) => {
|
|
163
|
+
const result = await prepareHandoff({
|
|
164
|
+
targetDir: process.cwd(), feature: opts.feature, stage: opts.stage, risk: opts.risk,
|
|
165
|
+
inputs: opts.input, acceptanceCriteria: opts.acceptance, allowedChangeScope: opts.scope,
|
|
166
|
+
unresolvedDecisions: opts.unresolved,
|
|
167
|
+
});
|
|
168
|
+
console.log(`交接清单已生成:${result.path}`);
|
|
169
|
+
console.log(`执行等级:${result.handoff.executorTier}`);
|
|
170
|
+
if (result.handoff.unresolvedDecisions.length) console.log(`警告:仍有 ${result.handoff.unresolvedDecisions.length} 个未决问题`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
workflow
|
|
174
|
+
.command('verify')
|
|
175
|
+
.description('验证交接输入、哈希、路由和未决问题')
|
|
176
|
+
.requiredOption('--feature <id>', '功能标识')
|
|
177
|
+
.option('--stage <stage>', '期望工作流阶段')
|
|
178
|
+
.action(async (opts) => {
|
|
179
|
+
const handoff = await verifyHandoff({ targetDir: process.cwd(), feature: opts.feature, expectedStage: opts.stage });
|
|
180
|
+
console.log(`交接校验通过:${handoff.featureId} / ${handoff.stage} / ${handoff.executorTier}`);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
workflow
|
|
184
|
+
.command('complete')
|
|
185
|
+
.description('校验阶段回执并保存到对应 run 目录')
|
|
186
|
+
.requiredOption('--feature <id>', '功能标识')
|
|
187
|
+
.requiredOption('--result <path>', '项目内阶段回执 JSON 路径')
|
|
188
|
+
.action(async (opts) => {
|
|
189
|
+
const recorded = await recordStageResult({ targetDir: process.cwd(), feature: opts.feature, resultPath: opts.result });
|
|
190
|
+
console.log(`阶段回执已记录:${recorded.path} / ${recorded.result.status}`);
|
|
191
|
+
});
|
|
192
|
+
|
|
102
193
|
program
|
|
103
194
|
.command('update')
|
|
104
195
|
.description('检查当前项目升级到本 CLI 版本所需的变更')
|
|
@@ -167,3 +258,7 @@ function printUpdatePlan(plan) {
|
|
|
167
258
|
console.log('升级计划完成');
|
|
168
259
|
if (process.argv.includes('--dry-run')) console.log('dry-run:未修改任何文件');
|
|
169
260
|
}
|
|
261
|
+
|
|
262
|
+
function collect(value, previous) {
|
|
263
|
+
return [...previous, value];
|
|
264
|
+
}
|
package/package.json
CHANGED
package/src/framework.js
CHANGED
|
@@ -12,6 +12,7 @@ export const CURRENT_SCHEMA_VERSION = 1;
|
|
|
12
12
|
export const METADATA_DEST = '.ai/ai-memory.json';
|
|
13
13
|
|
|
14
14
|
const USER_PREFIXES = [
|
|
15
|
+
'.ai/config/',
|
|
15
16
|
'.ai/memory/',
|
|
16
17
|
'docs/architecture/',
|
|
17
18
|
'docs/requirements/v',
|
|
@@ -88,7 +89,8 @@ export async function planFrameworkUpdate({ targetDir, templatesRoot, frameworkV
|
|
|
88
89
|
if (installation.kind === 'none') throw new Error('当前目录不是 ai-memory 项目,请先运行 init');
|
|
89
90
|
|
|
90
91
|
const tools = installation.kind === 'metadata' ? installation.metadata.tools : installation.tools;
|
|
91
|
-
const
|
|
92
|
+
const rawVars = installation.kind === 'metadata' ? installation.metadata.templateVars : installation.templateVars;
|
|
93
|
+
const vars = { modelProfile: 'inherit', ...rawVars, frameworkVersion };
|
|
92
94
|
if (installation.kind === 'metadata' && compareVersions(installation.metadata.frameworkVersion, frameworkVersion) > 0) {
|
|
93
95
|
throw new Error(`项目框架版本 ${installation.metadata.frameworkVersion} 高于当前 CLI ${frameworkVersion},请使用更新版本的 CLI`);
|
|
94
96
|
}
|
|
@@ -138,6 +140,7 @@ export async function planFrameworkUpdate({ targetDir, templatesRoot, frameworkV
|
|
|
138
140
|
frameworkVersion,
|
|
139
141
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
140
142
|
tools,
|
|
143
|
+
templateVars: vars,
|
|
141
144
|
migrations,
|
|
142
145
|
actions,
|
|
143
146
|
};
|
|
@@ -153,9 +156,7 @@ export async function applyFrameworkUpdate({ targetDir, templatesRoot, plan }) {
|
|
|
153
156
|
throw new Error(`存在不可自动执行的 Schema 迁移: ${manualMigrations.map(item => item.id).join(', ')}`);
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
const vars = plan.
|
|
157
|
-
? plan.installation.metadata.templateVars
|
|
158
|
-
: plan.installation.templateVars;
|
|
159
|
+
const vars = plan.templateVars;
|
|
159
160
|
const manifest = await buildManifest(templatesRoot, plan.tools);
|
|
160
161
|
const sources = new Map(manifest.map(entry => [entry.dest, entry.src]));
|
|
161
162
|
const candidates = [];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { assertNoSymlinkPath } from './path-safety.js';
|
|
4
|
+
|
|
5
|
+
export const MODEL_ROUTING_DEST = '.ai/config/model-routing.json';
|
|
6
|
+
export const MODEL_ROUTING_SCHEMA_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
export const STAGES = [
|
|
9
|
+
'brainstorm',
|
|
10
|
+
'requirement-finalize',
|
|
11
|
+
'feature-design',
|
|
12
|
+
'write-plan',
|
|
13
|
+
'implementation',
|
|
14
|
+
'routine-tests',
|
|
15
|
+
'test-execution',
|
|
16
|
+
'failure-diagnosis',
|
|
17
|
+
'adversarial-testing',
|
|
18
|
+
'final-review',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const PROFILES = {
|
|
22
|
+
inherit: Object.fromEntries(STAGES.map(stage => [stage, stage === 'test-execution' ? 'none' : 'inherit'])),
|
|
23
|
+
balanced: {
|
|
24
|
+
brainstorm: 'premium',
|
|
25
|
+
'requirement-finalize': 'premium',
|
|
26
|
+
'feature-design': 'premium',
|
|
27
|
+
'write-plan': 'premium',
|
|
28
|
+
implementation: 'standard',
|
|
29
|
+
'routine-tests': 'economy',
|
|
30
|
+
'test-execution': 'none',
|
|
31
|
+
'failure-diagnosis': 'standard',
|
|
32
|
+
'adversarial-testing': 'premium',
|
|
33
|
+
'final-review': 'premium',
|
|
34
|
+
},
|
|
35
|
+
quality: {
|
|
36
|
+
brainstorm: 'premium',
|
|
37
|
+
'requirement-finalize': 'premium',
|
|
38
|
+
'feature-design': 'premium',
|
|
39
|
+
'write-plan': 'premium',
|
|
40
|
+
implementation: 'premium',
|
|
41
|
+
'routine-tests': 'standard',
|
|
42
|
+
'test-execution': 'none',
|
|
43
|
+
'failure-diagnosis': 'premium',
|
|
44
|
+
'adversarial-testing': 'premium',
|
|
45
|
+
'final-review': 'premium',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const MODEL_PROFILES = Object.freeze(Object.keys(PROFILES));
|
|
50
|
+
|
|
51
|
+
export function createModelRoutingConfig(profile = 'inherit') {
|
|
52
|
+
assertProfile(profile);
|
|
53
|
+
return { schemaVersion: MODEL_ROUTING_SCHEMA_VERSION, profile, overrides: {} };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveModelRouting(config) {
|
|
57
|
+
validateModelRoutingConfig(config);
|
|
58
|
+
return { ...PROFILES[config.profile], ...config.overrides };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function readModelRoutingConfig(targetDir) {
|
|
62
|
+
const configPath = path.join(targetDir, ...MODEL_ROUTING_DEST.split('/'));
|
|
63
|
+
await assertNoSymlinkPath(targetDir, configPath);
|
|
64
|
+
let config;
|
|
65
|
+
try {
|
|
66
|
+
config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err.code === 'ENOENT') throw new Error(`缺少模型路由配置: ${MODEL_ROUTING_DEST};请先更新 ai-memory 框架`);
|
|
69
|
+
if (err instanceof SyntaxError) throw new Error(`模型路由配置不是有效 JSON: ${MODEL_ROUTING_DEST}`, { cause: err });
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
validateModelRoutingConfig(config);
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function writeModelRoutingConfig(targetDir, config) {
|
|
77
|
+
validateModelRoutingConfig(config);
|
|
78
|
+
const configPath = path.join(targetDir, ...MODEL_ROUTING_DEST.split('/'));
|
|
79
|
+
await assertNoSymlinkPath(targetDir, configPath);
|
|
80
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
81
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
82
|
+
return config;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function validateModelRoutingConfig(config) {
|
|
86
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
87
|
+
throw new Error('模型路由配置必须是对象');
|
|
88
|
+
}
|
|
89
|
+
if (config.schemaVersion !== MODEL_ROUTING_SCHEMA_VERSION) {
|
|
90
|
+
throw new Error(`不支持的模型路由 Schema: ${config.schemaVersion}`);
|
|
91
|
+
}
|
|
92
|
+
assertProfile(config.profile);
|
|
93
|
+
if (!config.overrides || typeof config.overrides !== 'object' || Array.isArray(config.overrides)) {
|
|
94
|
+
throw new Error('模型路由 overrides 必须是对象');
|
|
95
|
+
}
|
|
96
|
+
for (const [stage, tier] of Object.entries(config.overrides)) {
|
|
97
|
+
if (!STAGES.includes(stage)) throw new Error(`未知模型路由阶段: ${stage}`);
|
|
98
|
+
if (!['inherit', 'premium', 'standard', 'economy', 'none'].includes(tier)) {
|
|
99
|
+
throw new Error(`阶段 ${stage} 的模型等级无效: ${tier}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertProfile(profile) {
|
|
105
|
+
if (!MODEL_PROFILES.includes(profile)) {
|
|
106
|
+
throw new Error(`model profile 仅支持 ${MODEL_PROFILES.join('、')},收到: ${profile}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/workflow.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readModelRoutingConfig, resolveModelRouting, STAGES } from './model-routing.js';
|
|
5
|
+
import { assertNoSymlinkPath } from './path-safety.js';
|
|
6
|
+
|
|
7
|
+
const RISKS = ['S', 'M', 'L'];
|
|
8
|
+
|
|
9
|
+
export async function prepareHandoff({
|
|
10
|
+
targetDir, feature, stage, risk, inputs, acceptanceCriteria = [], allowedChangeScope = [], unresolvedDecisions = [],
|
|
11
|
+
}) {
|
|
12
|
+
validateFeature(feature);
|
|
13
|
+
if (!STAGES.includes(stage)) throw new Error(`未知工作流阶段: ${stage}`);
|
|
14
|
+
if (!RISKS.includes(risk)) throw new Error(`risk 仅支持 S、M、L,收到: ${risk}`);
|
|
15
|
+
if (!Array.isArray(inputs) || inputs.length === 0) throw new Error('至少需要一个 --input 正式输入文件');
|
|
16
|
+
if (requiresAcceptance(stage) && acceptanceCriteria.length === 0) {
|
|
17
|
+
throw new Error(`阶段 ${stage} 至少需要一个 --acceptance 验收标准编号`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const routing = resolveModelRouting(await readModelRoutingConfig(targetDir));
|
|
21
|
+
const records = [];
|
|
22
|
+
for (const input of [...new Set(inputs)]) {
|
|
23
|
+
const { relative, absolute } = resolveProjectInput(targetDir, input);
|
|
24
|
+
await assertNoSymlinkPath(targetDir, absolute);
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = await readFile(absolute);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code === 'ENOENT' || err.code === 'EISDIR') throw new Error(`交接输入不是可读文件: ${relative}`, { cause: err });
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
records.push({ path: relative, sha256: createHash('sha256').update(content).digest('hex') });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handoff = {
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
featureId: feature,
|
|
38
|
+
riskLevel: risk,
|
|
39
|
+
stage,
|
|
40
|
+
executorTier: routing[stage],
|
|
41
|
+
createdAt: new Date().toISOString(),
|
|
42
|
+
inputs: records,
|
|
43
|
+
acceptanceCriteria: uniqueStrings(acceptanceCriteria),
|
|
44
|
+
unresolvedDecisions: uniqueStrings(unresolvedDecisions),
|
|
45
|
+
allowedChangeScope: uniqueStrings(allowedChangeScope),
|
|
46
|
+
};
|
|
47
|
+
const handoffPath = handoffFile(targetDir, feature);
|
|
48
|
+
await assertNoSymlinkPath(targetDir, handoffPath);
|
|
49
|
+
await mkdir(path.dirname(handoffPath), { recursive: true });
|
|
50
|
+
await writeFile(handoffPath, JSON.stringify(handoff, null, 2) + '\n');
|
|
51
|
+
return { handoff, path: relativeFromRoot(targetDir, handoffPath) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function verifyHandoff({ targetDir, feature, expectedStage }) {
|
|
55
|
+
validateFeature(feature);
|
|
56
|
+
const handoffPath = handoffFile(targetDir, feature);
|
|
57
|
+
await assertNoSymlinkPath(targetDir, handoffPath);
|
|
58
|
+
let handoff;
|
|
59
|
+
try {
|
|
60
|
+
handoff = JSON.parse(await readFile(handoffPath, 'utf8'));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err.code === 'ENOENT') throw new Error(`缺少交接清单: ${relativeFromRoot(targetDir, handoffPath)}`);
|
|
63
|
+
if (err instanceof SyntaxError) throw new Error(`交接清单不是有效 JSON: ${relativeFromRoot(targetDir, handoffPath)}`, { cause: err });
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
validateHandoff(handoff, feature);
|
|
67
|
+
if (expectedStage && handoff.stage !== expectedStage) {
|
|
68
|
+
throw new Error(`交接阶段不匹配: 期望 ${expectedStage},实际 ${handoff.stage}`);
|
|
69
|
+
}
|
|
70
|
+
if (handoff.unresolvedDecisions.length) {
|
|
71
|
+
throw new Error(`仍有 ${handoff.unresolvedDecisions.length} 个未决问题,不能进入 ${handoff.stage}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const routing = resolveModelRouting(await readModelRoutingConfig(targetDir));
|
|
75
|
+
if (handoff.executorTier !== routing[handoff.stage]) {
|
|
76
|
+
throw new Error(`模型路由已变化: ${handoff.stage} 当前应使用 ${routing[handoff.stage]},请重新 prepare`);
|
|
77
|
+
}
|
|
78
|
+
for (const record of handoff.inputs) {
|
|
79
|
+
const { absolute } = resolveProjectInput(targetDir, record.path);
|
|
80
|
+
await assertNoSymlinkPath(targetDir, absolute);
|
|
81
|
+
let current;
|
|
82
|
+
try {
|
|
83
|
+
current = await readFile(absolute);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err.code === 'ENOENT') throw new Error(`交接输入已不存在: ${record.path}`);
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
const currentHash = createHash('sha256').update(current).digest('hex');
|
|
89
|
+
if (currentHash !== record.sha256) throw new Error(`交接输入已变化,请重新 prepare: ${record.path}`);
|
|
90
|
+
}
|
|
91
|
+
return handoff;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function recordStageResult({ targetDir, feature, resultPath }) {
|
|
95
|
+
const handoff = await verifyHandoff({ targetDir, feature });
|
|
96
|
+
const source = resolveProjectInput(targetDir, resultPath);
|
|
97
|
+
await assertNoSymlinkPath(targetDir, source.absolute);
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = JSON.parse(await readFile(source.absolute, 'utf8'));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err.code === 'ENOENT') throw new Error(`阶段回执不存在: ${source.relative}`);
|
|
103
|
+
if (err instanceof SyntaxError) throw new Error(`阶段回执不是有效 JSON: ${source.relative}`, { cause: err });
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
validateStageResult(result, handoff);
|
|
107
|
+
const destination = path.join(targetDir, '.ai', 'runs', feature, `${handoff.stage}-result.json`);
|
|
108
|
+
await assertNoSymlinkPath(targetDir, destination);
|
|
109
|
+
await writeFile(destination, JSON.stringify(result, null, 2) + '\n');
|
|
110
|
+
return { result, path: relativeFromRoot(targetDir, destination) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateHandoff(handoff, feature) {
|
|
114
|
+
if (!handoff || handoff.schemaVersion !== 1 || handoff.featureId !== feature) {
|
|
115
|
+
throw new Error(`交接清单格式或 featureId 无效: ${feature}`);
|
|
116
|
+
}
|
|
117
|
+
if (!STAGES.includes(handoff.stage) || !RISKS.includes(handoff.riskLevel)) {
|
|
118
|
+
throw new Error(`交接清单阶段或风险等级无效: ${feature}`);
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(handoff.inputs) || !handoff.inputs.length
|
|
121
|
+
|| !Array.isArray(handoff.acceptanceCriteria)
|
|
122
|
+
|| !Array.isArray(handoff.unresolvedDecisions)
|
|
123
|
+
|| !Array.isArray(handoff.allowedChangeScope)) {
|
|
124
|
+
throw new Error(`交接清单缺少必要字段: ${feature}`);
|
|
125
|
+
}
|
|
126
|
+
if (!['inherit', 'premium', 'standard', 'economy', 'none'].includes(handoff.executorTier)
|
|
127
|
+
|| handoff.inputs.some(record => !record || typeof record.path !== 'string' || !/^[a-f0-9]{64}$/.test(record.sha256))) {
|
|
128
|
+
throw new Error(`交接清单输入或执行等级无效: ${feature}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateStageResult(result, handoff) {
|
|
133
|
+
if (!result || typeof result !== 'object' || result.stage !== handoff.stage) {
|
|
134
|
+
throw new Error(`阶段回执 stage 必须为 ${handoff.stage}`);
|
|
135
|
+
}
|
|
136
|
+
if (!['completed', 'blocked', 'failed'].includes(result.status)) throw new Error(`阶段回执 status 无效: ${result.status}`);
|
|
137
|
+
for (const field of ['changedFiles', 'tests', 'designDeviations', 'unresolvedRisks']) {
|
|
138
|
+
if (!Array.isArray(result[field]) || result[field].some(value => typeof value !== 'string')) {
|
|
139
|
+
throw new Error(`阶段回执 ${field} 必须是字符串数组`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!result.acceptanceCoverage || typeof result.acceptanceCoverage !== 'object' || Array.isArray(result.acceptanceCoverage)) {
|
|
143
|
+
throw new Error('阶段回执 acceptanceCoverage 必须是对象');
|
|
144
|
+
}
|
|
145
|
+
for (const [id, coverage] of Object.entries(result.acceptanceCoverage)) {
|
|
146
|
+
if (!id.trim() || !['covered', 'partial', 'not-covered', 'not-applicable'].includes(coverage)) {
|
|
147
|
+
throw new Error(`阶段回执验收覆盖无效: ${id}=${coverage}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const missing = handoff.acceptanceCriteria.filter(id => !(id in result.acceptanceCoverage));
|
|
151
|
+
if (missing.length) throw new Error(`阶段回执缺少验收覆盖: ${missing.join(', ')}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveProjectInput(targetDir, input) {
|
|
155
|
+
if (typeof input !== 'string' || !input.trim() || path.isAbsolute(input)) throw new Error(`输入路径必须是项目内相对路径: ${input}`);
|
|
156
|
+
const root = path.resolve(targetDir);
|
|
157
|
+
const absolute = path.resolve(root, input);
|
|
158
|
+
const relative = path.relative(root, absolute);
|
|
159
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
160
|
+
throw new Error(`输入路径越出项目目录: ${input}`);
|
|
161
|
+
}
|
|
162
|
+
return { absolute, relative: relative.split(path.sep).join('/') };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handoffFile(targetDir, feature) {
|
|
166
|
+
return path.join(targetDir, '.ai', 'runs', feature, 'handoff.json');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function relativeFromRoot(targetDir, filePath) {
|
|
170
|
+
return path.relative(path.resolve(targetDir), filePath).split(path.sep).join('/');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateFeature(feature) {
|
|
174
|
+
if (typeof feature !== 'string' || !/^[\p{L}\p{N}][\p{L}\p{N}._-]*$/u.test(feature)) {
|
|
175
|
+
throw new Error(`feature 必须是单个安全目录名,收到: ${feature}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function uniqueStrings(values) {
|
|
180
|
+
return [...new Set(values.map(value => value.trim()).filter(Boolean))];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function requiresAcceptance(stage) {
|
|
184
|
+
return ['implementation', 'routine-tests', 'adversarial-testing', 'final-review'].includes(stage);
|
|
185
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: standard-implementer
|
|
3
|
+
description: 按已批准设计和计划实现功能。仅在模型路由要求 standard 实现或修复时使用。
|
|
4
|
+
model: sonnet
|
|
5
|
+
tools: Read, Edit, Write, Bash, Grep, Glob
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
先验证 handoff 并读取全部正式输入。只在 allowedChangeScope 内按计划实现和测试;不得重新定义需求或接口。发现设计缺口时停止并升级给 premium-planner。完成后写结构化阶段回执。
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- 交付前 → delivery-readiness skill,按风险验证契约、测试、迁移、回滚、性能与可观测性
|
|
11
11
|
- 记忆更新 → 只在可验收工程节点、关键决策、状态变化或会话切换时写;手动 /update-memory
|
|
12
12
|
- 反驳检查 → S 级精简自检;M/L 级或用户明确要求时用 /critic 独立审查
|
|
13
|
+
- 模型路由 → 读取 `.ai/config/model-routing.json`;非 inherit 时按 model-routing skill 创建/验证 handoff,再调用匹配的 planner/implementer/test-worker/reviewer
|
|
13
14
|
|
|
14
15
|
## 与 superpowers 的编排(未安装则忽略本节)
|
|
15
16
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
name = "premium_planner"
|
|
2
|
+
description = "高风险需求定稿、功能设计和实施计划;仅在模型路由要求 premium 规划时使用。"
|
|
3
|
+
model_reasoning_effort = "high"
|
|
4
|
+
developer_instructions = """
|
|
5
|
+
先验证 .ai/runs/<feature>/handoff.json,读取其中全部正式输入和 .ai/skills/model-routing.md。只输出需求、设计或计划,不修改实现。输入过期、矛盾或仍有未决问题时停止。
|
|
6
|
+
"""
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- 交付前 → delivery-readiness skill,按风险验证契约、测试、迁移、回滚、性能与可观测性
|
|
11
11
|
- 记忆更新 → 只在可验收工程节点、关键决策、状态变化或会话切换时写
|
|
12
12
|
- 反驳检查 → S 级精简自检;M/L 级或用户明确要求时用 critic skill
|
|
13
|
+
- 模型路由 → 读取 `.ai/config/model-routing.json`;非 inherit 时按 model-routing skill 创建/验证 handoff,再调用 `.codex/agents/` 中匹配的执行者
|
|
13
14
|
|
|
14
15
|
## 与 superpowers 的编排(未安装则忽略本节)
|
|
15
16
|
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
1. 读 `memory/MEMORY.md`(记忆索引)
|
|
9
9
|
2. 读 `memory/session-log.md` 末尾条目(接上进度)
|
|
10
10
|
3. 按任务类型按需加载 `skills/` 对应方法论
|
|
11
|
+
4. 跨模型阶段先按 `skills/model-routing.md` 创建并校验 handoff,不用对话摘要替代正式输入
|
|
11
12
|
|
|
12
13
|
## 目录
|
|
13
14
|
|
|
14
15
|
- `ai-memory.json` — 框架版本、Schema、启用工具、文件所有权与生成基线哈希;供安全升级预检使用
|
|
16
|
+
- `config/model-routing.json` — 可选阶段模型策略;默认 `inherit` 不改变旧行为
|
|
15
17
|
- `memory/` — 记忆层:MEMORY.md 索引、project-state 全景、session-log 流水、user-profile/feedback 用户级记忆、features/ 功能档案
|
|
16
|
-
- `
|
|
18
|
+
- `runs/` — 本地交接清单和阶段回执;默认不进入 Git
|
|
19
|
+
- `skills/` — 方法论层:project-inception、requirements-flow、feature-design、delivery-readiness、model-routing、architecture、code-review、critic、memory-update
|
|
17
20
|
|
|
18
21
|
## 与工具专属配置的关系
|
|
19
22
|
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
4. 将结果写入 `docs/design/...` 的“交付验证”章节;S 级无设计文档时记入 session-log。
|
|
17
17
|
5. 输出明确结论:`ready`、`conditional` 或 `not-ready`,并列出阻塞项。
|
|
18
18
|
|
|
19
|
+
启用模型路由时,机械性测试可交给 economy 执行者,但测试策略、安全/迁移/并发场景和最终审查仍由 premium 执行者负责。测试命令直接运行,不调用模型。reviewer 第一轮保持只读,修复后重新审查。
|
|
20
|
+
|
|
19
21
|
## 检查面
|
|
20
22
|
|
|
21
23
|
| 检查面 | 必须回答的问题 |
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
7. M 级做一次精简自检;L 级按 critic 方法做独立或聚焦审查。
|
|
20
20
|
8. 更新 `project-state.md` 为 `designed`,实现计划只引用本设计,不重新生成同义 spec。
|
|
21
21
|
|
|
22
|
+
启用模型路由时,`feature-design` 与需要的 `write-plan` 分别创建 handoff;实现阶段必须重新 prepare,引用最终需求、设计和计划的当前哈希。不要把 brainstorming 对话作为实现输入。
|
|
23
|
+
|
|
22
24
|
## Superpowers 路由
|
|
23
25
|
|
|
24
26
|
- S:跳过 brainstorming 和 writing-plans。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 分阶段模型路由与交接
|
|
2
|
+
|
|
3
|
+
模型路由只选择现有阶段的执行者,不得增加或跳过 S/M/L 风险流程。读取 `.ai/config/model-routing.json`:`inherit` 保持当前模型;`balanced` 使用高级规划、中等实现、便宜测试和高级审查;`quality` 提高实现与诊断等级。
|
|
4
|
+
|
|
5
|
+
执行 CLI 时优先使用当前环境的 `ai-memory`;若命令不可用,使用 `npx --yes @betterdanlins/ai-memory@{{frameworkVersion}}`。必须固定与项目框架一致的版本,不使用未固定的 latest。
|
|
6
|
+
|
|
7
|
+
## 执行协议
|
|
8
|
+
|
|
9
|
+
1. 先由 requirements-flow / feature-design 判断是否需要该阶段,再解析模型等级。
|
|
10
|
+
2. 跨模型执行前,用 `ai-memory workflow prepare` 从正式需求、设计和计划生成 `.ai/runs/<feature>/handoff.json`。
|
|
11
|
+
3. 执行者先运行 `ai-memory workflow verify`,再读取 handoff 中的全部输入;不得用对话摘要替代正式文档。
|
|
12
|
+
4. 输入缺失、哈希变化、路由变化或存在未决问题时停止,不得猜测。
|
|
13
|
+
5. 实现结束后在同一 run 目录写 `<stage>-result.json`,记录变更文件、验收覆盖、测试、设计偏差和未决风险。
|
|
14
|
+
6. reviewer 第一轮只读;修复交给 implementer,修复后重新审查。
|
|
15
|
+
|
|
16
|
+
## 能力边界与升级
|
|
17
|
+
|
|
18
|
+
- `premium`:需求定稿、重大方案、计划、对抗性测试和最终审查。
|
|
19
|
+
- `standard`:按已批准计划实现、普通失败诊断。
|
|
20
|
+
- `economy`:根据明确测试矩阵补机械性测试;不得修改生产代码或重新解释契约。
|
|
21
|
+
- `none`:直接运行确定性命令,不调用模型。
|
|
22
|
+
- economy 无法理解契约或发现生产缺陷 → standard;standard 发现设计缺口或连续两次失败 → premium。
|
|
23
|
+
- 安全、权限、资金、隐私、数据迁移和并发问题直接使用 premium。
|
|
24
|
+
|
|
25
|
+
模型不可用时可降级为 `inherit`,但必须报告一次;不得静默把高风险阶段降为 economy。
|
|
26
|
+
|
|
27
|
+
## 交接回执最小字段
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"stage": "implementation",
|
|
32
|
+
"status": "completed",
|
|
33
|
+
"changedFiles": [],
|
|
34
|
+
"acceptanceCoverage": {},
|
|
35
|
+
"tests": [],
|
|
36
|
+
"designDeviations": [],
|
|
37
|
+
"unresolvedRisks": []
|
|
38
|
+
}
|
|
39
|
+
```
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
5. 按下方路由选择下一步,不在本流程中启动重复 brainstorming。
|
|
24
24
|
6. 更新 `project-state.md` 为 `finalized`;在 `features/<需求名>.md` 记录外部契约与关键决策,并更新 MEMORY 索引。
|
|
25
25
|
|
|
26
|
+
若 `.ai/config/model-routing.json` 的 profile 不是 `inherit`,定稿前按 `.ai/skills/model-routing.md` 为 `requirement-finalize` 准备并校验 handoff,再使用对应等级执行者。模型路由不得改变下方 S/M/L 流程深度。
|
|
27
|
+
|
|
26
28
|
## 风险路由
|
|
27
29
|
|
|
28
30
|
| 等级 | 判断 | 下一步 |
|