dev-memory-cli 0.22.2 → 0.23.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 +37 -34
- package/bin/dev-memory.js +117 -0
- package/hooks/README.md +12 -1
- package/lib/dev_memory_capture.py +68 -5
- package/lib/dev_memory_common.py +28 -9
- package/lib/dev_memory_session_scan.py +578 -65
- package/lib/maintenance/archive.md +84 -0
- package/lib/maintenance/tidy.md +103 -0
- package/lib/ui-app.html +1 -1
- package/package.json +2 -1
- package/scripts/hooks/_common.py +4 -5
- package/suite-manifest.json +4 -4
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Dev Memory Skill Suite 为 Codex、Claude 等 coding agent 提供跨会话开发
|
|
|
9
9
|
## 核心能力
|
|
10
10
|
|
|
11
11
|
- **会话恢复**:`SessionStart` 自动注入当前 repo + branch 的浓缩记忆和权威文件路径。
|
|
12
|
-
-
|
|
12
|
+
- **后台语义沉淀**:定期读取完整会话,对照已有记忆后执行 append、rewrite、delete 或明确 skip。
|
|
13
13
|
- **纠错优先**:修正旧记忆时定位并改写原 entry,避免同时保留互相冲突的新旧结论。
|
|
14
14
|
- **分支隔离**:同一仓库的不同分支拥有独立工作记忆,可 fork、rename、reset 或归档。
|
|
15
15
|
- **跨分支共享**:稳定规则进入 repo 层;候选知识可在分支完成时提炼上提。
|
|
@@ -17,17 +17,13 @@ Dev Memory Skill Suite 为 Codex、Claude 等 coding agent 提供跨会话开发
|
|
|
17
17
|
- **多仓库工作区**:一个 workspace 下可同时加载多个 repo,并为主仓库保留更完整的上下文。
|
|
18
18
|
- **本地管理面板**:浏览、编辑已存储记忆,预览 SessionStart 注入文本,并查看会话扫描与 token 用量。
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## 一个常驻 Skill
|
|
21
21
|
|
|
22
22
|
| Skill | 职责 | 适用场景 |
|
|
23
23
|
| --- | --- | --- |
|
|
24
24
|
| `dev-memory-read` | 定位并搜索当前 repo/branch 的权威记忆,只读不写 | 主动恢复既有记忆、查询历史 TODO |
|
|
25
|
-
| `dev-memory-capture` | 写入、改写或删除记忆;同步 Git 派生索引 | 稳定结论沉淀、纠错、阶段 checkpoint |
|
|
26
|
-
| `dev-memory-setup` | 把 `unsorted.md` 中的内容分类合并到结构化文件 | 未分类内容整理、记忆结构初始化 |
|
|
27
|
-
| `dev-memory-tidy` | 批量审查陈旧、重复、错误或模板残留条目,备份后应用 | 结构化记忆校准与清理 |
|
|
28
|
-
| `dev-memory-graduate` | 提炼跨分支知识并归档已完成分支 | 分支收尾与归档 |
|
|
29
25
|
|
|
30
|
-
Skill
|
|
26
|
+
普通开发会话不再常驻 capture/setup/tidy/graduate Skill。会话写入由 session-scan 后台处理;整理和归档由 CLI 临时启动专用维护 Agent,并只把对应维护 prompt 放进那个独立会话。
|
|
31
27
|
|
|
32
28
|
## 安装
|
|
33
29
|
|
|
@@ -44,13 +40,13 @@ npx skills add xluos/dev-memory-skill-suite --list
|
|
|
44
40
|
安装到 Codex 全局:
|
|
45
41
|
|
|
46
42
|
```bash
|
|
47
|
-
npx skills add xluos/dev-memory-skill-suite --skill
|
|
43
|
+
npx skills add xluos/dev-memory-skill-suite --skill dev-memory-read -a codex -g -y
|
|
48
44
|
```
|
|
49
45
|
|
|
50
46
|
安装到检测到的所有 agent:
|
|
51
47
|
|
|
52
48
|
```bash
|
|
53
|
-
npx skills add xluos/dev-memory-skill-suite --all -g -y
|
|
49
|
+
npx skills add xluos/dev-memory-skill-suite --skill dev-memory-read --all -g -y
|
|
54
50
|
```
|
|
55
51
|
|
|
56
52
|
### 安装 CLI 与 Hook
|
|
@@ -81,31 +77,27 @@ dev-memory-cli session-scan install
|
|
|
81
77
|
dev-memory-cli session-scan status
|
|
82
78
|
```
|
|
83
79
|
|
|
84
|
-
扫描任务默认在本地时间 03:00
|
|
80
|
+
扫描任务默认在本地时间 03:00 和 13:00 运行,时间列表可配置。LaunchAgent 触发时会读取 macOS HID 空闲时长;最近 10 分钟有键鼠输入则记录 `skipped_active` 并退出,不扫描文件或调用模型。活跃检测失败时默认保守跳过。手工执行 `session-scan run` 不受该检测影响。`install-hooks codex` 只安装 CLI hook,不会隐式安装定时任务。
|
|
85
81
|
|
|
86
82
|
## 基本使用
|
|
87
83
|
|
|
88
|
-
|
|
84
|
+
记忆是显式 opt-in。首次使用先初始化,然后由生命周期 hook 恢复、由定期 session-scan 沉淀新会话语义。
|
|
89
85
|
|
|
90
86
|
```bash
|
|
87
|
+
# 为当前 repo + branch 初始化记忆骨架
|
|
88
|
+
dev-memory-cli init
|
|
89
|
+
|
|
91
90
|
# 查看当前仓库和分支对应的记忆路径
|
|
92
91
|
dev-memory-cli read show
|
|
93
92
|
|
|
94
93
|
# 只在当前 repo 的记忆范围内搜索
|
|
95
94
|
dev-memory-cli read search --query "发布流程" --query "回滚"
|
|
96
95
|
|
|
97
|
-
#
|
|
98
|
-
dev-memory-cli
|
|
99
|
-
--kind decision \
|
|
100
|
-
--content "发布前必须先跑完整打包检查"
|
|
101
|
-
|
|
102
|
-
# 写入一条跨分支规则
|
|
103
|
-
dev-memory-cli capture record \
|
|
104
|
-
--kind shared-decision \
|
|
105
|
-
--content "Python 测试优先使用项目虚拟环境"
|
|
96
|
+
# 启动独立整理 Agent(不会把整理 prompt 放进普通开发会话)
|
|
97
|
+
dev-memory-cli maintain tidy
|
|
106
98
|
|
|
107
|
-
#
|
|
108
|
-
dev-memory-cli
|
|
99
|
+
# 启动独立归档 Agent
|
|
100
|
+
dev-memory-cli maintain archive --branch feature/example
|
|
109
101
|
```
|
|
110
102
|
|
|
111
103
|
`read search` 默认搜索当前 branch + repo 共享层。跨分支和归档范围通过 `--scope` 显式指定:
|
|
@@ -115,11 +107,11 @@ dev-memory-cli read search --scope all-branches --query "关键词"
|
|
|
115
107
|
dev-memory-cli read search --scope archived --query "关键词"
|
|
116
108
|
```
|
|
117
109
|
|
|
118
|
-
###
|
|
110
|
+
### 底层写入路由
|
|
119
111
|
|
|
120
112
|

