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
21 changes: 19 additions & 2 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ CC-001/CC-002 were consumed by PR #24 fix bundle inline, with no standalone entr
| CC-480 | ✅ done | host-switch memory continuity:嚴格 resolution contract + Codex `pmctl pm prepare` 確定性 hydration + Claude↔Codex 共用同一 canonical memory E2E;v0.9.0 host 軸 continuity 驗收 | arch/memory | 2026-07-12 | — | P1 | design |
| CC-481 | ✅ done | test runner contract:`run-tests.sh` direct-impact iteration planner + `run-all-tests.sh` 相容 full wrapper + 共用 suite executor + tree-bound result verifier;PR-gate 維持 repo-agnostic,full suite 移出 gate lifecycle | ops/test | 2026-07-13 | feedback:2026-07-13 | P1 | design |
| CC-482 | ✅ done | Claude PR-gate reviewer definitions 最小讀取權限:headless `acceptEdits` 無法批准 `~/.claude/agents/*.md`;改用 workspace 內 run-scoped immutable snapshots,禁止擴大為任意 home read/`bypassPermissions` | ops/gate | 2026-07-13 | feedback:2026-07-13 | P1 | hygiene |
| CC-483 | 🔵 active | Codex PM workflow memory provider 優先權:使用者指定 `pmctl memory` 為 canonical substrate,不得默認優先使用 Codex native memory;盤點 prepare/guard/host instruction routing 與可觀測 provenance | arch/memory | 2026-07-13 | feedback:2026-07-13 | P1 | design |
| CC-483 | ✅ closed 2026-07-14 | Codex PM workflow memory provider 優先權:使用者指定 `pmctl memory` 為 canonical substrate,不得默認優先使用 Codex native memory;盤點 prepare/guard/host instruction routing 與可觀測 provenance | arch/memory | 2026-07-13 | pr:#399 | P1 | design |
| CC-484 | ✅ done | JapanJob 與 qa-testing-rules 的 `pmctl context` refresh 未生效:重現 session/index/update/pack 實際路徑、repo-root/project-key/DB freshness,補跨 repo live E2E 與 actionable diagnostics | ops/memory | 2026-07-13 | feedback:2026-07-13 | P1 | retrieval |
| CC-485 | ✅ done | 工具能力與維護者政策分離:通用 gate/test/PM 不規定使用流程;affected feedback 僅屬開發/PR,pm-dispatch release 固定由 `release-verify.sh --e2e`(內含 fresh full suite)+ checklist 驗收 | arch/process | 2026-07-13 | pr:#398 | P3 | design |
| CC-486 | ⏸ deferred | direct-impact test planner mapping 提前退出:changed path 含 `agents/*.md`/`commands/*.md` 時 `map_path` 呼叫未註冊的 `lint-frontmatter`,`add_suite` 回傳 1 並在 `set -e` 下無輸出終止,導致 `run-tests.sh --base ... --list` exit 1 | ops/test | 2026-07-13 | feedback:2026-07-13 | P2 | hygiene |
| 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 @@ -616,7 +617,7 @@ _Terminal_ (CC-378: swept OUT to `BACKLOG-ARCHIVE.md` by `scripts/archive-closed

---

## CC-483 — Codex workflow 優先使用 pmctl memory canonical substrate 🔵 active
## CC-483 — Codex workflow 優先使用 pmctl memory canonical substrate ✅ 2026-07-14

**Problem**: 現有 Codex workflow/host guidance 可能先使用 Codex 自身 memory surface,再把 `pmctl memory` 視為 fallback;但本專案的跨 host continuity 契約已由 [[CC-480]] 定義為 project-owned `pmctl memory`。若 provider priority 反轉,同一 repo 會產生兩套互不一致的記憶來源,Claude→Codex 切換表面成功、實際 retrieval/provenance 漂移。

Expand All @@ -635,6 +636,22 @@ _Terminal_ (CC-378: swept OUT to `BACKLOG-ARCHIVE.md` by `scripts/archive-closed

**Diagnostic evidence (2026-07-13)**: live `pmctl pm prepare --cd /home/screenleon/github/JapanJob --json` 已由 `pmctl memory resolve` 命中該 repo 的 canonical legacy memory dir/project key,且 `memory_context_status=hydrated`;pmctl coordinator 本身不是 native-first。缺口位於 Codex interactive host wiring:目前 live `~/.codex/hooks.json` 只有 Bash `PreToolUse` guard,沒有 prompt/session entry 將 preparation 固定導入 pmctl memory。拋棄式 `UserPromptSubmit` payload/injection probe 被中斷,未取得 runtime contract 前不先綁定未驗證 hook。

**Implementation handoff (session close, 2026-07-13)**: working tree 尚未 commit,CC-483 保持 active。已完成 host-neutral provenance(`provider=pmctl`、canonical project key/dir、resolution source、hit count/refs、native `auxiliary/unknown`)、Codex `UserPromptSubmit` 安裝、OpenCode `--host opencode` preparation、strict locked `pmctl memory append-episode`、Claude `/mem-log` 與 Stop skeleton writer 遷移、invalid explicit 讀寫 fail-closed、generic non-git resolver opt-in,以及 dispatch brief 的 canonical provenance。因目前 checkout/global `pmctl` 沒有 `simplify` 子命令,已用 Codex read-only simplify/reuse review 代行並依結果抽出共用 host enum、prepare/run hydration、resolver-owned generic fallback,且把 Stop 寫入納入同一 lock。Focused evidence:memory 67/67、pm 30/30、guards full 294/294(新增後 invalid-explicit 2/2、session-hook 11/11)、commands 277/277、Codex host 36/36、OpenCode host 13/13、Codex doctor 9/9;lint-scripts/agents/frontmatter/test-docstrings 均通過。Live isolated E2E 已實際驗證 Codex 0.144.1 prompt payload/injection 與 invalid explicit pre-model block、Claude Code 2.1.207 canonical injection、OpenCode 1.17.8 `/pm` preparation;後續不得再用 Claude 做 gate(使用者額度要求)。本 session 結束前 full suite 僅啟動後即依使用者要求停止,沒有 sign-off 結果;Codex-only PR gate 尚未執行。下一 session 先重跑 full suite,處理真正 regression(若有),再以 `pr-gate.sh --executor codex --allow-dirty` gate。direct-impact planner 自身的既有無輸出 exit 1 已拆為 [[CC-486]],不可誤報為 CC-483 產品失敗。

**Outcome**: 2026-07-14 完成 Claude、Codex、OpenCode 共用 canonical `pmctl memory` resolver/writer、可觀測 provenance、Codex prompt/session hooks 與 symlink-safe atomic episode append。Full suite 77/77、Claude full-tier gate Final GO,PR CI 全數通過;CC-486 維持獨立 deferred,不併入本票。

**See**: pr:#399

---

## CC-486 — direct-impact planner 未註冊 suite 觸發 `set -e` 提前退出 ⏸ deferred

**Problem**: `scripts/run-tests.sh --base origin/main --list` 在 changed paths 含 `agents/*.md` 或 `commands/*.md` 時,`map_path` 會呼叫 `add_suite lint-frontmatter`;但 `test-suite-runner.sh --list` 沒有註冊該 suite。`add_suite` 的最後一個條件式因此回傳 1,頂層 `set -e` 直接終止,沒有 planner diagnostics,exit 1。

**Acceptance**: 未註冊的 optional mapping 不得讓 planner 提前退出;應修正 mapping 名稱或讓 `add_suite` 明確 return 0,並新增包含 agent/command changed path 的 regression,確認 `--list` 輸出已選 suites、coverage gaps 與 exit 0。不得藉此弱化「沒有任何可用 suite 時 exit 2」的既有契約。

**Evidence**: CC-483 收尾時以 `bash -x scripts/run-tests.sh --base origin/main --list` 重現;trace 停在 `add_suite lint-frontmatter` 的 `[[ -n '' ]]`。同一批 CC-483 focused suites與 lint 均綠,故此項獨立追蹤,不視為 CC-483 產品 regression。

---

## CC-484 — JapanJob/qa-testing-rules pmctl context refresh 失效 ✅ 2026-07-13
Expand Down
8 changes: 8 additions & 0 deletions agents/project-pm.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ Use the legacy convention only when preparation supplied no canonical memory.
If canonical fields are present but invalid, surface the mismatch and do not
silently fall back to a different host-local memory.

When `memory_provenance` is present, require `provider: pmctl` and
`authority: canonical`; preserve its project key, resolution source, hit count,
and refs in any implementation brief as `canonical_memory_provenance`.
`auxiliary_memory` is never authoritative. A value of `status: unknown` means
unobserved, not empty, and must not be used to weaken or replace a canonical
constraint. If provenance names an invalid explicit resolution, stop instead
of consulting a legacy or host-native path.

# On invocation

1. **Identify project**: `pwd` and `ls ~/github/`. If user names a project use that; if ambiguous ask.
Expand Down
6 changes: 6 additions & 0 deletions cli/pmctl
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,12 @@ case "$cmd/$sub" in
fi
pmctl_memory_resolve "$@"
;;
memory/append-episode)
if ! declare -F pmctl_memory_append_episode >/dev/null; then
pmctl_die "memory append-episode unavailable"
fi
pmctl_memory_append_episode "$@"
;;
memory/doctor)
if ! declare -F pmctl_memory_doctor >/dev/null; then
pmctl_die "memory doctor unavailable"
Expand Down
32 changes: 14 additions & 18 deletions commands/mem-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,15 @@ Record what happened in this session to the episodic memory layer. Follow these
/mem-log "Session delivered first-pass migration plan and closed a hook exception."
```

## Step 1 — Find episodes.jsonl
## Step 1 — Confirm canonical memory

Run `pmctl memory dir` to locate the memory directory, then derive the episodes path:
Resolve the same strict canonical memory that every host uses:

```bash
mem="$(pmctl memory dir)" || { echo "No memory directory found for this project"; exit 1; }
ep="$mem/episodes.jsonl"
pmctl memory resolve --repo-root "$(pwd)" --json
```

If `pmctl memory dir` exits non-zero, report "No memory directory found for this project" and stop. The file `$ep` may not exist yet — that is expected when logging for the first time.
If resolution is unavailable or an explicit path is invalid, report the resolver error and stop. Never derive or guess a host-local path.

## Step 2 — Write the summary

Expand All @@ -46,23 +45,20 @@ If `$ARGUMENTS` is non-empty, incorporate it as a hint about what to emphasize.

Keep the summary factual and dense. No filler phrases.

## Step 3 — Update episodes.jsonl
## Step 3 — Append through the strict write API

**/mem-log owns summary creation.** Do not assume the Stop hook has already written a skeleton — the Stop hook runs at session end, after /mem-log. If a skeleton already exists (e.g., from a previous session), update it; otherwise append a new entry.
Call the host-neutral writer; do not open or mutate `episodes.jsonl` directly:

Read the episodes.jsonl file (it may not exist yet). Then:

1. **Find an updatable entry**: scan all lines for one where `session_id` matches the current session AND `summary` is empty. If found, replace that line with the full entry (same `date`/`cwd`/`session_id`, filled `summary`).
2. **Otherwise**: append a new entry with the current date and cwd.

The entry format:
```json
{"date":"<ISO8601>","cwd":"<absolute path>","session_id":"<id>","summary":"<3-5 line text>"}
```bash
pmctl memory append-episode \
--repo-root "$(pwd)" \
--host claude \
--session-id "" \
--summary "$SUMMARY" \
--json
```

