@finleywdx/ai-init 0.1.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/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/cli.js +437 -0
- package/package.json +52 -0
- package/templates/AGENTS.section.md +41 -0
- package/templates/agents-skills/finley-memory/SKILL.md +60 -0
- package/templates/agents-skills/finley-quality-gate/SKILL.md +62 -0
- package/templates/finley/config.yaml +51 -0
- package/templates/finley/scripts/add_session.py +280 -0
- package/templates/finley/scripts/gate.py +281 -0
- package/templates/finley/workspace/.gitkeep +0 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@finleywdx/ai-init",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI 工程化工作流初始化器:一条命令把 spec-kit 工作流 + 记忆层 + 质量门禁铺进你的新项目。",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"spec-kit",
|
|
8
|
+
"spec-driven-development",
|
|
9
|
+
"scaffold",
|
|
10
|
+
"cli",
|
|
11
|
+
"workflow",
|
|
12
|
+
"quality-gate"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"ai-init": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"templates",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"clean": "rm -rf dist",
|
|
31
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^15.0.0",
|
|
35
|
+
"execa": "^9.6.1",
|
|
36
|
+
"picocolors": "^1.1.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.10.0",
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "^5.7.2"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"author": "xfp",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/xufuping/Finley-main.git"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!-- FINLEY:START -->
|
|
2
|
+
# Finley 工作流(AI 工程化)
|
|
3
|
+
|
|
4
|
+
本项目由 [`@finleywdx/ai-init`](https://www.npmjs.com/package/@finleywdx/ai-init) 初始化,采用一套「spec-kit 规范驱动开发 + 质量门禁 + 跨会话记忆」的工作流。AI 助手在本项目工作时**必须遵循**下面的整体流程。
|
|
5
|
+
|
|
6
|
+
## 整体流程
|
|
7
|
+
|
|
8
|
+
1. **规范驱动开发(spec-kit)**:用 spec-kit 的斜杠命令按序推进,先想清楚「做什么/为什么」,再写代码:
|
|
9
|
+
- `/speckit.constitution` → 确立项目原则
|
|
10
|
+
- `/speckit.specify` → 写需求规格(不谈技术栈)
|
|
11
|
+
- `/speckit.clarify` → 澄清模糊点(plan 之前推荐)
|
|
12
|
+
- `/speckit.plan` → 定技术方案
|
|
13
|
+
- `/speckit.tasks` → 拆解可执行任务
|
|
14
|
+
- `/speckit.analyze` → 跨产物一致性检查(implement 之前推荐)
|
|
15
|
+
- `/speckit.implement` → 按任务实现
|
|
16
|
+
> Codex CLI 的 skills 模式用 `$speckit-*`;具体命令名以你的 agent 平台为准。
|
|
17
|
+
|
|
18
|
+
2. **质量门禁(提交前强制)**:`/speckit.implement` 写完代码后、提交前,**必须**运行:
|
|
19
|
+
```bash
|
|
20
|
+
python .finley/scripts/gate.py
|
|
21
|
+
```
|
|
22
|
+
它跑 lint / typecheck / test;有红项必须**修复后重跑**,直至全绿才算实现完成。详见 skill `finley-quality-gate`。
|
|
23
|
+
|
|
24
|
+
3. **收尾写 journal(记忆层)**:一段工作收尾时运行:
|
|
25
|
+
```bash
|
|
26
|
+
python .finley/scripts/add_session.py --title "..." --summary "..." --commit "..."
|
|
27
|
+
```
|
|
28
|
+
把本次进展写进 `.finley/workspace/<开发者>/`。**新会话开场**应主动读取该目录最近的 journal 恢复上下文。详见 skill `finley-memory`。
|
|
29
|
+
|
|
30
|
+
## 关键位置
|
|
31
|
+
|
|
32
|
+
- `.finley/config.yaml` — 质量门禁命令映射 + 开发者/journal 配置
|
|
33
|
+
- `.finley/scripts/gate.py` — 质量门禁脚本
|
|
34
|
+
- `.finley/scripts/add_session.py` — 记忆 journal 脚本
|
|
35
|
+
- `.finley/workspace/` — 按开发者存放的跨会话记忆 journal
|
|
36
|
+
- `.agents/skills/finley-quality-gate/` — 质量门禁 skill
|
|
37
|
+
- `.agents/skills/finley-memory/` — 记忆层 skill
|
|
38
|
+
- `.specify/` — spec-kit 自身的规格 / 模板 / 记忆(由 spec-kit 管理)
|
|
39
|
+
|
|
40
|
+
> 本区块由 Finley 管理:位于 FINLEY 起止标记之间的内容可能被后续 `ai-init` 更新覆盖;标记之外的内容会被保留。
|
|
41
|
+
<!-- FINLEY:END -->
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: finley-memory
|
|
3
|
+
description: "跨会话记忆:一段工作收尾时运行 python .finley/scripts/add_session.py 把本次做了什么写进 journal;新会话开场主动读取 .finley/workspace/<开发者>/ 最近的 journal 以恢复上下文。用于结束一段实现工作、或刚开始新会话需要了解此前进展时。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Finley 记忆层
|
|
7
|
+
|
|
8
|
+
Finley 的记忆层参考 Trellis 的做法:**把「本次会话发生了什么」持久化到仓库里的 journal**,让下一次会话(无论是你还是队友)不必从零理解项目。
|
|
9
|
+
|
|
10
|
+
对话会被压缩,文件不会。经验、进展、坑,都落盘到 `.finley/workspace/<开发者>/`。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 何时触发
|
|
15
|
+
|
|
16
|
+
- **会话开场(读)**:新会话开始、需要了解此前进展时——主动读取最近的 journal 恢复上下文。
|
|
17
|
+
- **工作收尾(写)**:完成一段实现 + 质量门禁通过后、准备结束这轮工作时——把这段会话写进 journal。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 步骤 A:会话开场恢复上下文(读)
|
|
22
|
+
|
|
23
|
+
1. 确认当前开发者目录:`.finley/workspace/<开发者>/`(开发者标识来自 git user.name / 环境变量 `FINLEY_DEVELOPER` / `.finley/config.yaml` 的 `developer`)。
|
|
24
|
+
2. 先读该目录的 `index.md` 看「当前状态」和「会话历史」表,定位活跃的 journal 文件。
|
|
25
|
+
3. 读最近的 `journal-N.md` 里最后 1~2 个 Session,了解上次做到哪、有什么待办和坑。
|
|
26
|
+
4. 用这些上下文指导本次工作,避免重复劳动。
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 例:查看当前开发者的索引与最新 journal
|
|
30
|
+
cat .finley/workspace/*/index.md
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 步骤 B:工作收尾写 journal(写)
|
|
34
|
+
|
|
35
|
+
在一段工作(通常是一次 `/speckit.implement` + 门禁全绿)收尾时运行:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python .finley/scripts/add_session.py \
|
|
39
|
+
--title "本次工作的标题" \
|
|
40
|
+
--summary "一两句话总结做了什么、结论是什么" \
|
|
41
|
+
--commit "abc1234,def5678"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
要点:
|
|
45
|
+
|
|
46
|
+
- `--title` 必填;`--summary` 写清楚「做了什么 + 结果」。
|
|
47
|
+
- `--commit` 填本次相关的 commit 哈希(逗号分隔);纯规划性会话可省略。
|
|
48
|
+
- 详细内容可用管道传入:`cat detail.md | python .finley/scripts/add_session.py --stdin --title "..."`。
|
|
49
|
+
- 开发者/分支默认自动探测,必要时用 `--developer` / `--branch` 覆盖。
|
|
50
|
+
|
|
51
|
+
脚本会把 Session 追加到 `journal-N.md`(超过 `journal.max_lines` 自动轮转到下一个),并更新 `index.md` 的会话历史表。
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 注意事项
|
|
56
|
+
|
|
57
|
+
- **只做手动收尾摘要**:Finley 记忆层不做会话开场的自动上下文汇总——恢复上下文靠你按「步骤 A」主动读 journal。
|
|
58
|
+
- **一段工作一条 Session**:不要把无关的多件事塞进一条;也不要每改一行就记一次。
|
|
59
|
+
- **个人 journal 独立**:按开发者分目录存放,天然避免团队冲突;共享的规范/规格才进公共目录评审。
|
|
60
|
+
- **与门禁配合**:推荐顺序是「实现 → `gate.py` 全绿 → `add_session.py` 收尾」。
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: finley-quality-gate
|
|
3
|
+
description: "强制质量门禁:在 /speckit.implement 写完代码后、提交前,必须运行 python .finley/scripts/gate.py 跑 lint/typecheck/test,红则修复再跑,直至全绿。用于任何写完或改完代码、准备结束一段实现工作或提交之前。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Finley 质量门禁
|
|
7
|
+
|
|
8
|
+
这是 Finley 工作流里**不可跳过**的一道关卡。它保证「写完的代码在提交前一定跑过 lint / typecheck / test」。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 何时触发
|
|
13
|
+
|
|
14
|
+
只要满足以下任一情况,就必须执行本 skill:
|
|
15
|
+
|
|
16
|
+
- 刚用 `/speckit.implement` 执行完一批任务、代码已落盘;
|
|
17
|
+
- 手动改完任意源码、准备结束这段实现工作;
|
|
18
|
+
- 准备 `git commit` / 发起提交之前。
|
|
19
|
+
|
|
20
|
+
> 原则:**没跑过 `gate.py` 且全绿,就不算「实现完成」,更不允许提交。**
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 步骤
|
|
25
|
+
|
|
26
|
+
### 1. 运行门禁
|
|
27
|
+
|
|
28
|
+
在项目根目录执行:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python .finley/scripts/gate.py
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
它会:
|
|
35
|
+
|
|
36
|
+
1. 读取 `.finley/config.yaml` 的 `quality.frontend` / `quality.backend` 命令;
|
|
37
|
+
2. 对占位命令自动探测(前端读 `package.json` 的 scripts,后端读 `pyproject.toml`);
|
|
38
|
+
3. 逐条运行 lint / typecheck / test,并在末尾聚合「通过 / 失败」清单。
|
|
39
|
+
|
|
40
|
+
### 2. 判读结果
|
|
41
|
+
|
|
42
|
+
- **全绿**(退出码 0):门禁通过,可以继续收尾 / 提交。
|
|
43
|
+
- **有红项**(退出码 1):按输出里列出的失败项定位问题。
|
|
44
|
+
- **未解析到命令**(退出码 2):说明门禁尚未配置——去 `.finley/config.yaml` 填入真实命令,或确认 `package.json` / `pyproject.toml` 里有可被探测的脚本,然后重跑。**不要因为"没有命令"就当作通过。**
|
|
45
|
+
|
|
46
|
+
### 3. 修复后重跑
|
|
47
|
+
|
|
48
|
+
若有失败:**先修复代码,再重新运行 `gate.py`**,如此循环,直到全部通过为止。不要用注释、跳过测试、放宽类型等方式绕过门禁。
|
|
49
|
+
|
|
50
|
+
### 4. 记录(可选)
|
|
51
|
+
|
|
52
|
+
门禁通过后,若这段工作到此收尾,按 `finley-memory` skill 运行 `add_session.py` 写 journal。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 注意事项
|
|
57
|
+
|
|
58
|
+
- **只增不减**:不要为了让门禁变绿而删除/禁用已有的 lint 规则、类型检查或测试。
|
|
59
|
+
- **占位不等于通过**:`.finley/config.yaml` 里形如 `<占位: ...>` 的命令会触发自动探测;探测不到时门禁会以退出码 2 失败,提醒你去配置。
|
|
60
|
+
- **前后端分离**:只想跑一侧时用 `--only frontend` 或 `--only backend`;但**提交前应跑全部**。
|
|
61
|
+
- **想先看会跑什么**:用 `python .finley/scripts/gate.py --dry-run` 预览命令而不执行。
|
|
62
|
+
- 门禁失败时进程会非零退出,CI / 提交前钩子可据此阻断。
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Finley 配置文件
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# 这是 @finleywdx/ai-init 铺进项目的「增量层」配置。
|
|
4
|
+
# 它只负责两件事:质量门禁命令映射(gate.py 读)、记忆 journal 设置(add_session.py 读)。
|
|
5
|
+
# spec-kit 自身的配置在 .specify/ 目录,与本文件互不干扰。
|
|
6
|
+
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# quality —— 质量门禁命令映射
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
# gate.py 会按下面的命令逐条执行;任一失败则整体门禁失败(非零退出)。
|
|
11
|
+
#
|
|
12
|
+
# 占位说明:值形如 "<占位: ...>" 表示尚未配置。gate.py 遇到占位会尝试
|
|
13
|
+
# 自动探测:
|
|
14
|
+
# - 前端(frontend)从目标项目的 package.json 的 scripts 里找
|
|
15
|
+
# lint / typecheck(或 type-check) / test。
|
|
16
|
+
# - 后端(backend)从 pyproject.toml 里探测工具链:
|
|
17
|
+
# 检测到 ruff -> `ruff check .`
|
|
18
|
+
# 检测到 mypy -> `mypy .`
|
|
19
|
+
# 检测到 pytest -> `pytest`
|
|
20
|
+
#
|
|
21
|
+
# 你可以随时把占位替换成项目里真实的命令,例如:
|
|
22
|
+
# frontend:
|
|
23
|
+
# lint: "npm run lint"
|
|
24
|
+
# typecheck: "npm run typecheck"
|
|
25
|
+
# test: "npm test"
|
|
26
|
+
quality:
|
|
27
|
+
# 前端 / Node 侧命令
|
|
28
|
+
frontend:
|
|
29
|
+
lint: "<占位: 前端 lint 命令,如 npm run lint>"
|
|
30
|
+
typecheck: "<占位: 前端类型检查命令,如 npm run typecheck>"
|
|
31
|
+
test: "<占位: 前端测试命令,如 npm test>"
|
|
32
|
+
# 后端 / Python 侧命令
|
|
33
|
+
backend:
|
|
34
|
+
lint: "<占位: 后端 lint 命令,如 ruff check .>"
|
|
35
|
+
typecheck: "<占位: 后端类型检查命令,如 mypy .>"
|
|
36
|
+
test: "<占位: 后端测试命令,如 pytest>"
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# developer —— 开发者标识
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# journal 会写到 .finley/workspace/<developer>/ 下。
|
|
42
|
+
# 留空时 add_session.py 自动探测:
|
|
43
|
+
# 环境变量 FINLEY_DEVELOPER > git config user.name > 系统用户名
|
|
44
|
+
developer: ""
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# journal —— 记忆层设置
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
journal:
|
|
50
|
+
# 单个 journal 文件超过该行数后,自动轮转到下一个 journal-N.md
|
|
51
|
+
max_lines: 2000
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Finley 记忆层 —— 在一段工作收尾时把「本次会话做了什么」写进 journal。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python .finley/scripts/add_session.py --title "标题" --summary "一句话总结"
|
|
7
|
+
python .finley/scripts/add_session.py --title "标题" --commit "abc123,def456" --summary "..."
|
|
8
|
+
python .finley/scripts/add_session.py --title "标题" --developer alice # 指定开发者
|
|
9
|
+
cat detail.md | python .finley/scripts/add_session.py --title "标题" --stdin
|
|
10
|
+
|
|
11
|
+
journal 写到 .finley/workspace/<开发者>/journal-N.md:
|
|
12
|
+
- 开发者标识解析顺序:--developer > 环境变量 FINLEY_DEVELOPER
|
|
13
|
+
> config.yaml 的 developer > git config user.name > 系统用户名。
|
|
14
|
+
- 单个 journal 超过 config.yaml 的 journal.max_lines(默认 2000)行后,
|
|
15
|
+
自动轮转到下一个 journal-(N+1).md。
|
|
16
|
+
- 同时维护该开发者目录下的 index.md(会话总数 / 活跃文件 / 历史表)。
|
|
17
|
+
|
|
18
|
+
纯标准库实现(os / re / subprocess / argparse),不依赖任何第三方包。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
JOURNAL_PREFIX = "journal-"
|
|
32
|
+
DEFAULT_MAX_LINES = 2000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# 配置读取(复用极简 YAML 思路,仅取需要的两个字段)
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
def _read_config(repo_root: Path) -> dict:
|
|
40
|
+
path = repo_root / ".finley" / "config.yaml"
|
|
41
|
+
result = {"developer": "", "max_lines": DEFAULT_MAX_LINES}
|
|
42
|
+
if not path.exists():
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
in_journal = False
|
|
46
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
47
|
+
line = raw.split("#", 1)[0].rstrip()
|
|
48
|
+
if not line.strip():
|
|
49
|
+
continue
|
|
50
|
+
indent = len(line) - len(line.lstrip(" "))
|
|
51
|
+
stripped = line.strip()
|
|
52
|
+
|
|
53
|
+
if indent == 0:
|
|
54
|
+
in_journal = stripped.startswith("journal:")
|
|
55
|
+
if stripped.startswith("developer:"):
|
|
56
|
+
val = stripped.split(":", 1)[1].strip().strip("'\"")
|
|
57
|
+
if val:
|
|
58
|
+
result["developer"] = val
|
|
59
|
+
elif in_journal and "max_lines" in stripped:
|
|
60
|
+
m = re.search(r"(\d+)", stripped)
|
|
61
|
+
if m:
|
|
62
|
+
result["max_lines"] = int(m.group(1))
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# 开发者与仓库定位
|
|
68
|
+
# =============================================================================
|
|
69
|
+
|
|
70
|
+
def get_repo_root() -> Path:
|
|
71
|
+
return Path(__file__).resolve().parent.parent.parent # .finley/scripts/ -> root
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _git(args: list[str], cwd: Path) -> str:
|
|
75
|
+
try:
|
|
76
|
+
out = subprocess.run(
|
|
77
|
+
["git", *args], cwd=str(cwd), capture_output=True, text=True, check=False
|
|
78
|
+
)
|
|
79
|
+
return out.stdout.strip()
|
|
80
|
+
except (OSError, subprocess.SubprocessError):
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resolve_developer(repo_root: Path, cli_value: str | None, config: dict) -> str:
|
|
85
|
+
for candidate in (
|
|
86
|
+
cli_value,
|
|
87
|
+
os.environ.get("FINLEY_DEVELOPER"),
|
|
88
|
+
config.get("developer"),
|
|
89
|
+
_git(["config", "user.name"], repo_root),
|
|
90
|
+
os.environ.get("USER") or os.environ.get("USERNAME"),
|
|
91
|
+
):
|
|
92
|
+
if candidate and str(candidate).strip():
|
|
93
|
+
# 规范化成适合作目录名的标识
|
|
94
|
+
return re.sub(r"[^\w.\-]+", "-", str(candidate).strip())
|
|
95
|
+
return "unknown"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def resolve_branch(repo_root: Path, cli_value: str | None) -> str | None:
|
|
99
|
+
if cli_value:
|
|
100
|
+
return cli_value
|
|
101
|
+
branch = _git(["branch", "--show-current"], repo_root)
|
|
102
|
+
return branch or None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# =============================================================================
|
|
106
|
+
# journal 文件管理
|
|
107
|
+
# =============================================================================
|
|
108
|
+
|
|
109
|
+
def get_latest_journal(dev_dir: Path) -> tuple[Path | None, int, int]:
|
|
110
|
+
"""返回 (最新 journal 路径, 编号, 行数)。目录为空时返回 (None, 0, 0)。"""
|
|
111
|
+
latest_file: Path | None = None
|
|
112
|
+
latest_num = -1
|
|
113
|
+
for f in dev_dir.glob(f"{JOURNAL_PREFIX}*.md"):
|
|
114
|
+
if not f.is_file():
|
|
115
|
+
continue
|
|
116
|
+
m = re.search(r"(\d+)$", f.stem)
|
|
117
|
+
if m and int(m.group(1)) > latest_num:
|
|
118
|
+
latest_num = int(m.group(1))
|
|
119
|
+
latest_file = f
|
|
120
|
+
if latest_file:
|
|
121
|
+
lines = len(latest_file.read_text(encoding="utf-8").splitlines())
|
|
122
|
+
return latest_file, latest_num, lines
|
|
123
|
+
return None, 0, 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_journal_file(dev_dir: Path, num: int, developer: str, today: str, max_lines: int) -> Path:
|
|
127
|
+
new_file = dev_dir / f"{JOURNAL_PREFIX}{num}.md"
|
|
128
|
+
if num <= 1:
|
|
129
|
+
cont_line = f"> 单文件约 {max_lines} 行后自动轮转到下一个 journal\n"
|
|
130
|
+
else:
|
|
131
|
+
cont_line = f"> 续自 `{JOURNAL_PREFIX}{num - 1}.md`(约 {max_lines} 行后轮转)\n"
|
|
132
|
+
header = (
|
|
133
|
+
f"# Journal - {developer} (Part {num})\n\n"
|
|
134
|
+
f"{cont_line}"
|
|
135
|
+
f"> 起始:{today}\n\n---\n"
|
|
136
|
+
)
|
|
137
|
+
new_file.write_text(header, encoding="utf-8")
|
|
138
|
+
return new_file
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_session_count(index_file: Path) -> int:
|
|
142
|
+
if not index_file.is_file():
|
|
143
|
+
return 0
|
|
144
|
+
for line in index_file.read_text(encoding="utf-8").splitlines():
|
|
145
|
+
if "会话总数" in line or "Total Sessions" in line:
|
|
146
|
+
m = re.search(r"[::]\s*(\d+)", line)
|
|
147
|
+
if m:
|
|
148
|
+
return int(m.group(1))
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def build_session_block(
|
|
153
|
+
session_num: int, title: str, today: str, task: str, summary: str,
|
|
154
|
+
detail: str, commit: str, branch: str | None,
|
|
155
|
+
) -> str:
|
|
156
|
+
if commit and commit != "-":
|
|
157
|
+
rows = "\n".join(
|
|
158
|
+
f"| `{c.strip()}` | (见 git log) |" for c in commit.split(",") if c.strip()
|
|
159
|
+
)
|
|
160
|
+
commit_table = "| Hash | 说明 |\n|------|------|\n" + rows
|
|
161
|
+
else:
|
|
162
|
+
commit_table = "(无提交 / 规划性会话)"
|
|
163
|
+
|
|
164
|
+
branch_line = f"\n**Branch**: `{branch}`" if branch else ""
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
f"\n\n## Session {session_num}: {title}\n\n"
|
|
168
|
+
f"**日期**: {today}\n"
|
|
169
|
+
f"**Task**: {task}{branch_line}\n\n"
|
|
170
|
+
f"### Summary\n\n{summary}\n\n"
|
|
171
|
+
f"### 主要改动\n\n{detail}\n\n"
|
|
172
|
+
f"### Git Commits\n\n{commit_table}\n\n"
|
|
173
|
+
f"### 状态\n\n✅ 已完成\n"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def write_index(index_file: Path, active_file: str, session_num: int, today: str,
|
|
178
|
+
title: str, commit: str, branch: str | None, developer: str) -> None:
|
|
179
|
+
commit_disp = "-"
|
|
180
|
+
if commit and commit != "-":
|
|
181
|
+
commit_disp = re.sub(r"([0-9a-f]{7,})", r"`\1`", commit.replace(",", ", "))
|
|
182
|
+
branch_disp = f"`{branch}`" if branch else "-"
|
|
183
|
+
|
|
184
|
+
header = (
|
|
185
|
+
f"# Journal 索引 - {developer}\n\n"
|
|
186
|
+
f"## 当前状态\n\n"
|
|
187
|
+
f"- **活跃文件**: `{active_file}`\n"
|
|
188
|
+
f"- **会话总数**: {session_num}\n"
|
|
189
|
+
f"- **最近活跃**: {today}\n\n"
|
|
190
|
+
f"## 会话历史\n\n"
|
|
191
|
+
f"| # | 日期 | 标题 | Commits | Branch |\n"
|
|
192
|
+
f"|---|------|------|---------|--------|\n"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
existing_rows: list[str] = []
|
|
196
|
+
if index_file.is_file():
|
|
197
|
+
capture = False
|
|
198
|
+
for line in index_file.read_text(encoding="utf-8").splitlines():
|
|
199
|
+
if line.startswith("|---") or re.match(r"^\|\s*#\s*\|", line):
|
|
200
|
+
capture = True
|
|
201
|
+
continue
|
|
202
|
+
if capture and line.startswith("|"):
|
|
203
|
+
existing_rows.append(line)
|
|
204
|
+
|
|
205
|
+
new_row = f"| {session_num} | {today} | {title} | {commit_disp} | {branch_disp} |"
|
|
206
|
+
body = "\n".join([new_row, *existing_rows])
|
|
207
|
+
index_file.write_text(header + body + "\n", encoding="utf-8")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# =============================================================================
|
|
211
|
+
# 主流程
|
|
212
|
+
# =============================================================================
|
|
213
|
+
|
|
214
|
+
def main() -> int:
|
|
215
|
+
parser = argparse.ArgumentParser(description="Finley:把本次会话写进 journal")
|
|
216
|
+
parser.add_argument("--title", required=True, help="会话标题")
|
|
217
|
+
parser.add_argument("--task", help="任务名(默认取 --title)")
|
|
218
|
+
parser.add_argument("--summary", default="(补充总结)", help="一句话总结")
|
|
219
|
+
parser.add_argument("--commit", default="-", help="逗号分隔的 commit 哈希")
|
|
220
|
+
parser.add_argument("--content-file", help="从文件读取详细内容")
|
|
221
|
+
parser.add_argument("--stdin", action="store_true", help="从标准输入读取详细内容")
|
|
222
|
+
parser.add_argument("--developer", help="开发者标识(默认自动探测)")
|
|
223
|
+
parser.add_argument("--branch", help="分支名(默认自动探测)")
|
|
224
|
+
args = parser.parse_args()
|
|
225
|
+
|
|
226
|
+
repo_root = get_repo_root()
|
|
227
|
+
config = _read_config(repo_root)
|
|
228
|
+
max_lines = int(config.get("max_lines", DEFAULT_MAX_LINES))
|
|
229
|
+
|
|
230
|
+
developer = resolve_developer(repo_root, args.developer, config)
|
|
231
|
+
branch = resolve_branch(repo_root, args.branch)
|
|
232
|
+
|
|
233
|
+
workspace = repo_root / ".finley" / "workspace"
|
|
234
|
+
dev_dir = workspace / developer
|
|
235
|
+
dev_dir.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
|
|
237
|
+
detail = "(补充细节)"
|
|
238
|
+
if args.content_file and Path(args.content_file).is_file():
|
|
239
|
+
detail = Path(args.content_file).read_text(encoding="utf-8")
|
|
240
|
+
elif args.stdin:
|
|
241
|
+
detail = sys.stdin.read()
|
|
242
|
+
|
|
243
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
244
|
+
index_file = dev_dir / "index.md"
|
|
245
|
+
session_num = get_session_count(index_file) + 1
|
|
246
|
+
|
|
247
|
+
block = build_session_block(
|
|
248
|
+
session_num, args.title, today, args.task or args.title,
|
|
249
|
+
args.summary, detail, args.commit, branch,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
journal_file, current_num, current_lines = get_latest_journal(dev_dir)
|
|
253
|
+
block_lines = len(block.splitlines())
|
|
254
|
+
|
|
255
|
+
if journal_file is None:
|
|
256
|
+
current_num = 1
|
|
257
|
+
journal_file = create_journal_file(dev_dir, 1, developer, today, max_lines)
|
|
258
|
+
elif current_lines + block_lines > max_lines:
|
|
259
|
+
current_num += 1
|
|
260
|
+
print(
|
|
261
|
+
f"[Finley] 当前 journal 超过 {max_lines} 行,轮转到 "
|
|
262
|
+
f"{JOURNAL_PREFIX}{current_num}.md",
|
|
263
|
+
file=sys.stderr,
|
|
264
|
+
)
|
|
265
|
+
journal_file = create_journal_file(dev_dir, current_num, developer, today, max_lines)
|
|
266
|
+
|
|
267
|
+
with journal_file.open("a", encoding="utf-8") as f:
|
|
268
|
+
f.write(block)
|
|
269
|
+
|
|
270
|
+
active_file = f"{JOURNAL_PREFIX}{current_num}.md"
|
|
271
|
+
write_index(index_file, active_file, session_num, today, args.title,
|
|
272
|
+
args.commit, branch, developer)
|
|
273
|
+
|
|
274
|
+
print(f"[Finley] ✅ 已记录 Session {session_num} 到 {dev_dir}/{active_file}")
|
|
275
|
+
print(f" 开发者:{developer} 分支:{branch or '-'}")
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
sys.exit(main())
|