@betterdanlins/ai-memory 0.4.0 → 0.5.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 CHANGED
@@ -44,6 +44,14 @@ An initialized project cannot be initialized again. A newer CLI first uses `upda
44
44
 
45
45
  ## Versions and compatibility
46
46
 
47
+ ### v0.5.0 — managed entry blocks
48
+
49
+ - Wraps the framework-owned sections of generated `AGENTS.md` and `CLAUDE.md` in explicit `ai-memory:managed` markers. Future updates replace only that block and preserve project commands and custom rules outside it.
50
+ - An unchanged v0.4.0 entry file can migrate to marked blocks automatically because its generated baseline hash proves it has not been edited. A modified unmarked v0.4.0 file requires one manual merge; after markers are established, later framework updates no longer conflict with user-section changes.
51
+ - Missing, duplicated, or malformed markers fail conservatively instead of triggering a whole-file overwrite. Managed-block updates also re-check the current file hash immediately before writing.
52
+ - `.claude/settings.json` remains a mixed JSON file and still requires review when both the framework and user changed it; Markdown markers are intentionally not applied to JSON.
53
+ - Fixes npm's automatic `.gitignore` template renaming and safely replaces the unchanged v0.4.0 `.ai/runs/.npmignore`; a locally edited obsolete file still requires review.
54
+
47
55
  ### v0.4.0 — staged model routing and verified handoffs
48
56
 
49
57
  - 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.
@@ -73,6 +81,15 @@ npx @betterdanlins/ai-memory@0.4.0 models configure --profile balanced
73
81
 
74
82
  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
83
 
84
+ ### Upgrade from v0.4.0 to v0.5.0
85
+
86
+ ```bash
87
+ npx @betterdanlins/ai-memory@0.5.0 update --dry-run
88
+ npx @betterdanlins/ai-memory@0.5.0 update --yes
89
+ ```
90
+
91
+ If `AGENTS.md` or `CLAUDE.md` is still the unchanged v0.4.0 generated version, it migrates automatically. If either unmarked file was customized, dry-run reports `merge`; compare it with a fresh v0.5.0 reference, keep project-specific content in the user block, and retry. Once markers exist, edit only outside `<!-- ai-memory:managed:start/end -->`.
92
+
76
93
  ## What it generates
77
94
 
78
95
  ```
@@ -141,6 +158,7 @@ The generator is a small set of focused modules:
141
158
  - **render** — substitutes `{{variable}}` placeholders and throws on any undefined variable, so a broken template fails loudly instead of shipping `{{...}}`.
142
159
  - **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).
143
160
  - **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.
161
+ - **managed entry blocks** — Markdown entry files update only their validated framework block. Unmarked or malformed customized files remain manual merges; JSON settings keep conservative whole-file review semantics.
144
162
  - **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
163
  - **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.
146
164
 
package/README.zh-CN.md CHANGED
@@ -44,6 +44,14 @@ npx @betterdanlins/ai-memory models configure --profile balanced
44
44
 
45
45
  ## 版本与兼容性
46
46
 
47
+ ### v0.5.0 —— 入口文件受管区块
48
+
49
+ - 为生成的 `AGENTS.md` 和 `CLAUDE.md` 框架区域增加明确的 `ai-memory:managed` 标记。后续升级只替换该区块,区块外的项目命令和自定义规则保持不变。
50
+ - 未修改的 v0.4.0 入口文件可依据生成基线哈希自动迁移为带标记结构。已经修改且没有标记的 v0.4.0 文件需要一次人工合并;标记建立后,后续框架升级不再与用户区块修改冲突。
51
+ - 标记缺失、重复或格式损坏时保守拒绝自动合并,不会退化为整文件覆盖。写入受管区块前还会再次核对当前文件哈希。
52
+ - `.claude/settings.json` 仍是混合 JSON 文件;框架和用户都修改时继续要求审查,不会向 JSON 强行加入 Markdown 标记。
53
+ - 修复 npm 自动改名 `.gitignore` 模板的问题,并安全替换未修改的 v0.4.0 `.ai/runs/.npmignore`;若该废弃文件被本地修改,仍要求人工审查。
54
+
47
55
  ### v0.4.0 —— 分阶段模型路由与可靠交接
48
56
 
49
57
  - 新增可选的 `inherit`、`balanced`、`quality` profile。新项目和升级项目默认保持 `inherit`,安装 v0.4.0 不会静默增加模型成本,也不会改变 v0.3.0 的工作流深度。