`session_id` is only available inside hook payloads; from a slash command it may not be accessible. Use an empty string `""` if it is unknown — the Stop hook will still skip this session correctly if the last entry for this cwd already has a non-empty summary.

Write the updated file (replace the matched line in-place or append; preserve all other lines).
`session_id` may be unavailable from a slash command; use `""` in that case. The API re-runs strict resolution, appends under a lock, rejects a symlink target, and fails closed instead of falling through to another host's memory.

## Step 4 — Confirm

Expand Down
6 changes: 4 additions & 2 deletions commands/pm.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ argument-hint: "<free-form request, e.g. \"status of foo\", \"add /health endpoi
**Before invoking the agent, capture a state snapshot.** From within the target repo's working directory, use the shared batch coordinator. This is also the snapshot path used by non-Claude hosts; Claude retains the interactive Agent layer after preparation:

```bash
PM_PREP_ARGS=(pm prepare --request "$ARGUMENTS" --json)
PM_PREP_ARGS=(pm prepare --request "$ARGUMENTS" --host claude --json)
PM_PREP_JSON="$(bash ${PM_DISPATCH_REPO}/cli/pmctl "${PM_PREP_ARGS[@]}")" || PM_PREP_JSON=""
SNAPSHOT_FILE=""
MEMORY_DIR=""
MEMORY_CONTEXT=""
MEMORY_PROVENANCE=""
[[ -n "$PM_PREP_JSON" ]] && SNAPSHOT_FILE="$(printf '%s\n' "$PM_PREP_JSON" | jq -r '.snapshot_file // empty')"
[[ -n "$PM_PREP_JSON" ]] && MEMORY_DIR="$(printf '%s\n' "$PM_PREP_JSON" | jq -r '.memory_resolution.memory_dir // empty')"
[[ -n "$PM_PREP_JSON" ]] && MEMORY_CONTEXT="$(printf '%s\n' "$PM_PREP_JSON" | jq -r '.memory_context // empty')"
[[ -n "$PM_PREP_JSON" ]] && MEMORY_PROVENANCE="$(printf '%s\n' "$PM_PREP_JSON" | jq -c '.memory_provenance // empty')"
```

