Merge pull request 'log 0.0.9 發佈:工作日誌待寫入暫存' (#13) from develop into master

Reviewed-on: #13
Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #13.
This commit is contained in:
2026-08-26 01:33:44 +00:00
12 changed files with 202 additions and 36 deletions
+3 -3
View File
@@ -43,7 +43,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
},
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
},
{
"name": "jsc-log",
@@ -75,7 +75,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
},
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
},
{
"name": "jsc-sdlc",
@@ -83,7 +83,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
},
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
}
]
}
+3 -3
View File
@@ -43,7 +43,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
},
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
},
{
"name": "jsc-log",
@@ -75,7 +75,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
},
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
},
{
"name": "jsc-sdlc",
@@ -83,7 +83,7 @@
"source": "url",
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
},
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
}
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "jsc-log",
"version": "0.0.6",
"version": "0.0.9",
"description": "工作日誌(LOG_{HASH} wiki 頁)、技能使用統計與教訓紀錄(LEARN_{HASH} wiki 頁)",
"skills": "./skills",
"author": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "jsc-log",
"version": "0.0.6",
"version": "0.0.9",
"description": "工作日誌(LOG_{HASH} wiki 頁)、技能使用統計與教訓紀錄(LEARN_{HASH} wiki 頁)",
"skills": "./skills"
}
+3 -1
View File
@@ -24,6 +24,8 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
| --- | --- |
| `tools/usage-stats.sh` | 聚合 `$JSC_HOME/usage/*.jsonl`:`skills` 列技能使用次數、`chains` 列呼叫鏈次數(皆降冪),`--cli <name>` 過濾 |
| `tools/worklog-target.sh` | 接收已由 `jsc-gitea/tools/hash-id` 算好的 `HASH`,組出 `LOG_{HASH}`、`LOG_CONTENTS`(本身不再計算 SHA-1) |
| `tools/worklog-pending.sh` | 待寫入日誌的暫存區,存放於 `$JSC_HOME/worklog-pending/{HASH}/`。`add {HASH} {檔案}` 存一段內容(`jsc-sdlc` 的階段回報發現沒寫日誌時會呼叫),`cat {HASH}` 依時間印出全部、`list` 列路徑、`clear` 清除。結束碼 `3` 代表沒有暫存內容。**寫進 wiki 成功之後才 clear**,先清再寫會兩邊都沒有 |
| `tools/token-usage.sh` | 讀單一 CLI 這次工作的 token 用量,印出「input(tab)output」;來源讀不到就印「N/A(tab)N/A」並正常結束。第二個參數傳 session id,就只讀該階段的 transcript,數字才會跟花費時間對得上。各 CLI 的取得方式寫在腳本開頭註解 |
## Skills 目錄
@@ -33,7 +35,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
### `worklog`
工作完成後蒐集十項資訊(存取庫、分支、計畫連結、工作包連結、花費時間、token 用量、任務狀態、執行細節、困難與解決、PR 目標分支),用 `tools/worklog-target.sh` 產生目標頁,再套範本附加到 `LOG_{HASH}` 與 `LOG_CONTENTS`。
工作完成後蒐集十項資訊(存取庫、分支、計畫連結、工作包連結、花費時間、token 用量、任務狀態、執行細節、困難與解決、PR 目標分支),用 `tools/worklog-target.sh` 產生目標頁,再套範本附加到 `LOG_{HASH}` 與 `LOG_CONTENTS`。寫入前先讀 `tools/worklog-pending.sh cat {HASH}`:之前有階段跑完沒寫日誌,內容會暫存在那裡,這次一併寫進去,**寫成功之後才清暫存**。
### `stats`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "jsc-log",
"version": "0.0.6",
"version": "0.0.9",
"description": "工作日誌(LOG_{HASH} wiki 頁)、技能使用統計與教訓紀錄(LEARN_{HASH} wiki 頁)",
"skills": "./skills/"
}
+1 -1
View File
@@ -10,7 +10,7 @@ Close the loop on skill runs: record what a run taught you, consult it before th
## Target pages
- Directory page: `LEARN_CONTENTS`. Content page: `LEARN_{HASH}`, one page per repository.
- Compute `{HASH}` from `{owner}/{repo}` with `jsc-gitea/tools/hash-id` (shared wiki hash rule: first 8 uppercase SHA-1 hex chars; `H` plus the first 7 chars when the raw hash starts with `0-9`, `A`, `B`, or `C`).
- Compute `{HASH}` from `{owner}/{repo}` with `jsc-gitea/tools/hash-id`.
- Wiki repo resolution: `JSC_WIKI_REPO_LEARN` first, then `JSC_WIKI_REPO`. Inspect the inherited shell environment variables first; ask the user per the `jsc-ask:ask` rules only when neither resolves. Never borrow another type's `JSC_WIKI_REPO_{TYPE}`.
- All wiki reads and writes go through `jsc-gitea:wiki`.
+2 -2
View File
@@ -17,5 +17,5 @@ Data is recorded continuously by `jsc-hooks/hooks/skill-usage.sh` under `$JSC_HO
## Reporting
1. Run the tool directly and present the output as a table.
2. When there is no data, explain that `jsc-hooks` must be installed and wired first via `jsc-hooks:hooks-install`.
1. Run the tool directly and present the output as a table. Done when every line the tool printed appears as one table row.
2. When the tool prints no rows, explain that `jsc-hooks` must be installed and wired first via `jsc-hooks:hooks-install`. Done when that instruction is reported and no table is shown.
+17 -23
View File
@@ -1,6 +1,6 @@
---
name: worklog
description: After finishing a work package, collect ten facts (repo, branch, plan link, work package link, elapsed time from session-timer, token usage per CLI, status, details, difficulties, PR target) and append a templated entry to wiki LOG_{HASH} plus LOG_CONTENTS. HASH follows the shared 8-char rule with the H-prefix fallback, and the work-week Friday still drives the page content. Trigger at the end of implement or maintain; not for planning notes.
description: After finishing a work package, collect ten facts (repo, branch, plan link, work package link, elapsed time from session-timer, token usage per CLI, status, details, difficulties, PR target) and append a templated entry to wiki LOG_{HASH} plus LOG_CONTENTS. HASH follows the shared 8-char rule with the H-prefix fallback, and the work-week Friday still drives the page content. Merge whatever tools/worklog-pending.sh holds for that HASH into the same write, then clear the pending area once the wiki write succeeded. Trigger at the end of implement or maintain; not for planning notes.
---
# worklog — work log
@@ -11,32 +11,26 @@ After work completes, collect the ten items below and append a `templates/log-en
| # | Item | Source |
| --- | --- | --- |
| 1 | Repository name | Parse `{owner}/{repo}` from `git remote get-url origin` |
| 1 | Repository name | Parse `{owner}/{repo}` from `git remote get-url origin`. This is the code repo — never pass it to `wiki-url`, which takes the wiki-hosting repo |
| 2 | Branch name | `git branch --show-current` |
| 3 | Plan name | Absolute link to the plan page: `[PLAN_{HASH}](<url>)`, where `<url>` comes from `jsc-gitea/tools/gitea.sh wiki-url {owner}/{repo} PLAN_{HASH}` — PLAN and LOG may live in different wiki repos, and `[[...]]` only resolves inside one wiki |
| 4 | Work package id | Absolute link to the work package heading: `[WP-xx](<url>#wp-xx)`, `<url>` from `gitea.sh wiki-url {owner}/{repo} ANALYZE_{HASH}` |
| 3 | Plan name | Absolute link to the plan page: `[PLAN_{HASH}](<url>)`. Resolve the hosting repo with `jsc-gitea/tools/gitea.sh wiki-repo PLAN`, then take `<url>` from `gitea.sh wiki-url <that repo> PLAN_{HASH}` — PLAN and LOG may live in different wiki repos, and `[[...]]` only resolves inside one wiki. `wiki-repo` exit 3 (no wiki repo configured for that type) or `wiki-url` exit 4 (page not found) → fill the literal 「無」 for this row and carry on; a `worklog` run triggered from `maintain` normally has no plan page |
| 4 | Work package id | Absolute link to the work package heading: `[WP-xx](<url>#wp-xx)`. Resolve the hosting repo with `gitea.sh wiki-repo ANALYZE`, then take `<url>` from `gitea.sh wiki-url <that repo> ANALYZE_{HASH}`. Same fallback as row 3: `wiki-repo` exit 3 or `wiki-url` exit 4 → fill 「無」 and carry on |
| 5 | Elapsed time | `jsc-hooks/hooks/session-timer.sh report {session_id}` (seconds; convert to h/m) |
| 6 | Token usage | See the table below; per-CLI methods differ. Fill `N/A` when unavailable |
| 7 | Task status | One of the literal values 「完成」、「部分完成」、「阻塞」 (with reason when blocked) |
| 8 | Details and outputs | Summarize what changed and which files or pages were produced |
| 9 | Difficulties and resolutions | One pair per line |
| 6 | Token usage | `tools/token-usage.sh <cli> {session_id}` per CLI that ran; it prints `input<TAB>output`. Pass the same `{session_id}` as row 5 so the elapsed time and the token count describe one session. Fill `N/A` in both columns when it prints `N/A`; exit 2 means the CLI name is not one of claude / codex / copilot / antigravity / kiro, so fix the name and rerun |
| 7 | Task status | One of the literal values 「完成」, 「部分完成」, 「阻塞」 (with reason when blocked). Derive it from the session when the session shows it; otherwise ask via `jsc-ask:ask`, offering those three literals as the options and stating each option's impact scope (「完成」 closes the work package, 「部分完成」 leaves the remainder open for the next run, 「阻塞」 records the blocker and hands it back to the operator) |
| 8 | Details and outputs | One line per changed file or produced page: what changed there and why |
| 9 | Difficulties and resolutions | One pair per line. Ask via `jsc-ask:ask` when the session does not show them |
| 10 | PR target branch | Link to the PR page |
### How to get token usage
| CLI | Method |
| --- | --- |
| claude | Sum the `usage` fields in transcript JSONL (`~/.claude/projects/**/*.jsonl`), or read `usage` from `claude -p --output-format json` |
| codex | Token counters under `~/.codex/sessions/**`, or `/status` inside the session |
| copilot | `/usage` inside the session |
| antigravity | Value shown in the UI; `N/A` when unreadable |
| kiro | No public source; fill `N/A` |
The `{HASH}` in every page name above is computed with `jsc-gitea/tools/hash-id`.
## Target page and work week
- Page name: `LOG_{HASH}`.
- Compute `{HASH}` from `{owner}/{repo}` with `jsc-gitea/tools/hash-id` (shared wiki hash rule: first 8 uppercase SHA-1 hex chars; `H` plus the first 7 chars when the raw hash starts with `0-9`, `A`, `B`, or `C`).
- Compute the target page by running `tools/worklog-target.sh "{HASH}" all`. Use `PAGE` for `LOG_{HASH}` and `CONTENTS` for `LOG_CONTENTS`.
- The work-week Friday still drives the page content and the row dates.
- Read the page via `jsc-gitea:wiki`. If it does not exist, create it with the structure described in `templates/log-contents.md`; otherwise APPEND the new entry at the end.
- Update `LOG_CONTENTS` in the same pass (apply `templates/log-contents.md`; add the row if missing).
1. Resolve the wiki repo hosting LOG pages: `JSC_WIKI_REPO_LOG` first, then `JSC_WIKI_REPO`, via `gitea.sh wiki-repo LOG`. Inspect the inherited shell environment variables first; ask the user per the `jsc-ask:ask` rules only when neither resolves. Never borrow another type's `JSC_WIKI_REPO_{TYPE}`. Done when the hosting `{owner}/{repo}` is known.
2. Compute `{HASH}` from the code repo's `{owner}/{repo}` with `jsc-gitea/tools/hash-id`. Done when the 8-character `{HASH}` is known.
3. Run `tools/worklog-target.sh "{HASH}" all`. Use `PAGE` for `LOG_{HASH}` and `CONTENTS` for `LOG_CONTENTS`. Done when both page names are known.
4. Fix the work week: the Friday of the current work week drives the page content and the row dates. Done when that Friday is fixed as a `yyyy-MM-dd` date.
5. Collect what the pending area holds for this `{HASH}`: run `tools/worklog-pending.sh cat {HASH}`. Exit 3 means nothing is pending — carry on with this run's entry alone. Anything it prints was written by an earlier SDLC stage that ended without a work log, so it goes into **this** write, ahead of this run's own entry, in the order printed. Done when the pending content is either merged into the entries about to be written, or confirmed empty.
6. Read `PAGE` via `jsc-gitea:wiki`. If it does not exist, create it with the structure described in `templates/log-entry.md`; otherwise APPEND the new entries at the end and never overwrite existing entries. Done when every entry from step 5 plus this run's own entry exists on `PAGE`.
7. Update `CONTENTS` in the same pass (apply `templates/log-contents.md`; add the row if missing, otherwise refresh its 條目數 and 最後更新). Done when the row for `PAGE` carries this week's Friday date.
8. Clear the pending area: run `tools/worklog-pending.sh clear {HASH}` **only after the wiki write of step 6 succeeded**. Clearing first and failing the write loses the content on both sides. Skip this when step 5 found nothing. Done when the script reports the cleared path, or step 5 was empty.
+1
View File
@@ -3,6 +3,7 @@
> 由 `jsc-log:worklog` 維護。每完成一項工作附加一個條目在文末。
> `HASH` 依共享規則計算;頁名不再寫入年月週數,但頁內仍依本工作週的週五整理內容。
> {PLAN 頁絕對網址}、{ANALYZE 頁絕對網址} 由 `jsc-gitea/tools/gitea.sh wiki-url` 取得——LOG 與 PLAN/ANALYZE 可能分屬不同存取庫,`[[頁名]]` 跨庫不通。
> 解不出 wiki 存取庫或頁面不存在時,該欄填「無」,其他欄照填。由 `maintain` 觸發的日誌本來就沒有計畫頁與分析頁。
---
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env sh
# token-usage.sh — 讀出單一 CLI 這次工作的 token 用量。
# 用法:
# token-usage.sh <cli> [session-id] # cli = claude|codex|copilot|antigravity|kiro
# 輸出: 一行「<input>(tab)<output>」。任何來源讀不到就印「N/A(tab)N/A」並 exit 0。
# 護欄: 沒給 cli 或 cli 名稱不認得,回傳 2。
#
# 各 CLI 的取得方式(原本寫在 worklog 的 SKILL.md,現在收在這裡):
# claude 加總 transcript JSONL(`$CLAUDE_CONFIG_DIR` 或 `~/.claude` 底下的
# `projects/**/*.jsonl`)每筆訊息的 `usage.input_tokens` 與
# `usage.output_tokens`。也可以改讀 `claude -p --output-format json`
# 回應裡的 `usage` 欄位。
# codex 讀 `~/.codex/sessions/**` 的 token 計數欄位;session 內打 `/status`
# 也看得到同一組數字。
# copilot 只有 session 內的 `/usage` 看得到,沒有可讀檔案 → N/A。
# antigravity 只顯示在 UI,沒有可讀檔案 → N/A。
# kiro 沒有公開來源 → N/A。
#
# 有給 session id 就優先取檔名含該 id 的 session 檔,因為日誌的花費時間也是按同一個
# session id 取的(session-timer.sh report {sid}),兩個數字必須描述同一個工作階段。
# 沒給 session id 才退回「最後修改的那一份」——那份可能是別的工作階段,或同一階段的
# sub agent 紀錄,數字只能當粗估。
set -u
na() { printf 'N/A\tN/A\n'; exit 0; }
cli="${1:-}"
sid="${2:-}"
if [ -z "$cli" ]; then
echo 'usage: token-usage.sh <cli> [session-id]' >&2
exit 2
fi
newest() { # <dir> <name-pattern> -> 最後修改的檔案路徑;找不到回非零
[ -d "$1" ] || return 1
f=$(find "$1" -type f -name "$2" -exec ls -t {} + 2>/dev/null | head -1)
[ -n "$f" ] || return 1
printf '%s\n' "$f"
}
pick() { # <dir> -> 先找 session id 相符的檔案,找不到才退回最後修改的
if [ -n "$sid" ]; then
if f=$(newest "$1" "*$sid*.jsonl"); then printf '%s\n' "$f"; return 0; fi
echo "warn: 找不到 session $sid 的 transcript,改用最後修改的那一份,數字可能不是本階段的。" >&2
fi
newest "$1" '*.jsonl'
}
sum_field() { # <file> <欄位名> -> 該欄位所有出現值的總和;沒有值就印空字串
grep -o "\"$2\"[[:space:]]*:[[:space:]]*[0-9][0-9]*" "$1" 2>/dev/null \
| grep -o '[0-9][0-9]*$' \
| awk '{ s += $1 } END { if (NR > 0) print s }'
}
case "$cli" in
claude)
src=$(pick "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects") || na ;;
codex)
src=$(pick "$HOME/.codex/sessions") || na ;;
copilot|antigravity|kiro)
na ;;
*)
echo "unknown cli: $cli" >&2
exit 2 ;;
esac
[ -r "$src" ] || na
# input 要把快取的部分加回來:Claude 的 usage 只在 input_tokens 記未快取的那幾個 token,
# 真正的輸入量落在 cache_creation_input_tokens 與 cache_read_input_tokens。只加第一欄,
# 報表會出現輸入幾百、輸出幾十萬的假數字。
input=$(sum_field "$src" input_tokens)
cache_new=$(sum_field "$src" cache_creation_input_tokens)
cache_hit=$(sum_field "$src" cache_read_input_tokens)
output=$(sum_field "$src" output_tokens)
[ -n "$input" ] && [ -n "$output" ] || na
input=$((input + ${cache_new:-0} + ${cache_hit:-0}))
printf '%s\t%s\n' "$input" "$output"
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env sh
# worklog-pending.sh — 待寫入工作日誌的暫存區(供 jsc-sdlc 階段回報與 jsc-log:worklog 使用)。
#
# 為什麼要有這支腳本:SDLC 每個階段結束都要回報工作日誌連結。階段跑完卻沒寫日誌時,
# 內容如果只留在對話裡,換一個工作階段就消失了,下次寫日誌也補不回來。所以暫存搬到檔案層:
# 階段回報發現沒有日誌,就把該階段的內容存進本區並警告使用者;下次 jsc-log:worklog 寫入時
# 先把本區的內容一併寫進去,寫完才清掉。
#
# 用法:
# worklog-pending.sh add <hash> <file> 把一段待寫入的日誌內容存起來,印出存放路徑
# worklog-pending.sh list <hash> 列出該 hash 的暫存檔路徑(一行一個,依時間排序)
# worklog-pending.sh cat <hash> 依時間順序印出全部暫存內容
# worklog-pending.sh clear <hash> 清掉該 hash 的暫存(寫入日誌成功後才做)
#
# <hash> 為 jsc-gitea/tools/hash-id 算出的 8 碼工作日誌 hash,也就是 LOG_{HASH} 的 HASH。
# 存放位置: $JSC_HOME/worklog-pending/{hash}/{UTC 時間}-{pid}.md(JSC_HOME 預設 ~/.jsc)
#
# 結束碼: 0=成功 1=讀寫失敗 2=用法錯誤 3=該 hash 沒有暫存內容
#
# 陷阱:
# - clear 只在日誌確實寫進 wiki 之後才呼叫。先清再寫,寫失敗就兩邊都沒有。
# - 檔名帶 UTC 時間與 pid,同一秒內兩個工作階段各存各的,不會互相覆蓋。
set -eu
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
ROOT="$JSC_HOME/worklog-pending"
usage() {
cat >&2 <<'EOF'
用法:
worklog-pending.sh add <hash> <file> 存一段待寫入的日誌內容
worklog-pending.sh list <hash> 列出暫存檔路徑
worklog-pending.sh cat <hash> 印出全部暫存內容
worklog-pending.sh clear <hash> 清掉暫存(寫入日誌成功後才做)
結束碼: 0=成功 1=讀寫失敗 2=用法錯誤 3=沒有暫存內容
EOF
exit 2
}
valid_hash() { # 只收 8 碼大寫英數,擋掉路徑穿越
case "$1" in
[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]) return 0 ;;
*) echo "[jsc][工作日誌暫存][ERR]:hash 須為 8 碼大寫英數,收到「${1:-空值}」。" >&2; exit 2 ;;
esac
}
cmd="${1:-}"; [ -n "$cmd" ] || usage
shift || true
hash="${1:-}"; [ -n "$hash" ] || usage
valid_hash "$hash"
dir="$ROOT/$hash"
case "$cmd" in
add)
src="${2:-}"
[ -n "$src" ] || usage
[ -f "$src" ] || { echo "[jsc][工作日誌暫存][ERR]:找不到內容檔「$src」。" >&2; exit 1; }
mkdir -p "$dir" || { echo "[jsc][工作日誌暫存][ERR]:建不出暫存目錄「$dir」。" >&2; exit 1; }
stamp=$(date -u +%Y%m%dT%H%M%SZ)
target="$dir/$stamp-$$.md"
cp "$src" "$target" || { echo "[jsc][工作日誌暫存][ERR]:寫不進「$target」。" >&2; exit 1; }
printf '%s\n' "$target"
;;
list)
[ -d "$dir" ] || exit 3
found=0
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
printf '%s\n' "$f"
found=1
done
[ "$found" -eq 1 ] || exit 3
;;
cat)
[ -d "$dir" ] || exit 3
found=0
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
cat "$f"
printf '\n'
found=1
done
[ "$found" -eq 1 ] || exit 3
;;
clear)
[ -d "$dir" ] || exit 3
rm -rf "$dir" || { echo "[jsc][工作日誌暫存][ERR]:清不掉「$dir」。" >&2; exit 1; }
printf '已清除暫存:%s\n' "$dir"
;;
*)
usage ;;
esac