Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ CC-001/CC-002 were consumed by PR #24 fix bundle inline, with no standalone entr
| CC-489 | ⏸ deferred | `scripts/` domain ownership 重整:host/runtime/ops/test entrypoints 移至對應模組,由 manifest/registry 統一發現並以相容 shim 分批遷移 | arch | 2026-07-14 | feedback:2026-07-14 | P2 | design |
| CC-490 | 🔵 active | project-scoped explicit memory config:取代全域單值 `dispatch.memory_dir`,避免多 repo 靜默共用 pm-dispatch canonical store | arch/memory | 2026-07-14 | feedback:2026-07-14 | P1 | design |
| CC-491 | 🔵 active | PR-gate pre-flight 機械式 evidence contract:傳遞 command、selected suites、逐項結果與 tree fingerprint,讓 reviewer reuse 已驗證結果並禁止無條件重跑 | ops/gate | 2026-07-14 | feedback:2026-07-14 | P1 | design |
| CC-492 | ✅ closed 2026-07-15 | Claude UserPromptSubmit context hook timeout envelope:升級 managed timeout、復原 files=0 殘缺 DB,避免 30 秒外層終止 | ops/memory | 2026-07-15 | pr:#403 | P1 | retrieval |
| CC-465 | 🔵 active | memory/context 關鍵詞管線 CJK 支援:抽出共用零依賴斷詞 lib,取代三處各自 ASCII-only 抽詞;工作序列起點(465→467→468→466)(2026-07-07 記憶系統深入分析) | memory | 2026-07-07 | feedback:2026-07-07 | P2 | retrieval |
| CC-466 | 🔵 active | 記憶卡片生命週期閉環:expires_at 執行 + 關窗式 supersede + usage sidecar 休眠偵測 + doctor→distill 接線;排在 CC-467 之後(需其遙測為前置)(2026-07-07 記憶系統分析 + 外部研究 Graphiti/mcp-memory-service) | memory | 2026-07-07 | feedback:2026-07-07 | P2 | retrieval |
| CC-467 | 🔵 active | `pmctl memory stats`:注入效益可視化(唯讀聚合器)——注入 bytes/卡片命中分佈/從未命中卡/episode 填寫率,回答「記憶有跟沒有差在哪」;排在 CC-466 之前(2026-07-07;業界僅離線 recall 評測,無 per-injection 遙測) | DX/memory | 2026-07-07 | — | P2 | retrieval |
Expand Down Expand Up @@ -791,6 +792,27 @@ _Terminal_ (CC-378: swept OUT to `BACKLOG-ARCHIVE.md` by `scripts/archive-closed

---

## CC-492 — Claude UserPromptSubmit context hook timeout envelope 與殘缺 DB 復原 ✅ 2026-07-15

**Problem**: Claude Code 的 `UserPromptSubmit` command hook 未明確設定 timeout,實際使用 30 秒預設值;但 `guard-inject-context.sh` 的既有索引刷新與初次建立預算分別為 45/120 秒。JapanJob live session 已觀察到 context hook 在 30.020 秒被外層終止,stdout 被丟棄;同時先前中斷留下「DB 檔存在但 files=0」的殘缺 cache,使 hook 誤判為既有索引並在後續 prompt 重複進入超時路徑。

**Requirement**:
1. Claude installer 必須為 managed `guard-inject-context.sh` 寫入明確、且大於內部最大預算並保留 cleanup 餘裕的 handler timeout;重複安裝要升級既有缺值/過短值,不得只處理新 hook。
2. context hook 必須辨識「DB 存在但沒有任何 committed file row」的未完成 initial build,沿用 initial-build timeout 與失敗清理,不得永久誤分類為 incremental cache。
3. doctor 必須診斷 managed context hook timeout 缺失或低於契約值;memory injection hook 與非 managed hook 不得被連帶改寫。
4. 回歸覆蓋 fresh install、existing-hook migration、idempotency、殘缺空 DB timeout cleanup、unrelated hook preservation,以及 handler/internal timeout envelope。
5. Claude permissions 使用其接受的 `Edit(<workspace>/**/.gate-results/**)`,installer 升級時移除舊 managed `Write(...)`;uninstaller 同時辨識並移除新 `Edit(...)` 與歷史 `Write(...)`。`/tmp/*` 權限不是 pm-dispatch installer 管理項,不得擅自新增或刪除。

**Acceptance**: sandbox install 後 context hook 具有明確 timeout,doctor 通過;模擬已存在的空 DB 與慢速 index 時,hook 在自身預算內 fail-open、清除殘缺 cache,且不再由 Claude 30 秒外層先行終止。permission upgrade 只留下 managed `Edit(.gate-results)`、不產生任何 `Write(...)`;uninstall 可清除新舊兩種 managed spelling 且保留 `/tmp/*` 等非 managed 權限。live Claude 設定升級後,JapanJob 下一次 prompt 不再出現 `UserPromptSubmit hook timed out after 30s`。

**Source**: 2026-07-15 JapanJob Claude session `30e97e81-4bcf-4308-aa1c-78c25d451ba3` live diagnosis。

**Closed 2026-07-15(Draft PR #403)**: managed context hook timeout 已提升為 150 秒,殘缺 files=0 DB 會重走 initial-build recovery;installer 與 uninstaller 已改用並對稱清理 managed `Edit(.gate-results)`,同時相容歷史 `Write(...)` 且保留非 managed `/tmp/*` 權限。Sequential full Codex gate `gate-20260715-003400-ab8888` 為 `Final: GO`,affected suites 全綠,live Claude doctor 26/26 通過,JapanJob prompt hook 12.77 秒完成並建立 1320-file index;受另一 session 並行刷新 live context DB 影響的 full-suite isolation 斷言,已獨立重跑 `test-pmctl-context` 114/114 通過。

**See**: pr:#403

---

## CC-484 — JapanJob/qa-testing-rules pmctl context refresh 失效 ✅ 2026-07-13

**Problem**: JapanJob 的 session 與 qa-testing-rules 實際工作流沒有順利用 `pmctl context` 刷新內容。[[CC-455]] 曾修正 context plane 預設 repo root 跟隨 caller CWD,但目前 live 行為仍可能在 repo-root resolution、worktree/project key、context.db 位置、mtime skip、session hook/prepare wiring 或 update/query 順序其中一層失效;只看 unit test 不能確認是哪一層。
Expand Down
12 changes: 9 additions & 3 deletions docs/context-retrieval.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,15 @@ when `sqlite3` is absent the hook skips pmctl entirely; when it is available,
the first eligible prompt automatically builds the repo-local DB. Initial builds
have a 120-second budget (`PM_DISPATCH_PROMPT_CONTEXT_INITIAL_TIMEOUT`), while
incremental refreshes use 45 seconds (`PM_DISPATCH_PROMPT_CONTEXT_TIMEOUT`). A
timed-out first build removes only its incomplete derived DB so the next prompt
can retry with the initial-build budget. Unchanged refreshes preserve the FTS
table instead of rebuilding it, keeping the normal prompt path short.
Claude install explicitly gives this handler 150 seconds, leaving cleanup time
outside the largest internal budget instead of inheriting Claude Code's shorter
`UserPromptSubmit` default. Re-running the installer upgrades an older managed
hook in place. A timed-out first build removes only its incomplete derived DB so
the next prompt can retry with the initial-build budget; a schema-only DB with
zero committed file rows is also treated as an interrupted initial build.
Unchanged refreshes preserve the FTS table instead of rebuilding it, keeping the
normal prompt path short. `doctor.sh` reports a managed context hook whose
handler timeout is absent or below this envelope.

Set `PM_DISPATCH_DISABLE_PROMPT_CONTEXT=1` to disable the scan entirely. Use
this whenever the live context DB must not be touched — for example while the
Expand Down
25 changes: 22 additions & 3 deletions scripts/guard-inject-context.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
# test suite is running against this repo).
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
# shellcheck source=scripts/lib/prompt-context-timeouts.sh
. "$SCRIPT_DIR/lib/prompt-context-timeouts.sh"

if [[ "${PM_DISPATCH_DISABLE_PROMPT_CONTEXT:-0}" == "1" ]]; then
exit 0
fi
Expand Down Expand Up @@ -60,12 +65,26 @@ _context_db="$repo_root/.pm-dispatch/ctx/context.db"
_initial_build=0
if [[ ! -f "$_context_db" ]]; then
_initial_build=1
_timeout_secs="${PM_DISPATCH_PROMPT_CONTEXT_INITIAL_TIMEOUT:-120}"
else
_timeout_secs="${PM_DISPATCH_PROMPT_CONTEXT_TIMEOUT:-45}"
# An interrupted initial build can leave a SQLite schema without committed
# file rows. Only a confirmed numeric zero is treated as incomplete; a locked
# or unreadable DB stays on the non-destructive incremental path.
_indexed_files="$(sqlite3 "$_context_db" 'SELECT count(*) FROM files;' 2>/dev/null || true)"
if [[ "$_indexed_files" == "0" ]]; then
_initial_build=1
fi
fi
if [[ "$_initial_build" -eq 1 ]]; then
_timeout_secs="${PM_DISPATCH_PROMPT_CONTEXT_INITIAL_TIMEOUT:-$PROMPT_CONTEXT_INITIAL_TIMEOUT_DEFAULT}"
else
_timeout_secs="${PM_DISPATCH_PROMPT_CONTEXT_TIMEOUT:-$PROMPT_CONTEXT_REFRESH_TIMEOUT_DEFAULT}"
fi
if [[ ! "$_timeout_secs" =~ ^[0-9]+$ ]]; then
[[ "$_initial_build" -eq 1 ]] && _timeout_secs=120 || _timeout_secs=45
if [[ "$_initial_build" -eq 1 ]]; then
_timeout_secs="$PROMPT_CONTEXT_INITIAL_TIMEOUT_DEFAULT"
else
_timeout_secs="$PROMPT_CONTEXT_REFRESH_TIMEOUT_DEFAULT"
fi
fi
_runner=(bash "$pmctl_cli")
if command -v timeout >/dev/null 2>&1; then
Expand Down
18 changes: 13 additions & 5 deletions scripts/install-guards.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=scripts/lib/prompt-context-timeouts.sh
. "$SCRIPT_DIR/lib/prompt-context-timeouts.sh"

# Use the same canonical Claude runtime config root as install/doctor/guards.
# CLAUDE_HOME remains a compatibility alias for standalone legacy callers.
Expand Down Expand Up @@ -253,6 +255,7 @@ MSYS2_ARG_CONV_EXCL='*' MSYS_NO_PATHCONV=1 jq \
--arg session "$session_cmd_q" \
--arg inject "$inject_cmd_q" \
--arg ctx_inject "$ctx_inject_cmd_q" \
--argjson ctx_inject_timeout "$CLAUDE_PROMPT_CONTEXT_HOOK_TIMEOUT" \
--arg statusline "$statusline_cmd_q" \
--argjson sl_present "$_statusline_already_wired" \
--argjson bg_guards "$_bg_json" \
Expand Down Expand Up @@ -374,7 +377,8 @@ MSYS2_ARG_CONV_EXCL='*' MSYS_NO_PATHCONV=1 jq \
.hooks.UserPromptSubmit |= map(
.hooks |= map(
if ((.command | split("/") | last) == ($inject | split("/") | last) and (.command | split("/") | .[-2]) == "scripts") then .command = $inject
elif ((.command | split("/") | last) == ($ctx_inject | split("/") | last) and (.command | split("/") | .[-2]) == "scripts") then .command = $ctx_inject
elif ((.command | split("/") | last) == ($ctx_inject | split("/") | last) and (.command | split("/") | .[-2]) == "scripts") then
.command = $ctx_inject | .timeout = $ctx_inject_timeout
else . end
)
) |
Expand Down Expand Up @@ -430,7 +434,7 @@ MSYS2_ARG_CONV_EXCL='*' MSYS_NO_PATHCONV=1 jq \
else . end
) |
( if $ctx_inject_present == 0 then
.hooks.UserPromptSubmit += [{"hooks": [{"type": "command", "command": $ctx_inject}]}]
.hooks.UserPromptSubmit += [{"hooks": [{"type": "command", "command": $ctx_inject, "timeout": $ctx_inject_timeout}]}]
else . end
) |
( if $sl_present == 0 then
Expand All @@ -440,7 +444,7 @@ MSYS2_ARG_CONV_EXCL='*' MSYS_NO_PATHCONV=1 jq \
' > "$tmp_new" < "$settings"

# --- Permissions merge for reviewer subagents ---
# Reviewer subagents spawned by pr-gate need Write(.gate-results) and Bash(pmctl guard check)
# Reviewer subagents spawned by pr-gate need Edit(.gate-results) and Bash(pmctl guard check)
# to write results and run guard checks. Workspace root detection is shared with
# uninstall-guards.sh via scripts/lib/gate-workspace.sh.
_workspace_root="$(gate_workspace_root "$repo_root" "$HOME")"
Expand All @@ -459,15 +463,19 @@ _pmctl_guard_tilde=""
_tmp_perms="$(mktemp)"
trap 'rm -f "$tmp_new" "$_tmp_perms"' EXIT
if ! jq \
--arg write_perm "Write(${_gate_glob})" \
--arg edit_perm "Edit(${_gate_glob})" \
--arg legacy_write_perm "Write(${_gate_glob})" \
--arg bash_guard "Bash(pmctl guard check:*)" \
--arg bash_guard_abs "$_pmctl_guard_abs" \
--arg bash_guard_tilde "$_pmctl_guard_tilde" \
--arg bash_mkdir "Bash(mkdir -p:*)" \
'
.permissions //= {} |
.permissions.allow //= [] |
([$write_perm, $bash_guard, $bash_guard_abs, $bash_guard_tilde, $bash_mkdir]
# Claude settings accepts Edit path permissions. Remove the legacy Write
# spelling during upgrade instead of preserving an invalid managed entry.
.permissions.allow |= map(select(. != $legacy_write_perm)) |
([$edit_perm, $bash_guard, $bash_guard_abs, $bash_guard_tilde, $bash_mkdir]
| map(select(. != ""))) as $required |
.permissions.allow |= (
. as $existing |
Expand Down
22 changes: 22 additions & 0 deletions scripts/lib/doctor-host-claude.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ _doctor_host_claude_hook_present() {
' "$settings" >/dev/null 2>&1
}

_doctor_host_claude_context_timeout_ok() {
local settings="$1"
# shellcheck disable=SC1091
# shellcheck source=scripts/lib/prompt-context-timeouts.sh
. "$REPO_ROOT/scripts/lib/prompt-context-timeouts.sh"
jq -e --argjson expected "$CLAUDE_PROMPT_CONTEXT_HOOK_TIMEOUT" '
[
(.hooks.UserPromptSubmit[]? | (.hooks // [])[]? |
select(((.command? // "") | gsub("\\\\"; "/") | split("/") | last) == "guard-inject-context.sh"))
] as $hooks |
($hooks | length) > 0 and
all($hooks[]; ((.timeout? // 0) | type) == "number" and (.timeout >= $expected))
' "$settings" >/dev/null 2>&1
}

_doctor_host_claude_adapter_bg_present() {
local adapter_name="$1" settings="$2"
jq -e --arg adapter_name "$adapter_name" '
Expand Down Expand Up @@ -247,6 +262,13 @@ _doctor_host_claude_check_hooks() {
emit_check hooks warn \
"${#_stale[@]} hook(s) wired from a different checkout (e.g. $(basename "${_stale[0]}"))" \
"bash '${REPO_ROOT}/install.sh' to re-wire hooks to this checkout"
elif ! _doctor_host_claude_context_timeout_ok "$settings"; then
# shellcheck disable=SC1091
# shellcheck source=scripts/lib/prompt-context-timeouts.sh
. "$REPO_ROOT/scripts/lib/prompt-context-timeouts.sh"
emit_check hooks fail \
"guard-inject-context.sh timeout missing or below ${CLAUDE_PROMPT_CONTEXT_HOOK_TIMEOUT}s" \
"bash '${REPO_ROOT}/scripts/install-guards.sh'"
else
emit_check hooks ok "$_total_hooks hooks present ($profile profile)"
fi
Expand Down
11 changes: 11 additions & 0 deletions scripts/lib/prompt-context-timeouts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Shared timeout defaults for the synchronous UserPromptSubmit context hook.
# The Claude handler timeout must remain greater than the hook's largest
# internal timeout so fail-open cleanup runs before Claude discards the hook.

# shellcheck disable=SC2034 # Public constant consumed by sourcing callers.
PROMPT_CONTEXT_REFRESH_TIMEOUT_DEFAULT=45
# shellcheck disable=SC2034 # Public constant consumed by sourcing callers.
PROMPT_CONTEXT_INITIAL_TIMEOUT_DEFAULT=120
# shellcheck disable=SC2034 # Public constant consumed by sourcing callers.
CLAUDE_PROMPT_CONTEXT_HOOK_TIMEOUT=150
2 changes: 2 additions & 0 deletions scripts/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ map_path() {
add_suite test-pmctl-memory; add_suite test-pmctl-context; add_suite test-migrate; add_suite test-guards; behavioral=1 ;;
scripts/lib/pmctl-memory-config.sh)
add_suite test-pmctl-memory; behavioral=1 ;;
scripts/lib/prompt-context-timeouts.sh)
add_suite test-guards; add_suite test-install; add_suite test-doctor; behavioral=1 ;;
scripts/install-guards-codex.sh|scripts/uninstall-guards-codex.sh|scripts/lib/doctor-host-codex.sh)
add_suite test-host-write-codex; add_suite test-doctor; behavioral=1 ;;
scripts/install-guards.sh|scripts/uninstall-guards.sh|scripts/lib/doctor-host-claude.sh)
Expand Down
Loading