@@ -73,6 +81,15 @@ npx @betterdanlins/ai-memory@0.4.0 models configure --profile balanced
73
81
 
74
82
  升级完成后 profile 仍为 `inherit`,只有显式配置才会改变。不要在已有 v0.3.0 项目重新运行 `init`。若 dry-run 出现 merge/review 项,应以新生成的 v0.4.0 文件为参考人工合并,然后重新执行 dry-run。
75
83
 
84
+ ### 从 v0.4.0 升级到 v0.5.0
85
+
86
+ ```bash
87
+ npx @betterdanlins/ai-memory@0.5.0 update --dry-run
88
+ npx @betterdanlins/ai-memory@0.5.0 update --yes
89
+ ```
90
+
91
+ 如果 `AGENTS.md` 或 `CLAUDE.md` 仍是未修改的 v0.4.0 生成版本,会自动迁移。若无标记入口已被定制,dry-run 会报告 `merge`;应对照全新 v0.5.0 参考文件,把项目内容放进 user 区块后重试。标记建立后,只在 `<!-- ai-memory:managed:start/end -->` 之外编辑。
92
+
76
93
  ## 生成什么
77
94
 
78
95
  ```
@@ -141,6 +158,7 @@ AGENTS.md + .agents/ + .codex/# Codex:skills 与分层自定义 agents
141
158
  - **render** —— 替换 `{{变量}}` 占位符,遇到任何未定义变量即抛错,让坏掉的模板大声失败,而不是把 `{{...}}` 发出去。
142
159
  - **scaffold** —— 预检安全目标路径和符号链接,处理冲突并在内存中完成全部读取/渲染后再逐个写入;提供 `--import` 时先校验导入目录,再从中拉取 `user-profile` / `feedback`(单个源文件缺失则回退到模板并明确报告)。
143
160
  - **framework metadata** —— 全新 init 记录 framework/schema 版本、文件所有权和生成哈希;update 据此区分安全更新与用户修改。无元数据的 v0.1 项目按 legacy 保守规划。
161
+ - **managed entry blocks** —— Markdown 入口只更新通过校验的框架区块;无标记或标记损坏的定制文件仍人工合并,JSON settings 继续采用保守整文件审查。
144
162
  - **model routing** —— 可选 profile 把现有阶段映射为 premium、standard、economy、inherit 或无模型执行;Claude/Codex 使用原生自定义 agent 配置,路由不会增加 S/M/L 阶段。
145
163
  - **workflow handoff** —— 本地清单只用路径和 SHA-256 引用正式需求/设计/计划;输入过期、未决问题、路由变化、路径越界或符号链接都会关闭式失败。
146
164
 
package/bin/cli.js CHANGED
@@ -203,8 +203,9 @@ program
203
203
  printUpdatePlan(plan);
204
204
  if (opts.yes) {
205
205
  const result = await applyFrameworkUpdate({ targetDir: process.cwd(), templatesRoot: TEMPLATES, plan });
206
- console.log(`安全更新完成:写入 ${result.written.length}`);
206
+ console.log(`安全更新完成:写入 ${result.written.length},移除 ${result.removed.length}`);
207
207
  for (const dest of result.written) console.log(` ${dest}`);
208
+ for (const dest of result.removed) console.log(` 移除 ${dest}`);
208
209
  }
209
210
  });
210
211
 
@@ -216,6 +217,10 @@ program.parseAsync().catch((e) => {
216
217
  if (e.name === 'ScaffoldError') {
217
218
  console.error(`已写入 ${e.summary.written.length}:`);
218
219
  for (const d of e.summary.written) console.error(` ${d}`);
220
+ if (e.summary.removed?.length) {
221
+ console.error(`已移除 ${e.summary.removed.length}:`);
222
+ for (const d of e.summary.removed) console.error(` ${d}`);
223
+ }
219
224
  console.error(`尚未写入 ${e.notWritten.length}:`);
220
225
  for (const d of e.notWritten) console.error(` ${d}`);
221
226
  if (e.summary.skipped.length) {
@@ -238,6 +243,8 @@ function printUpdatePlan(plan) {
238
243
  const groups = [
239
244
  ['add', '新增'],
240
245
  ['update', '可安全更新'],
246
+ ['update-managed', '更新受管区块'],
247
+ ['remove', '安全移除'],
241
248
  ['merge', '需要合并'],
242
249
  ['review', '需要人工审查'],
243
250
  ['review-remove', '需要确认移除'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterdanlins/ai-memory",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "AI memory + requirements workflow scaffolder for Claude Code & Codex",
5
5
  "type": "module",
6
6
  "bin": {
package/src/framework.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { access, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { buildManifest } from './manifest.js';
5
5
  import { matchesLegacyV01Baseline } from './legacy-v0.1.js';
6
+ import { hasValidManagedBlock, isManagedBlockFile, replaceManagedBlock } from './managed-blocks.js';
6
7
  import { migrationsBetween } from './migrations.js';
7
8
  import { assertNoSymlinkPath, resolveSafeDestination } from './path-safety.js';
8
9
  import { render } from './render.js';
@@ -111,6 +112,9 @@ export async function planFrameworkUpdate({ targetDir, templatesRoot, frameworkV
111
112
 
112
113
  const currentHash = hashContent(current);
113
114
  const desiredHash = hashContent(desired);
115
+ if (isManagedBlockFile(dest) && !hasValidManagedBlock(desired)) {
116
+ throw new Error(`受管模板缺少有效 managed 标记: ${dest}`);
117
+ }
114
118
  if (currentHash === desiredHash) {
115
119
  actions.push({ action: 'unchanged', dest, ownership });
116
120
  } else if (ownership === 'user') {
@@ -119,18 +123,33 @@ export async function planFrameworkUpdate({ targetDir, templatesRoot, frameworkV
119
123
  const matchesBaseline = installation.kind === 'metadata'
120
124
  ? installation.metadata.files[dest]?.sha256 === currentHash
121
125
  : matchesLegacyV01Baseline(dest, current, vars);
122
- const action = matchesBaseline ? 'update' : ownership === 'mixed' ? 'merge' : 'review';
123
- actions.push({ action, dest, ownership, currentHash, desiredHash });
126
+ if (isManagedBlockFile(dest) && hasValidManagedBlock(current)) {
127
+ const merged = replaceManagedBlock(current, desired);
128
+ actions.push({
129
+ action: merged === current ? 'unchanged' : 'update-managed',
130
+ dest, ownership, currentHash, desiredHash,
131
+ });
132
+ } else {
133
+ const action = matchesBaseline ? 'update' : ownership === 'mixed' ? 'merge' : 'review';
134
+ actions.push({ action, dest, ownership, currentHash, desiredHash });
135
+ }
124
136
  }
125
137
  }
126
138
 
127
139
  if (installation.kind === 'metadata') {
128
140
  for (const [dest, record] of Object.entries(installation.metadata.files)) {
129
- if (desiredDests.has(dest) || !(await exists(path.join(targetDir, ...dest.split('/'))))) continue;
141
+ const destPath = resolveSafeDestination(targetDir, dest);
142
+ const current = await readOptional(destPath);
143
+ if (desiredDests.has(dest) || current === undefined) continue;
144
+ const currentHash = hashContent(current);
145
+ const canRemove = record.ownership === 'framework'
146
+ && ownershipFor(dest) === 'framework'
147
+ && record.sha256 === currentHash;
130
148
  actions.push({
131
- action: record.ownership === 'user' ? 'preserve' : 'review-remove',
149
+ action: record.ownership === 'user' ? 'preserve' : canRemove ? 'remove' : 'review-remove',
132
150
  dest,
133
151
  ownership: record.ownership,
152
+ currentHash,
134
153
  });
135
154
  }
136
155
  }
@@ -161,26 +180,46 @@ export async function applyFrameworkUpdate({ targetDir, templatesRoot, plan }) {
161
180
  const sources = new Map(manifest.map(entry => [entry.dest, entry.src]));
162
181
  const candidates = [];
163
182
 
164
- for (const action of plan.actions.filter(item => item.action === 'add' || item.action === 'update')) {
183
+ for (const action of plan.actions.filter(item => ['add', 'update', 'update-managed', 'remove'].includes(item.action))) {
184
+ if (action.action === 'remove') {
185
+ const destPath = resolveSafeDestination(targetDir, action.dest);
186
+ await assertNoSymlinkPath(targetDir, destPath);
187
+ const current = await readFile(destPath, 'utf8');
188
+ if (hashContent(current) !== action.currentHash) {
189
+ throw new Error(`文件在预检后发生变化,请重新 dry-run: ${action.dest}`);
190
+ }
191
+ candidates.push({ ...action, destPath });
192
+ continue;
193
+ }
165
194
  const src = sources.get(action.dest);
166
195
  if (!src) throw new Error(`升级计划缺少当前模板: ${action.dest}`);
167
196
  const destPath = resolveSafeDestination(targetDir, action.dest);
168
197
  await assertNoSymlinkPath(targetDir, destPath);
169
- if (action.action === 'update') {
170
- const current = await readFile(destPath, 'utf8');
198
+ let current;
199
+ if (action.action !== 'add') {
200
+ current = await readFile(destPath, 'utf8');
171
201
  if (action.currentHash && hashContent(current) !== action.currentHash) {
172
202
  throw new Error(`文件在预检后发生变化,请重新 dry-run: ${action.dest}`);
173
203
  }
174
204
  }
175
- candidates.push({ ...action, destPath, content: render(await readFile(src, 'utf8'), vars) });
205
+ const desired = render(await readFile(src, 'utf8'), vars);
206
+ const content = action.action === 'update-managed'
207
+ ? replaceManagedBlock(current, desired)
208
+ : desired;
209
+ candidates.push({ ...action, destPath, content });
176
210
  }
177
211
 
178
- const summary = { written: [], skipped: [], imported: [], fallback: [] };
212
+ const summary = { written: [], removed: [], skipped: [], imported: [], fallback: [] };
179
213
  for (const [index, item] of candidates.entries()) {
180
214
  try {
181
- await mkdir(path.dirname(item.destPath), { recursive: true });
182
- await writeFile(item.destPath, item.content, { flag: item.action === 'add' ? 'wx' : 'w' });
183
- summary.written.push(item.dest);
215
+ if (item.action === 'remove') {
216
+ await unlink(item.destPath);
217
+ summary.removed.push(item.dest);
218
+ } else {
219
+ await mkdir(path.dirname(item.destPath), { recursive: true });
220
+ await writeFile(item.destPath, item.content, { flag: item.action === 'add' ? 'wx' : 'w' });
221
+ summary.written.push(item.dest);
222
+ }
184
223
  } catch (err) {
185
224
  throw new ScaffoldError({
186
225
  phase: 'update', dest: item.dest, destPath: item.destPath, cause: err, summary,
@@ -0,0 +1,62 @@
1
+ export const MANAGED_BLOCK_START = '<!-- ai-memory:managed:start -->';
2
+ export const MANAGED_BLOCK_END = '<!-- ai-memory:managed:end -->';
3
+
4
+ const MANAGED_BLOCK_FILES = new Set(['AGENTS.md', 'CLAUDE.md']);
5
+
6
+ export class ManagedBlockError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'ManagedBlockError';
10
+ }
11
+ }
12
+
13
+ export function isManagedBlockFile(dest) {
14
+ return MANAGED_BLOCK_FILES.has(dest);
15
+ }
16
+
17
+ export function replaceManagedBlock(current, desired) {
18
+ const currentBlock = locateManagedBlock(current, '当前文件');
19
+ const desiredBlock = locateManagedBlock(desired, '新版模板');
20
+ const eol = current.includes('\r\n') ? '\r\n' : '\n';
21
+ const replacement = normalizeEol(desired.slice(desiredBlock.start, desiredBlock.end), eol);
22
+ return current.slice(0, currentBlock.start) + replacement + current.slice(currentBlock.end);
23
+ }
24
+
25
+ export function hasValidManagedBlock(content) {
26
+ try {
27
+ locateManagedBlock(content, '文件');
28
+ return true;
29
+ } catch (err) {
30
+ if (err instanceof ManagedBlockError) return false;
31
+ throw err;
32
+ }
33
+ }
34
+
35
+ function locateManagedBlock(content, label) {
36
+ if (typeof content !== 'string') throw new ManagedBlockError(`${label}内容必须是字符串`);
37
+ const starts = occurrences(content, MANAGED_BLOCK_START);
38
+ const ends = occurrences(content, MANAGED_BLOCK_END);
39
+ if (starts.length !== 1 || ends.length !== 1) {
40
+ throw new ManagedBlockError(`${label}必须且只能包含一组 ai-memory managed 标记`);
41
+ }
42
+ const start = starts[0];
43
+ const end = ends[0] + MANAGED_BLOCK_END.length;
44
+ if (end <= start) throw new ManagedBlockError(`${label}的 ai-memory managed 标记顺序无效`);
45
+ return { start, end };
46
+ }
47
+
48
+ function occurrences(content, token) {
49
+ const positions = [];
50
+ let offset = 0;
51
+ while (offset < content.length) {
52
+ const index = content.indexOf(token, offset);
53
+ if (index === -1) break;
54
+ positions.push(index);
55
+ offset = index + token.length;
56
+ }
57
+ return positions;
58
+ }
59
+
60
+ function normalizeEol(content, eol) {
61
+ return content.replace(/\r?\n/g, eol);
62
+ }
package/src/manifest.js CHANGED
@@ -13,7 +13,7 @@ export async function buildManifest(templatesRoot, tools) {
13
13
  if (!enabled(tools)) continue;
14
14
  const groupDir = path.join(templatesRoot, group);
15
15
  for (const rel of await walk(groupDir, '')) {
16
- entries.push({ src: path.join(groupDir, ...rel.split('/')), dest: rel });
16
+ entries.push({ src: path.join(groupDir, ...rel.split('/')), dest: templateDestination(rel) });
17
17
  }
18
18
  }
19
19
  const sorted = entries.sort((a, b) => a.dest < b.dest ? -1 : a.dest > b.dest ? 1 : 0);
@@ -21,6 +21,12 @@ export async function buildManifest(templatesRoot, tools) {
21
21
  return sorted;
22
22
  }
23
23
 
24
+ function templateDestination(rel) {
25
+ return rel.endsWith('/.gitignore.template') || rel === '.gitignore.template'
26
+ ? rel.slice(0, -'.template'.length)
27
+ : rel;
28
+ }
29
+
24
30
  function assertUniqueDestinations(entries) {
25
31
  const sourcesByDest = new Map();
26
32
  for (const { src, dest } of entries) {
@@ -1,8 +1,10 @@
1
1
  # {{projectName}}
2
2
 
3
+ <!-- ai-memory:managed:start -->
3
4
  ## AI 协作框架(ai-memory)
4
5
 
5
6
  进场先读 `.ai/memory/MEMORY.md`,随后遵循 `.ai/README.md` 的进场协议。
7
+ 本 managed 区块由 ai-memory 更新;项目命令和自定义规则只写到下方 user 区块。
6
8
 
7
9
  - 0→1 项目/首次架构基线/重大重构 → project-inception skill,产物在 `docs/architecture/`;普通功能不要重复触发
8
10
  - 需求 what/why 与外部行为定稿 → requirements-flow skill,产物在 `docs/requirements/vX.Y.Z/{draft,final}/`
@@ -18,8 +20,11 @@
18
20
  2. S 级跳过 brainstorming/writing-plans;M 级只对未决高影响 how 选择性使用;L 级可使用完整流程
19
21
  3. 所有 Superpowers 结论必须回写 `docs/design/`,临时 spec/plan 不是唯一事实源
20
22
  4. code review 使用适合风险等级的流程,叠加 `.ai/skills/code-review.md` 的项目检查项
23
+ <!-- ai-memory:managed:end -->
21
24
 
25
+ <!-- ai-memory:user:start -->
22
26
  ## 项目信息
23
27
 
24
28
  - 技术栈:{{techStack}}
25
29
  <!-- 在此补充启动/测试/构建命令 -->
30
+ <!-- ai-memory:user:end -->
@@ -1,8 +1,10 @@
1
1
  # {{projectName}}
2
2
 
3
+ <!-- ai-memory:managed:start -->
3
4
  ## AI 协作框架(ai-memory)
4
5
 
5
6
  进场先读 `.ai/memory/MEMORY.md`,随后遵循 `.ai/README.md` 的进场协议。
7
+ 本 managed 区块由 ai-memory 更新;项目命令和自定义规则只写到下方 user 区块。
6
8
 
7
9
  - 0→1 项目/首次架构基线/重大重构 → project-inception skill,产物在 `docs/architecture/`;普通功能不要重复触发
8
10
  - 需求 what/why 与外部行为定稿 → requirements-flow skill,产物在 `docs/requirements/vX.Y.Z/{draft,final}/`
@@ -18,8 +20,11 @@
18
20
  2. S 级跳过 brainstorming/writing-plans;M 级只对未决高影响 how 选择性使用;L 级可使用完整流程
19
21
  3. 所有 Superpowers 结论必须回写 `docs/design/`,临时 spec/plan 不是唯一事实源
20
22
  4. code review 使用适合风险等级的流程,叠加 `.ai/skills/code-review.md` 的项目检查项
23
+ <!-- ai-memory:managed:end -->
21
24
 
25
+ <!-- ai-memory:user:start -->
22
26
  ## 项目信息
23
27
 
24
28
  - 技术栈:{{techStack}}
25
29
  <!-- 在此补充启动/测试/构建命令 -->
30
+ <!-- ai-memory:user:end -->