If preparation cannot create a snapshot (e.g. the target repo has no `BACKLOG.md` — common outside pm-dispatch), `$SNAPSHOT_FILE` will be empty; skip the snapshot but proceed with the dispatch — do not block. Rationale: solves the "PM spends its first phase re-verifying caller-claimed branch/ticket state" failure mode documented in `[[project_memory_architecture]]`.

Invoke `project-pm` via Agent with `run_in_background: true` (default). PM tasks routinely take 30–120s and burn 30–80K tokens; foregrounding holds the main thread idle while the user can't interject. Foreground only when PM's verdict is the sole input to the immediate next tool call AND no parallel main-thread prep work exists (rare). Do not force a model — inherit the main-thread model so the user's own session choice applies (see `docs/model-tier-policy.md` §`/pm`). Brief with: request ($ARGUMENTS), current working directory, **`snapshot_file: <abs-path>` when the snapshot was captured above** (PM agent uses the snapshot for orientation; see `agents/project-pm.md` `## Snapshot ingestion` for the git re-derivation rules that apply before trusting any snapshot field), **`memory_dir: <abs-path>` and the pointer-only `memory_context` pack when preparation returned them** (read the referenced cards from that directory before planning; never infer a different host-local memory path), and relevant prior-turn context the subagent won't otherwise see.
Invoke `project-pm` via Agent with `run_in_background: true` (default). PM tasks routinely take 30–120s and burn 30–80K tokens; foregrounding holds the main thread idle while the user can't interject. Foreground only when PM's verdict is the sole input to the immediate next tool call AND no parallel main-thread prep work exists (rare). Do not force a model — inherit the main-thread model so the user's own session choice applies (see `docs/model-tier-policy.md` §`/pm`). Brief with: request ($ARGUMENTS), current working directory, **`snapshot_file: <abs-path>` when the snapshot was captured above** (PM agent uses the snapshot for orientation; see `agents/project-pm.md` `## Snapshot ingestion` for the git re-derivation rules that apply before trusting any snapshot field), **`memory_provenance`, `memory_dir: <abs-path>`, and the pointer-only `memory_context` pack when preparation returned them** (pmctl/canonical takes precedence; host-native memory is auxiliary only; read the referenced cards from that directory before planning; never infer a different host-local memory path), and relevant prior-turn context the subagent won't otherwise see.

