Compare commits
86
Commits
5e84c8f5f0
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c55f5ae6d | ||
|
|
ad3be35445 | ||
|
|
48281c0985 | ||
|
|
b4524011b3 | ||
|
|
c42c4a8b09 | ||
|
|
186f140860 | ||
|
|
7b2ab8bf38 | ||
|
|
7ee80a87ee | ||
|
|
55fa8e7bf4 | ||
|
|
bd4b9b7a51 | ||
|
|
975ac7886e | ||
|
|
e9c021adbe | ||
|
|
814e81e037 | ||
|
|
67106b22c3 | ||
|
|
cd2d38f0b8 | ||
|
|
a1e68dafaf | ||
|
|
7b04cc6be6 | ||
|
|
2a24910790 | ||
|
|
4f76c40c1b | ||
|
|
01b890c21e | ||
|
|
2abe236c35 | ||
|
|
9fa1abbb6f | ||
|
|
49be143c11 | ||
|
|
f2e6176921 | ||
|
|
8391d4330a | ||
|
|
98c45c78d5 | ||
|
|
257f77ba68 | ||
|
|
1015f80d70 | ||
|
|
0ad1a173fe | ||
|
|
2dcc5f3ad0 | ||
|
|
847efa03b9 | ||
|
|
4ef3c39030 | ||
|
|
46e6c0ca7d | ||
|
|
1cff45bb1f | ||
|
|
9034ac978e | ||
|
|
aa3a5bfb43 | ||
|
|
6c5033579b | ||
|
|
110f5a078d | ||
|
|
e0df42c10f | ||
|
|
eb0e84981a | ||
|
|
58b9c04e52 | ||
|
|
82dfa3ebc7 | ||
|
|
4489908d8a | ||
|
|
26c9e62430 | ||
|
|
8fdf0bc5be | ||
|
|
a509e2272f | ||
|
|
96b08a1922 | ||
|
|
d5966eae21 | ||
|
|
d048b2d9de | ||
|
|
d81d58bc17 | ||
|
|
d1c83ac026 | ||
|
|
b43e0c87ec | ||
|
|
6ea9daea31 | ||
|
|
0790e300b4 | ||
|
|
6a273c4feb | ||
|
|
5d60170228 | ||
|
|
3b22ccfdac | ||
|
|
f02f5a1c5f | ||
|
|
9de3ecd56c | ||
|
|
136d2e5729 | ||
|
|
13666aeecd | ||
|
|
0237bcf55d | ||
|
|
eb79467a7f | ||
|
|
3a895690c0 | ||
|
|
c14b862488 | ||
|
|
9c94bca84e | ||
|
|
f8d02d228c | ||
|
|
07f04b513d | ||
|
|
0de2854365 | ||
|
|
d8d11bdc62 | ||
|
|
b8ac4bcb26 | ||
|
|
c8477d5cdf | ||
|
|
f8da961328 | ||
|
|
bad9f6563e | ||
|
|
14fb203290 | ||
|
|
72c26eb5fc | ||
|
|
91b95be2b0 | ||
|
|
eeea6927db | ||
|
|
b3ddc1fccb | ||
|
|
d1c13d8391 | ||
|
|
9ee6cf8efc | ||
|
|
1ef41e2aff | ||
|
|
a4028e4285 | ||
|
|
c191164f18 | ||
|
|
bbd03b7900 | ||
|
|
5c9748815e |
@@ -2,7 +2,7 @@
|
||||
"name": "doc",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "jsc",
|
||||
"name": "jsc-doc",
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/doc.git"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "doc",
|
||||
"description": "JSC 文件化 skills 的 Claude Code marketplace,提供 docker-compose 註解整理與 function XML 文件補齊。",
|
||||
"description": "JSC 文件化 skills 的 Claude Code marketplace,提供 docker-compose 註解整理、function XML 文件補齊、Gitea issue 文件流程、Gitea 通知處理與 worklog 工作紀錄。",
|
||||
"owner": {
|
||||
"name": "JSC"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "jsc",
|
||||
"name": "jsc-doc",
|
||||
"source": "./",
|
||||
"description": "JSC 文件化 skills:整理 docker-compose 註解、補齊 function XML 文件並重建 README 功能列表與使用範例。"
|
||||
"description": "JSC 文件化 skills:整理 docker-compose 註解、補齊 function XML 文件、同步 Gitea issue 文件流程、處理 Gitea 通知,並透過 worklog 記錄工作。"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jsc",
|
||||
"version": "0.1.2",
|
||||
"description": "JSC 文件化 skills(Claude Code / Codex / Antigravity / OpenCode):doc-docker 會整理 docker-compose.yaml 的行內註解與標題日期;doc-funcs 會為專案 functions 建立 .docs 草稿、補齊 XML 文件註解並重建 README 功能列表與使用範例;doc-issues-analyze-to-file 會讀取 Gitea issue、彙整需求、拆成多階段 issue 並產生實作草稿與交付留言;doc-issues-analyze 會把專案/議題/文件來源拆成小功能議題並依到期日實作;doc-issues-sync 會讀取 Gitea 專案或議題、依工作目錄檔案勾稽並同步議題的 TODO 進度與標籤並產生進度留言。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc: 前綴呼叫。",
|
||||
"name": "jsc-doc",
|
||||
"version": "0.0.4",
|
||||
"description": "JSC 文件化 skills(Claude Code / Codex / Antigravity / OpenCode / GitHub Copilot):docker 會整理 docker-compose.yaml 的行內註解與標題日期;funcs 會為專案 functions 建立 .docs 草稿、補齊 XML 文件註解並重建 README 功能列表與使用範例;issues-analyze-to-file 會讀取 Gitea issue、彙整需求、拆成多階段 issue 並產生實作草稿與交付留言;issues-analyze 會把專案/議題/文件來源(議題連同留言與附件一起讀取)拆成小功能議題(母議題須待所有子議題關閉後才可關閉)、分析完成後把屬於專案看板的議題移到「待處理」欄位並依到期日實作;issues-sync 會讀取 Gitea 專案或議題、依工作目錄檔案勾稽並同步議題的 TODO 進度、標籤與專案看板進度欄位並產生進度留言(指定關閉專案/專案完成時改為批次把專案所有議題搬到「已完成」並關閉);notifications 會讀取 Gitea 通知、依通知類型分組並照 REVIEW.md 流程處理,沒有流程時詢問使用者並把缺少流程的類別附加到 REVIEW.md;worklog 會以 README 定義的 headless CLI 將每輪工作整理成六欄工作紀錄並追加到 Gitea wiki。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc-doc: 前綴呼叫。",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
"name": "JSC"
|
||||
},
|
||||
"homepage": "https://gitea.jsc.idv.tw/plugins/doc",
|
||||
"repository": "https://gitea.jsc.idv.tw/plugins/doc.git",
|
||||
"keywords": ["doc", "documentation", "docker-compose", "xml-doc", "skills", "cross-tool", "jsc"]
|
||||
"keywords": ["doc", "documentation", "docker-compose", "xml-doc", "worklog", "skills", "cross-tool", "jsc"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc",
|
||||
"version": "0.1.2",
|
||||
"description": "JSC 文件化 skills:doc-docker 會整理 docker-compose.yaml 的行內註解與標題日期;doc-funcs 會為專案 functions 建立 .docs 草稿、補齊 XML 文件註解並重建 README 功能列表與使用範例;doc-issues-analyze-to-file 會讀取 Gitea issue、彙整需求、拆成多階段 issue 並產生實作草稿與交付留言;doc-issues-analyze 會把專案/議題/文件來源拆成小功能議題並依到期日實作;doc-issues-sync 會讀取 Gitea 專案或議題、依工作目錄檔案勾稽並同步議題的 TODO 進度與標籤並產生進度留言。所有 skills 以 SKILL.md 為共通標準。",
|
||||
"name": "jsc-doc",
|
||||
"version": "0.0.4",
|
||||
"description": "JSC 文件化 skills:docker 會整理 docker-compose.yaml 的行內註解與標題日期;funcs 會為專案 functions 建立 .docs 草稿、補齊 XML 文件註解並重建 README 功能列表與使用範例;issues-analyze-to-file 會讀取 Gitea issue、彙整需求、拆成多階段 issue 並產生實作草稿與交付留言;issues-analyze 會把專案/議題/文件來源(議題連同留言與附件一起讀取)拆成小功能議題(母議題須待所有子議題關閉後才可關閉)、分析完成後把屬於專案看板的議題移到「待處理」欄位並依到期日實作;issues-sync 會讀取 Gitea 專案或議題、依工作目錄檔案勾稽並同步議題的 TODO 進度、標籤與專案看板進度欄位並產生進度留言(指定關閉專案/專案完成時改為批次把專案所有議題搬到「已完成」並關閉);notifications 會讀取 Gitea 通知、依通知類型分組並照 REVIEW.md 流程處理,沒有流程時詢問使用者並把缺少流程的類別附加到 REVIEW.md;worklog 會以 README 定義的 headless CLI 將每輪工作整理成六欄工作紀錄並追加到 Gitea wiki(自動 Stop hook 僅相容 hook 環境支援)。所有 skills 以 SKILL.md 為共通標準。",
|
||||
"skills": "./skills"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# jsc — 共用 Skills(跨 AI 助理)
|
||||
# jsc-doc — 共用 Skills(跨 AI 助理)
|
||||
|
||||
本 repo 是一組以 **Agent Skills(`SKILL.md`)** 標準撰寫的共用 skills,可同時被 Claude Code、Codex、Antigravity、OpenCode 使用。
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
- 所有可用的 skills 位於本 repo 的 `skills/<name>/SKILL.md`。
|
||||
- 在處理任務前,先比對使用者需求與各 skill `SKILL.md` frontmatter 的 `description`,若相符請載入並依其步驟執行。
|
||||
- **呼叫慣例**:在 Claude Code 與 Antigravity 中,這些 skill 以 `/jsc:<name>` 呼叫;Codex 以 `$<name>`、OpenCode 由模型依描述自動觸發 — 兩者沒有 `/jsc:` 前綴,不需強制加。
|
||||
- **呼叫慣例**:在 Claude Code 與 Antigravity 中,這些 skill 以 `/jsc-doc:<name>` 呼叫;Codex 以 `$<name>`、OpenCode 由模型依描述自動觸發 — 兩者沒有 `/jsc-doc:` 前綴,不需強制加。
|
||||
- 完整清單與每個 skill 的用途,請見 `README.md` 的「Skills 目錄」。
|
||||
|
||||
## 慣例
|
||||
|
||||
@@ -1,55 +1,77 @@
|
||||
# jsc — 跨 AI 助理文件化 Skill 集合
|
||||
# jsc-doc — 跨 AI 助理文件化 Skill 集合
|
||||
|
||||
一個可同時被 **Claude Code、Codex、Antigravity、OpenCode** 安裝的文件化 skill 集合。
|
||||
目前內含五個實作型 skills:`doc-docker` 用於整理 `docker-compose.yaml` 的行內註解與標題日期;`doc-funcs` 用於掃描專案 functions、建立 `.docs/` 草稿、補齊 XML 文件註解並重建 README 功能列表與使用範例;`doc-issues-analyze-to-file` 用於讀取 Gitea issue、彙整需求並拆成多階段 issue、產生實作草稿與交付留言;`doc-issues-analyze` 用於把專案/議題/文件來源拆成小功能議題並依到期日實作;`doc-issues-sync` 用於讀取 Gitea 專案或議題,依工作目錄檔案勾稽並同步議題的 TODO 進度與標籤、產生進度留言。
|
||||
一個可同時被 **Claude Code、Codex、Antigravity、OpenCode、GitHub Copilot** 使用的文件化 skill 集合。
|
||||
目前內含七個實作型 skills:`docker` 用於整理 `docker-compose.yaml` 的行內註解與標題日期;`funcs` 用於掃描專案 functions、建立 `.docs/` 草稿、補齊 XML 文件註解並重建 README 功能列表與使用範例;`issues-analyze-to-file` 用於讀取 Gitea issue、彙整需求並拆成多階段 issue、產生實作草稿與交付留言;`issues-analyze` 用於把專案/議題/文件來源(議題連同留言與附件一起讀取)拆成小功能議題(母議題須待所有子議題關閉後才可關閉)、分析完成後把屬於專案看板的議題移到「待處理」欄位並依到期日排序留言(不實作程式碼,實作交由 code plugin 的 issues);`issues-sync` 用於讀取 Gitea 專案或議題,依工作目錄檔案勾稽並同步議題的 TODO 進度、標籤與專案看板進度欄位、產生進度留言;指定「關閉專案/專案完成」時改為批次把專案所有議題搬到「已完成」並關閉;`notifications` 用於讀取 Gitea 通知、依通知類型分組並照 `REVIEW.md` 流程處理,若 `REVIEW.md` 不存在則視為空白流程檔並直接詢問使用者如何定義流程;`worklog` 用於把 session stop 的內容透過 README 定義的 headless CLI 整理成六欄工作紀錄並追加到 Gitea wiki。
|
||||
核心是以 [Agent Skills(`SKILL.md`)](https://agentskills.io) 標準撰寫的共用 skills(唯一真實來源放在 `skills/`),
|
||||
搭配各助理各自的 plugin manifest,讓**同一個 repo** 可用各家**原生 plugin CLI** 安裝。
|
||||
在 Claude Code 與 Antigravity 中,skill 以 **`/jsc:` 前綴**呼叫(例如 `/jsc:doc-docker`)。
|
||||
在 Claude Code 與 Antigravity 中,skill 以 **`/jsc-doc:` 前綴**呼叫(例如 `/jsc-doc:docker`)。
|
||||
|
||||
---
|
||||
|
||||
## 前綴與呼叫方式
|
||||
|
||||
| 助理 | 安裝方式 | 呼叫 | `/jsc:` 前綴 |
|
||||
| 助理 | 安裝方式 | 呼叫 | `/jsc-doc:` 前綴 |
|
||||
| --- | --- | --- | --- |
|
||||
| Claude Code | `claude plugin`(marketplace) | `/jsc:<name>` 或自動觸發 | ✅ |
|
||||
| Claude Code | `claude plugin`(marketplace) | `/jsc-doc:<name>` 或自動觸發 | ✅ |
|
||||
| Codex | `codex plugin`(marketplace) | `$<name>` 或 `/skills` 選單 | ❌(用 `$name`) |
|
||||
| Antigravity | `agy plugin install` | `/jsc:<name>` 或自動觸發 | ✅ |
|
||||
| Antigravity | `agy plugin install` | `/jsc-doc:<name>` 或自動觸發 | ✅ |
|
||||
| OpenCode | skills 目錄(複製/clone) | 描述需求自動觸發 | ❌(依名稱) |
|
||||
| GitHub Copilot CLI | `copilot plugin`(marketplace) | 自然語言或 plugin skills | ❌(無 `/jsc-doc:` 前綴) |
|
||||
|
||||
> Codex 不支援自訂前綴(skill 以 `$name` 呼叫,例 `$doc-docker`);OpenCode 由模型依描述自動呼叫。兩者皆**不強制**前綴。
|
||||
> Codex 不支援自訂前綴(skill 以 `$name` 呼叫);OpenCode 由模型依描述自動呼叫;Copilot CLI 透過原生 plugin 安裝後以自然語言或 plugin skills 使用。三者皆**不強制**前綴。
|
||||
|
||||
---
|
||||
|
||||
## 目錄結構
|
||||
|
||||
同一個 repo 同時帶四種 manifest,彼此以路徑隔離、互不干擾;四家都讀同一份 `skills/`。
|
||||
同一個 repo 同時帶四種 manifest,彼此以路徑隔離、互不干擾;各助理都讀同一份 `skills/`。
|
||||
|
||||
```
|
||||
doc/
|
||||
├── .claude-plugin/
|
||||
│ ├── plugin.json # Claude 外掛定義(name: "jsc")
|
||||
│ ├── plugin.json # Claude 外掛定義(name: "jsc-doc")
|
||||
│ └── marketplace.json # Claude marketplace(name: "doc",source 指向本 repo)
|
||||
├── .codex-plugin/
|
||||
│ └── plugin.json # Codex 外掛定義(name: "jsc",skills: "./skills")
|
||||
│ └── plugin.json # Codex 外掛定義(name: "jsc-doc",skills: "./skills")
|
||||
├── .agents/plugins/
|
||||
│ └── marketplace.json # Codex marketplace(name: "doc",url source 指向本 repo)
|
||||
├── plugin.json # Antigravity 外掛定義(name: "jsc",skills: "./skills/")
|
||||
├── plugin.json # Antigravity 外掛定義(name: "jsc-doc",skills: "./skills/")
|
||||
├── hooks/
|
||||
│ └── hooks.json # Stop → worklog;Claude 用 plugin root,Codex fallback 到安裝 cache
|
||||
├── scripts/
|
||||
│ └── worklog/ # worklog 自動記錄的可執行元件(skill 與 hook 共用)
|
||||
│ ├── worklog.sh # 主流程:抽本輪 → 濃縮成六欄 → 遮蔽 → 追加到 wiki
|
||||
│ ├── wiki_api.py # Gitea wiki 讀寫、token 解析、append 重試、週頁命名
|
||||
│ └── transcript.py # transcript 本輪抽取、耗時估算與機密遮蔽
|
||||
├── skills/ # ★ 唯一真實來源:所有 skills
|
||||
│ ├── doc-docker/ # 對齊 docker-compose 註解(含 scripts/)
|
||||
│ ├── docker/ # 對齊 docker-compose 註解(含 scripts/)
|
||||
│ │ ├── SKILL.md
|
||||
│ │ └── scripts/
|
||||
│ ├── doc-funcs/SKILL.md # 為 function 補齊 XML 文件
|
||||
│ ├── doc-issues-analyze-to-file/SKILL.md # 讀 issue → 需求文件 → 拆階段 issue → 實作草稿 → 交付留言
|
||||
│ ├── doc-issues-analyze/SKILL.md # 讀來源 → 保存議題 → 小功能議題 → 排程實作 → PR
|
||||
│ └── doc-issues-sync/SKILL.md # 讀專案/議題 → 依工作目錄勾稽 TODO → 補 TODO/更新標籤 → 進度留言
|
||||
│ ├── funcs/ # 為 function 補齊 XML 文件、指令檔逐行註解(含 templates/)
|
||||
│ │ ├── SKILL.md
|
||||
│ │ └── templates/ # 指令檔開頭「用途/更新時間」標頭範本(command-header.md)
|
||||
│ ├── issues-analyze-to-file/SKILL.md # 讀 issue → 需求文件 → 拆階段 issue → 實作草稿 → 交付留言
|
||||
│ ├── issues-analyze/SKILL.md # 讀來源 → 保存議題 → 小功能議題(看板移待處理)→ 排序留言(不實作)
|
||||
│ ├── issues-sync/SKILL.md # 讀專案/議題 → 依工作目錄勾稽 TODO → 補 TODO/更新標籤/調整看板欄位 → 進度留言;關閉專案時批次搬「已完成」並關閉
|
||||
│ ├── notifications/SKILL.md # 讀取 Gitea 通知並依 REVIEW.md/空白流程處理
|
||||
│ └── worklog/SKILL.md # 工作證明自動記錄的操作與維護
|
||||
├── AGENTS.md # 跨助理共用指引
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### worklog 適用範圍
|
||||
|
||||
| 功能 | Claude Code | Codex | Antigravity | OpenCode | GitHub Copilot |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `Stop` hook 自動記錄 | ✅ | ✅ 需可讀 Codex session JSONL | ❌ | ❌ | ❌ |
|
||||
| `worklog` 手動模式 | ✅ | ⚠️ 需安裝後保留 `scripts/` | ⚠️ 同左 | ⚠️ 需完整 plugin 目錄 | ⚠️ 需安裝後保留 `scripts/` |
|
||||
| 摘要 CLI | `claude -p` | `codex exec` | `agy -p` | `opencode run` | `copilot -p` |
|
||||
|
||||
`worklog` 自動記錄仍依賴相容的 Stop hook 與 transcript JSONL 結構;Claude Code 會用 `CLAUDE_PLUGIN_ROOT` 定位腳本,Codex 會從 `~/.codex/plugins/cache/doc/jsc-doc` 找已安裝的 worklog 腳本並解析 Codex session JSONL。摘要執行器可用 `WORKLOG_CLI=auto|claude|codex|agy|opencode|copilot` 指定;預設 `auto` 會先依目前 hook/session 環境判斷正在使用的 CLI,判斷不到或該 CLI 不可執行時才 fallback 到已安裝工具。
|
||||
|
||||
---
|
||||
|
||||
## 安裝 / 更新 / 移除(各家原生 plugin CLI)
|
||||
## 安裝 / 更新 / 移除(各助理)
|
||||
|
||||
> 指令中的 repo 網址:`https://gitea.jsc.idv.tw/plugins/doc.git`
|
||||
>
|
||||
@@ -62,39 +84,39 @@ doc/
|
||||
```bash
|
||||
# 安裝
|
||||
claude plugin marketplace add https://gitea.jsc.idv.tw/plugins/doc.git
|
||||
claude plugin install jsc@doc
|
||||
claude plugin install jsc-doc@doc
|
||||
|
||||
# 更新
|
||||
claude plugin marketplace update doc
|
||||
claude plugin update jsc@doc
|
||||
claude plugin update jsc-doc@doc
|
||||
|
||||
# 移除
|
||||
claude plugin uninstall jsc@doc
|
||||
claude plugin uninstall jsc-doc@doc
|
||||
claude plugin marketplace remove doc
|
||||
```
|
||||
|
||||
- 工作階段內 slash 版(等價):把 `claude plugin` 換成 `/plugin`。
|
||||
- 本機開發(免 push):`claude plugin marketplace add C:\Users\h3285\source\repos.plugins\doc`(本地路徑)後再 install。
|
||||
- **呼叫**:`/jsc:<name>`(例 `/jsc:doc-docker`)。
|
||||
- **呼叫**:`/jsc-doc:<name>`(例 `/jsc-doc:docker`)。
|
||||
|
||||
### Codex
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
codex plugin marketplace add https://gitea.jsc.idv.tw/plugins/doc.git
|
||||
codex plugin add jsc@doc
|
||||
codex plugin add jsc-doc@doc
|
||||
|
||||
# 更新(重新抓取 marketplace 的 git 快照)
|
||||
codex plugin marketplace upgrade doc
|
||||
|
||||
# 移除
|
||||
codex plugin remove jsc@doc
|
||||
codex plugin remove jsc-doc@doc
|
||||
codex plugin marketplace remove doc
|
||||
```
|
||||
|
||||
- 安裝 token `jsc@doc` = plugin 名(`.codex-plugin/plugin.json` 的 `name`)@ marketplace 名(`.agents/plugins/marketplace.json` 的 `name`)。
|
||||
- 安裝 token `jsc-doc@doc` = plugin 名(`.codex-plugin/plugin.json` 的 `name`)@ marketplace 名(`.agents/plugins/marketplace.json` 的 `name`)。
|
||||
- 本 repo 的 Codex marketplace 以 `url` 來源指向自己,故 Codex **一律從 gitea 安裝**(需先 push);安裝後重啟 Codex。
|
||||
- **呼叫**:`$<name>`(例 `$doc-docker`),或用 `/skills` 選單。
|
||||
- **呼叫**:`$<name>`(例 `$docker`),或用 `/skills` 選單。
|
||||
|
||||
### Antigravity(`agy`)
|
||||
|
||||
@@ -107,16 +129,16 @@ agy plugin install ~/plugins/doc
|
||||
|
||||
# 更新(agy 無 update 子指令 → git pull 後重裝)
|
||||
git -C ~/plugins/doc pull
|
||||
agy plugin uninstall jsc
|
||||
agy plugin uninstall jsc-doc
|
||||
agy plugin install ~/plugins/doc
|
||||
|
||||
# 移除
|
||||
agy plugin uninstall jsc
|
||||
agy plugin uninstall jsc-doc
|
||||
```
|
||||
|
||||
- 若把 skills 放到 GitHub,則可直接 `agy plugin install https://github.com/<owner>/<repo>`。
|
||||
- 其他:`agy plugin list`、`agy plugin enable jsc` / `disable jsc`、`agy plugin validate <path>`。安裝後重啟工作階段。
|
||||
- **呼叫**:`/jsc:<name>`(例 `/jsc:doc-docker`)或依描述自動觸發。
|
||||
- 其他:`agy plugin list`、`agy plugin enable jsc-doc` / `disable jsc-doc`、`agy plugin validate <path>`。安裝後重啟工作階段。
|
||||
- **呼叫**:`/jsc-doc:<name>`(例 `/jsc-doc:docker`)或依描述自動觸發。
|
||||
|
||||
### OpenCode
|
||||
|
||||
@@ -125,39 +147,65 @@ OpenCode 會讀 `~/.config/opencode/skills/`(也會讀 `~/.claude/skills/`、`
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
git clone https://gitea.jsc.idv.tw/plugins/doc.git ~/jsc-plugin
|
||||
git clone https://gitea.jsc.idv.tw/plugins/doc.git ~/plugins/doc
|
||||
mkdir -p ~/.config/opencode/skills
|
||||
cp -r ~/jsc-plugin/skills/* ~/.config/opencode/skills/
|
||||
cp -r ~/plugins/doc/skills/* ~/.config/opencode/skills/
|
||||
|
||||
# 更新
|
||||
git -C ~/jsc-plugin pull
|
||||
cp -r ~/jsc-plugin/skills/* ~/.config/opencode/skills/
|
||||
git -C ~/plugins/doc pull
|
||||
cp -r ~/plugins/doc/skills/* ~/.config/opencode/skills/
|
||||
|
||||
# 移除
|
||||
rm -rf ~/.config/opencode/skills/doc-docker ~/.config/opencode/skills/doc-funcs ~/.config/opencode/skills/doc-issues-analyze-to-file ~/.config/opencode/skills/doc-issues-analyze ~/.config/opencode/skills/doc-issues-sync
|
||||
rm -rf ~/.config/opencode/skills/docker ~/.config/opencode/skills/funcs ~/.config/opencode/skills/issues-analyze-to-file ~/.config/opencode/skills/issues-analyze ~/.config/opencode/skills/issues-sync ~/.config/opencode/skills/worklog
|
||||
```
|
||||
|
||||
> **worklog 在 OpenCode 的 skills 目錄安裝不可用**:上面的複製只帶 `skills/`,不含 `scripts/` 與 `hooks/`,worklog 的所有模式都會失敗;若以完整 plugin 目錄執行並能解析 `scripts/worklog`,可用 `WORKLOG_CLI=opencode` 作為摘要 CLI。
|
||||
|
||||
> **Windows PowerShell**:`cp -r A B` → `Copy-Item A B -Recurse -Force`、`rm -rf X` → `Remove-Item X -Recurse -Force`、`~` → `$HOME`。
|
||||
|
||||
- **呼叫**:直接描述需求,模型會依 skill 描述自動透過 skill 工具呼叫。
|
||||
|
||||
### GitHub Copilot CLI
|
||||
|
||||
Copilot CLI 支援與 Claude Code 類似的原生 plugin / marketplace 指令,可直接從 marketplace 安裝、更新與移除本 plugin。
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
copilot plugin marketplace add https://gitea.jsc.idv.tw/plugins/doc.git
|
||||
copilot plugin install jsc-doc@doc
|
||||
|
||||
# 更新
|
||||
copilot plugin marketplace update doc
|
||||
copilot plugin update jsc-doc@doc
|
||||
|
||||
# 移除
|
||||
copilot plugin uninstall jsc-doc@doc
|
||||
copilot plugin marketplace remove doc
|
||||
```
|
||||
|
||||
- 安裝 token `jsc-doc@doc` = plugin 名(plugin manifest 的 `name`)@ marketplace 名。
|
||||
- `copilot plugin marketplace add` 支援 GitHub `owner/repo`、git URL 與本地路徑;Gitea repo 可用上方 HTTPS URL。
|
||||
- **呼叫**:在 Copilot CLI 中用自然語言描述需求,例如 `copilot -i "請使用 docker 整理 docker-compose 註解"`。
|
||||
- `worklog` 的 `Stop` hook 自動記錄仍只有相容 hook 環境會實際執行;Copilot CLI 可作為 `WORKLOG_CLI=copilot` 摘要執行器,但不會執行 Claude Code hook。
|
||||
|
||||
---
|
||||
|
||||
## 用 CLI 直接執行 skill(headless / 一次性)
|
||||
|
||||
安裝好之後,不必進互動介面,一行指令就能叫某個 skill 跑完並印出結果:
|
||||
|
||||
| 助理 | headless 指令 | 執行 `doc-docker` skill |
|
||||
| 助理 | headless 指令 | 執行 `docker` skill |
|
||||
| --- | --- | --- |
|
||||
| Claude Code | `claude -p "<prompt>"` | `claude -p "/jsc:doc-docker"` |
|
||||
| Codex | `codex exec "<prompt>"` | `codex exec '$doc-docker'` |
|
||||
| Antigravity | `agy -p "<prompt>"` | `agy -p "/jsc:doc-docker"` |
|
||||
| Claude Code | `claude -p "<prompt>"` | `claude -p "/jsc-doc:docker"` |
|
||||
| Codex | `codex exec "<prompt>"` | `codex exec '$docker'` |
|
||||
| Antigravity | `agy -p "<prompt>"` | `agy -p "/jsc-doc:docker"` |
|
||||
| OpenCode | `opencode run "<message>"` | `opencode run "整理 docker-compose 註解"` |
|
||||
| GitHub Copilot CLI | `copilot -p "<message>"` | `copilot -p "整理 docker-compose 註解"` |
|
||||
|
||||
- Claude / Antigravity 支援 `/jsc:` 前綴,直接 `-p "/jsc:<name>"` 即可。
|
||||
- Codex 以 `$<name>` 觸發;在 shell 請用**單引號**避免 `$` 被展開:`codex exec '$doc-docker'`。
|
||||
- OpenCode 沒有前綴,用自然語言描述需求,模型會自動透過 skill 工具呼叫。
|
||||
- 帶引數就接在後面,例如 `claude -p "/jsc:doc-docker docker-compose.yaml"`、`codex exec '$doc-docker docker-compose.yaml'`。
|
||||
- Claude / Antigravity 支援 `/jsc-doc:` 前綴,直接 `-p "/jsc-doc:<name>"` 即可。
|
||||
- Codex 以 `$<name>` 觸發;在 shell 請用**單引號**避免 `$` 被展開:`codex exec '$docker'`。
|
||||
- OpenCode 與 Copilot 沒有前綴,用自然語言描述需求;Copilot CLI 會讀取已安裝 plugin 提供的 skills。
|
||||
- 帶引數就接在後面,例如 `claude -p "/jsc-doc:docker docker-compose.yaml"`、`codex exec '$docker docker-compose.yaml'`。
|
||||
|
||||
---
|
||||
|
||||
@@ -168,61 +216,78 @@ rm -rf ~/.config/opencode/skills/doc-docker ~/.config/opencode/skills/doc-funcs
|
||||
|
||||
<!-- JSC-SKILLS:START -->
|
||||
|
||||
### `doc-docker`
|
||||
### `docker`
|
||||
|
||||
整理並對齊 `docker-compose.yaml` 的行內註解與標題區塊,優先透過內建 shell/awk 腳本批次處理或處理指定檔案。當使用者要對齊 docker-compose 註解、整理 compose 檔註解欄位、更新 compose 標題日期,或提到 docker-compose、dc-tidy、align_comments、註解對齊時使用此 skill。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc:doc-docker`
|
||||
- **Codex**:`$doc-docker`,或用 `/skills` 選單
|
||||
- **Claude Code / Antigravity**:`/jsc-doc:docker`
|
||||
- **Codex**:`$docker`,或用 `/skills` 選單
|
||||
- **OpenCode**:描述需求自動觸發
|
||||
|
||||
### `doc-funcs`
|
||||
### `funcs`
|
||||
|
||||
掃描目前專案所有可文件化的 function/method,建立 `.docs/doc-funcs-index.md` 與逐 function 草稿,再依草稿補齊 XML documentation comments;同時整理 `.gitea/workflows/readme.md` 的 workflow 說明、觸發條件與相關參數草稿,最後重建 README 專案列表、功能列表與使用範例。README 更新時間固定使用台灣時區(Asia/Taipei)與 `yyyy/MM/dd HH:mm:ss` 格式,專案列表會拆成「專案名稱/專案描述」、「專案名稱/參考專案列表」、「專案名稱/NuGet 套件列表」三張表,且專案名稱會連到 Gitea/GitHub 遠端上的專案資料夾;遇到跨專案或跨命名空間的同名型別時會在功能名稱補上模組/專案前綴,並會跳脫 Markdown 表格、link text、heading 中的 C# 泛型角括號,檢查功能列表連結與使用範例 anchor 一致後執行合適驗證。當使用者要補齊 function 文件、產生 XML doc、為每個 method 加 summary/param/remarks、整理 workflow README、建立 .docs 草稿,或提到 doc-funcs、function 文件化、workflow 文件化、XML documentation comments 時使用此 skill。
|
||||
掃描目前專案所有可文件化的 function/method,建立 `.docs/doc-funcs-index.md` 與逐 function 草稿,再依草稿補齊 XML documentation comments;指令檔(腳本/CI/部署設定檔)草稿開頭的「用途/更新時間」標頭固定依 `skills/funcs/templates/command-header.md` 範本產生(依檔案類型選 `#` 或 `::`/`REM` 變體);`Dockerfile` 與 README 的檔名會依專案內多數檔案的大小寫命名慣例正規化(無明顯多數則保留原檔名),改名時同步更新引用;同時整理 `.gitea/workflows/readme.md` 的 workflow 說明、觸發條件與相關參數草稿,最後重建 README 專案列表、功能列表與使用範例。README 更新時間固定使用台灣時區(Asia/Taipei)與 `yyyy/MM/dd HH:mm:ss` 格式,專案列表會拆成「專案名稱/專案描述」、「專案名稱/參考專案列表」、「專案名稱/NuGet 套件列表」三張表,且專案名稱會連到 Gitea/GitHub 遠端上的專案資料夾;遇到跨專案或跨命名空間的同名型別時會在功能名稱補上模組/專案前綴,並會跳脫 Markdown 表格、link text、heading 中的 C# 泛型角括號,檢查功能列表連結與使用範例 anchor 一致後執行合適驗證。當使用者要補齊 function 文件、產生 XML doc、為每個 method 加 summary/param/remarks、整理 workflow README、建立 .docs 草稿,或提到 funcs、function 文件化、workflow 文件化、XML documentation comments 時使用此 skill。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc:doc-funcs`
|
||||
- **Codex**:`$doc-funcs`,或用 `/skills` 選單
|
||||
- **Claude Code / Antigravity**:`/jsc-doc:funcs`
|
||||
- **Codex**:`$funcs`,或用 `/skills` 選單
|
||||
- **OpenCode**:描述需求自動觸發
|
||||
|
||||
### `doc-issues-analyze-to-file`
|
||||
### `issues-analyze-to-file`
|
||||
|
||||
讀取一或多筆 Gitea issue URL(優先用 `tea`,否則用 Gitea REST API + `curl` + `GITEA_TOKEN`,不依賴 `jq`),把 issue 正文與留言彙整成一份完整需求文件,依功能拆成多個實作階段並各建立一個 issue(沿用來源 issue 的里程碑與專案、依需求性質從既有標籤挑選填入),再配合使用者指定的 repositories 或 issue 所在 repo,對每個階段派 subagent 產生實作草稿,最後產出交付文件並依 issues 分組留言到對應 issue。建立 issue 與留言前會先產生全部草稿並以 AskUserQuestion 讓使用者確認執行方式(全部執行/只建立 issue/只產文件不動 Gitea/逐階段確認)。當使用者要分析 issue、把需求拆成多階段 issue、依 issue 產生實作規劃或交付留言,或提到 doc-issues-analyze-to-file、issue 需求分析、issue 拆階段、tea issues、Gitea issue 留言時使用此 skill。
|
||||
讀取一或多筆 Gitea issue URL(優先用 `tea`,否則用 Gitea REST API + `curl` + `GITEA_TOKEN`,不依賴 `jq`),把 issue 正文與留言彙整成一份完整需求文件,依功能拆成多個實作階段並各建立一個 issue(沿用來源 issue 的里程碑與專案、依需求性質從既有標籤挑選填入),再配合使用者指定的 repositories 或 issue 所在 repo,對每個階段派 subagent 產生實作草稿,最後產出交付文件並依 issues 分組留言到對應 issue。建立 issue 與留言前會先產生全部草稿並以 AskUserQuestion 讓使用者確認執行方式(全部執行/只建立 issue/只產文件不動 Gitea/逐階段確認)。當使用者明確要「產出需求文件/實作草稿/交付文件檔案」的 issue 分析,或提到 issues-analyze-to-file、issue 需求分析文件、issue 拆階段交付文件時使用此 skill;全程不落地檔案、以議題描述與留言保存中間成果的拆分流程改用 issues-analyze,兩者都可能符合時先詢問使用者要「檔案交付」還是「議題留言」。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc:doc-issues-analyze-to-file`
|
||||
- **Codex**:`$doc-issues-analyze-to-file`,或用 `/skills` 選單
|
||||
- **Claude Code / Antigravity**:`/jsc-doc:issues-analyze-to-file`
|
||||
- **Codex**:`$issues-analyze-to-file`,或用 `/skills` 選單
|
||||
- **OpenCode**:描述需求自動觸發
|
||||
|
||||
### `doc-issues-analyze`
|
||||
### `issues-analyze`
|
||||
|
||||
讀取使用者選擇的一或多種來源(專案編號、議題編號、檔案文件;至少一種;若選專案編號則只讀取該專案下開啟中的議題),先檢查 `tea` 與 `GITEA_TOKEN` 並詢問使用者要用 `tea` 或 Gitea API + token,將來源內容合併整理成保存議題內容,再拆分成多個小功能議題(標題、描述、阻擋關閉、依複雜度評估到期日),每個小功能議題都會詢問使用者描述是否有補充內容,所有議題描述最後都會依描述內容產生 TODO list,依到期日排序並在使用者逐議題確認後實作、留言進度、完成後 PR 到 develop 或 master。所有中間成果都不落地成草稿檔,一律使用 `tea` 或 Gitea API 保存到議題描述或留言。當使用者要把需求拆成小功能議題、依專案/議題/文件產生保存議題與功能議題、依到期日排程實作、或提到 doc-issues-analyze、issue breakdown、議題拆分、小功能議題、Gitea issue 拆解時使用此 skill。
|
||||
讀取使用者選擇的一或多種來源(專案編號、議題編號、檔案文件;至少一種;若選專案編號則只讀取該專案下開啟中的議題;處理議題時必須連同所有留言與附件一起讀取——文字附件直接取內容、圖片等二進位附件唯讀暫存讀取後即刪、無法讀取的附件列出檔名標註需人工確認),先檢查 `tea` 與 `GITEA_TOKEN` 並詢問使用者要用 `tea` 或 Gitea API + token,將來源內容合併整理成保存議題內容,再拆分成多個小功能議題(標題、描述、阻擋關閉、依複雜度評估到期日;形成子母議題時,母議題(保存議題)必須所有子議題都關閉後才可關閉——優先以 Gitea issue dependency 阻擋,不支援時在母議題描述加入子議題清單與關閉前檢查),每個小功能議題都會詢問使用者描述是否有補充內容,所有議題描述最後都會依描述內容產生 TODO list;分析完成後若議題屬於專案看板且欄位可對應進度語意(例如分析中/待處理/進行中/待測試/已完成),會把議題移到「待處理」欄位(不往回移、Gitea 介面不支援時改列建議清單請使用者手動拖曳);最後依到期日與相依關係排序小功能議題並把排序結果留言到保存議題。本 skill 到「議題拆分完成+排序留言」為止,**不實作程式碼**(不修改原始碼、不 commit、不 push、不開 PR),實作交由 `/jsc-code:issues`。所有中間成果都不落地成草稿檔,一律使用 `tea` 或 Gitea API 保存到議題描述或留言。當使用者要把需求拆成小功能議題、依專案/議題/文件產生保存議題與功能議題、或提到 issues-analyze、issue breakdown、議題拆分、小功能議題、Gitea issue 拆解時使用此 skill;要產出需求/交付文件檔案時改用 issues-analyze-to-file。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc:doc-issues-analyze`
|
||||
- **Codex**:`$doc-issues-analyze`,或用 `/skills` 選單
|
||||
- **Claude Code / Antigravity**:`/jsc-doc:issues-analyze`
|
||||
- **Codex**:`$issues-analyze`,或用 `/skills` 選單
|
||||
- **OpenCode**:描述需求自動觸發
|
||||
|
||||
### `doc-issues-sync`
|
||||
### `issues-sync`
|
||||
|
||||
讀取一個 Gitea 專案(project)或單一議題(優先用 `tea`,否則用 Gitea REST API + `curl` + `GITEA_TOKEN`,不依賴 `jq`);輸入是專案就讀取與此專案關聯的所有議題,輸入是議題就只同步該議題,找不到目標時以 AskUserQuestion 請使用者補齊。議題若有標籤就依標籤分組、以 AskUserQuestion(多選)讓使用者挑選要同步哪些標籤的議題(只有一個議題或全部無標籤則跳過)。接著一個議題派一個 subagent,**以工作目錄下的所有檔案為依據**:判斷議題內的 TODO(markdown 任務清單)是否足以追蹤議題描述的需求、不足就補 TODO 追加到正文、依需求從既有標籤更新議題標籤、逐條勾稽未完成 TODO(含新增)是否已完成、有異動就整理成一則留言。所有對 Gitea 的寫入(改正文/改標籤/留言)先產生草稿並以 AskUserQuestion 讓使用者確認執行方式再套用。當使用者要同步議題進度、依專案批次更新議題 TODO、依程式碼勾稽議題完成度、更新議題標籤與進度留言,或提到 doc-issues-sync、issue sync、議題同步、TODO 勾稽、Gitea 專案議題時使用此 skill。
|
||||
讀取一個 Gitea 專案(project)或單一議題(優先用 `tea`,否則用 Gitea REST API + `curl` + `GITEA_TOKEN`,不依賴 `jq`);輸入是專案時因 tea/Gitea API 無法直接查詢專案,改先取得該 repo 所有開啟中的議題、再過濾掉與此專案無關的議題,輸入是議題就只同步該議題,找不到目標時以 AskUserQuestion 請使用者補齊。議題若有標籤就依標籤分組、以 AskUserQuestion(多選)讓使用者挑選要同步哪些標籤的議題(只有一個議題或全部無標籤則跳過)。接著一個議題派一個 subagent,**以工作目錄下的所有檔案為依據**:判斷議題內的 TODO(markdown 任務清單)是否足以追蹤議題描述的需求、不足就補 TODO 追加到正文、依需求從既有標籤更新議題標籤、逐條勾稽未完成 TODO(含新增)是否已完成、有異動就整理成一則留言;若議題屬於專案看板且看板欄位可對應進度語意(例如分析中/待處理/進行中/待測試/已完成),依議題描述與勾稽結果建議議題應在的欄位,經確認後移動(Gitea 介面不支援 project 欄位操作時改列建議清單請使用者手動拖曳)。若輸入為專案且使用者明確指定「關閉專案/專案完成」,進入專案完成模式:只執行到取得專案議題清單,跳過其後所有同步步驟,列出議題清單(含仍有未完成 TODO 者)經使用者確認後,把專案擁有的所有議題搬到「已完成」欄位並關閉,單筆失敗不中斷整批並於回報列出。全程不落地任何檔案(不建立 `.docs/`、不寫草稿檔),所有中間成果只留在對話/subagent 回傳,最終只透過 tea 或 Gitea API 寫回議題正文/標籤/留言,且寫入前先以 AskUserQuestion 讓使用者確認執行方式。當使用者要同步議題進度、依專案批次更新議題 TODO、依程式碼勾稽議題完成度、更新議題標籤與進度留言,或提到 issues-sync、issue sync、議題同步、TODO 勾稽、Gitea 專案議題時使用此 skill。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc:doc-issues-sync`
|
||||
- **Codex**:`$doc-issues-sync`,或用 `/skills` 選單
|
||||
- **Claude Code / Antigravity**:`/jsc-doc:issues-sync`
|
||||
- **Codex**:`$issues-sync`,或用 `/skills` 選單
|
||||
- **OpenCode**:描述需求自動觸發
|
||||
|
||||
### `notifications`
|
||||
|
||||
讀取 Gitea 通知,依通知類型分組後逐組執行;若沒有通知就直接結束;先從目前工作目錄的 `REVIEW.md` 找對應流程,找不到或檔案不存在就視為空白流程檔並詢問使用者怎麼定義,之後把缺少流程的通知類型附加回 `REVIEW.md`。當使用者要整理 Gitea 通知、依通知類型批次處理、照 `REVIEW.md` 執行通知流程、或補齊 `REVIEW.md` 的通知類型說明時使用此 skill。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc-doc:notifications`
|
||||
- **Codex**:`$notifications`,或用 `/skills` 選單
|
||||
- **OpenCode**:描述需求自動觸發
|
||||
|
||||
### `worklog`
|
||||
|
||||
工作證明自動記錄與手動維護流程。相容的 `Stop` hook 會把每輪工作透過 `WORKLOG_CLI` 指定的 headless CLI 整理成六個固定欄位:專案/任務名稱、執行細節與產出、花費時間、任務狀態、遇到的困難、解決方式,並追加到 Gitea wiki 當週頁(`Worklog-yyyy-MM-W<週>`,**週以星期六起算**,換頁固定發生在星期六)。手動模式提供 `--init`、`--tune`(Claude Code 專屬)、`--diagnose`、`--append`、`--show`。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc-doc:worklog --diagnose`
|
||||
- **Codex**:`$worklog --diagnose`,或用 `/skills` 選單
|
||||
- **OpenCode / GitHub Copilot**:需完整 plugin 目錄保留 `scripts/`;可用 `WORKLOG_CLI=opencode` 或 `WORKLOG_CLI=copilot` 作為摘要 CLI
|
||||
|
||||
<!-- JSC-SKILLS:END -->
|
||||
|
||||
---
|
||||
|
||||
## 新增一個 skill
|
||||
|
||||
1. 建立目錄:`mkdir -p skills/<your-skill-name>`
|
||||
1. 複製既有 skill 作範本:`cp -r skills/docker skills/<your-skill-name>`
|
||||
2. 編輯 `skills/<your-skill-name>/SKILL.md` 的 frontmatter:
|
||||
- `name`:小寫、數字、連字號(`-`),最長 64 字元。**這就是 Claude Code / Antigravity 的 `/jsc:<name>`**。
|
||||
- `name`:小寫、數字、連字號(`-`),最長 64 字元。**這就是 Claude Code / Antigravity 的 `/jsc-doc:<name>`**。
|
||||
- `description`:第三人稱,寫清楚「何時用、何時不用」與觸發關鍵字 — 這是各助理自動載入的唯一依據。
|
||||
3. 在內文寫下 skill 的具體步驟。
|
||||
4. 手動把這個 skill 補進上方「Skills 目錄」區塊。
|
||||
5. **bump 版本並 push**:四家都以 git 內容/版本判斷更新,請把 `.claude-plugin/plugin.json`、`.codex-plugin/plugin.json`、`plugin.json` 三個 manifest 的 `version` 一起 bump,commit 後 push 到 gitea。
|
||||
5. **bump 版本並 push**:各助理都以 git 內容/版本判斷更新,請把 `.claude-plugin/plugin.json`、`.codex-plugin/plugin.json`、`plugin.json` 三個 manifest 的 `version` 一起 bump,commit 後 push 到 gitea。
|
||||
6. 讓各助理更新:
|
||||
- Claude:`claude plugin update jsc@doc`
|
||||
- Claude:`claude plugin update jsc-doc@doc`
|
||||
- Codex:`codex plugin marketplace upgrade doc`
|
||||
- Antigravity:`git -C ~/jsc-plugin pull && agy plugin uninstall jsc && agy plugin install ~/jsc-plugin`
|
||||
- Antigravity:`git -C ~/plugins/doc pull && agy plugin uninstall jsc-doc && agy plugin install ~/plugins/doc`
|
||||
- OpenCode:`git pull` 後重新複製 `skills/`
|
||||
- Copilot:`copilot plugin marketplace update doc && copilot plugin update jsc-doc@doc`
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "rel='scripts/worklog/worklog.sh'; own='doc'; plug='jsc-doc'; root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ] && [ -f \"$root/$rel\" ]; then exec \"$root/$rel\"; fi; for base in \"$HOME/.claude/plugins/cache\" \"$HOME/.codex/plugins/cache\"; do for dir in \"$base/$own/$plug\" \"$base\"; do s=$(find \"$dir\" -path \"*/$plug/*/$rel\" -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$s\" ]; then exec \"$s\"; fi; done; done; exit 0",
|
||||
"timeout": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc",
|
||||
"version": "0.1.2",
|
||||
"description": "JSC 文件化 skills:doc-docker 會整理 docker-compose.yaml 的行內註解與標題日期;doc-funcs 會為專案 functions 建立 .docs 草稿、補齊 XML 文件註解並重建 README 功能列表與使用範例;doc-issues-analyze-to-file 會讀取 Gitea issue、彙整需求、拆成多階段 issue 並產生實作草稿與交付留言;doc-issues-analyze 會把專案/議題/文件來源拆成小功能議題並依到期日實作;doc-issues-sync 會讀取 Gitea 專案或議題、依工作目錄檔案勾稽並同步議題的 TODO 進度與標籤並產生進度留言。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc: 前綴呼叫。",
|
||||
"name": "jsc-doc",
|
||||
"version": "0.0.4",
|
||||
"description": "JSC 文件化 skills:docker 會整理 docker-compose.yaml 的行內註解與標題日期;funcs 會為專案 functions 建立 .docs 草稿、補齊 XML 文件註解並重建 README 功能列表與使用範例;issues-analyze-to-file 會讀取 Gitea issue、彙整需求、拆成多階段 issue 並產生實作草稿與交付留言;issues-analyze 會把專案/議題/文件來源(議題連同留言與附件一起讀取)拆成小功能議題(母議題須待所有子議題關閉後才可關閉)、分析完成後把屬於專案看板的議題移到「待處理」欄位並依到期日實作;issues-sync 會讀取 Gitea 專案或議題、依工作目錄檔案勾稽並同步議題的 TODO 進度、標籤與專案看板進度欄位並產生進度留言(指定關閉專案/專案完成時改為批次把專案所有議題搬到「已完成」並關閉);notifications 會讀取 Gitea 通知、依通知類型分組並照 REVIEW.md 流程處理,沒有流程時詢問使用者並把缺少流程的類別附加到 REVIEW.md;worklog 會以 README 定義的 headless CLI 將每輪工作整理成六欄工作紀錄並追加到 Gitea wiki。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-doc: 前綴呼叫。",
|
||||
"skills": "./skills/"
|
||||
}
|
||||
|
||||
Executable
+314
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
# ==============================================================================
|
||||
# 用途:worklog 的 transcript 處理工具。負責 (1) 從 Claude Code/Codex
|
||||
# JSONL 抽出「本輪」對話片段(最後一筆使用者訊息之後的全部內容),
|
||||
# (2) 估算本輪花費時間,(3) 對文字做機密遮蔽(token/密碼/PII),
|
||||
# 作為寫入 wiki 前的第二道防線。
|
||||
# 更新時間:2026/07/27 22:16:00
|
||||
# 相依:Python 3 標準庫。全程僅走 stdin/stdout,不寫任何檔案。
|
||||
# ==============================================================================
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# 單則工具結果/參數的擷取上限,避免整份 transcript 塞進摘要輸入
|
||||
TOOL_RESULT_LIMIT = 200
|
||||
TOOL_INPUT_LIMIT = 160
|
||||
TOTAL_LIMIT = 24000
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 機密遮蔽規則:命中一律換成 ***
|
||||
# ------------------------------------------------------------------------------
|
||||
REDACT_PATTERNS = [
|
||||
(r"[A-Za-z0-9_\-]*:[A-Za-z0-9_\-]{16,}@", "***@"), # URL 內嵌憑證 user:token@
|
||||
(r"\b[0-9a-f]{40}\b", "***"), # Gitea 40 字元 token
|
||||
(r"\bgh[pousr]_[A-Za-z0-9_]{16,}\b", "***"), # GitHub token
|
||||
(r"\bsk-[A-Za-z0-9\-_]{16,}\b", "***"), # API key
|
||||
(r"(?i)\b(token|password|passwd|pwd|secret|api[_-]?key)\b\s*[:=]\s*\S+", r"\1=***"),
|
||||
(r"(?i)Authorization:\s*(token|bearer)\s+\S+", r"Authorization: \1 ***"),
|
||||
(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}", "***"), # Email
|
||||
(r"\b09\d{2}[-\s]?\d{3}[-\s]?\d{3}\b", "***"), # 台灣手機
|
||||
(r"\b[A-Z][12]\d{8}\b", "***"), # 身分證字號
|
||||
]
|
||||
|
||||
|
||||
def redact(text):
|
||||
"""對文字套用全部機密遮蔽規則,回傳遮蔽後的結果。"""
|
||||
for pattern, replacement in REDACT_PATTERNS:
|
||||
text = re.sub(pattern, replacement, text)
|
||||
return text
|
||||
|
||||
|
||||
def _is_real_user_message(entry):
|
||||
"""判斷 transcript 條目是否為真正的使用者輸入(排除工具回填與環境注入)。"""
|
||||
payload = entry.get("payload")
|
||||
if isinstance(payload, dict) and entry.get("type") == "event_msg":
|
||||
return payload.get("type") == "user_message" and bool(str(payload.get("message") or "").strip())
|
||||
|
||||
if entry.get("type") != "user":
|
||||
return False
|
||||
content = entry.get("message", {}).get("content")
|
||||
if isinstance(content, str):
|
||||
return bool(content.strip())
|
||||
if isinstance(content, list):
|
||||
return any(b.get("type") == "text" for b in content if isinstance(b, dict))
|
||||
return False
|
||||
|
||||
|
||||
def _blocks(entry):
|
||||
"""取出條目的 content blocks,統一為 list 形式。"""
|
||||
content = entry.get("message", {}).get("content")
|
||||
if isinstance(content, str):
|
||||
return [{"type": "text", "text": content}]
|
||||
return content if isinstance(content, list) else []
|
||||
|
||||
|
||||
def _payload_text_blocks(content):
|
||||
"""把 Codex response_item 的 content blocks 轉成純文字片段。"""
|
||||
if isinstance(content, str):
|
||||
return [content]
|
||||
if not isinstance(content, list):
|
||||
return []
|
||||
texts = []
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
if block.get("type") in ("input_text", "output_text", "text"):
|
||||
text = (block.get("text") or "").strip()
|
||||
if text:
|
||||
texts.append(text)
|
||||
return texts
|
||||
|
||||
|
||||
def _render_codex_payload(entry):
|
||||
"""將 Codex session JSONL 的 payload 格式轉為摘要輸入用純文字。"""
|
||||
payload = entry.get("payload")
|
||||
if not isinstance(payload, dict):
|
||||
return []
|
||||
|
||||
lines = []
|
||||
entry_type = entry.get("type")
|
||||
payload_type = payload.get("type")
|
||||
|
||||
if entry_type == "event_msg":
|
||||
if payload_type == "user_message":
|
||||
message = (payload.get("message") or "").strip()
|
||||
if message:
|
||||
lines.append(f"[user] {message}")
|
||||
elif payload_type == "agent_message":
|
||||
message = (payload.get("message") or "").strip()
|
||||
if message:
|
||||
phase = payload.get("phase") or "assistant"
|
||||
lines.append(f"[assistant:{phase}] {message}")
|
||||
return lines
|
||||
|
||||
if entry_type != "response_item":
|
||||
return lines
|
||||
|
||||
if payload_type == "message":
|
||||
role = payload.get("role") or "assistant"
|
||||
if role in ("system", "developer"):
|
||||
return lines
|
||||
for text in _payload_text_blocks(payload.get("content")):
|
||||
# Codex 會把 skill 內容以 user role 注入;避免把整份 SKILL.md 當成本輪工作。
|
||||
if role == "user" and text.lstrip().startswith("<skill>"):
|
||||
continue
|
||||
if role == "user" and text.lstrip().startswith("<environment_context>"):
|
||||
continue
|
||||
lines.append(f"[{role}] {text}")
|
||||
elif payload_type == "function_call":
|
||||
name = payload.get("name") or "?"
|
||||
raw = str(payload.get("arguments") or "").strip().replace("\n", " ")
|
||||
lines.append(f"[tool:{name}] {raw[:TOOL_INPUT_LIMIT]}")
|
||||
elif payload_type == "function_call_output":
|
||||
raw = str(payload.get("output") or "").strip().replace("\n", " ")
|
||||
if raw:
|
||||
lines.append(f"[result] {raw[:TOOL_RESULT_LIMIT]}")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _render(entry):
|
||||
"""將單一 transcript 條目轉為摘要輸入用的純文字行(工具結果僅取前段)。"""
|
||||
codex_lines = _render_codex_payload(entry)
|
||||
if codex_lines:
|
||||
return codex_lines
|
||||
|
||||
role = entry.get("type")
|
||||
lines = []
|
||||
for block in _blocks(entry):
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
kind = block.get("type")
|
||||
if kind == "text":
|
||||
text = (block.get("text") or "").strip()
|
||||
if text:
|
||||
lines.append(f"[{role}] {text}")
|
||||
elif kind == "tool_use":
|
||||
name = block.get("name", "?")
|
||||
raw = json.dumps(block.get("input", {}), ensure_ascii=False)
|
||||
lines.append(f"[tool:{name}] {raw[:TOOL_INPUT_LIMIT]}")
|
||||
elif kind == "tool_result":
|
||||
raw = block.get("content")
|
||||
if isinstance(raw, list):
|
||||
raw = " ".join(
|
||||
b.get("text", "") for b in raw if isinstance(b, dict) and b.get("type") == "text"
|
||||
)
|
||||
raw = str(raw or "").strip().replace("\n", " ")
|
||||
if raw:
|
||||
lines.append(f"[result] {raw[:TOOL_RESULT_LIMIT]}")
|
||||
return lines
|
||||
|
||||
|
||||
def _read_entries(path):
|
||||
"""讀取 transcript JSONL,忽略無法解析的列。"""
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
entries = []
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entries.append(json.loads(line))
|
||||
except ValueError:
|
||||
continue
|
||||
except OSError:
|
||||
return []
|
||||
return entries
|
||||
|
||||
|
||||
def _turn_start_index(entries):
|
||||
"""找出本輪起點:最後一筆真正使用者訊息的位置。"""
|
||||
start = 0
|
||||
for index in range(len(entries) - 1, -1, -1):
|
||||
if _is_real_user_message(entries[index]):
|
||||
start = index
|
||||
break
|
||||
return start
|
||||
|
||||
|
||||
def _parse_timestamp(value):
|
||||
"""解析常見 transcript timestamp 格式,失敗回 None。"""
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
return None
|
||||
raw = value.strip()
|
||||
if raw.endswith("Z"):
|
||||
raw = raw[:-1] + "+00:00"
|
||||
try:
|
||||
dt = datetime.fromisoformat(raw)
|
||||
except ValueError:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt
|
||||
|
||||
|
||||
def _entry_timestamp(entry):
|
||||
"""取出 transcript 條目的時間欄位。"""
|
||||
for key in ("timestamp", "created_at", "time"):
|
||||
dt = _parse_timestamp(entry.get(key))
|
||||
if dt:
|
||||
return dt
|
||||
message = entry.get("message")
|
||||
if isinstance(message, dict):
|
||||
for key in ("timestamp", "created_at", "time"):
|
||||
dt = _parse_timestamp(message.get(key))
|
||||
if dt:
|
||||
return dt
|
||||
return None
|
||||
|
||||
|
||||
def format_duration(seconds):
|
||||
"""把秒數格式化為精簡中文耗時。"""
|
||||
if seconds < 0:
|
||||
return "未判定"
|
||||
minutes = int(round(seconds / 60))
|
||||
if minutes <= 0:
|
||||
return "1 分鐘內"
|
||||
hours, mins = divmod(minutes, 60)
|
||||
if hours and mins:
|
||||
return f"{hours} 小時 {mins} 分鐘"
|
||||
if hours:
|
||||
return f"{hours} 小時"
|
||||
return f"{mins} 分鐘"
|
||||
|
||||
|
||||
def turn_duration(path):
|
||||
"""
|
||||
估算本輪花費時間:取本輪起點到最後一筆可解析 timestamp 的差距。
|
||||
|
||||
transcript 無時間欄位或本輪少於兩個時間點時回「未判定」,避免臆測。
|
||||
"""
|
||||
entries = _read_entries(path)
|
||||
if not entries:
|
||||
return "未判定"
|
||||
start = _turn_start_index(entries)
|
||||
stamps = [dt for dt in (_entry_timestamp(e) for e in entries[start:]) if dt]
|
||||
if len(stamps) < 2:
|
||||
return "未判定"
|
||||
return format_duration((max(stamps) - min(stamps)).total_seconds())
|
||||
|
||||
|
||||
def extract_turn(path):
|
||||
"""
|
||||
從 transcript JSONL 抽出本輪內容:最後一筆真正使用者訊息(含該筆)之後的全部條目。
|
||||
|
||||
不需任何狀態檔即可界定「本輪」,符合工作內容不落地的要求。
|
||||
回傳純文字字串;讀取失敗或無內容時回空字串。
|
||||
"""
|
||||
entries = _read_entries(path)
|
||||
if not entries:
|
||||
return ""
|
||||
start = _turn_start_index(entries)
|
||||
|
||||
|
||||
lines = []
|
||||
for entry in entries[start:]:
|
||||
lines.extend(_render(entry))
|
||||
|
||||
text = "\n".join(lines).strip()
|
||||
if len(text) > TOTAL_LIMIT:
|
||||
head = text[: TOTAL_LIMIT // 2]
|
||||
tail = text[-TOTAL_LIMIT // 2 :]
|
||||
text = f"{head}\n…(中段省略)…\n{tail}"
|
||||
return text
|
||||
|
||||
|
||||
USAGE = """用法:transcript.py <子命令> [參數]
|
||||
|
||||
extract <transcript 路徑> 抽出本輪內容並遮蔽機密後輸出到 stdout
|
||||
duration <transcript 路徑> 估算本輪花費時間,無法判定時輸出「未判定」
|
||||
redact 自 stdin 讀取文字,遮蔽機密後輸出到 stdout
|
||||
"""
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""CLI 進入點:解析子命令並執行抽取或遮蔽。"""
|
||||
if not argv or argv[0] in ("-h", "--help"):
|
||||
print(USAGE)
|
||||
return 0
|
||||
if argv[0] == "extract":
|
||||
if len(argv) < 2:
|
||||
return 2
|
||||
text = extract_turn(argv[1])
|
||||
if not text:
|
||||
return 1
|
||||
sys.stdout.write(redact(text))
|
||||
return 0
|
||||
if argv[0] == "duration":
|
||||
if len(argv) < 2:
|
||||
return 2
|
||||
sys.stdout.write(turn_duration(argv[1]))
|
||||
return 0
|
||||
if argv[0] == "redact":
|
||||
sys.stdout.write(redact(sys.stdin.read()))
|
||||
return 0
|
||||
print(USAGE)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Executable
+483
@@ -0,0 +1,483 @@
|
||||
#!/usr/bin/env python3
|
||||
# ==============================================================================
|
||||
# 用途:Gitea Wiki 讀寫工具(worklog 專用)。提供 token 解析、頁面讀取、
|
||||
# 建立、append 追加(read-modify-write + 寫後驗證重試),供 worklog.sh
|
||||
# 與 /jsc-doc:worklog skill 共用,避免兩份實作漂移。
|
||||
# 更新時間:2026/07/29 19:03:29
|
||||
# 相依:Python 3 標準庫(urllib、base64、json、re)。不需 requests、不需 jq。
|
||||
# 機密:token 一律從環境變數或本機憑證檔讀取,絕不輸出、絕不寫入任何檔案。
|
||||
# ==============================================================================
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import ssl
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
TAIPEI = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
def _ssl_context():
|
||||
"""
|
||||
建立 TLS 連線設定:維持完整憑證鏈驗證,僅關閉 VERIFY_X509_STRICT。
|
||||
|
||||
Python 3.13 起預設啟用 X509 嚴格檢查,內部 CA 憑證若缺少 Subject Key
|
||||
Identifier 會被拒絕(curl 不做此檢查,故 curl 可連而 Python 不行)。
|
||||
此處只放寬擴充欄位的嚴格檢查,主機名稱與憑證鏈驗證仍完整保留。
|
||||
"""
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.verify_flags &= ~ssl.VERIFY_X509_STRICT
|
||||
return ctx
|
||||
|
||||
|
||||
SSL_CONTEXT = _ssl_context()
|
||||
|
||||
|
||||
def now_str():
|
||||
"""取得台灣時區的 yyyy/MM/dd HH:mm:ss 時間字串。"""
|
||||
return datetime.now(TAIPEI).strftime("%Y/%m/%d %H:%M:%S")
|
||||
|
||||
|
||||
def log(level, message, stage="wiki_api"):
|
||||
"""輸出統一格式訊息([時間][階段][等級]: 訊息,一行一則),一律走 stderr 不污染 stdout。"""
|
||||
print(f"[{now_str()}][{stage}][{level}]: {message}", file=sys.stderr)
|
||||
|
||||
|
||||
def mask(text, secret):
|
||||
"""將字串中的 secret 遮蔽為 ***,避免 token 洩漏到輸出。"""
|
||||
if not secret:
|
||||
return text
|
||||
return text.replace(secret, "***")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# token 解析:GITEA_TOKEN → tea config → git-credentials
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
def _token_from_tea(host):
|
||||
"""從 tea 設定檔取出指定 host 的 token(找不到回 None)。"""
|
||||
for path in ("~/.config/tea/config.yml", "~/.tea/config.yml"):
|
||||
f = os.path.expanduser(path)
|
||||
if not os.path.isfile(f):
|
||||
continue
|
||||
try:
|
||||
raw = open(f, encoding="utf-8").read()
|
||||
except OSError:
|
||||
continue
|
||||
for block in re.split(r"(?m)^\s*-\s+name:", raw):
|
||||
if host not in block:
|
||||
continue
|
||||
m = re.search(r"(?m)^\s*token:\s*[\"']?([A-Za-z0-9_\-]+)", block)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def _token_from_git_credentials(host):
|
||||
"""從 ~/.git-credentials(credential.helper=store)取出指定 host 的密碼作為 token。"""
|
||||
f = os.path.expanduser("~/.git-credentials")
|
||||
if not os.path.isfile(f):
|
||||
return None
|
||||
try:
|
||||
lines = open(f, encoding="utf-8").read().splitlines()
|
||||
except OSError:
|
||||
return None
|
||||
for line in lines:
|
||||
m = re.match(r"https?://([^:]+):([^@]+)@(.+)$", line.strip())
|
||||
if m and m.group(3) == host:
|
||||
return urllib.parse.unquote(m.group(2))
|
||||
return None
|
||||
|
||||
|
||||
def resolve_token(host, repo):
|
||||
"""
|
||||
依固定優先序解析可用 token,並以 GET /repos/<repo> 實際驗證權限。
|
||||
|
||||
優先序:GITEA_TOKEN → tea 設定檔該 host 的 token → ~/.git-credentials。
|
||||
回傳 (token, 來源說明);全部失敗回 (None, 說明)。
|
||||
"""
|
||||
candidates = []
|
||||
env = os.environ.get("GITEA_TOKEN")
|
||||
if env:
|
||||
candidates.append((env, "GITEA_TOKEN"))
|
||||
tea = _token_from_tea(host)
|
||||
if tea and tea != env:
|
||||
candidates.append((tea, "tea 設定檔"))
|
||||
cred = _token_from_git_credentials(host)
|
||||
if cred and cred not in (env, tea):
|
||||
candidates.append((cred, "git-credentials"))
|
||||
|
||||
if not candidates:
|
||||
return None, "找不到任何可用憑證來源"
|
||||
|
||||
for token, source in candidates:
|
||||
code, _ = _request("GET", f"https://{host}/api/v1/repos/{repo}", token, None)
|
||||
if code == 200:
|
||||
return token, source
|
||||
log("DBG", f"{source} 對 {host} 驗證失敗(HTTP {code}),改試下一個來源")
|
||||
return None, f"{len(candidates)} 個憑證來源全部驗證失敗"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# HTTP
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
def _request(method, url, token, payload):
|
||||
"""發出 Gitea API 請求,回傳 (HTTP 狀態碼, 回應內文字串)。網路層錯誤以 0 表示。"""
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
|
||||
req = urllib.request.Request(url, data=data, method=method)
|
||||
req.add_header("Authorization", f"token {token}")
|
||||
req.add_header("Accept", "application/json")
|
||||
if data:
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30, context=SSL_CONTEXT) as resp:
|
||||
return resp.status, resp.read().decode("utf-8", "replace")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode("utf-8", "replace")
|
||||
except Exception as e: # 網路錯誤、逾時
|
||||
return 0, str(e)
|
||||
|
||||
|
||||
def _api_base(host, repo):
|
||||
"""組出 repo 層級的 wiki API base URL。"""
|
||||
return f"https://{host}/api/v1/repos/{repo}/wiki"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# wiki 操作
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
def list_pages(host, repo, token):
|
||||
"""
|
||||
列出 wiki 全部頁面(分頁完整讀取),回傳 (狀態, 頁面清單)。
|
||||
|
||||
狀態為 'ok'/'missing'(wiki 尚未初始化)/'error'。清單元素含 title 與 sub_url。
|
||||
"""
|
||||
pages = []
|
||||
page_no = 1
|
||||
limit = 50
|
||||
while True:
|
||||
url = f"{_api_base(host, repo)}/pages?page={page_no}&limit={limit}"
|
||||
code, body = _request("GET", url, token, None)
|
||||
if code == 404:
|
||||
return "missing", []
|
||||
if code != 200:
|
||||
return "error", []
|
||||
try:
|
||||
batch = json.loads(body)
|
||||
except ValueError:
|
||||
return "error", []
|
||||
if not isinstance(batch, list):
|
||||
return "error", []
|
||||
pages.extend(batch)
|
||||
if len(batch) < limit:
|
||||
return "ok", pages
|
||||
page_no += 1
|
||||
|
||||
|
||||
def resolve_sub_url(host, repo, token, title):
|
||||
"""
|
||||
以 title 查出 Gitea 實際的 sub_url。
|
||||
|
||||
Gitea wiki 會對 title 做轉義(`-` 代表空格,實際 dash 另有轉義形式,例如
|
||||
title `Worklog-2026-07-W4` 的 sub_url 為 `Worklog-2026-07-W4.-`),因此讀寫
|
||||
一律以查表得到的 sub_url 為準,不自行猜測轉義規則。
|
||||
找不到回 None。
|
||||
"""
|
||||
status, pages = list_pages(host, repo, token)
|
||||
if status != "ok":
|
||||
return None
|
||||
for item in pages:
|
||||
if item.get("title") == title:
|
||||
return item.get("sub_url") or title
|
||||
return None
|
||||
|
||||
|
||||
def get_page(host, repo, token, page):
|
||||
"""
|
||||
讀取 wiki 頁面內容(page 可傳 title 或 sub_url,內部會自動解析)。
|
||||
|
||||
回傳 (狀態, 內容字串);狀態為 'ok'(存在)、'missing'(404,頁面或 wiki 尚未建立)、
|
||||
'error'(其他失敗,內容為遮蔽後的錯誤訊息)。
|
||||
"""
|
||||
sub_url = resolve_sub_url(host, repo, token, page) or page
|
||||
url = f"{_api_base(host, repo)}/page/{urllib.parse.quote(sub_url)}"
|
||||
code, body = _request("GET", url, token, None)
|
||||
if code == 404:
|
||||
return "missing", ""
|
||||
if code != 200:
|
||||
return "error", mask(f"HTTP {code} {body[:200]}", token)
|
||||
try:
|
||||
data = json.loads(body)
|
||||
except ValueError:
|
||||
return "error", "回應不是合法 JSON"
|
||||
raw = data.get("content_base64") or ""
|
||||
try:
|
||||
return "ok", base64.b64decode(raw).decode("utf-8", "replace")
|
||||
except Exception:
|
||||
return "error", "content_base64 解碼失敗"
|
||||
|
||||
|
||||
def create_page(host, repo, token, page, content, message):
|
||||
"""建立新的 wiki 頁面(wiki 尚未初始化時亦由此初始化)。回傳 (是否成功, 訊息)。"""
|
||||
url = f"{_api_base(host, repo)}/new"
|
||||
payload = {
|
||||
"title": page,
|
||||
"content_base64": base64.b64encode(content.encode("utf-8")).decode("ascii"),
|
||||
"message": message,
|
||||
}
|
||||
code, body = _request("POST", url, token, payload)
|
||||
if code in (201, 200):
|
||||
return True, f"已建立頁面 {page}"
|
||||
return False, mask(f"建立頁面失敗 HTTP {code} {body[:200]}", token)
|
||||
|
||||
|
||||
def delete_page(host, repo, token, page):
|
||||
"""刪除 wiki 頁面(page 可傳 title 或 sub_url)。回傳 (是否成功, 訊息)。"""
|
||||
sub_url = resolve_sub_url(host, repo, token, page) or page
|
||||
url = f"{_api_base(host, repo)}/page/{urllib.parse.quote(sub_url)}"
|
||||
code, body = _request("DELETE", url, token, None)
|
||||
if code in (204, 200):
|
||||
return True, f"已刪除頁面 {page}"
|
||||
return False, mask(f"刪除頁面失敗 HTTP {code} {body[:200]}", token)
|
||||
|
||||
|
||||
def update_page(host, repo, token, page, content, message):
|
||||
"""整頁覆寫既有 wiki 頁面(append 由呼叫端先合併內容)。回傳 (是否成功, 訊息)。"""
|
||||
sub_url = resolve_sub_url(host, repo, token, page) or page
|
||||
url = f"{_api_base(host, repo)}/page/{urllib.parse.quote(sub_url)}"
|
||||
payload = {
|
||||
"title": page,
|
||||
"content_base64": base64.b64encode(content.encode("utf-8")).decode("ascii"),
|
||||
"message": message,
|
||||
}
|
||||
code, body = _request("PATCH", url, token, payload)
|
||||
if code in (200, 201):
|
||||
return True, f"已更新頁面 {page}"
|
||||
return False, mask(f"更新頁面失敗 HTTP {code} {body[:200]}", token)
|
||||
|
||||
|
||||
def append_entry(host, repo, token, page, header, entry, marker, retries=3):
|
||||
"""
|
||||
將條目追加到週頁尾端:讀取現有內容 → 合併 → 寫回 → 寫後讀取驗證。
|
||||
|
||||
marker 為條目內唯一字串(時間戳+session 短碼),用於驗證自己的內容確實落地;
|
||||
多個 session 同時寫入時,驗證失敗會重讀最新內容重試,避免互相覆蓋。
|
||||
回傳 (是否成功, 訊息)。
|
||||
"""
|
||||
for attempt in range(1, retries + 1):
|
||||
status, current = get_page(host, repo, token, page)
|
||||
if status == "error":
|
||||
return False, f"讀取頁面失敗:{current}"
|
||||
|
||||
if status == "missing":
|
||||
content = f"{header}\n\n{entry}\n"
|
||||
ok, msg = create_page(host, repo, token, page, content, f"worklog: 建立 {page}")
|
||||
if not ok:
|
||||
# wiki 已存在但頁面不存在時,建立可能失敗;下一輪改走更新
|
||||
log("WRN", f"第 {attempt} 次建立失敗:{msg}")
|
||||
continue
|
||||
else:
|
||||
if marker in current:
|
||||
return True, "條目已存在,無需重複寫入"
|
||||
body = current.rstrip("\n")
|
||||
if not body:
|
||||
body = header
|
||||
content = f"{body}\n\n{entry}\n"
|
||||
ok, msg = update_page(host, repo, token, page, content, f"worklog: 追加 {marker}")
|
||||
if not ok:
|
||||
log("WRN", f"第 {attempt} 次寫入失敗:{msg}")
|
||||
continue
|
||||
|
||||
verify_status, verify_content = get_page(host, repo, token, page)
|
||||
if verify_status == "ok" and marker in verify_content:
|
||||
return True, f"條目已寫入 {page}(第 {attempt} 次嘗試)"
|
||||
log("WRN", f"第 {attempt} 次寫後驗證未找到條目,準備重試")
|
||||
|
||||
return False, f"重試 {retries} 次仍未成功寫入 {page}"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 週頁命名
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# 週的定義:星期六起算(六~五),週頁以該週起始的星期六為錨點命名。
|
||||
# 舊規則以 ceil(日/7) 分週,換頁點固定落在每月 8/15/22/29 號,會把同一個工作週
|
||||
# 切成兩頁(例:2026/07/28 二 在 W4、07/29 三 卻跳到 W5),使用者開著舊頁會誤判成
|
||||
# 「worklog 停止記錄」。改以星期六為界後,換頁一律發生在週六,與星期對齊。
|
||||
WEEK_START_WEEKDAY = 5 # Python weekday():週一 0、週二 1 …… 週六 5、週日 6
|
||||
|
||||
|
||||
def week_start(when=None):
|
||||
"""取得指定時間所屬工作週的起始日(該週的星期六;當天就是星期六時回傳當天)。"""
|
||||
when = when or datetime.now(TAIPEI)
|
||||
return when - timedelta(days=(when.weekday() - WEEK_START_WEEKDAY) % 7)
|
||||
|
||||
|
||||
def week_start_from_page(page):
|
||||
"""
|
||||
從週頁名稱反推該週起始的星期六。
|
||||
|
||||
供手動指定頁面時產生正確標題;格式不符或該月不存在第 n 個星期六時回傳 None。
|
||||
"""
|
||||
if not page:
|
||||
return None
|
||||
matched = re.match(r"^Worklog-(\d{4})-(\d{2})-W(\d)$", page.strip())
|
||||
if not matched:
|
||||
return None
|
||||
year, month, week = (int(matched.group(i)) for i in (1, 2, 3))
|
||||
try:
|
||||
first_day = datetime(year, month, 1, tzinfo=TAIPEI)
|
||||
except ValueError:
|
||||
return None
|
||||
first_saturday = first_day + timedelta(days=(WEEK_START_WEEKDAY - first_day.weekday()) % 7)
|
||||
start = first_saturday + timedelta(days=7 * (week - 1))
|
||||
return start if start.month == month else None
|
||||
|
||||
|
||||
def week_page_name(when=None):
|
||||
"""
|
||||
依台灣時區產生週頁名稱 Worklog-yyyy-MM-W<n>。
|
||||
|
||||
週以星期六起算(六~五),n =該週起始的星期六是當月第幾個星期六。
|
||||
跨月的一週歸屬起始星期六所在的月份,確保同一週只會有一頁
|
||||
(例:2026/08/29 六 ~ 09/04 五 都寫入 Worklog-2026-08-W5)。
|
||||
"""
|
||||
start = week_start(when)
|
||||
week = (start.day - 1) // 7 + 1
|
||||
return f"Worklog-{start.year:04d}-{start.month:02d}-W{week}"
|
||||
|
||||
|
||||
def week_page_header(page=None, when=None):
|
||||
"""
|
||||
產生週頁首行標題(例:# 2026 年 07 月 第 4 週工作紀錄(07/25 六 ~ 07/31 五))。
|
||||
|
||||
標題含日期範圍,讓開頁的人一眼看出這頁涵蓋哪幾天,不必回頭推算週次。
|
||||
傳入 page 時以頁名反推所屬週,避免手動補寫舊頁時寫入當下這週的標題。
|
||||
"""
|
||||
start = week_start_from_page(page) or week_start(when)
|
||||
end = start + timedelta(days=6)
|
||||
week = (start.day - 1) // 7 + 1
|
||||
return (
|
||||
f"# {start.year} 年 {start.month:02d} 月 第 {week} 週工作紀錄"
|
||||
f"({start.month:02d}/{start.day:02d} 六 ~ {end.month:02d}/{end.day:02d} 五)"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
USAGE = """用法:wiki_api.py <子命令> [參數]
|
||||
|
||||
probe 檢查 host/repo/token/wiki API 可用性
|
||||
page-name 印出當週頁面名稱
|
||||
pages 列出全部頁面(title 與實際 sub_url)
|
||||
show [頁面] 印出指定頁面內容(預設當週頁)
|
||||
append <marker> [頁面] 自 stdin 讀取條目內容並追加(預設當週頁)
|
||||
init [頁面] 若當週頁不存在則建立(僅含標題)
|
||||
delete <頁面> 刪除指定頁面
|
||||
|
||||
環境變數:WORKLOG_HOST(必要)、WORKLOG_REPO(必要)、GITEA_TOKEN(選用,會自動 fallback)
|
||||
"""
|
||||
|
||||
|
||||
def _env():
|
||||
"""讀取並檢查必要環境變數,回傳 (host, repo);缺少時結束程式。"""
|
||||
host = os.environ.get("WORKLOG_HOST", "").strip()
|
||||
repo = os.environ.get("WORKLOG_REPO", "").strip()
|
||||
if not host or not repo:
|
||||
log("ERR", "缺少 WORKLOG_HOST 或 WORKLOG_REPO")
|
||||
sys.exit(2)
|
||||
return host, repo
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""CLI 進入點:解析子命令並執行對應 wiki 操作。"""
|
||||
if not argv or argv[0] in ("-h", "--help"):
|
||||
print(USAGE)
|
||||
return 0
|
||||
|
||||
cmd = argv[0]
|
||||
|
||||
if cmd == "page-name":
|
||||
print(week_page_name())
|
||||
return 0
|
||||
|
||||
host, repo = _env()
|
||||
token, source = resolve_token(host, repo)
|
||||
if not token:
|
||||
log("ERR", f"無可用 token:{source}")
|
||||
return 2
|
||||
|
||||
if cmd == "probe":
|
||||
log("INF", f"token 來源:{source}")
|
||||
code, body = _request("GET", f"https://{host}/api/v1/version", token, None)
|
||||
log("INF", f"Gitea 版本查詢 HTTP {code} {body[:80]}")
|
||||
status, _ = get_page(host, repo, token, week_page_name())
|
||||
log("INF", f"當週頁 {week_page_name()} 狀態:{status}")
|
||||
return 0
|
||||
|
||||
if cmd == "pages":
|
||||
status, pages = list_pages(host, repo, token)
|
||||
if status != "ok":
|
||||
log("WRN" if status == "missing" else "ERR", f"頁面清單狀態:{status}")
|
||||
return 0 if status == "missing" else 1
|
||||
for item in pages:
|
||||
print(f"{item.get('title')}\t{item.get('sub_url')}")
|
||||
return 0
|
||||
|
||||
if cmd == "delete":
|
||||
if len(argv) < 2:
|
||||
log("ERR", "delete 需要頁面名稱")
|
||||
return 2
|
||||
ok, msg = delete_page(host, repo, token, argv[1])
|
||||
log("INF" if ok else "ERR", msg)
|
||||
return 0 if ok else 1
|
||||
|
||||
if cmd == "show":
|
||||
page = argv[1] if len(argv) > 1 else week_page_name()
|
||||
status, content = get_page(host, repo, token, page)
|
||||
if status == "ok":
|
||||
print(content)
|
||||
return 0
|
||||
log("WRN" if status == "missing" else "ERR", f"頁面 {page} 狀態:{status} {content}")
|
||||
return 0 if status == "missing" else 1
|
||||
|
||||
if cmd == "init":
|
||||
page = argv[1] if len(argv) > 1 else week_page_name()
|
||||
status, _ = get_page(host, repo, token, page)
|
||||
if status == "ok":
|
||||
log("INF", f"頁面 {page} 已存在,不重建")
|
||||
return 0
|
||||
ok, msg = create_page(host, repo, token, page, week_page_header(page) + "\n", f"worklog: 初始化 {page}")
|
||||
log("INF" if ok else "ERR", msg)
|
||||
return 0 if ok else 1
|
||||
|
||||
if cmd == "append":
|
||||
if len(argv) < 2:
|
||||
log("ERR", "append 需要 marker 參數")
|
||||
return 2
|
||||
marker = argv[1]
|
||||
page = argv[2] if len(argv) > 2 else week_page_name()
|
||||
entry = sys.stdin.read().strip()
|
||||
if not entry:
|
||||
log("WRN", "條目內容為空,不寫入")
|
||||
return 0
|
||||
ok, msg = append_entry(host, repo, token, page, week_page_header(page), entry, marker)
|
||||
log("INF" if ok else "ERR", msg)
|
||||
return 0 if ok else 1
|
||||
|
||||
log("ERR", f"未知子命令:{cmd}")
|
||||
print(USAGE)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Executable
+284
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# 用途:工作證明自動記錄(worklog)。由支援 hook 的 CLI 觸發,
|
||||
# 抽出本輪工作內容 → 呼叫已安裝 CLI 濃縮成精簡條目 → 機密遮蔽 →
|
||||
# 追加到 Gitea wiki 的當週工作紀錄頁。工作內容全程不落地。
|
||||
# 更新時間:2026/07/27 17:27:54
|
||||
# 相依:python3、README 定義的任一 headless CLI、curl(wiki 走 Python urllib,不需 curl 亦可)。
|
||||
# 機密:token 僅由環境變數/本機憑證讀取,不 echo、不寫檔;輸出前套用遮蔽規則。
|
||||
# 退出碼:一律 0 —— hook 絕不可阻斷使用者的工作流程。
|
||||
# ==============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STAGE="worklog"
|
||||
SUPPORTED_CLIS="claude codex agy opencode copilot"
|
||||
FALLBACK_MODEL="claude-haiku-4-5-20251001"
|
||||
MODEL_CACHE="${HOME}/.claude/worklog/model"
|
||||
CACHE_MAX_AGE_DAYS=30
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 共用函式
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
log() {
|
||||
# 輸出統一格式訊息([時間][階段][等級]: 訊息,一行一則),一律走 stderr
|
||||
local level="$1" message="$2" stamp
|
||||
stamp="$(TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M:%S')"
|
||||
printf '[%s][%s][%s]: %s\n' "$stamp" "$STAGE" "$level" "$message" >&2
|
||||
if [ -n "${WORKLOG_ERRLOG:-}" ] && [ "$level" = "ERR" ]; then
|
||||
printf '[%s][%s][%s]: %s\n' "$stamp" "$STAGE" "$level" "$message" >> "${WORKLOG_ERRLOG}" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
die_quiet() {
|
||||
# 記錄原因後以 0 結束:hook 不得阻斷使用者流程
|
||||
log "${2:-DBG}" "$1"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 遞迴防護:摘要用的子 CLI 行程可能再次觸發 Stop hook,必須在此擋掉
|
||||
# ------------------------------------------------------------------------------
|
||||
[ -n "${WORKLOG_CHILD:-}" ] && exit 0
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 啟用檢查:未設定 WORKLOG_* 的環境完全不動作(他人匯入 plugin 零影響)
|
||||
# ------------------------------------------------------------------------------
|
||||
[ "${WORKLOG_ENABLED:-}" = "1" ] || exit 0
|
||||
[ -n "${WORKLOG_HOST:-}" ] || die_quiet "未設定 WORKLOG_HOST,略過記錄" "WRN"
|
||||
[ -n "${WORKLOG_REPO:-}" ] || die_quiet "未設定 WORKLOG_REPO,略過記錄" "WRN"
|
||||
|
||||
command -v python3 >/dev/null 2>&1 || die_quiet "找不到 python3,略過記錄" "WRN"
|
||||
|
||||
select_worklog_cli() {
|
||||
# 依目前 hook/session 環境優先選擇摘要執行器;可用 WORKLOG_CLI 強制指定。
|
||||
local requested="${WORKLOG_CLI:-auto}" cli current_cli
|
||||
if [ "$requested" != "auto" ]; then
|
||||
case " ${SUPPORTED_CLIS} " in
|
||||
*" ${requested} "*) ;;
|
||||
*) die_quiet "WORKLOG_CLI 不支援:${requested}(可用:auto ${SUPPORTED_CLIS})" "WRN" ;;
|
||||
esac
|
||||
command -v "$requested" >/dev/null 2>&1 || die_quiet "找不到 ${requested} CLI,略過記錄" "WRN"
|
||||
printf '%s' "$requested"
|
||||
return 0
|
||||
fi
|
||||
current_cli="$(detect_current_cli)"
|
||||
if [ -n "$current_cli" ]; then
|
||||
if command -v "$current_cli" >/dev/null 2>&1; then
|
||||
printf '%s' "$current_cli"
|
||||
return 0
|
||||
fi
|
||||
log "WRN" "目前環境判定為 ${current_cli},但找不到 ${current_cli} CLI,改用可用摘要 CLI"
|
||||
fi
|
||||
for cli in $SUPPORTED_CLIS; do
|
||||
if command -v "$cli" >/dev/null 2>&1; then
|
||||
printf '%s' "$cli"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
die_quiet "找不到可用摘要 CLI(需要其一:${SUPPORTED_CLIS})" "WRN"
|
||||
}
|
||||
|
||||
detect_current_cli() {
|
||||
# 判斷實際觸發本輪 hook 的助理環境,避免 auto 因 PATH 順序誤顯其他 CLI。
|
||||
if [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_CI:-}" ] || [ -n "${CODEX_MANAGED_PACKAGE_ROOT:-}" ]; then
|
||||
printf 'codex'
|
||||
return 0
|
||||
fi
|
||||
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || [ -n "${CLAUDE_CODE_SSE_PORT:-}" ]; then
|
||||
printf 'claude'
|
||||
return 0
|
||||
fi
|
||||
if [ -n "${AGY_SESSION_ID:-}" ] || [ -n "${AGY_WORKSPACE_ID:-}" ]; then
|
||||
printf 'agy'
|
||||
return 0
|
||||
fi
|
||||
if [ -n "${OPENCODE_SESSION_ID:-}" ] || [ -n "${OPENCODE_CONFIG:-}" ]; then
|
||||
printf 'opencode'
|
||||
return 0
|
||||
fi
|
||||
if [ -n "${COPILOT_AGENT_ID:-}" ] || [ -n "${GITHUB_COPILOT_TOKEN:-}" ]; then
|
||||
printf 'copilot'
|
||||
return 0
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
run_summary_cli() {
|
||||
# 各 CLI 依 README 的 headless 指令呼叫;不把工作內容寫入檔案。
|
||||
local cli="$1" prompt="$2" model="$3"
|
||||
case "$cli" in
|
||||
claude)
|
||||
WORKLOG_CHILD=1 timeout 45 claude -p "$prompt" --model "$model" 2>/dev/null
|
||||
;;
|
||||
codex)
|
||||
WORKLOG_CHILD=1 timeout 45 codex exec "$prompt" 2>/dev/null
|
||||
;;
|
||||
agy)
|
||||
WORKLOG_CHILD=1 timeout 45 agy -p "$prompt" 2>/dev/null
|
||||
;;
|
||||
opencode)
|
||||
WORKLOG_CHILD=1 timeout 45 opencode run "$prompt" 2>/dev/null
|
||||
;;
|
||||
copilot)
|
||||
WORKLOG_CHILD=1 timeout 45 copilot -p "$prompt" 2>/dev/null
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
SUMMARY_CLI="$(select_worklog_cli)"
|
||||
[ -n "$SUMMARY_CLI" ] || exit 0
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 讀取 hook 傳入的 JSON(session_id/transcript_path/cwd/stop_hook_active)
|
||||
# ------------------------------------------------------------------------------
|
||||
HOOK_INPUT="$(cat)"
|
||||
[ -n "$HOOK_INPUT" ] || die_quiet "hook 輸入為空,略過記錄" "WRN"
|
||||
|
||||
read -r SESSION_ID TRANSCRIPT_PATH STOP_ACTIVE HOOK_CWD <<EOF_HOOK
|
||||
$(printf '%s' "$HOOK_INPUT" | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
except ValueError:
|
||||
d = {}
|
||||
print(
|
||||
d.get("session_id") or d.get("thread_id") or d.get("conversation_id") or "-",
|
||||
d.get("transcript_path") or d.get("session_path") or d.get("conversation_path") or d.get("path") or "-",
|
||||
"1" if d.get("stop_hook_active") else "0",
|
||||
d.get("cwd", "") or "-",
|
||||
)
|
||||
')
|
||||
EOF_HOOK
|
||||
|
||||
[ "$STOP_ACTIVE" = "1" ] && die_quiet "stop_hook_active 為 true,避免迴圈不重複記錄"
|
||||
|
||||
if [ ! -f "$TRANSCRIPT_PATH" ] && [ -n "${CODEX_THREAD_ID:-}" ]; then
|
||||
TRANSCRIPT_PATH="$(find "${HOME}/.codex/sessions" -type f -name "*${CODEX_THREAD_ID}.jsonl" -print -quit 2>/dev/null)"
|
||||
[ -n "$TRANSCRIPT_PATH" ] || TRANSCRIPT_PATH="-"
|
||||
fi
|
||||
|
||||
[ -f "$TRANSCRIPT_PATH" ] || die_quiet "找不到 transcript:${TRANSCRIPT_PATH}" "WRN"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 記錄範圍:WORKLOG_SCOPE 以冒號分隔的路徑前綴,未設定則全部 session 都記
|
||||
# ------------------------------------------------------------------------------
|
||||
if [ -n "${WORKLOG_SCOPE:-}" ]; then
|
||||
in_scope=0
|
||||
IFS=':' read -r -a scopes <<< "${WORKLOG_SCOPE}"
|
||||
for scope in "${scopes[@]}"; do
|
||||
case "$HOOK_CWD" in "${scope%/}"*) in_scope=1 ;; esac
|
||||
done
|
||||
[ "$in_scope" = "1" ] || die_quiet "cwd 不在 WORKLOG_SCOPE 範圍內:${HOOK_CWD}"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 專案判定:git remote 的 <owner>/<repo> 優先,其次目錄名
|
||||
# ------------------------------------------------------------------------------
|
||||
PROJECT="$(basename "$HOOK_CWD")"
|
||||
if git -C "$HOOK_CWD" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
origin="$(git -C "$HOOK_CWD" remote get-url origin 2>/dev/null)"
|
||||
if [ -n "$origin" ]; then
|
||||
cleaned="${origin%.git}"
|
||||
cleaned="${cleaned##*://}"
|
||||
cleaned="${cleaned#*@}"
|
||||
owner_repo="$(printf '%s' "$cleaned" | awk -F/ 'NF>=2 {print $(NF-1)"/"$NF}')"
|
||||
[ -n "$owner_repo" ] && PROJECT="$owner_repo"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 抽出本輪內容(最後一筆使用者訊息之後),並先做一次機密遮蔽
|
||||
# ------------------------------------------------------------------------------
|
||||
TURN="$(python3 "${SCRIPT_DIR}/transcript.py" extract "$TRANSCRIPT_PATH" 2>/dev/null)"
|
||||
[ -n "$TURN" ] || die_quiet "本輪無可記錄內容"
|
||||
DURATION="$(python3 "${SCRIPT_DIR}/transcript.py" duration "$TRANSCRIPT_PATH" 2>/dev/null)"
|
||||
[ -n "$DURATION" ] || DURATION="未判定"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 模型決定:只有 claude CLI 使用 WORKLOG_MODEL/快取檔;其他 CLI 使用各自預設模型
|
||||
# ------------------------------------------------------------------------------
|
||||
MODEL=""
|
||||
MODEL_NOTE=""
|
||||
if [ "$SUMMARY_CLI" = "claude" ]; then
|
||||
if [ -n "${WORKLOG_MODEL:-}" ]; then
|
||||
MODEL="${WORKLOG_MODEL}"
|
||||
elif [ -f "$MODEL_CACHE" ]; then
|
||||
if [ -n "$(find "$MODEL_CACHE" -mtime "+${CACHE_MAX_AGE_DAYS}" 2>/dev/null)" ]; then
|
||||
MODEL="$FALLBACK_MODEL"
|
||||
MODEL_NOTE=" (cli: claude, model: fallback)"
|
||||
log "WRN" "模型快取已超過 ${CACHE_MAX_AGE_DAYS} 天,改用保底模型,建議重跑 /jsc-doc:worklog --tune"
|
||||
else
|
||||
MODEL="$(grep -m1 -E '^model=' "$MODEL_CACHE" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')"
|
||||
fi
|
||||
fi
|
||||
if [ -z "$MODEL" ]; then
|
||||
MODEL="$FALLBACK_MODEL"
|
||||
MODEL_NOTE=" (cli: claude, model: fallback)"
|
||||
log "WRN" "無模型快取,改用保底模型,建議執行 /jsc-doc:worklog --tune"
|
||||
elif [ -z "$MODEL_NOTE" ]; then
|
||||
MODEL_NOTE=" (cli: claude)"
|
||||
fi
|
||||
else
|
||||
MODEL_NOTE=" (cli: ${SUMMARY_CLI})"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 濃縮:交給選定 CLI 產出精簡條目(子行程帶 WORKLOG_CHILD=1 阻斷遞迴)
|
||||
# ------------------------------------------------------------------------------
|
||||
PROMPT="$(cat <<EOF_PROMPT
|
||||
你是工作紀錄濃縮器。輸入是一段 AI 助理與使用者的對話片段(含工具呼叫)。
|
||||
請濃縮成工作紀錄條目,規則:
|
||||
|
||||
已判定專案:${PROJECT}
|
||||
已估算花費時間:${DURATION}
|
||||
|
||||
1. 只輸出 6 個 markdown bullet(以「- 」開頭),不要標題、不要前言、不要結語。
|
||||
2. 六個 bullet 必須依序使用下列欄位名稱,格式固定為「- 欄位名稱:內容」:
|
||||
- 專案/任務名稱
|
||||
- 執行細節與產出
|
||||
- 花費時間
|
||||
- 任務狀態
|
||||
- 遇到的困難
|
||||
- 解決方式
|
||||
3. 使用繁體中文(台灣用語),每個 bullet 一行、不超過 90 字,聚焦「做了什麼、動到什麼、結果如何」。
|
||||
4. 保留關鍵事實:檔案/專案/指令/數量/分支/PR/議題編號;不要抄程式碼、不要貼指令全文。
|
||||
5. 花費時間優先使用「已估算花費時間」;無法判定時寫「未判定」。
|
||||
6. 若沒有遇到明確困難,遇到的困難寫「未遇到明確困難」,解決方式寫「不需額外處理」。
|
||||
7. 嚴禁輸出任何憑證與個資:token、密碼、API key、連線字串、Email、電話、姓名、身分證號。
|
||||
8. 若這段對話沒有實質工作產出(純閒聊、純提問、僅讀取資訊而未產生結論),只輸出一行:SKIP
|
||||
|
||||
對話片段:
|
||||
${TURN}
|
||||
EOF_PROMPT
|
||||
)"
|
||||
|
||||
SUMMARY="$(run_summary_cli "$SUMMARY_CLI" "$PROMPT" "$MODEL")"
|
||||
if [ -z "$SUMMARY" ]; then
|
||||
log "WRN" "摘要產出為空(CLI ${SUMMARY_CLI}),略過本輪"
|
||||
exit 0
|
||||
fi
|
||||
printf '%s' "$SUMMARY" | grep -qiE '^\s*SKIP\s*$' && die_quiet "模型判定本輪無實質工作產出"
|
||||
|
||||
# 第二道防線:對模型輸出再做一次機密遮蔽
|
||||
SUMMARY="$(printf '%s' "$SUMMARY" | python3 "${SCRIPT_DIR}/transcript.py" redact 2>/dev/null)"
|
||||
# 只保留 bullet 行,避免模型帶出多餘敘述
|
||||
SUMMARY="$(printf '%s\n' "$SUMMARY" | grep -E '^\s*[-*]\s+' | sed -E 's/^\s*[*]/-/' | head -6)"
|
||||
[ -n "$SUMMARY" ] || die_quiet "摘要不含合法條目,略過本輪" "WRN"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 組條目並追加到當週 wiki 頁
|
||||
# ------------------------------------------------------------------------------
|
||||
STAMP="$(TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M:%S')"
|
||||
MARKER="worklog:$(TZ='Asia/Taipei' date +'%Y%m%d%H%M%S')-${SESSION_ID:0:8}"
|
||||
|
||||
ENTRY="$(printf '## %s — %s%s <!-- %s -->\n%s\n' "$STAMP" "$PROJECT" "$MODEL_NOTE" "$MARKER" "$SUMMARY")"
|
||||
|
||||
export WORKLOG_HOST WORKLOG_REPO
|
||||
if printf '%s' "$ENTRY" | python3 "${SCRIPT_DIR}/wiki_api.py" append "$MARKER" 2>&1 | grep -q '\[ERR\]'; then
|
||||
log "ERR" "寫入 wiki 失敗(專案 ${PROJECT})"
|
||||
else
|
||||
log "INF" "已記錄工作條目(專案 ${PROJECT},CLI ${SUMMARY_CLI})"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
name: doc-issues-analyze
|
||||
description: 讀取使用者選擇的一或多種來源(專案編號、議題編號、檔案文件;至少一種;若選專案編號則只讀取該專案下開啟中的議題),先檢查 tea 與 GITEA_TOKEN 並詢問使用者要用 tea 或 Gitea API + token,將來源內容合併整理成保存議題內容,再拆分成多個小功能議題(標題、描述、阻擋關閉、依複雜度評估到期日),每個小功能議題都必須詢問使用者描述是否有補充內容,所有議題都要根據描述內容在描述最後產生 TODO list,依到期日排序並在使用者逐議題確認後實作、留言進度、完成後 PR 到 develop 或 master。當使用者要把需求拆成小功能議題、依專案/議題/文件產生保存議題與功能議題、依到期日排程實作、或提到 doc-issues-analyze、issue breakdown、議題拆分、小功能議題、Gitea issue 拆解時使用此 skill。
|
||||
---
|
||||
|
||||
# 分析多來源需求並保存為議題
|
||||
|
||||
你要先做工具可用性檢查並選擇工具;第二步詢問使用者要讀取哪些來源:專案編號、議題編號、檔案文件,至少選一種,接著讀取選定來源並彙整成保存議題內容。第三步必須把上個步驟產生的議題內容拆分成多個小功能議題,並為每個小功能議題產生標題、描述、阻擋關閉規則與依複雜度評估的到期日。第四步必須將小功能議題依到期日排序,逐個議題實作並將進度留言到議題,完成後 PR 到 `develop` 或 `master`;**實作任何議題前必須先詢問使用者並取得確認,不得擅自開始修改程式碼;但使用者確認開始實作該議題後,可在該議題範圍內自行 commit、push 與開 PR**。所有中間成果都不准落地成草稿檔,必須一律使用 `tea` 或 Gitea API 保存到議題描述或留言。
|
||||
|
||||
## 前置:輸入與工具
|
||||
|
||||
- **輸入來源**:使用者必須選擇要讀取的來源種類,可多選且數量必須 `>= 1`:
|
||||
- **專案編號**:Gitea project 編號或可定位 project 的 URL/識別資訊;若選取專案編號,必須只讀取該專案下開啟中的議題(含可取得的卡片/欄位/描述);也可作為保存整理結果的目標專案。
|
||||
- **議題編號**:Gitea issue 編號或 issue URL,可多筆。
|
||||
- **檔案文件**:本機文件路徑,可多筆;支援 Markdown、純文字與其他可直接讀取的需求文件。
|
||||
- **保存目標**:合併整理後必須在指定專案建立一張議題保存;若輸入來源未包含可作為保存目標的專案編號,必須詢問使用者提供專案編號,不得自行臆測。
|
||||
- **repositories 位置**:可另外指定本機含多個專案的資料夾;若未指定,實作參考以來源議題所在 repo、保存目標 repo 或使用者指定 repo 為準。
|
||||
- **工具選擇**:在解析與讀取 Gitea 來源前,先檢查本機是否可用 `tea`、`tea login list` 是否有對應 login、以及環境變數 `GITEA_TOKEN` 是否已設定;接著詢問使用者要使用 `tea` 或 Gitea REST API + `curl` + token。使用者已明確指定工具時才可跳過詢問。
|
||||
- **不要依賴 `jq`(環境未安裝)**:需要解析 JSON 時,用 `tea` 的結構化輸出(例如 `--fields ... --output csv`),或把原始 JSON 交給 subagent 解析,不要在指令中 pipe 到 `jq`。
|
||||
- **禁止草稿落地**:所有流程都不准建立 `.docs/` 或其他本機草稿檔;需求整理、小功能拆分、排序、進度與交付資訊一律使用 `tea` 或 Gitea API 保存到對應議題描述或留言。
|
||||
- **TODO list**:所有建立或更新的議題描述最後都必須加上依該描述內容推導出的 `## TODO` 區塊,使用 Markdown checklist(`- [ ] ...`);TODO 必須可執行、可驗收,且不得加入描述未提及或無法合理推得的工作。
|
||||
|
||||
## 第 1 步:工具可用性檢查與使用方式選擇
|
||||
|
||||
先檢查可用工具並選擇後續使用方式。除非使用者已明確指定 `tea` 或 `api`,否則不得自行決定。
|
||||
|
||||
1. 檢查 `tea` 是否存在:`command -v tea`。
|
||||
2. 若 `tea` 存在,執行 `tea login list`,記錄可用 login 與其 host;若失敗,記錄失敗原因但不要中止。
|
||||
3. 檢查 `GITEA_TOKEN` 是否已設定,只輸出「已設定/未設定」,不得輸出 token 內容。
|
||||
4. 依檢查結果詢問使用者要使用哪一種方式:
|
||||
- `tea`:只有在 `tea` 可執行時才可選;後續解析來源後仍需確認來源 host 有對應 login。
|
||||
- `api`:只有在 `GITEA_TOKEN` 已設定時才可選;後續使用 Gitea REST API + `curl`,帶標頭 `Authorization: token $GITEA_TOKEN`。
|
||||
5. 若兩種方式都不可用,停止並回報缺少 `tea login` 或 `GITEA_TOKEN`;不要要求使用者把 token 貼進對話。
|
||||
|
||||
## 第 2 步:選擇讀取來源、讀取內容並保存議題內容
|
||||
|
||||
這是必要決策。若使用者尚未明確提供來源種類,必須先詢問要讀取哪些來源種類,並要求至少選一種:
|
||||
|
||||
1. 專案編號。
|
||||
2. 議題編號。
|
||||
3. 檔案文件。
|
||||
|
||||
選定後收集對應輸入:
|
||||
|
||||
- 選「專案編號」:收集 project 編號或 project URL,並確認其 host/owner/repo/project id(若資訊不足,先詢問補齊);後續必須只讀取該專案下開啟中的議題。
|
||||
- 選「議題編號」:收集 issue 編號或 issue URL;若只提供編號,必須確認其 host/owner/repo。
|
||||
- 選「檔案文件」:收集本機檔案路徑並確認存在;不存在的檔案先回報並請使用者修正。
|
||||
|
||||
合併整理後一定要建立一張保存議題:
|
||||
|
||||
- 若已提供專案編號,詢問是否使用該專案作為保存目標;使用者可改指定其他專案。
|
||||
- 若未提供專案編號,必須詢問保存用專案編號或 project URL。
|
||||
- 不得在缺少保存目標專案時繼續到對外建立議題的步驟。
|
||||
|
||||
接著執行:
|
||||
|
||||
1. 解析每一筆來源:
|
||||
- 專案:解析出 `host`、`owner`、`repo`、`project id` 或可定位 project 的資訊。
|
||||
- 議題:解析出 `host`、`owner`、`repo`、`index`(例如 `https://<host>/<owner>/<repo>/issues/<index>`)。
|
||||
- 檔案:解析出本機絕對路徑、檔名與格式。
|
||||
2. 依第 1 步選定的工具建立每筆 Gitea 來源的存取設定:
|
||||
- `tea`:找出對應 host 的 login,後續命令一律帶 `--login <name> --repo <owner>/<repo>`。
|
||||
- `api`:base 為 `https://<host>/api/v1/repos/<owner>/<repo>`。
|
||||
- 若多筆專案/議題分屬不同 host,選擇 `tea` 時必須確認每個 host 都有對應 login;選擇 `api` 時同一個 `GITEA_TOKEN` 必須可存取全部專案/議題,否則在讀取階段回報權限不足並停止。
|
||||
3. 顯示本次處理的基本資料,至少包含:
|
||||
- 選定工具:`tea` 或 `api`。
|
||||
- 若選 `tea`:每個 host 對應的 login 名稱;若選 `api`:顯示 `GITEA_TOKEN` 已設定,不顯示 token 內容。
|
||||
- 讀取來源種類:專案編號/議題編號/檔案文件,至少一種。
|
||||
- 來源專案:每筆 project 的 host、owner、repo、project id(若有)。
|
||||
- 來源議題:每筆 issue 的 URL、host、owner、repo、index(若有)。
|
||||
- 來源檔案:每筆檔案的路徑與格式(若有)。
|
||||
- 保存目標專案:host、owner、repo、project id。
|
||||
- target repositories 來源:使用者指定的 repositories 位置,或「以來源議題所在 repo/保存目標 repo 為準」。
|
||||
4. 若使用者指定了 repositories 位置,先確認該路徑存在並列出其中的專案;若未指定,記錄「以來源議題所在 repo/保存目標 repo 為準」,並確認本機是否已 clone 對應 repo(沒有就在保存議題留言中標註需人工提供或 clone)。
|
||||
5. 依選定來源讀取內容:
|
||||
- 專案:讀取 project 描述、欄位/卡片、project metadata,並只讀取該專案下**開啟中的議題**(若 API 有分頁必須完整分頁讀取)。若 Gitea 版本不支援 project API 或無法由 project 取得開啟中的 issue 清單,標註「此 Gitea 版本不支援 project API,需人工處理」,並請使用者改提供議題編號或可匯出的 project 文件。
|
||||
- 議題:對每一筆 issue,讀取完整內容:`title`、`body`、`state`、`labels`、`milestone`、`assignees`、以及**所有 comments**;若 Gitea 版本支援,另讀該 issue 所屬 `project`。
|
||||
- 檔案文件:讀取文件全文;若格式無法直接讀取,標註需人工轉換或提供純文字/Markdown。
|
||||
6. 同時盤點該 repo 既有的分類資源,供後續階段沿用:
|
||||
- 標籤:`GET {base}/labels`(tea:`tea labels list`)
|
||||
- 里程碑:`GET {base}/milestones`(tea:`tea milestones list`)
|
||||
- 專案(若該 Gitea 版本有此 API):`GET {base}/projects`;若不支援就記錄「此 Gitea 版本不支援 project API,需人工處理」。
|
||||
7. 把所有來源內容彙整成保存議題內容,使用 `tea` 或 Gitea API 建立或更新保存議題;不得寫入本機草稿檔。保存議題描述至少包含:
|
||||
- 來源清單:每筆專案/議題/檔案的來源資訊、標題或名稱、狀態、現有 labels/milestone/project(若適用)。
|
||||
- 完整需求描述:整合專案、議題、檔案文件的內容,去除重複、補齊上下文,形成單一連貫的需求敘述。
|
||||
- 驗收條件/預期結果:能從來源內容推得的,逐條列出;不能確定的標註「需人工確認」。
|
||||
- 保存議題分類:labels/milestone/project 掛載方式。
|
||||
- `## TODO`:根據保存議題描述內容產生 Markdown checklist,放在描述最後。
|
||||
|
||||
需求彙整只做整理與歸納,不得編造來源內容未提及的需求;無法確定處保守描述並標註。
|
||||
|
||||
## 第 3 步:拆分小功能議題
|
||||
|
||||
將第 2 步保存到議題的內容拆分成多個小功能議題,使用 `tea` 或 Gitea API 建立/更新小功能議題或將小功能清單留言到保存議題;不得寫入本機草稿檔。每個小功能議題至少包含:
|
||||
|
||||
- 標題:能清楚表示單一小功能交付範圍。
|
||||
- 描述:描述內容必須先詢問使用者想要包含哪些段落或資訊,至少提供可選項,例如需求背景、功能範圍、驗收條件、技術提示、測試方式、相依關係、風險與備註;依使用者選擇組成描述,不得自行固定格式。每個小功能議題建立或更新前,都必須逐一詢問使用者該議題描述是否有補充內容;使用者提供補充時,必須整合到該小功能議題描述中,若使用者明確表示沒有補充才可繼續建立或更新。描述最後必須加入 `## TODO` 區塊,根據該小功能描述內容產生 Markdown checklist。
|
||||
- 阻擋關閉:小功能議題建立後必須以可追溯方式阻擋其被直接關閉,直到驗收條件完成。可用方式包含加上既有 blocking/blocked 類標籤、在 body 中加入「關閉前檢查清單」、建立與保存議題的追溯連結,或依 Gitea 支援能力設定 issue dependency;不得使用不存在的標籤或 API,找不到支援方式時標註需人工處理。若小功能有前後相依,較先完成的前置議題必須阻擋較後完成的後置議題(前置 issue blocks 後置 issue;後置 issue is blocked by 前置 issue),不得反向設定。
|
||||
- 複雜度:依工作量、跨模組程度、風險、未知數與測試成本評估為 `S`/`M`/`L`/`XL`。
|
||||
- 到期日:根據複雜度評估 due date,預設從建立日往後推算:`S` 3 個工作天、`M` 5 個工作天、`L` 10 個工作天、`XL` 15 個工作天;若遇週末順延到下一個工作天。若小功能有相依關係,必須先排定相依順序,後置功能的到期日不得早於其前置功能的到期日,且應從最後一個前置功能的到期日之後再依自身複雜度推算。若 Gitea API 不支援 due date,寫入 issue body 並回報需人工設定。
|
||||
- 相依關係:列出與保存議題、來源議題與其他小功能議題的關聯;若有前後依賴,必須標明前置功能、後置功能、阻擋方向與到期日排程依據。
|
||||
|
||||
決定要對照的 target repositories:使用者指定位置底下的所有專案、來源議題所在 repo,或使用者指定 repo。可研究相關專案程式碼以補充小功能議題描述,但所有分析結果必須直接保存到小功能議題描述或留言,不得建立本機草稿檔。
|
||||
|
||||
## 第 4 步:依到期日排序並逐議題實作
|
||||
|
||||
將第 3 步產生的小功能議題依到期日由早到晚排序;若到期日相同,依相依關係排序,前置議題必須排在後置議題前。排序結果必須使用 `tea` 或 Gitea API 留言到保存議題或相關小功能議題,不得寫入本機檔案。
|
||||
|
||||
開始實作前必須逐個議題詢問使用者,至少提供該議題的標題、URL(若已建立)、到期日、相依關係、預計修改範圍與驗證方式。未取得使用者確認前,不得修改任何原始碼、不得 commit、不得 push、不得開 PR。使用者確認開始實作某一議題後,即授權在該議題範圍內自行 commit、push 工作分支並開 PR,不需要對每個 git 動作再次詢問。
|
||||
|
||||
使用者確認某一議題後,才可對該議題執行:
|
||||
|
||||
- 建立或切換工作分支,分支名稱應包含議題編號或小功能識別。
|
||||
- 依議題描述實作,過程中定期將進度留言到該議題;至少包含開始實作、主要變更完成、驗證結果、PR 連結。
|
||||
- 只修改該議題必要範圍;若發現需要擴大範圍或改動其他議題,先停止並詢問使用者。
|
||||
- 執行適合專案的測試/建置/驗證;失敗時留言說明失敗原因與下一步。
|
||||
- 完成後提交變更並推送工作分支,向 `develop` 開 PR;若遠端沒有 `develop`,改向 `master` 開 PR。不得直接 push 到 `develop` 或 `master`。
|
||||
- PR 內容必須連結對應小功能議題,並摘要變更、測試結果、風險與需人工確認項目。
|
||||
|
||||
若小功能議題尚未實際建立到 Gitea,本步只能產生排序與實作計畫,不得開始實作;必須先回到建立小功能議題的確認流程。
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
name: doc-issues-sync
|
||||
description: 讀取一個 Gitea 專案(project)或單一議題(優先用 tea,否則用 Gitea REST API + curl + GITEA_TOKEN,不依賴 jq);若給的是專案就讀取與此專案關聯的所有議題,若給的是議題就只同步該議題。議題若有標籤就依標籤分組並以 AskUserQuestion 讓使用者挑選要同步哪些標籤的議題(只有一個議題或全部無標籤則跳過)。接著一個議題派一個 subagent,基於工作目錄下的所有檔案:分析議題描述的需求並判斷議題內的 TODO(markdown 任務清單)是否足以追蹤議題描述的需求、不足就補上 TODO 追加到議題正文、依需求從既有標籤更新議題標籤、逐條判斷未完成 TODO(含新增)是否已完成、有異動就整理成一則留言。所有對 Gitea 的寫入(改正文/改標籤/留言)先產生草稿並經使用者確認再執行。當使用者要同步議題進度、依專案批次更新議題 TODO、依程式碼勾稽議題完成度、更新議題標籤與進度留言,或提到 doc-issues-sync、issue sync、議題同步、TODO 勾稽、tea issues、Gitea 專案議題時使用此 skill。
|
||||
---
|
||||
|
||||
# 依工作目錄同步 Gitea 專案/議題的 TODO 進度與標籤
|
||||
|
||||
你要讀取使用者提供的一個 Gitea **專案(project)**或**單一議題**,取得要同步的議題清單,然後一個議題派一個 subagent,**以目前工作目錄下的所有檔案為依據**,勾稽並更新每個議題的 TODO(markdown 任務清單)與標籤,最後把 TODO 的異動整理成留言。**修改議題正文、變更議題標籤、留言都是對外且不易復原的動作,subagent 只產生草稿,實際寫入 Gitea 前必須先讓使用者確認**。請依下列階段依序完成。
|
||||
|
||||
## 前置:輸入與工具
|
||||
|
||||
- **輸入**:一個 Gitea 專案(project)或一筆議題的參照(URL 最佳,或 `owner/repo` + project id/issue index)。
|
||||
- **依據來源**:所有「TODO 是否完成」「該補哪些 TODO」「該掛哪些標籤」的判斷,一律以**目前工作目錄下的檔案內容**為準(程式碼、設定、文件等),不得臆測。
|
||||
- **工具優先序**:
|
||||
1. 若該 host 在 `tea login list` 中有對應 login,優先用 `tea`(`tea issues`、`tea comment`、`tea labels` 等),並以 `--login <name> --repo <owner>/<repo>` 指定目標。
|
||||
2. 否則改用 Gitea REST API + `curl`,帶標頭 `Authorization: token $GITEA_TOKEN`(環境變數 `GITEA_TOKEN` 已設定;未設定則停下請使用者提供)。
|
||||
- **不要依賴 `jq`(環境未安裝)**:需要解析 JSON 時,用 `tea` 的結構化輸出(例如 `--fields ... --output csv`),或把原始 JSON 交給 subagent 解析,不要在指令中 pipe 到 `jq`。
|
||||
- **TODO 的定義**:議題正文(body)中的 markdown 任務清單項目,`- [ ]`(未完成)與 `- [x]`(已完成)。本 skill 所有「TODO 追蹤/勾稽/新增」都在這種任務清單上操作。
|
||||
- **工作目錄**:所有草稿放在 `.docs/doc-issues-sync/`。
|
||||
|
||||
## 第 0 步:解析輸入、判斷專案或議題、準備工具
|
||||
|
||||
1. 從輸入解析出 `host`、`owner`、`repo`,並判斷這是**專案**還是**議題**:
|
||||
- 議題 URL 形如 `https://<host>/<owner>/<repo>/issues/<index>` → 議題。
|
||||
- 專案 URL 形如 `https://<host>/<owner>/<repo>/projects/<id>` 或組織層級 `https://<host>/<owner>/-/projects/<id>` → 專案。
|
||||
2. 執行 `tea login list`,判斷該 host 走 tea 還是 API(API base 為 `https://<host>/api/v1`)。
|
||||
3. 建立 `.docs/doc-issues-sync/`(若不存在)。
|
||||
4. **找不到就詢問使用者(AskUserQuestion)**:若無法從輸入判斷是專案還是議題、或依輸入查不到對應的專案/議題(例如 API 回 404、專案 id 不存在、repo 拼錯),必須用 AskUserQuestion 請使用者補齊或更正(host/owner/repo、專案 id 或議題 URL)。取得可解析的目標前,不進入下一步。
|
||||
|
||||
## 第 1 步:取得要同步的議題清單
|
||||
|
||||
- **若輸入是議題**:清單就是這一筆議題,**跳過本步的專案展開**,直接進入第 2 步。
|
||||
- **若輸入是專案**:讀取與此專案關聯的所有議題。
|
||||
- tea:優先用 tea 對應指令列出專案關聯議題;若該版本 tea 無法列專案議題,改用 API。
|
||||
- API:以 Gitea 的 project/board API 取得該專案掛載的所有議題(column/card → issue),彙整成議題清單(每筆記下 `owner/repo`、`index`、`title`、`labels`)。
|
||||
- 若該 Gitea 版本不支援 project API 或查不到關聯議題,用 AskUserQuestion 告知並請使用者改提供議題清單(或改給單一議題 URL),不要自行臆測要同步哪些議題。
|
||||
|
||||
## 第 2 步:依標籤分組並詢問要同步哪些(AskUserQuestion)
|
||||
|
||||
1. 讀取清單中每個議題的 `labels`。
|
||||
2. **跳過條件**:若清單只有**一個議題**,或**所有議題都沒有標籤**,跳過本步、同步全部清單。
|
||||
3. 否則依標籤把議題分組(一個議題有多個標籤時,各組都出現),用 **AskUserQuestion(multiSelect)** 讓使用者挑選要同步「哪些標籤」的議題:
|
||||
- 每個選項是一個標籤(附該標籤下的議題數量),讓使用者多選。
|
||||
- 標籤數量超過 AskUserQuestion 選項上限(4)時,改在訊息中列出全部標籤與各自議題數,請使用者回覆要同步哪些(可用「其他」自訂輸入)。
|
||||
4. 依選取的標籤過濾清單:保留**帶有任一選取標籤**的議題,作為後續要同步的最終清單。未被選取標籤涵蓋的議題不同步。
|
||||
|
||||
## 第 3 步:逐議題派 subagent 產生同步草稿(每個議題一個 subagent)
|
||||
|
||||
對最終清單中的**每一個議題各派一個 subagent**。subagent **以目前工作目錄下的所有檔案為依據**,只讀檔案與議題、**只在 `.docs/doc-issues-sync/` 底下寫草稿**,**不得修改任何工作目錄的原始碼、不得直接改議題正文/標籤、不得直接留言**。每個 subagent 依序做:
|
||||
|
||||
1. **讀取議題**:`title`、`body`(含其中的 TODO 任務清單)、`labels`、以及既有 comments。
|
||||
- tea:`tea issues <index> --repo <owner>/<repo> --login <name> --comments`。
|
||||
- API:`GET {base}/repos/{owner}/{repo}/issues/{index}` 與 `.../comments`。
|
||||
- 一併盤點該 repo 既有標籤(供第 3.2 用):`GET {base}/repos/{owner}/{repo}/labels`(tea:`tea labels list`)。
|
||||
2. **3.1 判斷 TODO 是否足以追蹤議題描述的需求,不足就補**:分析議題描述的需求,逐項對照現有 TODO,判斷目前的 TODO 清單是否足以追蹤議題描述的需求。若不足,補上缺少的 TODO(以未完成 `- [ ]` 形式),規劃**追加到議題正文**(草稿中給出「追加後的正文」與「新增了哪些 TODO」)。補的 TODO 必須能對應到議題描述的需求,不得編造需求未涵蓋的項目。
|
||||
3. **3.2 依需求更新可用標籤**:依議題需求性質,從該 repo **既有標籤**中挑選應掛上(或應移除)的標籤,草稿中列出「建議的標籤異動」(新增哪些、移除哪些、維持哪些)。**不自行新建標籤**,除非使用者要求;找不到合適標籤就維持原樣並標註。
|
||||
4. **3.3 逐條勾稽未完成 TODO 是否已完成**:對所有**未完成**的 TODO(含 3.1 新增的),逐條依工作目錄下的檔案內容判斷是否已完成。已完成者標記為 `- [x]` 並在草稿記下判斷依據(以 `path:line` 指出對應實作位置);無法從檔案可靠判斷者維持未完成並標註「需人工確認」。
|
||||
5. **3.4 整理 TODO 異動留言**:若本議題有任何 TODO 異動(**新增**的 TODO,或**狀態變更**——由未完成改為完成),整理成一則留言草稿,內容包含:本次新增了哪些 TODO、哪些 TODO 判定為完成(附對應實作位置)、哪些仍未完成(含原因/需人工確認)。若沒有任何 TODO 異動,草稿標明「無異動、不需留言」。
|
||||
|
||||
每個 subagent 產出一份 `.docs/doc-issues-sync/issue-{owner}-{repo}-{index}.md`,至少包含:議題參照與標題、追加後的完整正文(標明新增與勾稽的變更)、建議的標籤異動、TODO 異動留言草稿(或「無異動」)、以及所有「需人工確認」項目。
|
||||
|
||||
## 第 4 步:草稿品質檢查
|
||||
|
||||
實際寫入 Gitea 前,主 agent 必須檢查所有議題草稿:
|
||||
|
||||
- 內容以繁體中文為主、英文為輔,無亂碼或破損文字。
|
||||
- 每個要同步的議題都有對應草稿;正文的 TODO 變更(新增/勾稽)與留言草稿的敘述一致。
|
||||
- 標籤異動只用到該 repo 既有標籤(名稱/id 對得上第 3.1 盤點結果),未擅自新建標籤。
|
||||
- 「已完成」的勾稽都有工作目錄檔案的依據;無依據者標為未完成或「需人工確認」,未被誤判為完成。
|
||||
- 有問題先修正草稿並重新檢查,通過後才進入下一步。
|
||||
|
||||
## 第 5 步:詢問使用者要如何執行(AskUserQuestion)
|
||||
|
||||
草稿通過檢查後,主 agent 用 AskUserQuestion 讓使用者確認要如何對 Gitea 執行寫入,至少提供:
|
||||
|
||||
1. 全部執行:追加/勾稽 TODO 到議題正文、套用標籤異動、對有異動的議題留言。
|
||||
2. 只更新議題(正文+標籤),先不留言。
|
||||
3. 只產生草稿、先不動 Gitea:僅輸出本機草稿供檢視。
|
||||
4. 逐議題確認:每處理完一個議題就回報,待使用者確認後再做下一個。
|
||||
5. 其他(由使用者輸入自訂方式)。
|
||||
|
||||
未獲確認前不得對 Gitea 做任何寫入。
|
||||
|
||||
## 第 6 步:套用到 Gitea
|
||||
|
||||
依使用者選擇,對最終清單的每個議題執行(主 agent 執行,非 subagent):
|
||||
|
||||
- **更新正文(TODO 追加+勾稽)**:以草稿中「追加後的完整正文」更新議題 body。
|
||||
- tea:對應的 issue 編輯指令;API:`PATCH {base}/repos/{owner}/{repo}/issues/{index}`,body `{"body":"<新正文>"}`。
|
||||
- 更新前先重新讀一次議題正文,若與 subagent 讀到的版本已不同(他人期間有改動),停下該議題並回報,避免覆蓋他人變更。
|
||||
- **標籤異動**:套用建議的新增/移除。
|
||||
- tea:`tea labels`/issue 編輯對應指令;API:`POST`/`DELETE {base}/repos/{owner}/{repo}/issues/{index}/labels`(用既有 label id)。
|
||||
- **留言**:對有 TODO 異動的議題張貼留言草稿。
|
||||
- tea:`tea comment --repo <owner>/<repo> --login <name> <index> "<留言內容>"`;API:`POST {base}/repos/{owner}/{repo}/issues/{index}/comments`,body `{"body":"<留言內容>"}`。
|
||||
- 無異動的議題不留言。
|
||||
- 若選「逐議題確認」,每處理完一個就回報並等待確認再繼續。
|
||||
|
||||
## 第 7 步:回報與清理
|
||||
|
||||
- 回報:輸入是專案或議題、(若為專案)關聯議題數與依標籤篩選後的最終清單、每個議題新增了哪些 TODO、勾稽為完成的 TODO(附實作位置)、標籤異動、是否留言,以及所有「需人工確認」或「Gitea 版本不支援」項目。
|
||||
- 草稿(`.docs/doc-issues-sync/`)預設保留供檢視;使用者要求清理時才刪除本次產生的檔案,不得刪除 `.docs/` 內其他既有檔案。
|
||||
|
||||
## 重要限制
|
||||
|
||||
- 修改議題正文、變更標籤、留言都是對外且不易復原的動作,**必須先經第 5 步使用者確認**;未確認前只產生本機草稿。
|
||||
- 「TODO 是否完成」「該補哪些 TODO」「該掛哪些標籤」一律以**工作目錄下的檔案**為依據;無法可靠判斷就標「需人工確認」,不得臆測或編造需求未涵蓋的內容。
|
||||
- subagent 與各步驟只讀檔案與議題、只寫 `.docs/` 草稿,**不得修改任何工作目錄原始碼**。
|
||||
- 標籤只從既有標籤挑選,不自行新建(除非使用者要求)。
|
||||
- 不要依賴 `jq`(未安裝);JSON 解析改用 tea 結構化輸出或由 subagent 解析。
|
||||
- 留言與草稿不得洩漏個資(PII);若議題內容含個資,於草稿與留言中僅保留必要資訊或去識別化。
|
||||
- 文件、留言與草稿以繁體中文為主、英文為輔;API 名稱、型別名稱與程式碼片段可保留英文。
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: doc-docker
|
||||
name: docker
|
||||
description: 整理並對齊 docker-compose.yaml 的行內註解與標題區塊。當使用者要對齊 docker-compose 註解、整理 compose 檔註解欄位、更新 compose 標題日期,或提到 docker-compose、dc-tidy、align_comments、註解對齊時使用此 skill。
|
||||
---
|
||||
|
||||
@@ -7,7 +7,7 @@ description: 整理並對齊 docker-compose.yaml 的行內註解與標題區塊
|
||||
|
||||
整理並對齊 `docker-compose.yaml` 的行內註解與標題。請優先使用自動化腳本執行。
|
||||
|
||||
以下範例中的 `skill_dir` 是本 skill 所在目錄,也就是包含此 `SKILL.md` 與 `scripts/` 的資料夾。不要假設目標專案內存在 `plugins/skills/doc-docker/`。
|
||||
以下範例中的 `skill_dir` 是本 skill 所在目錄,也就是包含此 `SKILL.md` 與 `scripts/` 的資料夾。不要假設目標專案內存在 `plugins/skills/docker/`。
|
||||
|
||||
## 全專案批次處理(未指定檔案時)
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
---
|
||||
name: doc-funcs
|
||||
description: 先判斷專案語言,再為每個 function 與每個指令檔(腳本/CI/部署設定檔)建立 .docs/ 草稿並補齊註解,並整理 `.gitea/workflows/readme.md` 的 workflow 說明、觸發條件與相關參數草稿;由使用者選擇實作方式後寫回原始碼/覆蓋指令檔/更新 workflow readme,接著保守優化被文件化原始碼的效能與排版,最後重建 README 專案列表、功能列表與使用範例。當使用者要補齊 function 文件、產生 XML doc、為每個 method 加 summary/param/remarks、為腳本或 CI/部署設定檔逐行加註解、整理 workflow README、建立 .docs 草稿,或提到 doc-funcs、function 文件化、指令檔註解、workflow 文件化、XML documentation comments 時使用此 skill。
|
||||
name: funcs
|
||||
description: 先判斷專案語言,再為每個 function 與每個指令檔(腳本/CI/部署設定檔)建立 .docs/ 草稿並補齊註解,並整理 `.gitea/workflows/readme.md` 的 workflow 說明、觸發條件與相關參數草稿;由使用者選擇實作方式後寫回原始碼/覆蓋指令檔/更新 workflow readme,並依專案內多數檔案的大小寫命名慣例正規化 Dockerfile 與 README 檔名(含同步更新引用),接著保守優化被文件化原始碼的效能(排版依原本方式維持原樣,僅修正有誤處),最後重建 README 專案列表、功能列表與使用範例。當使用者要補齊 function 文件、產生 XML doc、為每個 method 加 summary/param/remarks、為腳本或 CI/部署設定檔逐行加註解、整理 workflow README、建立 .docs 草稿,或提到 funcs、function 文件化、指令檔註解、workflow 文件化、XML documentation comments 時使用此 skill。
|
||||
---
|
||||
|
||||
# 補齊 function 與指令檔文件
|
||||
|
||||
你要替目前工作區內的專案補齊 function 文件、指令檔註解與 workflow README。所有草稿一律由 subagent 產生,草稿全部完成後再詢問使用者如何實作,實作完成後優化被文件化原始碼的效能與排版,最後重建 README。請依下列階段依序完成。
|
||||
你要替目前工作區內的專案補齊 function 文件、指令檔註解與 workflow README。所有草稿一律由 subagent 產生,草稿全部完成後再詢問使用者如何實作,實作完成後保守優化被文件化原始碼的效能(排版依原本方式維持原樣,僅修正有誤處),最後重建 README。請依下列階段依序完成。
|
||||
|
||||
## 共用規範(shared plugin,必要前置)
|
||||
|
||||
執行本 skill 前,先以 Skill 工具載入下列共用規範並全程遵守;**任一載入不到(shared plugin 未安裝)時,先詢問使用者是否安裝 shared plugin(`https://gitea.jsc.idv.tw/plugins/shared.git`),使用者不安裝則直接中斷本 skill**,不得只憑下方一行摘要繼續執行:
|
||||
|
||||
- `/jsc-shared:spec-output`:繁體中文為主英文為輔、UTF-8(不含 BOM)無亂碼、subagent 提示需帶入本規範。
|
||||
- `/jsc-shared:spec-execution`:不臆測/需人工確認、不擴及無關檔案(generated/bin/obj/.git/.docs 與第三方依賴)。
|
||||
- `/jsc-shared:spec-time-log`:更新時間 Asia/Taipei `yyyy/MM/dd HH:mm:ss`、輸出訊息格式 `[時間][階段][等級]: 訊息`、一行一則。
|
||||
|
||||
## 第 0 步:先判斷語言與生態
|
||||
|
||||
@@ -13,7 +21,11 @@ description: 先判斷專案語言,再為每個 function 與每個指令檔(
|
||||
|
||||
- 檢查專案檔與設定檔(例如 `*.csproj`/`*.sln`、`package.json`、`pyproject.toml`/`requirements.txt`、`go.mod`、`pom.xml`/`build.gradle`、`Cargo.toml` 等)、主要副檔名分布與 README,推斷主要語言。
|
||||
- 依語言決定 function 註解格式:C# 用 XML documentation comments(`<summary>`/`<param>`/`<remarks>`);其他語言改用該語言慣用的文件註解格式(例如 JS/TS 用 JSDoc、Python 用 docstring、Go 用 doc comment、Java 用 Javadoc)。
|
||||
- 在 `.docs/doc-funcs-index.md` 開頭記錄判斷出的主要語言與將採用的註解格式,作為後續所有 subagent 的依據。
|
||||
- 統計專案檔名的大小寫慣例,作為後續 `Dockerfile` 與 README 檔名正規化(第 6 步、第 8 步)的依據:
|
||||
- 樣本範圍排除 generated/bin/obj/.git/.docs 與第三方依賴;把檔名歸類為「全大寫」(如 `README.md`、`CHANGELOG.md`)、「首字大寫」(如 `Dockerfile`、`Makefile`)、「全小寫」(如 `readme.md`、`dockerfile`)等風格。
|
||||
- 優先以同類型檔案為樣本(README 看其他 `*.md` 文件檔、Dockerfile 看其他容器/建置相關檔);同類型樣本不足 3 個時,改看全專案檔名分布。
|
||||
- 只有某一風格明顯過半才視為「專案多數慣例」;無明顯多數時不做檔名正規化,保留原檔名。
|
||||
- 在 `.docs/doc-funcs-index.md` 開頭記錄判斷出的主要語言、將採用的註解格式,以及檔名命名慣例的判斷結果(多數風格或「無明顯多數」)。
|
||||
|
||||
## 第 1 步:掃描 function 與指令檔
|
||||
|
||||
@@ -53,7 +65,7 @@ description: 先判斷專案語言,再為每個 function 與每個指令檔(
|
||||
|
||||
對每個指令檔,subagent 要**複製原始指令檔的完整內容**到草稿,並補上註解,作為實作時直接覆蓋原檔的版本。草稿檔放在 `.docs/doc-funcs/commands/{relative-path}` (保留原副檔名,便於語法檢查)。草稿內容規則:
|
||||
|
||||
- 檔案開頭必須有一段註解區塊,且「該份指令檔的用途」與「更新日期」必須包在同一個區塊內,不得拆成兩個分開的註解區塊。更新日期使用台灣時區(Asia/Taipei)並固定輸出為 `yyyy/MM/dd HH:mm:ss`(可用 `TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M:%S'` 取得)。
|
||||
- 檔案開頭必須有一段註解區塊,且「該份指令檔的用途」與「更新日期」必須包在同一個區塊內,不得拆成兩個分開的註解區塊。更新日期使用台灣時區(Asia/Taipei)並固定輸出為 `yyyy/MM/dd HH:mm:ss`(可用 `TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M:%S'` 取得)。標頭格式必須依本 skill 的範本 `templates/command-header.md` 產生:依檔案類型選用 `#` 或 `::`/`REM` 變體,並遵守範本的佔位符與放置規則(shebang/`@echo off` 之後、外框成對)。派 subagent 產生指令檔草稿時,必須把該範本內容一併提供給 subagent。
|
||||
- 原指令檔的每一行有效指令之間必須換行,且每行都要有對應的註解說明,解釋這行在做什麼、為何需要、重要參數或副作用。
|
||||
- 註解符號必須符合該檔案類型:`*.sh`/`*.bash`/`*.ps1`/Makefile/yaml/Dockerfile 用 `#`;`*.bat`/`*.cmd` 用 `REM` 或 `::`。若該行語法不允許行尾註解(例如某些 yaml 值),改用該行上方獨立一行註解。
|
||||
- 必須保留原始指令的實際行為與順序,只新增註解與開頭用途/日期區塊,不得變更指令邏輯;若發現原指令可能有問題,於草稿中以註解標註「需人工確認」,不要逕自修改。
|
||||
@@ -73,7 +85,7 @@ description: 先判斷專案語言,再為每個 function 與每個指令檔(
|
||||
在實作到原始碼之前,主 agent 必須檢查所有草稿:
|
||||
|
||||
- 內容以繁體中文為主、英文為輔,且沒有任何亂碼、編碼錯誤、不可讀字元或明顯破損文字。
|
||||
- 指令檔草稿的指令本體與原檔一致、註解符號正確、開頭含用途與更新日期、每行皆有註解。
|
||||
- 指令檔草稿的指令本體與原檔一致、註解符號正確、每行皆有註解,且開頭標頭符合 `templates/command-header.md` 範本(用途與更新時間同一區塊、外框成對、位置正確)。
|
||||
- workflow README 草稿需完整涵蓋 `.gitea/workflows/` 底下所有 workflow 檔案,且每個 workflow 都要有用途、觸發條件與相關參數說明。
|
||||
- 若發現問題,先修正草稿並重新檢查,通過後才能進入下一步。
|
||||
|
||||
@@ -93,27 +105,23 @@ description: 先判斷專案語言,再為每個 function 與每個指令檔(
|
||||
|
||||
- function 草稿:依第 0 步判斷的語言把建議文件寫入原始碼(C# 用 XML documentation comments)。註解盡量使用繁體中文;保留既有正確文件,僅補齊缺漏或明顯不足處;此步驟不得為了文件改變 runtime 行為。
|
||||
- 指令檔草稿:用草稿內容**覆蓋原始指令檔**(草稿已是含用途/日期/逐行註解的完整版本)。
|
||||
- Dockerfile 檔名正規化:覆蓋 Dockerfile 類指令檔時,若實際檔名大小寫與第 0 步判斷的專案多數命名慣例不符(例如專案多數為全小寫但檔名為 `Dockerfile`,或反之),用 `git mv` 把檔名調整為慣例風格;在大小寫不敏感的檔案系統上需兩段式改名(先 `git mv Dockerfile Dockerfile.tmp` 再 `git mv Dockerfile.tmp dockerfile`)。改名後必須同步更新專案內引用該檔名的位置(例如 docker-compose 的 `dockerfile:`、CI workflow 的 build 參數、文件內連結),確保建置行為不變;此檔名與引用調整不視為變更指令邏輯。第 0 步判斷為「無明顯多數」時保留原檔名,不做改名。
|
||||
- workflow README 草稿:用草稿內容**覆蓋 `.gitea/workflows/readme.md`**,保留 workflow 實際設定不變,僅整理成說明文件。
|
||||
- 若選「逐個草稿實作」,每完成一個就回報並等待使用者確認。
|
||||
- 若遇到大量目標,仍要分批持續處理,不要只做示範。若 token 或時間不足,先完成已列入 index 的批次,並在 `.docs/doc-funcs-index.md` 標記 pending。
|
||||
- 輸出訊息格式:若該 function 或指令檔有輸出訊息(例如 log、console 輸出、echo、回傳給使用者的提示訊息),訊息格式必須統一為 `[{階段}?][{等級:INF/WRN/ERR/TRC/DBG}][{時間}]: {訊息}`。其中 `階段` 為選填(沿用該訊息所屬區塊的原始名稱、保留原文不翻譯,例如中文區塊名就用中文;無對應階段時省略整個 `[{階段}]` 區塊);`等級` 必須是 `INF`/`WRN`/`ERR`/`TRC`/`DBG` 其中之一;`時間` 使用台灣時區(Asia/Taipei)。調整輸出訊息格式僅限本次被文件化的原始碼或被覆蓋的指令檔,且不得改變訊息所反映的實際行為或判斷邏輯。
|
||||
- 區塊內 log 的階段命名:若被調整格式的 log 被包在某個有名稱的區塊內,必須將該區塊名稱作為該 log 的 `階段` 名稱(沿用原始名稱、保留原文不翻譯),套用完成後移除標示該區塊的包裹/標題本身(僅移除標記與包裹,保留區塊內原有的指令與行為)。有名稱的區塊包含但不限於:
|
||||
- 原始碼:`#region 名稱`/`#endregion`、或其他帶名稱的包裹結構。
|
||||
- 指令檔:以「印出分隔線+區塊標題+分隔線」這種橫幅(banner)方式宣告的段落(例如先 echo `====`、再 echo 區塊名稱、再 echo `----`)。此時橫幅顯示的標題即為該段所有 log 的 `階段` 名稱,且必須移除這幾行印出橫幅的輸出指令,改成把 `階段` 名稱併進該段每一行訊息的前綴。
|
||||
- 例外:指令檔開頭「用途/更新日期」的檔案說明標頭(含其外框分隔線)屬於檔案標頭、不是階段區塊,必須原樣保留,不可被移除或轉成 `階段` 前綴。
|
||||
- 一行一則訊息:每一則輸出訊息(原始碼或指令檔皆適用)都必須是獨立的單行輸出指令,一則訊息對應一行;不得用任何區塊(例如多行字串、字串拼接累積成一坨、迴圈外層包住整段訊息的結構)把多則訊息包成一個輸出。原本被包成一坨輸出的多則訊息,必須拆成逐行、逐則的輸出,且每則仍套用上述統一訊息格式。
|
||||
- 輸出訊息格式:依 `/jsc-shared:spec-time-log` — 若該 function 或指令檔有輸出訊息,統一為 `[yyyy/MM/dd HH:mm:ss][階段][等級]: 訊息`(`階段` 選填、沿用所屬區塊原始名稱不翻譯;`等級` 限 `INF`/`WRN`/`ERR`/`TRC`/`DBG`;時間 Asia/Taipei);區塊階段命名(`#region`/橫幅段落名稱作為 `階段` 前綴並移除包裹/橫幅本身、僅移除標記保留指令與行為)、檔案標頭保留例外、一行一則規則皆依該 spec。調整僅限本次被文件化的原始碼或被覆蓋的指令檔,且不得改變訊息所反映的實際行為或判斷邏輯。
|
||||
|
||||
## 第 7 步:實作註解後,優化被文件化原始碼的效能與排版
|
||||
## 第 7 步:實作註解後,保守優化效能並僅修正有誤的排版
|
||||
|
||||
完成註解實作後,對「本次被文件化的原始碼」做保守的效能與排版優化:
|
||||
完成註解實作後,對「本次被文件化的原始碼」做保守的效能優化;排版**依照原本的排版方式維持原樣**,僅修正確實有誤之處:
|
||||
|
||||
- 效能:在不改變對外行為與輸出的前提下,優化明顯可改善處(例如不必要的重複計算、可提前 return、低效集合操作)。任何不確定是否等價的改動一律不做,並以註解或回報標註建議人工評估。
|
||||
- 排版:套用該語言/專案既有的格式化慣例(縮排、空白、括號風格、import/using 排序),不引入與專案風格衝突的格式。
|
||||
- 每次優化後必須執行可用的格式化/建置/測試驗證行為未被破壞;若無法執行,明確說明原因並標註風險。優化僅限本次被文件化的檔案,不擴及無關檔案。
|
||||
- 排版:**預設維持檔案原本的排版方式,不重排**。只有排版確實有誤時才修正,例如:縮排錯亂或與同檔明顯不一致、tab/空白混用造成語法或建置錯誤、括號/區塊對齊錯誤造成誤讀、編碼或行尾字元異常。修正僅限有誤之處並比照該檔既有慣例;不得順手重排其他正確區塊、不得對全檔套用 formatter、不得引入新的排版風格(含 import/using 重新排序)。
|
||||
- 每次優化後必須執行可用的建置/測試(或至少語法檢查)驗證行為未被破壞;不得以套用全檔 formatter 作為驗證方式。若無法執行,明確說明原因並標註風險。優化僅限本次被文件化的檔案,不擴及無關檔案。
|
||||
|
||||
## 第 8 步:重建 README
|
||||
|
||||
補齊後,重建專案根目錄的 README.md。若根目錄已有 README.md,先刪除既有檔案,再產生新的 README.md;不要保留或合併舊內容。README 必須包含更新時間,更新時間必須使用台灣時區(Asia/Taipei)並固定輸出為 `yyyy/MM/dd HH:mm:ss` 格式,例如 `2026/06/22 18:30:05`;README 必須在功能列表前加入專案列表,並只列出所有專案內的公開方法(public method、public constructor、public extension method、public operator),但不得列出單元測試方法;若方法位於測試專案、測試檔案、測試型別,或帶有測試框架屬性/命名(例如 `Test`、`Fact`、`Theory`、`TestMethod`、`TestCase`、`SetUp`、`TearDown`、`Initialize`、`Cleanup`),即使是 public 也要排除。產生 README 前,必須先為每個公開方法決定「最終功能名稱」:
|
||||
補齊後,重建專案根目錄的 README。README 檔名依第 0 步判斷的專案多數命名慣例決定(例如多數全大寫用 `README.md`、多數全小寫用 `readme.md`);第 0 步判斷為「無明顯多數」或無法判斷時,沿用既有 README 檔名,完全沒有既有 README 時預設 `README.md`。若根目錄已有 README(不論大小寫),先刪除既有檔案,再以慣例檔名產生新的 README;不要保留或合併舊內容,也不得同時留下兩種大小寫的 README。若檔名大小寫因此改變,需同步更新專案內引用舊 README 檔名的位置(例如文件連結、CI、套件描述檔)。README 必須包含更新時間,更新時間必須使用台灣時區(Asia/Taipei)並固定輸出為 `yyyy/MM/dd HH:mm:ss` 格式,例如 `2026/06/22 18:30:05`;README 必須在功能列表前加入專案列表,並只列出所有專案內的公開方法(public method、public constructor、public extension method、public operator),但不得列出單元測試方法;若方法位於測試專案、測試檔案、測試型別,或帶有測試框架屬性/命名(例如 `Test`、`Fact`、`Theory`、`TestMethod`、`TestCase`、`SetUp`、`TearDown`、`Initialize`、`Cleanup`),即使是 public 也要排除。產生 README 前,必須先為每個公開方法決定「最終功能名稱」:
|
||||
|
||||
- 預設功能名稱為 `Type.Method`。
|
||||
- 若公開方法所在的型別簡名在不同專案或不同命名空間中重複,最終功能名稱不得只使用 `Type.Method`,必須在型別前加入可辨識的專案或模組前綴,格式為 `Module.Type.Method`。例如 `Hangfire.ServiceCollectionExtension.AddHangfireOptions`、`Hangfire.SqlServer.ServiceCollectionExtension.AddSqlServerHangfire`、`Swagger.ServiceCollectionExtension.AddSwagger`、`Swagger.ApplicationBuilderExtension.UseSwaggerUI`。
|
||||
@@ -157,11 +165,13 @@ README 錨點檢查通過後,刪除本次產生的所有草稿與索引:`.do
|
||||
|
||||
## 重要限制
|
||||
|
||||
- 不要修改 generated/bin/obj/.git/.docs 以外的非原始碼/非指令檔,除非是建立草稿、索引,依流程實作註解、依草稿覆蓋指令檔、優化本次被文件化原始碼,或刪除並重建根目錄 README.md。
|
||||
- 不要修改 generated/bin/obj/.git/.docs 以外的非原始碼/非指令檔,除非是建立草稿、索引,依流程實作註解、依草稿覆蓋指令檔、優化本次被文件化原始碼、刪除並重建根目錄 README,或依第 0 步判斷的專案多數命名慣例正規化 `Dockerfile` 與 README 檔名(含同步更新引用舊檔名的位置)。
|
||||
- `Dockerfile` 與 README 的檔名正規化只在專案多數慣例明確(某一風格明顯過半)時執行;無明顯多數就保留原檔名。改名時必須同步更新所有引用,不得造成建置或連結失效。
|
||||
- 不要新增與文件無關的 helper、測試或重構。
|
||||
- 草稿是實作依據,不能跳過;所有草稿一律由 subagent 產生。
|
||||
- function 註解步驟不得為了文件改變 runtime 行為;效能優化僅限第 7 步、僅限本次被文件化原始碼,且必須保持對外行為等價並驗證。
|
||||
- 指令檔草稿只新增註解與開頭用途/日期區塊,不得變更指令邏輯;唯一例外是第 6 步的輸出訊息格式正規化(可移除區塊橫幅、把區塊名稱併入每行前綴、統一訊息格式),但不得改變訊息反映的實際行為,且開頭用途/更新日期標頭必須保留。指令檔開頭的用途與更新日期必須包在同一個註解區塊內。
|
||||
- function 或指令檔若有輸出訊息,訊息格式必須統一為 `[{階段}?][{等級:INF/WRN/ERR/TRC/DBG}][{時間}]: {訊息}`(`階段` 選填、沿用所屬區塊原始名稱並保留原文不翻譯、`等級` 限 `INF`/`WRN`/`ERR`/`TRC`/`DBG`、`時間` 用 Asia/Taipei 時區),且不得藉此改變訊息反映的實際行為。若該 log 被包在有名稱的區塊內(含指令檔以分隔線+標題+分隔線宣告的橫幅段落),須將區塊名稱當作 `階段` 名稱後移除該包裹/橫幅,且僅移除包裹、保留區塊內原有指令與行為;但開頭用途/更新日期標頭不算階段區塊,必須保留。每則訊息必須一行一則、各自為獨立的單行輸出指令,不得用區塊或字串拼接把多則訊息包成一坨輸出。
|
||||
- 排版一律依照檔案原本的排版方式;只有排版確實有誤(縮排錯亂、tab/空白混用致錯、對齊錯誤造成誤讀、編碼/行尾異常)才修正該處,不得全檔重排、不得套用 formatter 改變原有風格。
|
||||
- 指令檔草稿只新增註解與開頭用途/日期區塊,不得變更指令邏輯;唯一例外是第 6 步的輸出訊息格式正規化(可移除區塊橫幅、把區塊名稱併入每行前綴、統一訊息格式),但不得改變訊息反映的實際行為,且開頭用途/更新日期標頭必須保留。指令檔開頭的用途與更新日期必須包在同一個註解區塊內,且格式依本 skill 的 `templates/command-header.md` 範本。
|
||||
- function 或指令檔若有輸出訊息,訊息格式、區塊階段命名、檔案標頭保留與一行一則規則一律依 `/jsc-shared:spec-time-log`,且不得藉此改變訊息反映的實際行為。
|
||||
- 若 function 或指令行為無法可靠推論,文件中要保守描述並標註不確定點,不要編造。
|
||||
- 原始碼註解與 README 盡量使用繁體中文;專有名詞、API 名稱、型別名稱與程式碼範例可保留英文。
|
||||
- 原始碼註解與 README 的語言依 `/jsc-shared:spec-output`(繁體中文為主;專有名詞、API 名稱、型別名稱與程式碼範例可保留英文)。
|
||||
@@ -0,0 +1,59 @@
|
||||
# 指令檔開頭「用途/更新時間」標頭範本
|
||||
|
||||
本範本定義 funcs 第 3-2 步指令檔草稿開頭必備的註解區塊格式。「用途」與「更新時間」必須包在同一個註解區塊內,不得拆成兩個分開的區塊;此標頭屬於檔案說明標頭(含外框分隔線),實作與後續輸出訊息格式正規化時必須原樣保留,不得移除或轉成 `階段` 前綴。
|
||||
|
||||
## 佔位符
|
||||
|
||||
- `{用途說明}`:一到三行,說明這份指令檔做什麼、在什麼情境被呼叫、重要副作用;多行時每行開頭都要有註解符號。
|
||||
- `{更新時間}`:台灣時區(Asia/Taipei),固定格式 `yyyy/MM/dd HH:mm:ss`,可用下列指令取得:
|
||||
|
||||
```bash
|
||||
TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M:%S'
|
||||
```
|
||||
|
||||
## 變體 A:`#` 註解(`*.sh`、`*.bash`、`*.ps1`、`Makefile`、yaml、`Dockerfile`、docker-compose)
|
||||
|
||||
```
|
||||
# ============================================================================
|
||||
# 用途:{用途說明}
|
||||
# 更新時間:{更新時間}
|
||||
# ============================================================================
|
||||
```
|
||||
|
||||
填入後範例:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# 用途:打包 Web 專案並上傳部署壓縮檔至部署主機,供 CI 部署階段呼叫。
|
||||
# 更新時間:2026/07/15 14:30:00
|
||||
# ============================================================================
|
||||
```
|
||||
|
||||
## 變體 B:`::` 註解(`*.bat`、`*.cmd`)
|
||||
|
||||
```
|
||||
:: ===========================================================================
|
||||
:: 用途:{用途說明}
|
||||
:: 更新時間:{更新時間}
|
||||
:: ===========================================================================
|
||||
```
|
||||
|
||||
填入後範例:
|
||||
|
||||
```bat
|
||||
@echo off
|
||||
:: ===========================================================================
|
||||
:: 用途:清理建置輸出目錄並重新建置方案,供本機開發快速重建使用。
|
||||
:: 更新時間:2026/07/15 14:30:00
|
||||
:: ===========================================================================
|
||||
```
|
||||
|
||||
(改用 `REM` 亦可,但同一份檔案內擇一使用並保持一致。)
|
||||
|
||||
## 放置規則
|
||||
|
||||
- 標頭放在檔案最前面;若第一行是必須位於首行的宣告(例如 shebang `#!...`、`@echo off`),標頭緊接在其後。
|
||||
- yaml 檔若有 document marker(`---`),`#` 標頭放在 `---` 之前即可。
|
||||
- 外框分隔線長度不強制,但上下外框必須成對出現,且整個標頭(含外框)視為同一個註解區塊。
|
||||
- 「用途」與「更新時間」兩行的中文標籤與順序依本範本,不得只留其中一項。
|
||||
+13
-6
@@ -1,20 +1,29 @@
|
||||
---
|
||||
name: doc-issues-analyze-to-file
|
||||
description: 讀取一或多筆 Gitea issue URL(優先用 tea,否則用 Gitea API),彙整成一份完整需求文件,依功能拆成多個實作階段並各建立一個 issue(沿用來源 issue 的里程碑/專案,依需求性質填入標籤),再配合使用者指定的 repositories 或 issue 所在 repo 產生實作草稿,最後產出交付文件並依 issues 分組留言到對應 issue。當使用者要分析 issue、把需求拆成多階段 issue、依 issue 產生實作規劃或交付留言,或提到 doc-issues-analyze-to-file、issue 需求分析、issue 拆階段、tea issues、Gitea issue 留言時使用此 skill。
|
||||
name: issues-analyze-to-file
|
||||
description: 讀取一或多筆 Gitea issue URL(優先用 tea,否則用 Gitea API),彙整成一份完整**需求文件檔案**,依功能拆成多個實作階段並各建立一個 issue(沿用來源 issue 的里程碑/專案,依需求性質填入標籤),再配合使用者指定的 repositories 或 issue 所在 repo 產生**實作草稿檔**,最後產出**交付文件檔**並依 issues 分組留言到對應 issue;本 skill 以「先落地草稿檔、經使用者確認再寫回 Gitea」為核心,適合需要保留需求文件與交付文件檔案的流程。當使用者明確要「產出需求文件/實作草稿/交付文件檔案」的 issue 分析、或提到 issues-analyze-to-file、issue 需求分析文件、issue 拆階段交付文件時使用此 skill。不適用於:全程不落地檔案、以議題描述與留言保存中間成果的需求拆分(用 issues-analyze);實作議題程式碼(用 issues)。兩者都可能符合、使用者未指明時,先詢問要「檔案交付」還是「議題留言」再選擇。
|
||||
---
|
||||
|
||||
# 分析 Issue 並拆解為實作階段與交付留言
|
||||
|
||||
你要讀取使用者提供的一或多筆 Gitea issue,彙整成完整需求文件,依功能拆成多個實作階段(每階段建立一個 issue),配合指定的 repositories 產生實作草稿,最後產出交付文件並依 issues 分組留言。**建立 issue 與留言屬於對外且不易復原的動作,必須先讓使用者確認過草稿再執行**。所有需求彙整、階段拆分與實作草稿一律先產生草稿檔,再詢問使用者是否實際建立 issue / 留言。請依下列階段依序完成。
|
||||
|
||||
## 共用規範(shared plugin,必要前置)
|
||||
|
||||
執行本 skill 前,先以 Skill 工具載入下列共用規範並全程遵守;**任一載入不到(shared plugin 未安裝)時,先詢問使用者是否安裝 shared plugin(`https://gitea.jsc.idv.tw/plugins/shared.git`),使用者不安裝則直接中斷本 skill**,不得只憑下方一行摘要繼續執行:
|
||||
|
||||
- `/jsc-shared:spec-output`:繁體中文為主英文為輔、UTF-8(不含 BOM)無亂碼、Mermaid 呈現、個資(PII)去識別化。
|
||||
- `/jsc-shared:spec-execution`:不臆測/需人工確認、不擴及無關檔案。
|
||||
- `/jsc-shared:spec-gitea`:`GITEA_TOKEN` 機密保護、不依賴 `jq`、API 呼叫慣例(`Authorization: token`、分頁完整讀取)。
|
||||
|
||||
## 前置:輸入與工具
|
||||
|
||||
- **輸入**:至少一筆 issue URL(可多筆)。可另外指定「repositories 位置」(本機含多個專案的資料夾);若未指定,實作草稿以各 issue 所在的 repository 為準。
|
||||
- **工具優先序**:
|
||||
1. 若該 issue host 在 `tea login list` 中有對應 login,優先用 `tea`(`tea issues`、`tea comment` 等),並以 `--login <name> --repo <owner>/<repo>` 指定目標。
|
||||
2. 否則改用 Gitea REST API + `curl`,帶標頭 `Authorization: token $GITEA_TOKEN`(環境變數 `GITEA_TOKEN` 已設定)。
|
||||
- **不要依賴 `jq`(環境未安裝)**:需要解析 JSON 時,用 `tea` 的結構化輸出(例如 `--fields ... --output csv`),或把原始 JSON 交給 subagent 解析,不要在指令中 pipe 到 `jq`。
|
||||
- **不要依賴 `jq`**:依 `/jsc-shared:spec-gitea`(JSON 用 tea 結構化輸出或交給 subagent 解析,不 pipe 到 `jq`)。
|
||||
- **工作目錄**:所有草稿與文件放在 `.docs/doc-issues-analyze-to-file/`。
|
||||
- **議題描述流程圖**:依 `/jsc-shared:spec-output` — 產生要寫進 issue 的描述(尤其各階段 issue 的 body)時,有助理解就加入 Mermaid 流程圖(處理流程、狀態轉移、階段相依關係),忠實反映需求與拆分結果、不得杜撰。
|
||||
|
||||
## 第 0 步:解析 issue URL 與準備工具
|
||||
|
||||
@@ -121,7 +130,5 @@ description: 讀取一或多筆 Gitea issue URL(優先用 tea,否則用 Gite
|
||||
- 建立 issue 與留言是對外且不易復原的動作,**必須先經第 6 步使用者確認**;未確認前只產生本機草稿。
|
||||
- 新 issue 一律沿用來源 issue 的里程碑與專案;標籤只從既有標籤中依需求性質挑選,不自行新建(除非使用者要求)。
|
||||
- subagent 與各步驟只讀程式碼與 issue、只寫 `.docs/` 草稿,**不得修改任何原始碼**;本 skill 的產出是需求文件、階段 issue、實作草稿與交付留言,不含改動程式邏輯。
|
||||
- 不要依賴 `jq`(未安裝);JSON 解析改用 tea 結構化輸出或由 subagent 解析。
|
||||
- JSON 解析(不依賴 `jq`)依 `/jsc-shared:spec-gitea`;個資保護(PII)與語言規範依 `/jsc-shared:spec-output`。
|
||||
- 需求、階段與實作草稿若無法可靠推論,一律保守描述並標註「需人工確認」,不得編造 issue 未提及的內容。
|
||||
- 留言與交付文件不得洩漏個資(PII);若 issue 內容含個資,於文件與留言中僅保留必要資訊或去識別化。
|
||||
- 文件、留言與草稿以繁體中文為主、英文為輔;API 名稱、型別名稱與程式碼片段可保留英文。
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: issues-analyze
|
||||
description: 讀取使用者選擇的一或多種來源(專案編號、議題編號、檔案文件;至少一種;若選專案編號則只讀取該專案下開啟中的議題;處理議題時必須連同所有留言與附件一起讀取,附件內容一併納入需求分析),先檢查 tea 與 GITEA_TOKEN 並詢問使用者要用 tea 或 Gitea API + token,在產生保存議題前必須完整釐清需求、任何不清楚的部分都要詢問使用者、絕不臆測或編造,確認清楚後才將來源內容合併整理成保存議題內容,再拆分成多個小功能議題(標題、描述、阻擋關閉、依複雜度評估到期日;形成子母議題時母議題必須所有子議題關閉後才可關閉,優先以 issue dependency 阻擋),每個小功能議題都必須詢問使用者描述是否有補充內容,所有議題都要根據描述內容在描述最後產生 TODO list;分析完成後若議題屬於專案看板且欄位可對應進度語意(例如分析中/待處理/進行中/待測試/已完成),把議題移到「待處理」欄位(不往回移、介面不支援時改列建議清單請人工調整);最後依到期日與相依關係排序小功能議題並把排序結果留言到保存議題。本 skill 到「議題拆分完成+排序留言」為止,**不實作程式碼**(不修改原始碼、不 commit、不 push、不開 PR),實作交由 /jsc-code:issues。當使用者要把需求拆成小功能議題、依專案/議題/文件產生保存議題與功能議題、或提到 issues-analyze、issue breakdown、議題拆分、小功能議題、Gitea issue 拆解時使用此 skill。不適用於:實作議題程式碼(用 issues)、要把彙整結果與實作草稿落地成文件檔案交付的流程(用 issues-analyze-to-file;本 skill 全程不落地任何檔案)。
|
||||
---
|
||||
|
||||
# 分析多來源需求並保存為議題
|
||||
|
||||
你要先做工具可用性檢查並選擇工具;第二步詢問使用者要讀取哪些來源:專案編號、議題編號、檔案文件,至少選一種,接著讀取選定來源,並在產生保存議題前完整釐清需求——只要有任何不清楚的部分都必須詢問使用者,絕對不可以幻想——確認清楚後才彙整成保存議題內容。第三步必須把上個步驟產生的議題內容拆分成多個小功能議題,並為每個小功能議題產生標題、描述、阻擋關閉規則與依複雜度評估的到期日;若形成子母議題(保存議題為母、小功能議題為子),母議題必須所有子議題都關閉後才可關閉(優先以 issue dependency 阻擋);分析完成後,若議題屬於專案看板且欄位可對應進度語意(例如分析中/待處理/進行中/待測試/已完成),把議題移到「待處理」欄位。第四步必須將小功能議題依到期日與相依關係排序,並把排序結果留言到保存議題;**本 skill 不實作程式碼**——不修改原始碼、不 commit、不 push、不開 PR,後續實作交由 `/jsc-code:issues` 或使用者另行處理。所有中間成果都不准落地成草稿檔,必須一律使用 `tea` 或 Gitea API 保存到議題描述或留言。
|
||||
|
||||
## 共用規範(shared plugin,必要前置)
|
||||
|
||||
執行本 skill 前,先以 Skill 工具載入下列共用規範並全程遵守;**任一載入不到(shared plugin 未安裝)時,先詢問使用者是否安裝 shared plugin(`https://gitea.jsc.idv.tw/plugins/shared.git`),使用者不安裝則直接中斷本 skill**,不得只憑下方一行摘要繼續執行:
|
||||
|
||||
- `/jsc-shared:spec-output`:繁體中文為主英文為輔、UTF-8(不含 BOM)無亂碼、表格/Mermaid 呈現、個資(PII)去識別化。
|
||||
- `/jsc-shared:spec-execution`:不臆測/需人工確認、已知資訊跳過詢問。
|
||||
- `/jsc-shared:spec-gitea`:tea/API 工具選擇與檢查、`GITEA_TOKEN` 機密保護、不依賴 `jq`、API 分頁完整讀取。
|
||||
- `/jsc-shared:spec-project-board`:看板欄位語意對應、GET 探測(404/501 不支援)、不往回移、不得新建欄位。
|
||||
|
||||
## 絕對準則(不可違反)
|
||||
|
||||
- **全程不得在磁碟落地任何檔案**:不建立 `.docs/`、不寫草稿檔、不寫暫存檔、不用檔案傳遞中間結果。所有中間成果(需求彙整、保存議題內容、小功能拆分、到期日排序、交付摘要)一律留在**對話內容**與 **subagent 的回傳值**,並透過 `tea` 或 Gitea API **保存到議題描述或留言**。例外只有一個:為了讀取議題附件(圖片等二進位檔)而**唯讀暫存下載到系統暫存目錄**,讀取完畢後立即刪除,不得下載到工作目錄或任何 repo 內、不得用暫存檔傳遞其他中間成果。除此之外不產生任何本機檔案。
|
||||
|
||||
## 前置:輸入與工具
|
||||
|
||||
- **輸入來源**:使用者必須選擇要讀取的來源種類,可多選且數量必須 `>= 1`:
|
||||
- **專案編號**:Gitea project 編號或可定位 project 的 URL/識別資訊;若選取專案編號,必須只讀取該專案下開啟中的議題(含可取得的卡片/欄位/描述);也可作為保存整理結果的目標專案。
|
||||
- **議題編號**:Gitea issue 編號或 issue URL,可多筆。
|
||||
- **檔案文件**:本機文件路徑,可多筆;支援 Markdown、純文字與其他可直接讀取的需求文件。
|
||||
- **保存目標**:合併整理後必須在指定專案建立一張議題保存;若輸入來源未包含可作為保存目標的專案編號,必須詢問使用者提供專案編號,不得自行臆測。
|
||||
- **repositories 位置**:可另外指定本機含多個專案的資料夾;若未指定,程式碼分析參考(僅供研究、補充議題描述,不修改)以來源議題所在 repo、保存目標 repo 或使用者指定 repo 為準。
|
||||
- **工具選擇**:依 `/jsc-shared:spec-gitea` 的工具選擇流程(檢查 `tea`/`tea login list`/`GITEA_TOKEN` 後詢問使用者用 `tea` 或 `api`;已明確指定工具時才可跳過詢問;不依賴 `jq`,JSON 改用 tea 結構化輸出或交給 subagent 解析)。
|
||||
- **議題必須連同留言與附件一起讀取**:處理任何議題(含專案底下展開的議題)時,除了 `title`/`body` 等欄位,必須一併讀取**所有留言(comments)**與**所有附件(attachments/assets,含議題本身與各留言的附件)**,其內容都是需求分析的依據:
|
||||
- 附件清單:`tea` 目前沒有附件指令,一律走 API — 議題附件 `GET {base}/repos/{owner}/{repo}/issues/{index}/assets`、留言附件 `GET {base}/repos/{owner}/{repo}/issues/comments/{id}/assets`,取得每個附件的檔名、類型與下載 URL。
|
||||
- 文字類附件(Markdown、純文字、CSV、JSON 等):以 `curl` 直接取得內容到對話中分析,不落地。
|
||||
- 圖片或其他二進位附件:依絕對準則的例外**唯讀暫存下載到系統暫存目錄**讀取(例如圖片以視覺方式讀取內容),讀取完畢後立即刪除暫存檔。
|
||||
- 無法讀取的格式(或僅有 `tea` 而無 token 可下載附件):在保存議題內容中列出附件檔名與 URL 並標註「附件無法讀取,需人工確認」,不得忽略附件的存在,也不得臆測其內容。
|
||||
- **禁止草稿落地**:所有流程都不准建立 `.docs/` 或其他本機草稿檔;需求整理、小功能拆分、排序、進度與交付資訊一律使用 `tea` 或 Gitea API 保存到對應議題描述或留言。
|
||||
- **TODO list**:所有建立或更新的議題描述最後都必須加上依該描述內容推導出的 `## TODO` 區塊,使用 Markdown checklist(`- [ ] ...`);TODO 必須可執行、可驗收,且不得加入描述未提及或無法合理推得的工作。
|
||||
- **議題描述流程圖**:依 `/jsc-shared:spec-output` — 產生保存議題或小功能議題的描述時,有助理解就加入 Mermaid 流程圖(需求流程、狀態轉移、相依/阻擋關係),忠實反映需求與拆分結果、不得杜撰。
|
||||
|
||||
## 第 1 步:工具可用性檢查與使用方式選擇
|
||||
|
||||
依 `/jsc-shared:spec-gitea` 的工具選擇流程執行:檢查 `tea`(`command -v tea`、`tea login list`,失敗記錄原因不中止)與 `GITEA_TOKEN`(只輸出「已設定/未設定」)→ 詢問使用者要用 `tea` 或 `api`(除非使用者已明確指定,不得自行決定;選 `tea` 後續仍需確認來源 host 有對應 login)→ 兩種方式都不可用則停止並回報缺少 `tea login` 或 `GITEA_TOKEN`(不要要求使用者把 token 貼進對話)。
|
||||
|
||||
## 第 2 步:選擇讀取來源、讀取內容並保存議題內容
|
||||
|
||||
這是必要決策。若使用者尚未明確提供來源種類,必須先詢問要讀取哪些來源種類,並要求至少選一種:
|
||||
|
||||
1. 專案編號。
|
||||
2. 議題編號。
|
||||
3. 檔案文件。
|
||||
|
||||
選定後收集對應輸入:
|
||||
|
||||
- 選「專案編號」:收集 project 編號或 project URL,並確認其 host/owner/repo/project id(若資訊不足,先詢問補齊);後續必須只讀取該專案下開啟中的議題。
|
||||
- 選「議題編號」:收集 issue 編號或 issue URL;若只提供編號,必須確認其 host/owner/repo。
|
||||
- 選「檔案文件」:收集本機檔案路徑並確認存在;不存在的檔案先回報並請使用者修正。
|
||||
|
||||
合併整理後一定要建立一張保存議題:
|
||||
|
||||
- 若已提供專案編號,詢問是否使用該專案作為保存目標;使用者可改指定其他專案。
|
||||
- 若未提供專案編號,必須詢問保存用專案編號或 project URL。
|
||||
- 不得在缺少保存目標專案時繼續到對外建立議題的步驟。
|
||||
|
||||
接著執行:
|
||||
|
||||
1. 解析每一筆來源:
|
||||
- 專案:解析出 `host`、`owner`、`repo`、`project id` 或可定位 project 的資訊。
|
||||
- 議題:解析出 `host`、`owner`、`repo`、`index`(例如 `https://<host>/<owner>/<repo>/issues/<index>`)。
|
||||
- 檔案:解析出本機絕對路徑、檔名與格式。
|
||||
2. 依第 1 步選定的工具建立每筆 Gitea 來源的存取設定:
|
||||
- `tea`:找出對應 host 的 login,後續命令一律帶 `--login <name> --repo <owner>/<repo>`。
|
||||
- `api`:base 為 `https://<host>/api/v1/repos/<owner>/<repo>`。
|
||||
- 若多筆專案/議題分屬不同 host,選擇 `tea` 時必須確認每個 host 都有對應 login;選擇 `api` 時同一個 `GITEA_TOKEN` 必須可存取全部專案/議題,否則在讀取階段回報權限不足並停止。
|
||||
3. 顯示本次處理的基本資料,至少包含:
|
||||
- 選定工具:`tea` 或 `api`。
|
||||
- 若選 `tea`:每個 host 對應的 login 名稱;若選 `api`:顯示 `GITEA_TOKEN` 已設定,不顯示 token 內容。
|
||||
- 讀取來源種類:專案編號/議題編號/檔案文件,至少一種。
|
||||
- 來源專案:每筆 project 的 host、owner、repo、project id(若有)。
|
||||
- 來源議題:每筆 issue 的 URL、host、owner、repo、index(若有)。
|
||||
- 來源檔案:每筆檔案的路徑與格式(若有)。
|
||||
- 保存目標專案:host、owner、repo、project id。
|
||||
- target repositories 來源:使用者指定的 repositories 位置,或「以來源議題所在 repo/保存目標 repo 為準」。
|
||||
4. 若使用者指定了 repositories 位置,先確認該路徑存在並列出其中的專案;若未指定,記錄「以來源議題所在 repo/保存目標 repo 為準」,並確認本機是否已 clone 對應 repo(沒有就在保存議題留言中標註需人工提供或 clone)。
|
||||
5. 依選定來源讀取內容:
|
||||
- 專案:讀取 project 描述、欄位/卡片、project metadata,並只讀取該專案下**開啟中的議題**(若 API 有分頁必須完整分頁讀取;每筆議題都依「議題必須連同留言與附件一起讀取」完整讀取)。若 Gitea 版本不支援 project API 或無法由 project 取得開啟中的 issue 清單,標註「此 Gitea 版本不支援 project API,需人工處理」,並請使用者改提供議題編號或可匯出的 project 文件。
|
||||
- 議題:對每一筆 issue,讀取完整內容:`title`、`body`、`state`、`labels`、`milestone`、`assignees`、**所有 comments**、以及**議題與各留言的所有附件**(讀取方式見前置「議題必須連同留言與附件一起讀取」);若 Gitea 版本支援,另讀該 issue 所屬 `project`。
|
||||
- 檔案文件:讀取文件全文;若格式無法直接讀取,標註需人工轉換或提供純文字/Markdown。
|
||||
6. 同時盤點該 repo 既有的分類資源,供後續階段沿用:
|
||||
- 標籤:`GET {base}/labels`(tea:`tea labels list`)
|
||||
- 里程碑:`GET {base}/milestones`(tea:`tea milestones list`)
|
||||
- 專案(若該 Gitea 版本有此 API):`GET {base}/projects`;若不支援就記錄「此 Gitea 版本不支援 project API,需人工處理」。
|
||||
7. **需求釐清(產生保存議題前的必要關卡)**:讀取完所有來源後、建立或更新保存議題前,必須先完整釐清需求才可以繼續:
|
||||
- 逐一盤點來源內容中所有不清楚、有歧義、互相矛盾、缺少上下文或無法確定的部分(包含:需求範圍不明、驗收條件缺漏、來源之間說法不一致、附件無法讀取造成的資訊缺口、名詞或系統指涉不明等)。
|
||||
- 只要有**任何**不清楚的部分,都必須以 AskUserQuestion 或對話詢問使用者,直到全部釐清;問題可分批詢問,但不得略過任何一項。
|
||||
- **絕對不可以幻想**:不得用臆測、腦補或「合理推測」填補資訊缺口來代替詢問;使用者明確表示某項「先保留、之後再確認」時,才可在保存議題中將該項標註「需人工確認」後繼續。
|
||||
- 所有不清楚的部分都已由使用者釐清(或明確指示保留標註)之前,不得進入下一項建立或更新保存議題。
|
||||
8. 把所有來源內容彙整成保存議題內容,使用 `tea` 或 Gitea API 建立或更新保存議題;不得寫入本機草稿檔。保存議題描述至少包含:
|
||||
- 來源清單:每筆專案/議題/檔案的來源資訊、標題或名稱、狀態、現有 labels/milestone/project(若適用),以及議題的留言數與附件清單(檔名;無法讀取的附件標註「需人工確認」)。
|
||||
- 完整需求描述:整合專案、議題(含留言與附件內容)、檔案文件的內容,去除重複、補齊上下文,形成單一連貫的需求敘述。
|
||||
- 驗收條件/預期結果:能從來源內容推得的,逐條列出;不能確定的標註「需人工確認」。
|
||||
- 保存議題分類:labels/milestone/project 掛載方式。
|
||||
- `## TODO`:根據保存議題描述內容產生 Markdown checklist,放在描述最後。
|
||||
|
||||
需求彙整只做整理與歸納,不得編造來源內容未提及的需求;無法確定處必須依上方「需求釐清」關卡先詢問使用者,只有使用者明確指示保留的項目才可標註「需人工確認」後寫入。
|
||||
|
||||
## 第 3 步:拆分小功能議題
|
||||
|
||||
將第 2 步保存到議題的內容拆分成多個小功能議題,使用 `tea` 或 Gitea API 建立/更新小功能議題或將小功能清單留言到保存議題;不得寫入本機草稿檔。每個小功能議題至少包含:
|
||||
|
||||
- 標題:能清楚表示單一小功能交付範圍。
|
||||
- 描述:描述內容必須先詢問使用者想要包含哪些段落或資訊,至少提供可選項,例如需求背景、功能範圍、驗收條件、技術提示、測試方式、相依關係、風險與備註;依使用者選擇組成描述,不得自行固定格式。每個小功能議題建立或更新前,都必須逐一詢問使用者該議題描述是否有補充內容;使用者提供補充時,必須整合到該小功能議題描述中,若使用者明確表示沒有補充才可繼續建立或更新。描述最後必須加入 `## TODO` 區塊,根據該小功能描述內容產生 Markdown checklist。
|
||||
- 阻擋關閉:小功能議題建立後必須以可追溯方式阻擋其被直接關閉,直到驗收條件完成。可用方式包含加上既有 blocking/blocked 類標籤、在 body 中加入「關閉前檢查清單」、建立與保存議題的追溯連結,或依 Gitea 支援能力設定 issue dependency;不得使用不存在的標籤或 API,找不到支援方式時標註需人工處理。若小功能有前後相依,較先完成的前置議題必須阻擋較後完成的後置議題(前置 issue blocks 後置 issue;後置 issue is blocked by 前置 issue),不得反向設定。
|
||||
- 複雜度:依工作量、跨模組程度、風險、未知數與測試成本評估為 `S`/`M`/`L`/`XL`。
|
||||
- 到期日:根據複雜度評估 due date,預設從建立日往後推算:`S` 3 個工作天、`M` 5 個工作天、`L` 10 個工作天、`XL` 15 個工作天;若遇週末順延到下一個工作天。若小功能有相依關係,必須先排定相依順序,後置功能的到期日不得早於其前置功能的到期日,且應從最後一個前置功能的到期日之後再依自身複雜度推算。若 Gitea API 不支援 due date,寫入 issue body 並回報需人工設定。
|
||||
- 相依關係:列出與保存議題、來源議題與其他小功能議題的關聯;若有前後依賴,必須標明前置功能、後置功能、阻擋方向與到期日排程依據。
|
||||
|
||||
**子母議題關閉規則**:若本次拆分實際建立了子母關係(保存議題為**母議題**、拆出的小功能議題為**子議題**),母議題必須在**所有子議題都關閉後才可關閉**,建立子議題時就要把這個限制落實:
|
||||
|
||||
- 優先用 Gitea issue dependency 實作:把母議題設為 blocked by **每一個**子議題(API `POST {base}/issues/{母議題 index}/dependencies`,body 帶子議題資訊;先以 GET 探測該實例是否啟用 dependency 功能,404/501 視為不支援),讓 Gitea 在子議題尚未全部關閉前直接阻止關閉母議題。
|
||||
- dependency 不支援或未啟用時:在母議題描述加入「子議題清單」markdown 任務清單(每項連結一個子議題,例如 `- [ ] #<index> <子議題標題>`)與「關閉前檢查:所有子議題皆已關閉」字樣,並在回報中標註此限制需人工遵守;不得對未確認存在的端點做寫入。
|
||||
- 後續新增或補拆子議題時,必須同步補上對應的 dependency 或母議題子議題清單項目,不得遺漏。
|
||||
|
||||
決定要對照的 target repositories:使用者指定位置底下的所有專案、來源議題所在 repo,或使用者指定 repo。可研究相關專案程式碼以補充小功能議題描述,但所有分析結果必須直接保存到小功能議題描述或留言,不得建立本機草稿檔。
|
||||
|
||||
**分析完成後:把議題移到看板「待處理」欄位**。保存議題與所有小功能議題都建立/更新完成後(即分析階段結束),對其中**確實屬於某個專案看板(project board)**的議題調整進度欄位:
|
||||
|
||||
- 依 `/jsc-shared:spec-project-board` 執行(欄位語意以看板實際名稱為準、不往回移、先 GET 探測端點且 404/501 視為不支援、不得對未確認端點寫入、不得新建欄位):把議題移動到「待處理」欄位,代表需求分析已完成、等待實作;議題已在「待處理」或更後面的欄位時維持原欄位。
|
||||
- 看板沒有可對應「待處理」語意的欄位、或介面不支援時,不移動、不視為錯誤:改在回報與保存議題留言中列出「議題 → 待處理」建議清單,請使用者到看板手動拖曳。
|
||||
|
||||
## 第 4 步:依到期日排序並交棒實作
|
||||
|
||||
將第 3 步產生的小功能議題依到期日由早到晚排序;若到期日相同,依相依關係排序,前置議題必須排在後置議題前。排序結果必須使用 `tea` 或 Gitea API 留言到保存議題或相關小功能議題,不得寫入本機檔案。
|
||||
|
||||
**本 skill 的範圍到「議題拆分完成+排序留言」為止,不實作程式碼**:不修改任何原始碼、不 commit、不 push、不開 PR、不關閉議題。排序留言完成後:
|
||||
|
||||
- 回報整體結果:保存議題連結、小功能議題清單(標題/到期日/相依關係)、看板欄位調整狀況、標註「需人工確認」的項目。
|
||||
- 提示使用者後續可用 `/jsc-code:issues` 對這些小功能議題逐項實作(該 skill 會彙整 TODO、逐項實作並留言進度);是否實作、何時實作由使用者另行決定,不在本 skill 範圍內。
|
||||
- **子母議題關閉規則的後續遵守**:提醒使用者(或後續實作流程)——子議題全部關閉前不得關閉母議題;有 dependency 阻擋時由 Gitea 強制,否則依母議題的「關閉前檢查」人工確認。
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: issues-sync
|
||||
description: 讀取一個 Gitea 專案(project)或單一議題(優先用 tea,否則用 Gitea REST API + curl + GITEA_TOKEN,不依賴 jq);若給的是專案,因 tea/Gitea API 目前無法直接查詢專案,就先取得該 repo 底下所有開啟中的議題、再過濾掉與此專案無關的議題;若給的是議題就只同步該議題。議題若有標籤就依標籤分組並以 AskUserQuestion 讓使用者挑選要同步哪些標籤的議題(只有一個議題或全部無標籤則跳過)。接著一個議題派一個 subagent,基於工作目錄下的所有檔案:分析議題描述的需求並判斷議題內的 TODO(markdown 任務清單)是否足以追蹤議題描述的需求、不足就補上 TODO 追加到議題正文、依需求從既有標籤更新議題標籤、逐條判斷未完成 TODO(含新增)是否已完成、有異動就整理成一則留言;若議題屬於專案看板且看板欄位可對應進度語意(例如分析中/待處理/進行中/待測試/已完成),依議題描述與勾稽結果建議並調整議題所在欄位,介面不支援時改列建議清單請使用者手動調整。若輸入為專案且使用者指定「關閉專案」或「專案完成」,則進入專案完成模式:只執行到取得專案議題清單,跳過其後所有同步步驟,經使用者確認後把專案擁有的所有議題搬到「已完成」欄位並關閉。全程不落地任何檔案:所有中間成果一律留在對話/subagent 回傳內容,最終只透過 tea 或 Gitea API 寫回議題正文/標籤/留言,且寫入前先經使用者確認。當使用者要同步議題進度、依專案批次更新議題 TODO、依程式碼勾稽議題完成度、更新議題標籤與進度留言,或提到 issues-sync、issue sync、議題同步、TODO 勾稽、tea issues、Gitea 專案議題時使用此 skill。
|
||||
---
|
||||
|
||||
# 依工作目錄同步 Gitea 專案/議題的 TODO 進度與標籤
|
||||
|
||||
你要讀取使用者提供的一個 Gitea **專案(project)**或**單一議題**,取得要同步的議題清單,然後一個議題派一個 subagent,**以目前工作目錄下的所有檔案為依據**,勾稽並更新每個議題的 TODO(markdown 任務清單)與標籤,最後把 TODO 的異動整理成留言。例外:輸入為專案且使用者指定「關閉專案/專案完成」時,進入**專案完成模式**(見第 1 步之後的專節),跳過標籤分組與逐議題勾稽,改為批次搬移至「已完成」並關閉議題。**修改議題正文、變更議題標籤、留言都是對外且不易復原的動作,subagent 只回傳同步計畫(不落地任何檔案),實際寫入 Gitea 前必須先讓使用者確認**。請依下列階段依序完成。
|
||||
|
||||
## 共用規範(shared plugin,必要前置)
|
||||
|
||||
執行本 skill 前,先以 Skill 工具載入下列共用規範並全程遵守;**任一載入不到(shared plugin 未安裝)時,先詢問使用者是否安裝 shared plugin(`https://gitea.jsc.idv.tw/plugins/shared.git`),使用者不安裝則直接中斷本 skill**,不得只憑下方一行摘要繼續執行:
|
||||
|
||||
- `/jsc-shared:spec-output`:繁體中文為主英文為輔、UTF-8(不含 BOM)無亂碼、Mermaid 呈現、個資(PII)去識別化。
|
||||
- `/jsc-shared:spec-execution`:不臆測/需人工確認。
|
||||
- `/jsc-shared:spec-gitea`:`GITEA_TOKEN` 機密保護、不依賴 `jq`、API 呼叫慣例(分頁完整讀取、GET 探測版本相依端點)。
|
||||
- `/jsc-shared:spec-project-board`:看板欄位語意對應與建議欄位規則、404/501 視為不支援、不得新建欄位。
|
||||
|
||||
## 絕對準則(不可違反)
|
||||
|
||||
- **全程不得在磁碟落地任何檔案**:不建立 `.docs/`、不寫草稿檔、不寫暫存檔、不用檔案傳遞中間結果。所有中間成果(議題清單、需求分析、追加後的正文、標籤異動、勾稽結果、留言內容)一律留在**對話內容**與 **subagent 的回傳值**裡。最終產物只透過 `tea` 或 Gitea API **寫回議題正文/標籤/留言**,除此之外不產生任何本機檔案。
|
||||
|
||||
## 前置:輸入與工具
|
||||
|
||||
- **輸入**:一個 Gitea 專案(project)或一筆議題的參照(URL 最佳,或 `owner/repo` + project id/issue index)。
|
||||
- **依據來源**:所有「TODO 是否完成」「該補哪些 TODO」「該掛哪些標籤」的判斷,一律以**目前工作目錄下的檔案內容**為準(程式碼、設定、文件等),不得臆測。
|
||||
- **工具優先序**:
|
||||
1. 若該 host 在 `tea login list` 中有對應 login,優先用 `tea`(`tea issues`、`tea comment`、`tea labels` 等),並以 `--login <name> --repo <owner>/<repo>` 指定目標。
|
||||
2. 否則改用 Gitea REST API + `curl`,帶標頭 `Authorization: token $GITEA_TOKEN`(環境變數 `GITEA_TOKEN` 已設定;未設定則停下請使用者提供)。
|
||||
- **不要依賴 `jq`**:依 `/jsc-shared:spec-gitea`(JSON 用 tea 結構化輸出或交給 subagent 解析,不 pipe 到 `jq`)。
|
||||
- **TODO 的定義**:議題正文(body)中的 markdown 任務清單項目,`- [ ]`(未完成)與 `- [x]`(已完成)。本 skill 所有「TODO 追蹤/勾稽/新增」都在這種任務清單上操作。
|
||||
- **專案進度欄位(project column)**:依 `/jsc-shared:spec-project-board`(欄位語意以看板實際名稱為準、不得假設五欄都存在、對不上或介面不支援時不移動只回報建議);本 skill 會依議題描述、需求與 TODO 勾稽結果建議議題應在的欄位,並在使用者確認後調整,對應規則見第 3.5 步。
|
||||
- **議題描述流程圖**:依 `/jsc-shared:spec-output` — 補進議題正文或進度留言的內容有助理解時(需求流程、TODO 先後/相依),加入 Mermaid 流程圖,忠實反映議題需求與 TODO 現況、不得杜撰。
|
||||
|
||||
## 第 0 步:解析輸入、判斷專案或議題、準備工具
|
||||
|
||||
1. 從輸入解析出 `host`、`owner`、`repo`,並判斷這是**專案**還是**議題**:
|
||||
- 議題 URL 形如 `https://<host>/<owner>/<repo>/issues/<index>` → 議題。
|
||||
- 專案 URL 形如 `https://<host>/<owner>/<repo>/projects/<id>` 或組織層級 `https://<host>/<owner>/-/projects/<id>` → 專案。
|
||||
2. 執行 `tea login list`,判斷該 host 走 tea 還是 API(API base 為 `https://<host>/api/v1`)。
|
||||
3. **判斷是否進入專案完成模式**:若輸入是**專案**,且使用者明確指定「關閉專案」「專案完成」(或同義表述,例如「這個專案做完了,收尾」),標記為專案完成模式 — 第 1 步取得議題清單後,改走「專案完成模式」專節,不進入第 2 步之後的同步流程。輸入是單一議題時不適用此模式;使用者語意不明確(看不出是要同步還是要收尾關閉)時,用 AskUserQuestion 確認,不得自行認定要關閉。
|
||||
4. **找不到就詢問使用者(AskUserQuestion)**:若無法從輸入判斷是專案還是議題、或依輸入查不到對應的專案/議題(例如 API 回 404、專案 id 不存在、repo 拼錯),必須用 AskUserQuestion 請使用者補齊或更正(host/owner/repo、專案 id 或議題 URL)。取得可解析的目標前,不進入下一步。
|
||||
|
||||
## 第 1 步:取得要同步的議題清單
|
||||
|
||||
- **若輸入是議題**:清單就是這一筆議題,**跳過本步的專案展開**,直接進入第 2 步。
|
||||
- **若輸入是專案**:**`tea` 與 Gitea API 目前都無法直接查詢專案掛載的議題**,因此改用「先撈全部、再過濾」:
|
||||
1. 先取得該 `owner/repo` 底下**所有開啟中(open)的議題**:
|
||||
- tea:`tea issues list --repo <owner>/<repo> --login <name> --state open`(必要時加 `--fields index,title,labels` 等)。
|
||||
- API:`GET {base}/repos/{owner}/{repo}/issues?state=open&type=issues`(注意分頁,逐頁取完)。
|
||||
2. **過濾掉與此專案無關的議題**,只保留屬於目標專案的議題:依每個議題可取得的專案關聯資訊(issue 物件上的 project 欄位、或該議題所屬 project id/名稱)比對目標專案;比對得上才留下。彙整成議題清單(每筆記下 `owner/repo`、`index`、`title`、`labels`)。
|
||||
3. 若逐議題都**無法可靠判斷是否屬於此專案**(tea/API 完全取不到議題的專案關聯),用 AskUserQuestion 告知此限制,請使用者選擇要如何處理(例如:把該 repo 全部 open 議題都視為要同步、由使用者提供屬於此專案的議題清單/編號、或改給單一議題 URL),不要自行臆測。
|
||||
|
||||
## 專案完成模式:指定關閉專案/專案完成時(跳過第 2 步之後的所有步驟)
|
||||
|
||||
只在第 0 步標記為專案完成模式時進入本節。沿用第 1 步取得的「屬於此專案的議題清單」,之後**不做**標籤分組、不派 subagent、不勾稽 TODO、不更新標籤、不留言進度,改依下列流程把專案擁有的所有議題搬到「已完成」並關閉:
|
||||
|
||||
1. **列出將處理的議題清單**:每筆列出 `owner/repo`、編號、標題、目前狀態與所在看板欄位(可取得時),以及該議題是否還有未完成 TODO(僅從議題正文的任務清單計數,不做工作目錄勾稽)。
|
||||
2. **使用者確認(AskUserQuestion,必要,不可跳過)**:關閉議題是對外且不易復原的動作,未確認前不得寫入。至少提供選項:
|
||||
- 全部搬到「已完成」並關閉。
|
||||
- 逐議題確認(每關一筆回報,確認後再做下一筆)。
|
||||
- 取消(不動任何議題)。
|
||||
清單中若有議題仍有未完成 TODO,必須在詢問時明確標出這些議題與其未完成數量,讓使用者知道將照關。
|
||||
3. **逐議題執行**(確認後):
|
||||
- **搬到「已完成」欄位**:若議題屬於專案看板且看板有可對應「已完成」語意的欄位,依既有規則先探測 project/column API(404/501 視為不支援、不對未確認端點寫入),可用就把議題移到該欄位;不支援或欄位對不上就跳過搬移(議題關閉後看板通常會自行呈現完成狀態),於回報註明。
|
||||
- **關閉議題**:tea:`tea issues close --repo <owner>/<repo> --login <name> <index>`;API:`PATCH {base}/repos/{owner}/{repo}/issues/{index}`,body `{"state":"closed"}`。
|
||||
- 單筆失敗(權限不足、議題被鎖定等)不中斷整批:記錄失敗原因後繼續下一筆。
|
||||
4. **回報**:搬移成功/跳過(含原因)筆數、關閉成功/失敗(含原因)清單、照關但仍有未完成 TODO 的議題清單(供追溯);專案看板本身的關閉/封存 Gitea 不一定支援 API 操作,如需關閉專案本身,提示使用者到 Gitea 介面手動處理。
|
||||
|
||||
本模式全程仍遵守「不落地檔案」絕對準則;若第 1 步取得的清單為空(專案沒有 open 議題),直接回報並結束,不需確認。
|
||||
|
||||
## 第 2 步:依標籤分組並詢問要同步哪些(AskUserQuestion)
|
||||
|
||||
1. 讀取清單中每個議題的 `labels`。
|
||||
2. **跳過條件**:若清單只有**一個議題**,或**所有議題都沒有標籤**,跳過本步、同步全部清單。
|
||||
3. 否則依標籤把議題分組(一個議題有多個標籤時,各組都出現),用 **AskUserQuestion(multiSelect)** 讓使用者挑選要同步「哪些標籤」的議題:
|
||||
- 每個選項是一個標籤(附該標籤下的議題數量),讓使用者多選。
|
||||
- 標籤數量超過 AskUserQuestion 選項上限(4)時,改在訊息中列出全部標籤與各自議題數,請使用者回覆要同步哪些(可用「其他」自訂輸入)。
|
||||
4. 依選取的標籤過濾清單:保留**帶有任一選取標籤**的議題,作為後續要同步的最終清單。未被選取標籤涵蓋的議題不同步。
|
||||
|
||||
## 第 3 步:逐議題派 subagent 產生同步計畫(每個議題一個 subagent)
|
||||
|
||||
對最終清單中的**每一個議題各派一個 subagent**。subagent **以目前工作目錄下的所有檔案為依據**,只讀檔案與議題、**把結果以結構化內容回傳給主 agent,不得在磁碟寫任何檔案**、**不得修改任何工作目錄的原始碼、不得直接改議題正文/標籤、不得直接留言**。每個 subagent 依序做:
|
||||
|
||||
1. **讀取議題**:`title`、`body`(含其中的 TODO 任務清單)、`labels`、以及既有 comments。
|
||||
- tea:`tea issues <index> --repo <owner>/<repo> --login <name> --comments`。
|
||||
- API:`GET {base}/repos/{owner}/{repo}/issues/{index}` 與 `.../comments`。
|
||||
- 一併盤點該 repo 既有標籤(供第 3.2 用):`GET {base}/repos/{owner}/{repo}/labels`(tea:`tea labels list`)。
|
||||
2. **3.1 判斷 TODO 是否足以追蹤議題描述的需求,不足就補**:分析議題描述的需求,逐項對照現有 TODO,判斷目前的 TODO 清單是否足以追蹤議題描述的需求。若不足,補上缺少的 TODO(以未完成 `- [ ]` 形式),規劃**追加到議題正文**(回傳內容中給出「追加後的正文」與「新增了哪些 TODO」)。補的 TODO 必須能對應到議題描述的需求,不得編造需求未涵蓋的項目。
|
||||
3. **3.2 依需求更新可用標籤**:依議題需求性質,從該 repo **既有標籤**中挑選應掛上(或應移除)的標籤,回傳內容中列出「建議的標籤異動」(新增哪些、移除哪些、維持哪些)。**不自行新建標籤**,除非使用者要求;找不到合適標籤就維持原樣並標註。
|
||||
4. **3.3 逐條勾稽未完成 TODO 是否已完成**:對所有**未完成**的 TODO(含 3.1 新增的),逐條依工作目錄下的檔案內容判斷是否已完成。已完成者標記為 `- [x]` 並在回傳內容記下判斷依據(以 `path:line` 指出對應實作位置);無法從檔案可靠判斷者維持未完成並標註「需人工確認」。
|
||||
5. **3.4 整理 TODO 異動留言**:若本議題有任何 TODO 異動(**新增**的 TODO,或**狀態變更**——由未完成改為完成),整理成一則留言內容,包含:本次新增了哪些 TODO、哪些 TODO 判定為完成(附對應實作位置)、哪些仍未完成(含原因/需人工確認)。若沒有任何 TODO 異動,回傳標明「無異動、不需留言」。
|
||||
6. **3.5 建議專案進度欄位**:若本議題屬於某個專案看板且能取得看板的欄位清單與議題目前所在欄位,依議題描述、需求與 3.1/3.3 的結果,從**看板實際存在的欄位**中建議議題應在的欄位;語意對應規則依 `/jsc-shared:spec-project-board` 的建議欄位表(分析中/待處理/進行中/待測試/已完成,欄位名稱以看板實際名稱為準、語意相近即可對應)。
|
||||
回傳內容需含:目前欄位、建議欄位、判斷依據。建議欄位與目前欄位相同時標明「欄位無異動」;看板欄位語意對不上(或取不到欄位資訊)時標明「無法對應、維持原欄位」並列出實際欄位名稱,不得硬套。議題不屬於任何專案看板時跳過本項。
|
||||
|
||||
每個 subagent **回傳**一份結構化同步計畫(**不落地成檔案**),至少包含:議題參照與標題、追加後的完整正文(標明新增與勾稽的變更)、建議的標籤異動、TODO 異動留言內容(或「無異動」)、專案進度欄位建議(目前欄位/建議欄位/判斷依據,或「不屬於專案看板」「無法對應」)、以及所有「需人工確認」項目。
|
||||
|
||||
## 第 4 步:同步計畫品質檢查
|
||||
|
||||
實際寫入 Gitea 前,主 agent 必須檢查所有 subagent 回傳的同步計畫(僅在對話中檢查,不寫檔):
|
||||
|
||||
- 內容以繁體中文為主、英文為輔,無亂碼或破損文字。
|
||||
- 每個要同步的議題都有對應的同步計畫;正文的 TODO 變更(新增/勾稽)與留言內容的敘述一致。
|
||||
- 標籤異動只用到該 repo 既有標籤(名稱/id 對得上第 3.1 盤點結果),未擅自新建標籤。
|
||||
- 「已完成」的勾稽都有工作目錄檔案的依據;無依據者標為未完成或「需人工確認」,未被誤判為完成。
|
||||
- 進度欄位建議只使用看板實際存在的欄位,且與 TODO 勾稽結果一致(例如仍有未完成 TODO 的議題不得建議「已完成」、仍有「需人工確認」項目的議題不得越過「待測試」)。
|
||||
- 有問題先在對話中修正同步計畫並重新檢查,通過後才進入下一步。
|
||||
|
||||
## 第 5 步:詢問使用者要如何執行(AskUserQuestion)
|
||||
|
||||
同步計畫通過檢查後,主 agent 用 AskUserQuestion 讓使用者確認要如何對 Gitea 執行寫入,至少提供:
|
||||
|
||||
1. 全部執行:追加/勾稽 TODO 到議題正文、套用標籤異動、調整專案進度欄位、對有異動的議題留言。
|
||||
2. 只更新議題(正文+標籤+進度欄位),先不留言。
|
||||
3. 只呈現同步計畫、先不動 Gitea:僅在對話中列出計畫供檢視(不落地檔案、不寫入議題)。
|
||||
4. 逐議題確認:每處理完一個議題就回報,待使用者確認後再做下一個。
|
||||
5. 其他(由使用者輸入自訂方式)。
|
||||
|
||||
未獲確認前不得對 Gitea 做任何寫入。
|
||||
|
||||
## 第 6 步:套用到 Gitea
|
||||
|
||||
依使用者選擇,對最終清單的每個議題執行(主 agent 執行,非 subagent):
|
||||
|
||||
- **更新正文(TODO 追加+勾稽)**:以同步計畫中「追加後的完整正文」更新議題 body。
|
||||
- tea:對應的 issue 編輯指令;API:`PATCH {base}/repos/{owner}/{repo}/issues/{index}`,body `{"body":"<新正文>"}`。
|
||||
- 更新前先重新讀一次議題正文,若與 subagent 讀到的版本已不同(他人期間有改動),停下該議題並回報,避免覆蓋他人變更。
|
||||
- **標籤異動**:套用建議的新增/移除。
|
||||
- tea:`tea labels`/issue 編輯對應指令;API:`POST`/`DELETE {base}/repos/{owner}/{repo}/issues/{index}/labels`(用既有 label id)。
|
||||
- **調整專案進度欄位**:對「建議欄位與目前欄位不同」的議題,依 `/jsc-shared:spec-project-board` 把議題移到建議欄位(先 GET 探測端點、404/501 視為不支援且不得對未確認端點寫入;介面可用時一次一個議題並確認回應成功;不可用時不視為錯誤,改在第 7 步回報列「議題 → 建議欄位」清單請使用者手動拖曳;只在欄位確實存在且語意對應明確時移動,有疑慮就不動並回報)。「欄位無異動」「無法對應」「不屬於專案看板」的議題跳過。
|
||||
- **留言**:對有 TODO 異動的議題張貼留言。
|
||||
- tea:`tea comment --repo <owner>/<repo> --login <name> <index> "<留言內容>"`;API:`POST {base}/repos/{owner}/{repo}/issues/{index}/comments`,body `{"body":"<留言內容>"}`。
|
||||
- 無異動的議題不留言。
|
||||
- 若需要把 API body 帶入 `curl`,用管線/heredoc/變數帶入,**不要為此在磁碟落地暫存檔**。
|
||||
- 若選「逐議題確認」,每處理完一個就回報並等待確認再繼續。
|
||||
|
||||
## 第 7 步:回報
|
||||
|
||||
- 回報:輸入是專案或議題、(若為專案)該 repo open 議題數/過濾後屬於此專案的議題數與依標籤篩選後的最終清單、每個議題新增了哪些 TODO、勾稽為完成的 TODO(附實作位置)、標籤異動、進度欄位異動(目前欄位 → 新欄位;介面不支援時改列「議題 → 建議欄位」清單請使用者手動調整)、是否留言,以及所有「需人工確認」或「無法判斷是否屬於此專案」項目。
|
||||
- 因全程不落地檔案,**沒有本機草稿需要清理**;成果都在對話與已寫回的議題正文/標籤/留言中。
|
||||
|
||||
## 重要限制
|
||||
|
||||
- **全程不得在磁碟落地任何檔案**(見上方「絕對準則」):中間成果只留在對話與 subagent 回傳值,最終只寫回議題正文/標籤/留言。
|
||||
- 修改議題正文、變更標籤、留言都是對外且不易復原的動作,**必須先經第 5 步使用者確認**;未確認前不寫入 Gitea,成果只留在對話中。
|
||||
- 「TODO 是否完成」「該補哪些 TODO」「該掛哪些標籤」一律以**工作目錄下的檔案**為依據;無法可靠判斷就標「需人工確認」,不得臆測或編造需求未涵蓋的內容。
|
||||
- subagent 與各步驟只讀檔案與議題、只回傳結構化內容,**不得在磁碟寫任何檔案、不得修改任何工作目錄原始碼**。
|
||||
- 標籤只從既有標籤挑選,不自行新建(除非使用者要求)。
|
||||
- 進度欄位調整依 `/jsc-shared:spec-project-board`,且必須經第 5 步使用者確認後執行。
|
||||
- 專案完成模式只在輸入為專案且使用者**明確**指定關閉/完成時進入;語意不明就用 AskUserQuestion 確認,不得自行認定。批次關閉議題前必須經使用者確認;含未完成 TODO 的議題要在確認時明確標出。不得透過此模式關閉不屬於該專案的議題。
|
||||
- JSON 解析(不依賴 `jq`)依 `/jsc-shared:spec-gitea`;個資保護(PII)與語言規範依 `/jsc-shared:spec-output`。
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: notifications
|
||||
description: 讀取 Gitea 通知,依通知類型分組後逐組執行;若沒有通知就直接結束;先從目前工作區的 REVIEW.md 找對應流程,找不到就詢問使用者怎麼處理,並把缺少流程的通知類型附加回 REVIEW.md。當使用者要整理 Gitea 通知、依通知類型批次處理、照 REVIEW.md 執行通知流程、或補齊 REVIEW.md 的通知類型說明時使用此 skill。
|
||||
---
|
||||
|
||||
# 依 REVIEW.md 處理 Gitea 通知
|
||||
|
||||
你要讀取目前使用者在目標 Gitea 主機上的通知,先依通知類型分組,再逐組套用 `REVIEW.md` 內定義的處理流程。若沒有通知,直接結束,不改任何檔案,也不寫回 Gitea。
|
||||
|
||||
## 第 0 步:先決條件
|
||||
|
||||
1. 先依 `/jsc-shared:spec-gitea` 確認工具可用性,並決定使用 `tea` 或 Gitea REST API + `GITEA_TOKEN`。
|
||||
2. 決定 Gitea host 時,優先使用目前工作區 repo 的 `origin`,再看 `$GITEA_HOST`,都沒有才詢問使用者。
|
||||
3. 若 `tea` 可用且該 host 有對應 login,優先用 `tea`;否則用 API + `GITEA_TOKEN`。
|
||||
|
||||
## 第 1 步:讀取通知
|
||||
|
||||
1. 以 `tea` 或 Gitea API 讀取目前使用者通知。
|
||||
2. 分頁要抓完整,直到沒有下一頁為止。
|
||||
3. 預設只處理 `unread` 與 `pinned` 通知;若實作環境或 `REVIEW.md` 明確要求納入其他狀態,再依需求擴充。
|
||||
4. 若沒有任何通知,直接結束。
|
||||
|
||||
## 第 2 步:依通知類型分組
|
||||
|
||||
1. 以通知的 `subject.type` 分組。
|
||||
2. 同一組內依通知原始順序逐一處理。
|
||||
3. 每處理完一組才進下一組。
|
||||
|
||||
## 第 3 步:從 `REVIEW.md` 找處理流程
|
||||
|
||||
1. 先讀目前工作目錄根目錄的 `REVIEW.md`。
|
||||
2. 以通知類型名稱尋找對應流程,優先找同名標題或清楚對應的段落。
|
||||
3. 找到流程就照流程執行。
|
||||
4. 找不到流程時,立刻詢問使用者這個通知類型要怎麼處理,不要自行猜。
|
||||
|
||||
## 第 4 步:處理缺流程的類別
|
||||
|
||||
1. 找不到流程的通知類型要先暫存,等使用者回答後再處理。
|
||||
2. 把這個類型與使用者最後確認的處理方式附加到 `REVIEW.md`。
|
||||
3. 若 `REVIEW.md` 不存在,先詢問使用者要在 repo root 建立,還是改用其他 review 檔案。
|
||||
|
||||
## 第 5 步:執行與收尾
|
||||
|
||||
1. 逐組完成後,回報本次讀到的通知總數、分組結果、已套用的流程,以及哪些通知類型沒有既有流程。
|
||||
2. 若有新增到 `REVIEW.md`,明確回報更新位置。
|
||||
3. 全程不要把通知內容寫成草稿檔或暫存檔。
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: worklog
|
||||
description: 工作證明自動記錄(worklog)的操作與維護 skill。搭配相容的 Stop hook,把每輪工作內容透過 README 定義的 headless CLI(claude/codex/agy/opencode/copilot)濃縮成精簡條目並追加到 Gitea wiki 的當週工作紀錄頁(Worklog-yyyy-MM-W<週>),工作內容全程不落地。提供 --init(初始化週頁與環境變數指引)、--tune(判定並快取最適合的 Claude 摘要模型)、--diagnose(診斷 hook 為何沒動作)、--append(手動補寫一筆)、--show(讀當週頁回顧)五個模式。當使用者說工作證明、工作紀錄、worklog、週報自動化、把工作內容寫到 wiki、記錄到 Gitea wiki、hook 沒有寫入 wiki、補寫工作紀錄、看本週做了什麼、重新判定摘要模型,或提到 WORKLOG_ENABLED/WORKLOG_HOST/WORKLOG_REPO/WORKLOG_MODEL/WORKLOG_CLI/WORKLOG_SCOPE 時觸發。不適用於:Gitea 議題操作(用 issues-sync/issues)、專案文件化(用 funcs)。
|
||||
---
|
||||
|
||||
# worklog — 工作證明自動記錄
|
||||
|
||||
把「每輪做了什麼」濃縮成一則條目,追加到 Gitea wiki 的當週工作紀錄頁。**自動記錄由相容的 `Stop` hook 完成,不需使用者同意、不需人工觸發**;本 skill 負責自動路徑之外的人工操作:初始化、模型判定、診斷、補寫、回顧。
|
||||
|
||||
| 元件 | 觸發者 | 職責 |
|
||||
| --- | --- | --- |
|
||||
| `hooks/hooks.json` 的 `Stop` hook | harness 自動 | 每輪結束抽本輪內容 → 濃縮 → 遮蔽 → 追加到當週頁 |
|
||||
| 本 skill `/jsc-doc:worklog` | 使用者/助理手動 | `--init`/`--tune`/`--diagnose`/`--append`/`--show` |
|
||||
| `scripts/worklog/worklog.sh` | 上述兩者共用 | 主流程(單一實作,避免漂移):依 `WORKLOG_CLI` 呼叫 headless CLI,每筆整理成六個固定欄位 |
|
||||
| `scripts/worklog/wiki_api.py` | 上述兩者共用 | token 解析、wiki 讀寫、append 重試、週頁命名 |
|
||||
| `scripts/worklog/transcript.py` | 上述兩者共用 | 抽本輪片段、估算花費時間、機密遮蔽 |
|
||||
|
||||
### 各助理支援範圍
|
||||
|
||||
| 功能 | Claude Code | Codex | Antigravity | OpenCode | GitHub Copilot |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `Stop` hook 自動記錄 | ✅ | ✅ 需可讀 Codex session JSONL | ❌ | ❌ | ❌ |
|
||||
| `--init`/`--diagnose`/`--append`/`--show` | ✅ | ⚠️ 需 plugin 目錄保留 `scripts/`(安裝後請實測一次) | ⚠️ 同左 | ⚠️ 需完整 plugin 目錄 | ⚠️ 需 plugin 目錄保留 `scripts/` |
|
||||
| 摘要 CLI | `claude -p` | `codex exec` | `agy -p` | `opencode run` | `copilot -p` |
|
||||
| `--tune` | ✅ | ❌ 無 `claude-api` skill 可載入 | ❌ 同左 | ❌ | ❌ |
|
||||
|
||||
兩個限制的來源:
|
||||
|
||||
- **`Stop` hook 只有相容 hook 環境實際執行**;Claude Code 先用 `CLAUDE_PLUGIN_ROOT` 定位腳本,找不到時再掃 `~/.claude/plugins/cache` 與 `~/.codex/plugins/cache`,最後命中 `*/jsc-doc/*/scripts/worklog/worklog.sh`。`transcript.py` 目前支援 Claude Code transcript JSONL(`type` / `message.content` blocks)與 Codex session JSONL(`payload` events / response items),其他助理若提供等效 hook,必須先補對應 transcript 解析器。
|
||||
- **OpenCode 以「複製 `skills/` 目錄」安裝**時不會帶入 `scripts/`,本 skill 的所有模式都無法執行;若以完整 plugin 目錄執行並能解析 `scripts/worklog`,可用 `WORKLOG_CLI=opencode` 作為摘要 CLI。
|
||||
- 其他助理若要用 `--append`/`--show` 等純 wiki 操作,只需 `python3`;摘要路徑需要 README 定義的任一 headless CLI。`--tune` 仍是 Claude Code 專屬,其他 CLI 使用各自預設模型或手動設定其 CLI 行為。
|
||||
|
||||
### 腳本路徑解析(重要)
|
||||
|
||||
skill 執行時的工作目錄是**使用者的專案目錄**,不是 plugin 根目錄,因此**絕不可用相對路徑呼叫腳本**。先解析出 plugin 根目錄再組絕對路徑:
|
||||
|
||||
| 環境 | plugin 根目錄 |
|
||||
| --- | --- |
|
||||
| Claude Code | `${CLAUDE_PLUGIN_ROOT}` |
|
||||
| 其他助理 | 本 skill 載入時提示的 base directory(`.../skills/worklog`)往上兩層 |
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
WORKLOG_DIR="${CLAUDE_PLUGIN_ROOT}/scripts/worklog"
|
||||
|
||||
# 其他助理:以 skill base directory 推導(<base>/../.. 即 plugin 根)
|
||||
WORKLOG_DIR="<skill base directory>/../../scripts/worklog"
|
||||
```
|
||||
|
||||
以下各模式的指令一律以 `${WORKLOG_DIR}` 表示該目錄。若解析不到或該目錄不存在,回報「plugin 目錄未包含 scripts/worklog,本 skill 在此環境不可用」並停止,不要改用相對路徑重試。
|
||||
|
||||
---
|
||||
|
||||
## 共用規範(必要前置)
|
||||
|
||||
執行本 skill 前,先以 Skill 工具載入下列共用規範並全程遵守;**任一載入不到時先詢問使用者是否安裝 shared plugin(`https://gitea.jsc.idv.tw/plugins/shared.git`),不安裝則中斷**:
|
||||
|
||||
- `/jsc-shared:spec-output`:繁體中文(台灣用語)、UTF-8 無 BOM、表格與 Mermaid 優先、**寫入外部系統不得洩漏 PII**。
|
||||
- `/jsc-shared:spec-execution`:自動執行原則(必要決策才中斷)、不臆測。
|
||||
- `/jsc-shared:spec-gitea`:token 機密保護(不 echo、遮蔽、不落地)、API 分頁、host 決定順序。
|
||||
- `/jsc-shared:spec-time-log`:時間戳固定 Asia/Taipei `yyyy/MM/dd HH:mm:ss`;訊息格式 `[時間][階段][等級]: 訊息`、一行一則。
|
||||
|
||||
本 skill 特有補充:
|
||||
|
||||
- **工作內容不落地**:transcript 片段只在程序記憶體與 stdin/stdout 間傳遞、wiki 走 API 不 clone,全程不產生暫存檔。唯一允許落地的是**模型快取檔** `~/.claude/worklog/model`(僅含模型 id 與判定時間,不含任何工作內容)。
|
||||
- **絕不阻斷**:hook 路徑任何失敗都以 exit 0 結束,只在 stderr 留訊息。
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
| 變數 | 必要 | 說明 | 未設定 |
|
||||
| --- | --- | --- | --- |
|
||||
| `WORKLOG_ENABLED` | ✅ | 總開關,設為 `1` 才啟用 | hook 立即結束,完全不動作 |
|
||||
| `WORKLOG_HOST` | ✅ | Gitea 主機,如 `gitea.housefun.com.tw` | 不啟用 |
|
||||
| `WORKLOG_REPO` | ✅ | wiki 所在 repo,如 `H3285/WorkLog` | 不啟用 |
|
||||
| `WORKLOG_MODEL` | | 強制指定摘要模型 | 讀快取檔 → 保底 `claude-haiku-4-5-20251001` |
|
||||
| `WORKLOG_CLI` | | 摘要執行器:`auto`/`claude`/`codex`/`agy`/`opencode`/`copilot` | `auto`,先依目前 hook/session 環境判斷正在使用的 CLI,判斷不到或該 CLI 不可執行時才 fallback 到已安裝工具 |
|
||||
| `WORKLOG_SCOPE` | | 冒號分隔的路徑前綴,僅這些路徑下的 session 才記 | 全部 session 都記 |
|
||||
| `WORKLOG_ERRLOG` | | 錯誤訊息額外寫入的檔案路徑(只記錯誤、不含工作內容) | 只走 stderr |
|
||||
|
||||
**token 不需另設變數**,依固定優先序自動解析並實際驗證:
|
||||
|
||||
```
|
||||
GITEA_TOKEN →(對目標 host 驗證失敗時)→ tea 設定檔中該 host 的 token → ~/.git-credentials
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模式
|
||||
|
||||
### `--init`
|
||||
|
||||
1. 執行 `python3 "${WORKLOG_DIR}/wiki_api.py" probe`,回報 token 來源、Gitea 版本、當週頁狀態。
|
||||
2. 當週頁不存在 → 執行 `python3 "${WORKLOG_DIR}/wiki_api.py" init` 建立(wiki 尚未初始化時一併初始化)。
|
||||
3. 以表格印出應寫入 `~/.bashrc` 的 `WORKLOG_*` 變數清單;**不自動改使用者的 shell profile**(需人工確認的狀態變更)。
|
||||
|
||||
### `--tune`(Claude Code 專屬)
|
||||
|
||||
決定「目前最適合的摘要模型」並快取,`Stop` hook 只讀快取、**絕不自行呼叫 AI 判斷**(否則就變成雞生蛋,還會拖慢使用者的等待路徑)。
|
||||
|
||||
本模式需要 Claude Code 內建的 `claude-api` skill 與 `claude` CLI,**其他助理無法執行**:請改為手動設定 `WORKLOG_MODEL` 環境變數指定模型,或沿用保底模型。
|
||||
|
||||
| 步驟 | 動作 |
|
||||
| --- | --- |
|
||||
| 1 | 以 Skill 工具載入 `claude-api` 取當下模型清單與定價,**不憑記憶** |
|
||||
| 2 | 依本任務條件評分:延遲敏感(在使用者等待路徑上)、輸出短篇六欄工作紀錄、需嚴守機密過濾指令、每輪都跑一次故成本敏感 |
|
||||
| 3 | Smoke test:`WORKLOG_CHILD=1 claude -p "回 OK" --model <選定 id>` 確認該模型在此帳號可用 |
|
||||
| 4 | 寫入 `~/.claude/worklog/model`(`model=<id>`、`tuned_at=<時間>`、`reason=<一行理由>`),並回報選擇與理由 |
|
||||
|
||||
快取超過 **30 天** 視為過期:hook 改用保底模型,並在條目標記 `(model: fallback)`,`--diagnose` 會提醒重跑 `--tune`。
|
||||
|
||||
### `--diagnose`
|
||||
|
||||
逐項檢查並以表格回報,用於「hook 沒有寫入 wiki」時定位:
|
||||
|
||||
| 檢查項 | 判準 |
|
||||
| --- | --- |
|
||||
| `python3`/摘要 CLI | `python3` 與 `WORKLOG_CLI` 指定或 auto 選到的 CLI 是否找得到 |
|
||||
| `WORKLOG_*` 變數 | 必要三項是否齊全、`WORKLOG_SCOPE` 是否把當前路徑排除 |
|
||||
| `scripts/worklog` 目錄 | `${WORKLOG_DIR}` 是否解析成功且三支腳本存在(不存在=此助理不支援) |
|
||||
| token | `python3 "${WORKLOG_DIR}/wiki_api.py" probe` 的 token 來源與驗證結果 |
|
||||
| wiki API | Gitea 版本、`repos/<repo>` 與當週頁狀態 |
|
||||
| 摘要設定 | `WORKLOG_CLI`、選到的 CLI、Claude 模型快取是否存在與是否過期 |
|
||||
| hook 註冊 | `hooks/hooks.json` 是否存在且 plugin 已啟用 |
|
||||
|
||||
### `--append "<內容>"`
|
||||
|
||||
手動補寫一筆(hook 漏記、離線工作、或事後補充)。條目格式與自動路徑一致:
|
||||
|
||||
```
|
||||
## <時間> — <專案> <!-- worklog:<時間戳>-manual -->
|
||||
- 專案/任務名稱:<專案或任務>
|
||||
- 執行細節與產出:<做了什麼、動到什麼、產出為何>
|
||||
- 花費時間:<實際耗時或未判定>
|
||||
- 任務狀態:<完成/進行中/待確認/受阻>
|
||||
- 遇到的困難:<困難或未遇到明確困難>
|
||||
- 解決方式:<處理方式或不需額外處理>
|
||||
```
|
||||
|
||||
專案取當前工作目錄的 `<owner>/<repo>`;內容仍會過 `python3 "${WORKLOG_DIR}/transcript.py" redact` 遮蔽後才寫入。
|
||||
|
||||
### `--show`
|
||||
|
||||
讀當週頁(`python3 "${WORKLOG_DIR}/wiki_api.py" show`)並以表格摘要本週工作,用於回顧與週報。
|
||||
|
||||
---
|
||||
|
||||
## 條目與頁面格式
|
||||
|
||||
- **週以星期六起算(六~五)**,因此換頁一律發生在星期六,與星期對齊。
|
||||
- 週頁名稱:`Worklog-<yyyy>-<MM>-W<n>`,`n` =該週起始的星期六是當月第幾個星期六(例:`2026/07/29 三` 屬於 `07/25 六` 那一週 → `Worklog-2026-07-W4`)。
|
||||
- 跨月的一週歸屬**起始星期六**所在的月份,確保同一週只有一頁(例:`2026/08/29 六 ~ 09/04 五` 全部寫入 `Worklog-2026-08-W5`)。
|
||||
- 頁首標題:`# <yyyy> 年 <MM> 月 第 <n> 週工作紀錄(<起始日> 六 ~ <結束日> 五)`,日期範圍讓人一眼看出這頁涵蓋哪幾天。
|
||||
- 每筆條目:`## <時間> — <專案>` +六個固定 bullet(專案/任務名稱、執行細節與產出、花費時間、任務狀態、遇到的困難、解決方式);標題行尾帶 HTML 註解 marker(`<!-- worklog:… -->`)供寫後驗證與去重,wiki 渲染時不顯示。
|
||||
- 多 session 同時寫入:`append_entry` 採「讀取 → 合併 → 寫回 → 寫後讀取驗證 marker」,未落地則重讀最新內容重試,最多 3 次。
|
||||
|
||||
---
|
||||
|
||||
## 機密與 PII(兩道防線)
|
||||
|
||||
| 防線 | 位置 | 內容 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 濃縮提示詞 | 明令不得輸出 token/密碼/API key/連線字串/Email/電話/姓名/身分證號 |
|
||||
| 2 | `transcript.py` 的 `redact` | 正則遮蔽:URL 內嵌憑證、40 字元 hex token、`gh?_`/`sk-` token、`token=`/`password=`、`Authorization:`、Email、台灣手機、身分證號 |
|
||||
|
||||
第二道防線不可移除 —— 模型有可能沒遵守指令,而 wiki 一旦寫入就留在 git 歷史裡。
|
||||
|
||||
---
|
||||
|
||||
## 呼叫方式
|
||||
|
||||
| 助理 | 呼叫 |
|
||||
| --- | --- |
|
||||
| Claude Code / Antigravity | `/jsc-doc:worklog --init`、`/jsc-doc:worklog --tune`、`/jsc-doc:worklog --diagnose`、`/jsc-doc:worklog --append "修正 X 的 Y 問題"`、`/jsc-doc:worklog --show` |
|
||||
| Codex | `$worklog --diagnose`,或用 `/skills` 選單;可設 `WORKLOG_CLI=codex` |
|
||||
| OpenCode / GitHub Copilot | 需完整 plugin 目錄保留 `scripts/`;可設 `WORKLOG_CLI=opencode` 或 `WORKLOG_CLI=copilot` |
|
||||
Reference in New Issue
Block a user