|
|
121
113
|
|
|
122
|
-
`capture record` 支持三种输入方式:
|
|
114
|
+
`capture` 是 session-scan 和维护 Agent 使用的底层写入引擎,同时保留为高级管理 CLI;它不再对应常驻 Skill。`capture record` 支持三种输入方式:
|
|
123
115
|
|
|
124
116
|
- **显式 kind**:调用方明确指定内容语义,直接写目标 section。
|
|
125
117
|
- **自动分类**:`--auto` 根据文本信号判断 decision、risk、glossary;无法明确分类时进入 `unsorted.md`。
|
|
@@ -150,19 +142,18 @@ dev-memory-cli capture rewrite-entry --id <entry-id> --content "修正后的完
|
|
|
150
142
|
dev-memory-cli capture delete-entry --id <entry-id>
|
|
151
143
|
```
|
|
152
144
|
|
|
153
|
-
符合跨分支复用判定的内容会同时进入 `pending-promotion.md
|
|
145
|
+
符合跨分支复用判定的内容会同时进入 `pending-promotion.md`,由归档维护 Agent 在分支收尾阶段审核;候选内容不会直接写入 repo 共享层。
|
|
154
146
|
|
|
155
147
|
### 整理与归档
|
|
156
148
|
|
|
157
|
-
|
|
149
|
+
普通入口只有两个,都会启动独立交互式 Agent:
|
|
158
150
|
|
|
159
|
-
|
|
|
151
|
+
| 命令 | 处理对象 | 结果 |
|
|
160
152
|
| --- | --- | --- |
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `graduate` | 已完成分支的有效记忆 | 上提跨分支知识,并把分支目录移入 `_archived` |
|
|
153
|
+
| `dev-memory-cli maintain tidy` | `unsorted.md` 与已结构化但漂移的内容 | 分类、proposal HTML 审核、备份后 edit/delete/reset |
|
|
154
|
+
| `dev-memory-cli maintain archive` | 已完成分支的有效记忆 | dry-run、上提跨分支知识、确认后移入 `_archived` |
|
|
164
155
|
|
|
165
|
-
`tidy apply` 和 `graduate apply`
|
|
156
|
+
维护 Agent 内部使用 `setup`、`tidy`、`graduate` 等低层子命令。`tidy apply` 和 `graduate apply` 会改变或移动已有记忆,仍然受人工审核和显式确认门禁保护。用 `--print-prompt` 可以只查看将传给独立 Agent 的完整维护指令。
|
|
166
157
|
|
|
167
158
|
### 分支记忆操作
|
|
168
159
|
|
|
@@ -223,11 +214,19 @@ dev-memory-cli session-scan run --dry-run --json
|
|
|
223
214
|
dev-memory-cli session-scan run
|
|
224
215
|
dev-memory-cli session-scan stats --json
|
|
225
216
|
dev-memory-cli session-scan history --limit 20
|
|
217
|
+
dev-memory-cli session-scan replay --run-id <run-id> --session-id <session-id> [--session-id <session-id> ...] [--executor codex]
|
|
226
218
|
dev-memory-cli session-scan config show
|
|
227
219
|
dev-memory-cli session-scan config set-executor codex
|
|
228
220
|
dev-memory-cli session-scan config set-model codex <model>
|
|
221
|
+
dev-memory-cli session-scan config set-schedule 03:00 13:00
|
|
222
|
+
dev-memory-cli session-scan config set-active-minutes 10
|
|
223
|
+
dev-memory-cli session-scan config set-timeout 360
|
|
229
224
|
```
|
|
230
225
|
|
|
226
|
+
已安装定时任务时,`set-schedule` 会自动重载 LaunchAgent,使新的时间列表立即生效。
|
|
227
|
+
|
|
228
|
+
`replay` 会按历史 run 记录的 `cursor_before` / `cursor_after` 精确重放指定会话,不回退当前会话游标;`--executor` 只覆盖本次运行,不修改持久配置。单分块会话只调用一次模型;最终结果必须包含至少一个记忆变更,或提供非空 `skip_reason`。空对象、只有 `title`、以及没有产生语义 action 且未说明原因的结果都会标记失败并保留重试空间。单次模型调用默认 360 秒超时,超时不会立即重试。run 账本会记录输出字段计数、`skip_reason`、payload 哈希、有效/观测字节数和语义 action 数,不保存原始总结正文。
|
|
229
|
+
|
|
231
230
|
## 运行模式
|
|
232
231
|
|
|
233
232
|
| 模式 | 检测条件 | 行为 |
|
|
@@ -243,7 +242,7 @@ dev-memory-cli workspace show
|
|
|
243
242
|
dev-memory-cli workspace primary <repo-basename>
|
|
244
243
|
```
|
|
245
244
|
|
|
246
|
-
`DEV_MEMORY_PRIMARY_REPO` 可作为临时 override
|
|
245
|
+
`DEV_MEMORY_PRIMARY_REPO` 可作为临时 override。维护命令在 workspace 中应使用 `--repo <repo-path-or-basename>` 明确目标仓库。
|
|
247
246
|
|
|
248
247
|
## 存储结构
|
|
249
248
|
|
|
@@ -319,6 +318,9 @@ dev-memory-cli context injection-preview \
|
|
|
319
318
|
|
|
320
319
|
```text
|
|
321
320
|
dev-memory-cli read <show|search>
|
|
321
|
+
dev-memory-cli init [--repo PATH] [--branch NAME]
|
|
322
|
+
dev-memory-cli maintain <tidy|archive> [--repo PATH] [--branch NAME] [--executor auto|codex|coco]
|
|
323
|
+
# low-level mutation/admin commands:
|
|
322
324
|
dev-memory-cli capture <show|suggest-kind|classify|record|list-entries|find-candidates|rewrite-entry|delete-entry|apply-summary-output|sync-working-tree|record-head>
|
|
323
325
|
dev-memory-cli setup <init|merge-unsorted|mark-completed>
|
|
324
326
|
dev-memory-cli tidy <prepare|apply>
|
|
@@ -327,7 +329,7 @@ dev-memory-cli branch [list|inspect|rename|fork|delete|init|inherit-worktree-bas
|
|
|
327
329
|
dev-memory-cli context <show|sync|injection-preview>
|
|
328
330
|
dev-memory-cli workspace <show|primary>
|
|
329
331
|
dev-memory-cli summary <extract-core>
|
|
330
|
-
dev-memory-cli session-scan <run|install|status|stats|history|show|uninstall|config>
|
|
332
|
+
dev-memory-cli session-scan <run|replay|install|status|stats|history|show|uninstall|config>
|
|
331
333
|
dev-memory-cli hook <session-start|pre-compact|stop|session-end>
|
|
332
334
|
dev-memory-cli ui
|
|
333
335
|
dev-memory-cli install-hooks <codex|claude|--all>
|
|
@@ -352,11 +354,12 @@ dev-memory 的能力边界不包括:
|
|
|
352
354
|
```text
|
|
353
355
|
bin/dev-memory.js # CLI 入口与 Node 侧命令
|
|
354
356
|
lib/dev_memory_*.py # 记忆、分支、会话扫描、整理与归档实现
|
|
357
|
+
lib/maintenance/ # 按需注入独立维护 Agent 的 prompt
|
|
355
358
|
lib/ui-server.js # 本地管理面板服务
|
|
356
359
|
lib/ui-app.html # 管理面板前端
|
|
357
360
|
scripts/hooks/ # 生命周期 hook 实现
|
|
358
361
|
hooks/ # Codex / Claude hook 模板
|
|
359
|
-
skills/
|
|
362
|
+
skills/dev-memory-read/ # 唯一常驻 Skill
|
|
360
363
|
tests/ # Python 测试
|
|
361
364
|
docs/diagrams/ # README 图表及源文件
|
|
362
365
|
```
|
package/bin/dev-memory.js
CHANGED
|
@@ -376,6 +376,112 @@ function commandPySubcommand(name, rawArgs) {
|
|
|
376
376
|
runPython(scriptPath, rawArgs);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
const MAINTENANCE_MODES = new Set(["tidy", "archive"]);
|
|
380
|
+
const MAINTENANCE_MARKER = "DEV_MEMORY_INTERNAL_MAINTENANCE_AGENT_V1";
|
|
381
|
+
|
|
382
|
+
function maintenanceHelp() {
|
|
383
|
+
process.stdout.write(`Usage:
|
|
384
|
+
dev-memory-cli maintain tidy [--repo PATH] [--branch NAME] [--scope branch|branch+repo]
|
|
385
|
+
[--executor auto|codex|coco] [--model MODEL]
|
|
386
|
+
dev-memory-cli maintain archive [--repo PATH] [--branch NAME]
|
|
387
|
+
[--executor auto|codex|coco] [--model MODEL]
|
|
388
|
+
dev-memory-cli maintain <tidy|archive> --print-prompt [other options]
|
|
389
|
+
|
|
390
|
+
The command starts a dedicated interactive maintenance-agent session. Tidy
|
|
391
|
+
must not apply destructive changes before HTML review; archive must not apply
|
|
392
|
+
before dry-run review and explicit confirmation.
|
|
393
|
+
`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function resolveMaintenanceExecutor(requested) {
|
|
397
|
+
const name = requested || process.env.DEV_MEMORY_MAINTENANCE_EXECUTOR || "auto";
|
|
398
|
+
if (!new Set(["auto", "codex", "coco"]).has(name)) {
|
|
399
|
+
fail(`unsupported maintenance executor: ${name}`);
|
|
400
|
+
}
|
|
401
|
+
if (name !== "auto") return name;
|
|
402
|
+
for (const candidate of ["codex", "coco"]) {
|
|
403
|
+
if (commandExists(candidate)) return candidate;
|
|
404
|
+
}
|
|
405
|
+
fail("no maintenance executor found; install codex/coco or use --print-prompt");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function buildMaintenancePrompt(mode, options, repoRoot) {
|
|
409
|
+
const promptPath = packageScript("lib", "maintenance", `${mode}.md`);
|
|
410
|
+
if (!fs.existsSync(promptPath)) fail(`missing maintenance prompt: ${promptPath}`);
|
|
411
|
+
const workflow = fs.readFileSync(promptPath, "utf8").trim();
|
|
412
|
+
const cliPath = path.resolve(process.argv[1]);
|
|
413
|
+
const branch = options.branch || "<current-git-branch>";
|
|
414
|
+
const scope = mode === "tidy" ? (options.scope || "branch") : "branch";
|
|
415
|
+
return `${MAINTENANCE_MARKER}
|
|
416
|
+
|
|
417
|
+
你是 dev-memory 的专用维护 Agent。本会话只处理下面指定仓库的记忆维护,不承担普通开发任务。
|
|
418
|
+
|
|
419
|
+
目标仓库:${repoRoot}
|
|
420
|
+
目标分支:${branch}
|
|
421
|
+
维护模式:${mode}
|
|
422
|
+
整理范围:${scope}
|
|
423
|
+
本次必须使用的 CLI:node ${JSON.stringify(cliPath)}
|
|
424
|
+
|
|
425
|
+
不要依赖全局 dev-memory capture/setup/tidy/graduate Skill;完整维护流程已经随本提示提供。
|
|
426
|
+
涉及删除、改写或归档时,必须遵守下面流程中的人工审核和确认门禁。
|
|
427
|
+
|
|
428
|
+
${workflow}
|
|
429
|
+
`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function commandMaintain(rawArgs) {
|
|
433
|
+
const mode = rawArgs[0];
|
|
434
|
+
if (!mode || mode === "--help" || mode === "-h") {
|
|
435
|
+
maintenanceHelp();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (!MAINTENANCE_MODES.has(mode)) {
|
|
439
|
+
fail(`unknown maintenance mode: ${mode}`);
|
|
440
|
+
}
|
|
441
|
+
const { positional, options } = parseArgs(rawArgs.slice(1));
|
|
442
|
+
if (positional.length) fail(`unexpected maintenance argument: ${positional[0]}`);
|
|
443
|
+
const repoRoot = path.resolve(options.repo || process.cwd());
|
|
444
|
+
if (!fs.existsSync(repoRoot) || !fs.statSync(repoRoot).isDirectory()) {
|
|
445
|
+
fail(`maintenance repo does not exist: ${repoRoot}`);
|
|
446
|
+
}
|
|
447
|
+
if (mode === "tidy" && options.scope && !new Set(["branch", "branch+repo"]).has(options.scope)) {
|
|
448
|
+
fail(`unsupported tidy scope: ${options.scope}`);
|
|
449
|
+
}
|
|
450
|
+
const prompt = buildMaintenancePrompt(mode, options, repoRoot);
|
|
451
|
+
if (options["print-prompt"] || options["dry-run"]) {
|
|
452
|
+
process.stdout.write(prompt);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const executor = resolveMaintenanceExecutor(options.executor);
|
|
457
|
+
let args;
|
|
458
|
+
if (executor === "codex") {
|
|
459
|
+
const modelArgs = options.model ? ["--model", String(options.model)] : [];
|
|
460
|
+
args = [
|
|
461
|
+
"-C", repoRoot,
|
|
462
|
+
"--sandbox", "danger-full-access",
|
|
463
|
+
"--ask-for-approval", "on-request",
|
|
464
|
+
"--dangerously-bypass-hook-trust",
|
|
465
|
+
...modelArgs,
|
|
466
|
+
prompt,
|
|
467
|
+
];
|
|
468
|
+
} else {
|
|
469
|
+
const modelArgs = options.model ? ["--config", `model=${String(options.model)}`] : [];
|
|
470
|
+
args = ["--permission-mode", "default", ...modelArgs, prompt];
|
|
471
|
+
}
|
|
472
|
+
const result = spawnSync(executor, args, {
|
|
473
|
+
cwd: repoRoot,
|
|
474
|
+
env: {
|
|
475
|
+
...process.env,
|
|
476
|
+
DEV_MEMORY_MAINTENANCE_AGENT: "1",
|
|
477
|
+
DEV_MEMORY_MAINTENANCE_MODE: mode,
|
|
478
|
+
},
|
|
479
|
+
stdio: "inherit",
|
|
480
|
+
});
|
|
481
|
+
if (result.error) fail(`unable to start ${executor}: ${result.error.message}`);
|
|
482
|
+
if (result.status !== 0) process.exit(result.status || 1);
|
|
483
|
+
}
|
|
484
|
+
|
|
379
485
|
function branchScript() {
|
|
380
486
|
return packageScript("lib", "dev_memory_branch.py");
|
|
381
487
|
}
|
|
@@ -692,8 +798,11 @@ function printHelp() {
|
|
|
692
798
|
dev-memory-cli install-hooks --all [--repo PATH] [--global]
|
|
693
799
|
dev-memory-cli ui [--port N] [--host HOST] [--no-open] [--read-only]
|
|
694
800
|
dev-memory-cli workspace <show|primary> [...]
|
|
801
|
+
dev-memory-cli init [--repo PATH] [--branch NAME]
|
|
695
802
|
dev-memory-cli read <show|search> [...]
|
|
803
|
+
dev-memory-cli maintain <tidy|archive> [...] # starts a dedicated interactive agent
|
|
696
804
|
dev-memory-cli context <show|...> [...]
|
|
805
|
+
# Low-level mutation/admin commands used by session-scan and maintenance agents:
|
|
697
806
|
dev-memory-cli capture <record|show|sync-working-tree|record-head|suggest-kind|classify> [...]
|
|
698
807
|
dev-memory-cli setup <init|merge-unsorted|mark-completed> [...]
|
|
699
808
|
dev-memory-cli graduate <dry-run|apply|index> [...]
|
|
@@ -719,6 +828,14 @@ function main() {
|
|
|
719
828
|
commandPySubcommand(command, argv.slice(1));
|
|
720
829
|
return;
|
|
721
830
|
}
|
|
831
|
+
if (command === "init") {
|
|
832
|
+
commandPySubcommand("setup", ["init", ...argv.slice(1)]);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (command === "maintain") {
|
|
836
|
+
commandMaintain(argv.slice(1));
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
722
839
|
if (command === "branch") {
|
|
723
840
|
commandBranch(argv.slice(1));
|
|
724
841
|
return;
|
package/hooks/README.md
CHANGED
|
@@ -130,7 +130,9 @@ dev-memory-cli session-scan install
|
|
|
130
130
|
dev-memory-cli session-scan status
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
任务默认每天本地时间 03:00
|
|
133
|
+
任务默认每天本地时间 03:00 和 13:00 运行。LaunchAgent 使用 `session-scan run --scheduled`,运行前检查 macOS HID 空闲时长;最近 10 分钟有键鼠输入时写入 `skipped_active` 记录并退出,不读取会话正文、不调用模型。检测不可用时默认保守跳过。手工执行 `session-scan run` 不启用活跃检测。
|
|
134
|
+
|
|
135
|
+
首次扫描只回看最近 3 天;后续使用持久化字节游标补扫上次成功处理后产生的全部数据。`install-hooks codex` 与 `session-scan install` 相互独立。
|
|
134
136
|
|
|
135
137
|
扫描器只提取尚未处理的 user/assistant 语义消息,不限制消息数量,也不截断单条消息。材料超过单次模型上下文时按顺序分块,每个分块都进入中间摘要,再结合现有 memory 生成最终结构化结果。游标只在最终结果成功应用后推进。
|
|
136
138
|
|
|
@@ -143,6 +145,10 @@ dev-memory-cli session-scan status
|
|
|
143
145
|
"session_scan": {
|
|
144
146
|
"executor": "auto",
|
|
145
147
|
"order": ["coco", "codex", "claude"],
|
|
148
|
+
"schedule_times": ["03:00", "13:00"],
|
|
149
|
+
"skip_when_computer_active": true,
|
|
150
|
+
"active_within_minutes": 10,
|
|
151
|
+
"activity_check_fail_closed": true,
|
|
146
152
|
"chunk_chars": 60000,
|
|
147
153
|
"idle_minutes": 60,
|
|
148
154
|
"first_lookback_days": 3,
|
|
@@ -162,9 +168,14 @@ dev-memory-cli session-scan config show
|
|
|
162
168
|
dev-memory-cli session-scan config set-executor codex
|
|
163
169
|
dev-memory-cli session-scan config set-model codex <model>
|
|
164
170
|
dev-memory-cli session-scan config set-profile codex <profile>
|
|
171
|
+
dev-memory-cli session-scan config set-schedule 03:00 13:00
|
|
172
|
+
dev-memory-cli session-scan config set-active-minutes 10
|
|
173
|
+
dev-memory-cli session-scan config set-active-check on
|
|
165
174
|
dev-memory-cli session-scan config validate
|
|
166
175
|
```
|
|
167
176
|
|
|
177
|
+
已安装定时任务时,`set-schedule` 会自动重载 LaunchAgent;活跃阈值和开关由每次运行动态读取,无需重装。
|
|
178
|
+
|
|
168
179
|
`auto` 只在命令不存在或 preset 被禁用时选择下一个执行器。模型调用失败不会自动换供应商,以免重复产生不可控费用。
|
|
169
180
|
|
|
170
181
|
### 递归防护
|
|
@@ -16,9 +16,11 @@ Subcommands:
|
|
|
16
16
|
|
|
17
17
|
import argparse
|
|
18
18
|
import difflib
|
|
19
|
+
import fcntl
|
|
19
20
|
import json
|
|
20
21
|
import os
|
|
21
22
|
import sys
|
|
23
|
+
from contextlib import contextmanager
|
|
22
24
|
from pathlib import Path
|
|
23
25
|
|
|
24
26
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
@@ -27,6 +29,7 @@ from dev_memory_common import (
|
|
|
27
29
|
AUTO_END,
|
|
28
30
|
AUTO_START,
|
|
29
31
|
PLACEHOLDER_MARKERS,
|
|
32
|
+
atomic_write_text,
|
|
30
33
|
append_log_event,
|
|
31
34
|
append_to_section,
|
|
32
35
|
asset_paths,
|
|
@@ -34,6 +37,7 @@ from dev_memory_common import (
|
|
|
34
37
|
collect_git_facts,
|
|
35
38
|
ensure_branch_paths_exist,
|
|
36
39
|
get_head_commit,
|
|
40
|
+
get_branch_paths,
|
|
37
41
|
get_setup_completed,
|
|
38
42
|
is_cross_branch_candidate,
|
|
39
43
|
join_sections,
|
|
@@ -140,7 +144,7 @@ def _append_with_separator(path, title, body, *, enforce_limit=True, max_entries
|
|
|
140
144
|
else (body_stripped, 0)
|
|
141
145
|
)
|
|
142
146
|
updated.append((title, bounded))
|
|
143
|
-
path
|
|
147
|
+
atomic_write_text(path, join_sections(prefix, updated))
|
|
144
148
|
return pruned
|
|
145
149
|
|
|
146
150
|
|
|
@@ -770,7 +774,7 @@ def _entry_mutation(paths, eid, *, new_text=None, delete=False):
|
|
|
770
774
|
|
|
771
775
|
new_sections = list(sections)
|
|
772
776
|
new_sections[section_idx] = (section_title, new_body)
|
|
773
|
-
target_path
|
|
777
|
+
atomic_write_text(target_path, join_sections(prefix, new_sections))
|
|
774
778
|
return {
|
|
775
779
|
"id": eid,
|
|
776
780
|
"file_key": file_key,
|
|
@@ -1100,7 +1104,7 @@ def _prune_repo_shared_section(path, section_title, *, max_entries, max_entry_ch
|
|
|
1100
1104
|
updated.append((existing_title, new_body))
|
|
1101
1105
|
if not changed:
|
|
1102
1106
|
return None
|
|
1103
|
-
path
|
|
1107
|
+
atomic_write_text(path, join_sections(prefix, updated))
|
|
1104
1108
|
return result
|
|
1105
1109
|
|
|
1106
1110
|
|
|
@@ -1168,7 +1172,7 @@ def prune_bounded_memory(paths, *, max_entries=None):
|
|
|
1168
1172
|
"max_entries": max_entries,
|
|
1169
1173
|
})
|
|
1170
1174
|
if changed:
|
|
1171
|
-
path
|
|
1175
|
+
atomic_write_text(path, join_sections(prefix, updated))
|
|
1172
1176
|
return touched
|
|
1173
1177
|
|
|
1174
1178
|
|
|
@@ -1278,6 +1282,8 @@ def _decision_content(item):
|
|
|
1278
1282
|
if isinstance(item, str):
|
|
1279
1283
|
return item.strip()
|
|
1280
1284
|
if isinstance(item, dict):
|
|
1285
|
+
if not _decision_summary(item):
|
|
1286
|
+
return ""
|
|
1281
1287
|
return decision_body(item)
|
|
1282
1288
|
return ""
|
|
1283
1289
|
|
|
@@ -1962,6 +1968,45 @@ def command_apply_summary_output(args):
|
|
|
1962
1968
|
"reason": item.get("reason"),
|
|
1963
1969
|
})
|
|
1964
1970
|
|
|
1971
|
+
# Repair schema placeholder prose left by malformed historical summary
|
|
1972
|
+
# output. Valid entries are top-level bullets; these exact unbulleted
|
|
1973
|
+
# words are generator scaffolding and should never survive in memory.
|
|
1974
|
+
placeholder_words = {
|
|
1975
|
+
"decision", "summary", "reason", "impact", "risk", "mitigation",
|
|
1976
|
+
"term", "definition", "name", "url", "note", "label", "path", "content",
|
|
1977
|
+
}
|
|
1978
|
+
for file_key in ("decisions", "risks", "glossary", "repo_glossary"):
|
|
1979
|
+
path = paths[file_key]
|
|
1980
|
+
if not path.exists():
|
|
1981
|
+
continue
|
|
1982
|
+
content = path.read_text(encoding="utf-8")
|
|
1983
|
+
prefix, sections = split_sections(content)
|
|
1984
|
+
updated = []
|
|
1985
|
+
removed = 0
|
|
1986
|
+
for section_title, section_body in sections:
|
|
1987
|
+
kept_lines = []
|
|
1988
|
+
section_removed = 0
|
|
1989
|
+
for line in section_body.splitlines():
|
|
1990
|
+
stripped = line.strip().strip("::").lower()
|
|
1991
|
+
is_bullet = line.lstrip().startswith(("- ", "* ", "+ "))
|
|
1992
|
+
if stripped in placeholder_words and not is_bullet:
|
|
1993
|
+
section_removed += 1
|
|
1994
|
+
continue
|
|
1995
|
+
kept_lines.append(line)
|
|
1996
|
+
if section_removed:
|
|
1997
|
+
rec = {
|
|
1998
|
+
"file": _label(file_key),
|
|
1999
|
+
"section": section_title,
|
|
2000
|
+
"mode": "remove-placeholder-orphans",
|
|
2001
|
+
"removed": section_removed,
|
|
2002
|
+
}
|
|
2003
|
+
touched.append(rec)
|
|
2004
|
+
actions.append({"op": "remove-placeholder-orphans", **rec})
|
|
2005
|
+
removed += section_removed
|
|
2006
|
+
updated.append((section_title, "\n".join(kept_lines).strip()))
|
|
2007
|
+
if removed:
|
|
2008
|
+
atomic_write_text(path, join_sections(prefix, updated))
|
|
2009
|
+
|
|
1965
2010
|
bounded = prune_bounded_memory(paths)
|
|
1966
2011
|
if bounded:
|
|
1967
2012
|
touched.extend(bounded)
|
|
@@ -2177,6 +2222,20 @@ def _add_common_args(parser):
|
|
|
2177
2222
|
parser.add_argument("--branch", help="Branch name. Defaults to the current checked-out branch")
|
|
2178
2223
|
|
|
2179
2224
|
|
|
2225
|
+
@contextmanager
|
|
2226
|
+
def _repo_command_lock(args):
|
|
2227
|
+
"""Serialize capture commands per repo so read-modify-write stays coherent."""
|
|
2228
|
+
_, _, _, _, _, repo_dir, _ = get_branch_paths(args.repo, args.context_dir, args.branch)
|
|
2229
|
+
lock_path = repo_dir / "jobs" / "capture.lock"
|
|
2230
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2231
|
+
with lock_path.open("a+", encoding="utf-8") as lock_stream:
|
|
2232
|
+
fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
|
|
2233
|
+
try:
|
|
2234
|
+
yield
|
|
2235
|
+
finally:
|
|
2236
|
+
fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
|
|
2237
|
+
|
|
2238
|
+
|
|
2180
2239
|
def main():
|
|
2181
2240
|
parser = argparse.ArgumentParser(
|
|
2182
2241
|
description="Unified write entrypoint for dev-memory repo+branch memory (merges old sync + update).",
|
|
@@ -2297,7 +2356,11 @@ def main():
|
|
|
2297
2356
|
"sync-working-tree": command_sync_working_tree,
|
|
2298
2357
|
"record-head": command_record_head,
|
|
2299
2358
|
}
|
|
2300
|
-
|
|
2359
|
+
handler = handlers[args.command]
|
|
2360
|
+
if args.command == "suggest-kind":
|
|
2361
|
+
return handler(args)
|
|
2362
|
+
with _repo_command_lock(args):
|
|
2363
|
+
return handler(args)
|
|
2301
2364
|
except Exception as exc:
|
|
2302
2365
|
print(json.dumps({"error": str(exc)}, ensure_ascii=False), file=sys.stderr)
|
|
2303
2366
|
return 1
|
package/lib/dev_memory_common.py
CHANGED
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import re
|
|
7
7
|
import shutil
|
|
8
8
|
import subprocess
|
|
9
|
+
import tempfile
|
|
9
10
|
import time
|
|
10
11
|
import uuid
|
|
11
12
|
from collections import Counter
|
|
@@ -511,11 +512,29 @@ def asset_paths(repo_dir, branch_dir):
|
|
|
511
512
|
|
|
512
513
|
def ensure_file(path, content):
|
|
513
514
|
if not path.exists():
|
|
514
|
-
path
|
|
515
|
+
atomic_write_text(path, content)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def atomic_write_text(path, content):
|
|
519
|
+
"""Atomically replace a UTF-8 text file without exposing partial bytes."""
|
|
520
|
+
path = Path(path)
|
|
521
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
522
|
+
mode = (path.stat().st_mode & 0o777) if path.exists() else 0o644
|
|
523
|
+
fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
|
524
|
+
tmp_path = Path(tmp_name)
|
|
525
|
+
try:
|
|
526
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
527
|
+
stream.write(content)
|
|
528
|
+
stream.flush()
|
|
529
|
+
os.fsync(stream.fileno())
|
|
530
|
+
os.chmod(tmp_path, mode)
|
|
531
|
+
os.replace(tmp_path, path)
|
|
532
|
+
finally:
|
|
533
|
+
tmp_path.unlink(missing_ok=True)
|
|
515
534
|
|
|
516
535
|
|
|
517
536
|
def write_json(path, payload):
|
|
518
|
-
path
|
|
537
|
+
atomic_write_text(path, json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
519
538
|
|
|
520
539
|
|
|
521
540
|
def read_json(path):
|
|
@@ -1705,7 +1724,7 @@ def ensure_progress_auto_block(path):
|
|
|
1705
1724
|
else:
|
|
1706
1725
|
updated = content.rstrip() + auto_section
|
|
1707
1726
|
|
|
1708
|
-
path
|
|
1727
|
+
atomic_write_text(path, updated + ("" if updated.endswith("\n") else "\n"))
|
|
1709
1728
|
return path.read_text(encoding="utf-8")
|
|
1710
1729
|
|
|
1711
1730
|
|
|
@@ -1762,7 +1781,7 @@ def upsert_markdown_section(path, title, body):
|
|
|
1762
1781
|
updated.append((existing_title, existing_body))
|
|
1763
1782
|
if not replaced:
|
|
1764
1783
|
updated.append((title, body))
|
|
1765
|
-
path
|
|
1784
|
+
atomic_write_text(path, join_sections(prefix, updated))
|
|
1766
1785
|
|
|
1767
1786
|
|
|
1768
1787
|
def _section_is_placeholder_only(text):
|
|
@@ -1822,7 +1841,7 @@ def append_to_section(path, title, body, *, max_entries=None):
|
|
|
1822
1841
|
if not matched:
|
|
1823
1842
|
bounded, _pruned = limit_markdown_entries(body.strip(), max_entries=max_entries)
|
|
1824
1843
|
updated.append((title, bounded))
|
|
1825
|
-
path
|
|
1844
|
+
atomic_write_text(path, join_sections(prefix, updated))
|
|
1826
1845
|
|
|
1827
1846
|
|
|
1828
1847
|
def upsert_progress_section(path, title, body):
|
|
@@ -1849,7 +1868,7 @@ def upsert_progress_section(path, title, body):
|
|
|
1849
1868
|
if not replaced:
|
|
1850
1869
|
updated.append((title, body))
|
|
1851
1870
|
rewritten = join_sections(prefix, updated).rstrip() + "\n\n" + marker + after
|
|
1852
|
-
path
|
|
1871
|
+
atomic_write_text(path, rewritten)
|
|
1853
1872
|
|
|
1854
1873
|
|
|
1855
1874
|
def sync_progress(paths, facts):
|
|
@@ -1864,7 +1883,7 @@ def sync_progress(paths, facts):
|
|
|
1864
1883
|
)
|
|
1865
1884
|
current = ensure_progress_auto_block(paths["progress"])
|
|
1866
1885
|
updated = replace_auto_block(current, build_auto_block(facts))
|
|
1867
|
-
paths["progress"]
|
|
1886
|
+
atomic_write_text(paths["progress"], updated)
|
|
1868
1887
|
|
|
1869
1888
|
|
|
1870
1889
|
# ---------------------------------------------------------------------------
|
|
@@ -1918,7 +1937,7 @@ def append_log_event(log_path, action, *, kind=None, summary=None, details=None)
|
|
|
1918
1937
|
return
|
|
1919
1938
|
if not log_path.exists():
|
|
1920
1939
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1921
|
-
log_path
|
|
1940
|
+
atomic_write_text(log_path, template_log("unknown"))
|
|
1922
1941
|
|
|
1923
1942
|
header_parts = [f"## [{now_iso()}] {action}"]
|
|
1924
1943
|
if kind:
|
|
@@ -1938,7 +1957,7 @@ def append_log_event(log_path, action, *, kind=None, summary=None, details=None)
|
|
|
1938
1957
|
existing = log_path.read_text(encoding="utf-8")
|
|
1939
1958
|
if not existing.endswith("\n"):
|
|
1940
1959
|
existing += "\n"
|
|
1941
|
-
log_path
|
|
1960
|
+
atomic_write_text(log_path, existing + "\n" + block)
|
|
1942
1961
|
|
|
1943
1962
|
|
|
1944
1963
|
# ---------------------------------------------------------------------------
|