Relay the PM's user-facing summary. Do not do the PM's job yourself.

Expand Down
40 changes: 30 additions & 10 deletions docs/memory-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,22 +208,42 @@ remains compatible, and absence is reported as `unavailable`.
Exit codes are `0` resolved, `1` unavailable, `2` usage error, and `3`
invalid explicit selection.

Lifecycle adapters that can receive a real directory outside a Git worktree
use `--allow-non-git`. That option keeps env/config validation and legacy
discovery inside this same resolver, while deriving a deterministic path key;
hooks must not reconstruct a synthetic resolution record themselves. Normal PM
preparation and dispatch remain Git-only.

**Injection is a per-tool adapter, not part of the portable core.** The
portable core is the retrieval API: `pmctl context --source memory` (and
`pmctl memory doctor` for health checks). Claude Code's `UserPromptSubmit`
hook (`guard-inject-memory.sh`) is one adapter that calls into this core
automatically every turn. A tool without an equivalent hook (codex, opencode)
gets the same memory by calling `pmctl context --source memory` directly
there is no requirement to replicate Claude's hook-based injection timing.

For the Codex batch PM interface, this call is deterministic rather than a
model convention: `pmctl pm prepare` runs strict resolution and a bounded,
portable core is strict resolution, `pmctl context --source memory`, and the
canonical write API. Claude and Codex can both run the host-neutral
`guard-inject-memory.sh` on `UserPromptSubmit`; OpenCode's installed `/pm`
command calls its `pm_prepare` custom tool; a host with neither mechanism uses
`pmctl pm prepare --host generic` directly. All four routes expose pmctl as
canonical and label unobserved host-native memory `auxiliary/unknown`.

For every batch PM interface, this call is deterministic rather than a model
convention: `pmctl pm prepare --host <name>` runs strict resolution and a bounded,
request-scoped memory query on every preparation. Its JSON contract carries
`memory_resolution`, `memory_context_status`, and the retrieved
`memory_resolution`, `memory_provenance`, `memory_context_status`, and the retrieved
`memory_context`, so a calling host can verify which canonical memory it used.
No memory or zero hits is fail-open; an invalid explicit selection is
fail-closed to prevent silent continuity loss.

Writes use the same resolver. `pmctl memory append-episode --repo-root <repo>
--host <name> --summary <text>` appends one locked JSONL record to the resolved
canonical `episodes.jsonl`. It refuses invalid explicit paths, unwritable
directories, and symlink episode targets; it never accepts a caller-guessed
memory directory. `/mem-log` and Claude's Stop skeleton writer both use this
API; skeleton session-id dedupe happens inside the same append lock.

| Host | Deterministic read entry | Canonical write entry | Native memory |
| --- | --- | --- | --- |
| Claude | `/pm` calls `pm prepare --host claude`; `UserPromptSubmit` runs `guard-inject-memory.sh` | `pmctl memory append-episode --host claude` | auxiliary; `unknown` unless separately observed |
| Codex | `UserPromptSubmit` runs `guard-inject-memory.sh`; batch PM uses `--host codex` | `pmctl memory append-episode --host codex` | auxiliary; `unknown` unless separately observed |
| OpenCode | `/pm` calls the installed `pm_prepare` tool with `--host opencode` | `pmctl memory append-episode --host opencode` | auxiliary; `unknown` unless separately observed |
| Generic/no hook | `pmctl pm prepare --host generic` | `pmctl memory append-episode --host generic` | auxiliary; `unknown` |

## Practical conventions

- Keep `MEMORY.md` short and high-signal.
Expand Down
5 changes: 3 additions & 2 deletions hosts/codex/host.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ install_targets:

# Hook runtime facts probed end-to-end: the hook config lives in hooks.json
# using the Claude-Code-compatible hooks block shape (NOT a config.toml
# section), and deny decisions return via hookSpecificOutput.permissionDecision
# on the hook script's stdout.
# section), deny decisions return via hookSpecificOutput.permissionDecision,
# and UserPromptSubmit provides cwd/prompt while plain stdout becomes model
# context. The install module uses that verified contract for canonical memory.
hook_surface:
config_format: codex-hooks-json
events:
Expand Down
Loading