throughline 0.3.25 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/tl.md +6 -21
- package/CHANGELOG.md +49 -0
- package/README.ja.md +46 -58
- package/README.md +53 -69
- package/bin/throughline.mjs +8 -10
- package/docs/INHERITANCE_ON_CLEAR_ONLY.md +13 -0
- package/docs/PUBLIC_RELEASE_PLAN.md +2 -1
- package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +286 -0
- package/package.json +1 -1
- package/src/baton.mjs +17 -45
- package/src/baton.test.mjs +4 -41
- package/src/cli/install.mjs +1 -1
- package/src/cli/install.test.mjs +1 -3
- package/src/db-schema.test.mjs +2 -3
- package/src/db.mjs +14 -1
- package/src/hook-entrypoints.test.mjs +0 -40
- package/src/resume-context.mjs +31 -48
- package/src/resume-context.test.mjs +18 -13
- package/src/session-start.mjs +77 -21
- package/.claude/commands/tl-trim.md +0 -42
- package/src/cli/save-inflight.mjs +0 -81
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Throughline `/clear` 自動引継ぎ計画 (TODO 兼)
|
|
2
|
+
|
|
3
|
+
`/clear` をトリガーにした **自動かつ軽量な引継ぎ** を実現する計画。
|
|
4
|
+
2026-05-08 セッションの議論と実機検証、外部仕様調査に基づく。
|
|
5
|
+
A 案 (= /clear で自動引継ぎ + /tl は逃げ道として残す + /tl-trim 廃止) **採択確定**。
|
|
6
|
+
|
|
7
|
+
> 過去の経緯 (なぜ `/tl` バトンを採用したか) は [INHERITANCE_ON_CLEAR_ONLY.md](INHERITANCE_ON_CLEAR_ONLY.md) を参照。
|
|
8
|
+
> 本書は **2026-05-08 時点の現状検証 + 新理想設計** を扱う。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. 確定した事実 (実機検証済み)
|
|
13
|
+
|
|
14
|
+
### 1.1 `/clear` 後の SessionStart `source` は 2.1.128 で reliable
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
inheritance-decision.log (2026-05-08 12:26 検証)
|
|
18
|
+
12:26:08.481Z source="startup" session=05735717 ← 新 chat 開始
|
|
19
|
+
12:26:52.257Z source="clear" session=b2addc4a ← /clear 後
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Claude Code `2.1.128` (VSCode native extension, Linux/WSL2) で `/clear` 後の SessionStart は **確実に `source='clear'`** を payload に乗せる
|
|
23
|
+
- 過去の [GitHub issue #49937](https://github.com/anthropics/claude-code/issues/49937) (= VSCode 拡張で /clear 後も `source='startup'` になっていたバグ) は **解決済み**
|
|
24
|
+
- v2.1.105 (VSCode `/clear` not clearing conversation context fix) と v2.1.126 (Windows SessionStart hook env files apply) のリリースで段階的に修正された
|
|
25
|
+
|
|
26
|
+
### 1.2 SessionStart `source` の 4 値 (公式 docs)
|
|
27
|
+
|
|
28
|
+
| 値 | 意味 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `startup` | 新 chat / VSCode 再起動 / 別 project / cold start |
|
|
31
|
+
| `resume` | `--resume` / `--continue` / `/resume` |
|
|
32
|
+
| `clear` | `/clear` 直後 |
|
|
33
|
+
| `compact` | 自動 / 手動 compaction 直後 |
|
|
34
|
+
|
|
35
|
+
source: [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks)
|
|
36
|
+
|
|
37
|
+
### 1.3 `/clear` 等価の user-defined slash command は **作れない**
|
|
38
|
+
|
|
39
|
+
調査結果 (claude-code-guide Agent + bundled `claude-code` 検査):
|
|
40
|
+
|
|
41
|
+
- `.claude/commands/*.md` (skill markdown) は **prompt 拡張**であり、built-in `/clear` を programmatic に invoke できない
|
|
42
|
+
- "subcommand" や "exec built-in" 構文は無い
|
|
43
|
+
- `/clear` には built-in alias が存在しない
|
|
44
|
+
- CLI flag `claude clear` も無い
|
|
45
|
+
- cross-platform で「新ウィンドウ起動」する built-in も無い (`/branch` は full history copy で軽量化と矛盾、VSCode URL scheme は VSCode 専用)
|
|
46
|
+
|
|
47
|
+
→ **「引継ぎたい /clear」と「reset したい /clear」を `source` 値で区別する手段は無い**
|
|
48
|
+
|
|
49
|
+
### 1.4 `/rewind` は fork 動作 (caveat 記録済み)
|
|
50
|
+
|
|
51
|
+
- `/rewind` Continue 確認画面に "A new forked conversation will be created after rewinding" と明示
|
|
52
|
+
- 同 thread 内 rollback ではなく、新 fork session id を生成
|
|
53
|
+
- 詳細は caveat `claude-code/claude-code-rewind-fork-conversation-rollback-primitive` を参照
|
|
54
|
+
- 本計画では `/rewind` は **対象外**
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 2. 採用する理想設計
|
|
59
|
+
|
|
60
|
+
### 2.1 引継ぎ発火条件 (2 経路)
|
|
61
|
+
|
|
62
|
+
| 経路 | 条件 | 起動 |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **auto path** | `source='clear'` かつ env `THROUGHLINE_DISABLE_AUTO_HANDOFF` の値が `'1'` でない | 自動引継ぎ |
|
|
65
|
+
| **baton path** | `handoff_batons` テーブルに TTL (1 時間) 内 baton あり (= ユーザーが `/tl` を打った) | `source` 値関係なく引継ぎ |
|
|
66
|
+
|
|
67
|
+
判定ロジック (擬似コード):
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
on SessionStart(source, session_id, project_path):
|
|
71
|
+
baton = consumeBaton(project_path) // atomic SELECT + DELETE, TTL 超過は sessionId=null で返る
|
|
72
|
+
if baton.sessionId:
|
|
73
|
+
inject(curated_memory) // baton path
|
|
74
|
+
return
|
|
75
|
+
if source == 'clear' and env.THROUGHLINE_DISABLE_AUTO_HANDOFF != '1':
|
|
76
|
+
inject(curated_memory) // auto path
|
|
77
|
+
return
|
|
78
|
+
// 何もしない
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`consumeBaton` が先発なので「両方同時成立」は構造上発生しない (= baton ありなら baton 経路、無ければ source 判定)。
|
|
82
|
+
|
|
83
|
+
### 2.2 注入内容: L1 + L2 + L3 refs のみ (baton/auto どちらの経路でも同一)
|
|
84
|
+
|
|
85
|
+
含める:
|
|
86
|
+
- ヘッダ + Reading Contract framing (= Codex 側 `renderCodexRolloutMemoryPreview` の写像)
|
|
87
|
+
- **L1 summaries** (古い turn の一行要約)
|
|
88
|
+
- **L2 bodies** (直近 20 turn の verbatim)
|
|
89
|
+
- **L3 references** (= `throughline detail <時刻>` の取り出しコマンド一覧、Codex 風の `- ${kind}: ${detailCommand}` フォーマット)
|
|
90
|
+
- Continuation Instruction (= 「これは過去ログではなく現在進行中の作業」と明示)
|
|
91
|
+
|
|
92
|
+
含めない (= 削除):
|
|
93
|
+
- 中断直前の in-flight memo (memo セクション)
|
|
94
|
+
- 中断直前の thinking (extended thinking セクション)
|
|
95
|
+
- 既存の Claude 向け footer の冗長な使い方説明
|
|
96
|
+
|
|
97
|
+
理由: L2 全文があれば最後の assistant turn 自体に「次に何をしようとしていたか」が含まれている。memo / thinking は redundant。
|
|
98
|
+
|
|
99
|
+
### 2.3 `/tl` の役割: **残すが簡素化**
|
|
100
|
+
|
|
101
|
+
- `/tl` slash command 自体は **維持** (= 明示意思マーカー = baton path のトリガー)
|
|
102
|
+
- 簡素化:
|
|
103
|
+
- memo 4 項目入力要求を **削除** ([.claude/commands/tl.md](../.claude/commands/tl.md) を「baton 立てるだけ」の最小実装に)
|
|
104
|
+
- `save-inflight` CLI を **削除** (memo を baton.memo_text に保存する役目だった)
|
|
105
|
+
- `handoff_batons.memo_text` 列を **drop** (schema v8 migration)
|
|
106
|
+
- `src/baton.mjs` の `updateBatonMemo` 関数を **削除** (memo_text 列が drop されるため)
|
|
107
|
+
- `prompt-submit.mjs` の baton 書き込み path は **維持** (`UserPromptSubmit` hook で `/tl` 検出 + writeBaton)
|
|
108
|
+
|
|
109
|
+
ユーザーから見た `/tl` の使い方:
|
|
110
|
+
- 自動引継ぎ ON (デフォルト): `/tl` を打たなくても `/clear` で自動引継ぎ。打っても挙動は同じ
|
|
111
|
+
- 自動引継ぎ OFF (env で disable): `/tl` を打ってから新セッションスタートすれば baton 経由で引継ぎ
|
|
112
|
+
|
|
113
|
+
### 2.4 `/tl-trim` 廃止 (Codex 側を壊さない)
|
|
114
|
+
|
|
115
|
+
- 元機能: memo 入力 + dry-run preview 表示
|
|
116
|
+
- 新仕様で memo 廃止 + 軽量化方針 → 役割なし
|
|
117
|
+
- 削除対象:
|
|
118
|
+
- [.claude/commands/tl-trim.md](../.claude/commands/tl-trim.md) (slash command)
|
|
119
|
+
- [src/cli/trim.mjs](../src/cli/trim.mjs) の **Claude path 部分のみ** 削除 (`describeTrimHost('claude')` ブランチ、Claude 用 memory preview 経路など)
|
|
120
|
+
- 関連 test
|
|
121
|
+
- **維持** (Codex 側を壊さないため):
|
|
122
|
+
- [src/cli/trim.mjs](../src/cli/trim.mjs) の Codex path (`--host codex`, `--codex-thread-id`, `--preflight`, `--execute`, etc.) はすべて維持
|
|
123
|
+
- [src/trim-model.mjs](../src/trim-model.mjs) の `describeTrimHost('codex')` / `buildTrimPlan` の Codex 関連
|
|
124
|
+
- [src/codex-app-server.mjs](../src/codex-app-server.mjs), [src/codex-rollout-memory.mjs](../src/codex-rollout-memory.mjs), `codex-*` CLI 全般
|
|
125
|
+
- `bin/throughline.mjs` の `trim` dispatch (Codex 経路で必要)
|
|
126
|
+
- Codex skill ([codex/skills/throughline](../codex/skills/throughline)) の trim 機能 (= 機能自体は無変更、SKILL.md 内の `/tl-trim` 言及があれば 4 TODO で update)
|
|
127
|
+
|
|
128
|
+
### 2.5 `THROUGHLINE_DISABLE_AUTO_HANDOFF` env var
|
|
129
|
+
|
|
130
|
+
- 値が `'1'` のとき auto path を skip
|
|
131
|
+
- それ以外の値、または未設定 → auto path 有効 (= デフォルト ON)
|
|
132
|
+
- 判定箇所: [src/session-start.mjs](../src/session-start.mjs)
|
|
133
|
+
- 設定方法: ユーザーが `.bashrc` / `.zshrc` / VSCode terminal env / `~/.claude/settings.json` の `env` セクションで設定
|
|
134
|
+
|
|
135
|
+
### 2.6 トレードオフ (受容する)
|
|
136
|
+
|
|
137
|
+
- ⚠️ 「reset したい /clear」も auto path で引継ぎ発火する → 受容 (= 不要なら env で OFF)
|
|
138
|
+
- ⚠️ baton TTL は 1 時間 (= 既存仕様維持)
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 3. 確定した内部判断
|
|
143
|
+
|
|
144
|
+
### 3.1 `/rewind` source 検証 — 不要
|
|
145
|
+
|
|
146
|
+
A 採択により本計画は `/rewind` を扱わない。`/rewind` 後の `source` 値は将来要件で再検討。
|
|
147
|
+
|
|
148
|
+
### 3.2 `handoff_batons` テーブル — 残す + memo_text 列だけ drop
|
|
149
|
+
|
|
150
|
+
- table 自体は baton path で必要なので **維持**
|
|
151
|
+
- `memo_text TEXT` 列を schema v8 migration で drop
|
|
152
|
+
- 既存の memo データは廃棄 (受容)
|
|
153
|
+
- **SQLite DROP COLUMN 互換確認必須**: `ALTER TABLE ... DROP COLUMN` は SQLite 3.35.0+ で利用可。Node.js v22.5+ 同梱の SQLite バージョンで動作するか実装時に検証
|
|
154
|
+
|
|
155
|
+
### 3.3 旧 `/tl` ユーザー移行 — `/tl` 自体は continue、memo 関連だけ breaking
|
|
156
|
+
|
|
157
|
+
- `/tl` slash command 自体は使い続けられる (簡素化されただけ)
|
|
158
|
+
- 廃止される機能: memo 4 項目入力、`save-inflight` CLI、`/tl-trim`
|
|
159
|
+
- breaking change として CHANGELOG に明示
|
|
160
|
+
|
|
161
|
+
### 3.4 `source='compact'` 扱い — 引継ぎしない
|
|
162
|
+
|
|
163
|
+
auto-compaction は Claude Code 内部の context 圧縮で、conversation 連続性は host 側が担保している。Throughline 側で別途引継ぎを発火する必要なし。
|
|
164
|
+
|
|
165
|
+
### 3.5 ログファイルの扱い
|
|
166
|
+
|
|
167
|
+
- `~/.throughline/logs/inflight-memo.log`: `save-inflight` CLI 削除で **新規書き込みなし**。既存ファイルは削除提案を README / CHANGELOG に書く (= 自動削除はしない、ユーザー手動)
|
|
168
|
+
- `~/.throughline/logs/inheritance-decision.log` 内の `baton_has_memo` フィールド: memo 廃止で意味を失うため、`logDecision()` から **削除**
|
|
169
|
+
- `~/.throughline/logs/baton-write.log`: 維持 (= `/tl` baton 書き込みログとして引き続き有用)
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 4. 実装 TODO (実装開始可)
|
|
174
|
+
|
|
175
|
+
優先度順:
|
|
176
|
+
|
|
177
|
+
- [ ] **schema v8 migration** ([src/db.mjs](../src/db.mjs)): `ALTER TABLE handoff_batons DROP COLUMN memo_text`
|
|
178
|
+
- SQLite 3.35.0+ サポート確認 (Node.js v22.5+ 同梱版)
|
|
179
|
+
- 動かない場合は `CREATE TABLE` + `INSERT SELECT` + `DROP` の rebuild migration に切り替え
|
|
180
|
+
- [ ] **`src/baton.mjs`**:
|
|
181
|
+
- `consumeBaton` 戻り値から `memoText` プロパティを削除
|
|
182
|
+
- `updateBatonMemo` 関数を **削除**
|
|
183
|
+
- `BATON_TTL_MS`, `writeBaton`, `consumeBaton` は維持
|
|
184
|
+
- [x] **`src/handoff-record.mjs`**: **維持** (Codex 側 codex-handoff.mjs / codex-resume / codex-handoff-smoke 等が `memory.inflightMemo` / `memory.latestThinking` を参照しているため、削除すると Codex を壊す)。Claude 側で「使わない」のは resume-context.mjs 側で実現済み
|
|
185
|
+
- [ ] **`src/resume-context.mjs`**: 注入テキストを新仕様に書き換え:
|
|
186
|
+
- memo セクション削除
|
|
187
|
+
- thinking セクション削除
|
|
188
|
+
- L3 references 一覧追加 (Codex `renderCodexRolloutMemoryPreview` 形式)
|
|
189
|
+
- footer 簡素化 (Continuation Instruction だけ残す)
|
|
190
|
+
- [ ] **`src/session-start.mjs`** を 2.1 のロジックに改修:
|
|
191
|
+
- `consumeBaton` 先発 → `baton.sessionId` あれば inject
|
|
192
|
+
- 無ければ `source==='clear'` かつ env が `'1'` でない場合に inject
|
|
193
|
+
- それ以外は何もしない
|
|
194
|
+
- `logDecision()` から `baton_has_memo` フィールド削除
|
|
195
|
+
- [ ] **`src/cli/save-inflight.mjs`** 削除
|
|
196
|
+
- [ ] **`bin/throughline.mjs`** の `save-inflight` dispatch 削除 (`trim` dispatch は **維持**)
|
|
197
|
+
- [ ] **`src/prompt-submit.mjs`**: 維持 (baton 書き込み + ensureMonitorTaskFile)
|
|
198
|
+
- [ ] **[.claude/commands/tl.md](../.claude/commands/tl.md)**: memo 4 項目入力要求を削除、純粋に「baton 立てるだけ」の最小実装に書き換え
|
|
199
|
+
- [x] **`/tl-trim` 関連削除**:
|
|
200
|
+
- [.claude/commands/tl-trim.md](../.claude/commands/tl-trim.md) ファイル削除
|
|
201
|
+
- **`src/cli/trim.mjs` 自体は維持**: Codex 経路 (`--host codex`, `--preflight`, `--execute`, `--codex-app-server-bin` 等) と doctor `--trim --host claude` で使う `describeTrimHost('claude')` の dry-run 表示が依存しているため、コード削除はしない (= ユーザーが直接 `throughline trim --host claude --dry-run` を打つ余地は残す。実用は SessionStart 自動経路に置き換わる)
|
|
202
|
+
- [ ] **[src/cli/install.mjs](../src/cli/install.mjs)**: Throughline 管理 slash commands の copy 対象リストから `tl-trim.md` を除外。`tl.md` は維持。`src/cli/install.test.mjs` の関連 test も update
|
|
203
|
+
- [ ] **[bin/throughline.mjs](../bin/throughline.mjs) の `showHelp()` 文言 update**:
|
|
204
|
+
- `save-inflight` 関連 help 文言を削除
|
|
205
|
+
- `/tl-trim` / Claude trim 関連の help 文言を削除
|
|
206
|
+
- Codex trim 関連 (`trim --dry-run`, `--preflight`, `--execute --host codex` など) は **維持**
|
|
207
|
+
- `bin/throughline.mjs` の `save-inflight` dispatch case 削除 (上の TODO と重複するが help text だけ別作業)
|
|
208
|
+
- [ ] **[codex/skills/throughline/SKILL.md](../codex/skills/throughline/SKILL.md)**: `/tl-trim` への言及があれば削除し、`throughline trim --execute --host codex` 直接呼び出しに統一。Codex 側 trim 案内自体は維持
|
|
209
|
+
- [ ] **[.codex-sidecar.yml](../.codex-sidecar.yml)** 確認: `/tl-trim` / `save-inflight` 経路の参照があれば削除。無ければ no-op
|
|
210
|
+
- [ ] **テスト全部更新**:
|
|
211
|
+
- `src/baton.test.mjs` → memo 関連 test 削除、`updateBatonMemo` test 削除
|
|
212
|
+
- `src/session-merger.test.mjs` → source='clear' 自動経路の test 追加
|
|
213
|
+
- `src/resume-context.test.mjs` → memo/thinking 削除を反映
|
|
214
|
+
- `src/handoff-record.test.mjs` → projection 簡素化
|
|
215
|
+
- `src/hook-entrypoints.test.mjs` → save-inflight subprocess test ケース削除 (= 独立ファイルではなく本ファイル内の test)
|
|
216
|
+
- `src/turn-processor.test.mjs` → 既存維持
|
|
217
|
+
- `src/trim-cli.test.mjs` / `src/trim-model.test.mjs` → Claude path 関連テストのみ削除、Codex 経路テストは維持
|
|
218
|
+
- 新規: env var 判定 test、source='clear' auto path test
|
|
219
|
+
- [ ] **docs 更新**:
|
|
220
|
+
- [CLAUDE.md](../CLAUDE.md): 設計の核を書き換え (「`/tl` バトンのみ」→「`source='clear'` 自動 + `/tl` 逃げ道」)
|
|
221
|
+
- [README.md](../README.md): 以下範囲を update:
|
|
222
|
+
- Quick Start: 「`/clear` で自動引継ぎ」中心に書き直し
|
|
223
|
+
- 「Explicit handoff via `/tl`」セクション: `save-inflight` / memo 4 項目要求の記述を削除、`/tl` は逃げ道として簡素な記述に
|
|
224
|
+
- Troubleshoot / How it compares 等の他セクション: `/tl-trim`, `save-inflight`, `inflight-memo.log` への言及をすべて削除
|
|
225
|
+
- `THROUGHLINE_DISABLE_AUTO_HANDOFF` env var 紹介を新規追加
|
|
226
|
+
- 既存 `inflight-memo.log` ファイルは新版で書き込み停止することを README で告知 (= 手動削除提案)
|
|
227
|
+
- [CHANGELOG.md](../CHANGELOG.md): breaking change を明示 (memo 廃止、save-inflight 削除、/tl-trim 削除、`updateBatonMemo` 削除、baton_has_memo フィールド削除)
|
|
228
|
+
- [INHERITANCE_ON_CLEAR_ONLY.md](INHERITANCE_ON_CLEAR_ONLY.md): 「2026-04 段階の検証 → 2026-05 でバグ修正により案 A 成立、本書は履歴扱い」note 追加
|
|
229
|
+
- [PUBLIC_RELEASE_PLAN.md](PUBLIC_RELEASE_PLAN.md): version bump + breaking change 反映
|
|
230
|
+
- [ ] **package.json**: **0.4.0** に bump (semver minor、pre-1.0 の breaking)
|
|
231
|
+
- [ ] **caveat 記録**: 「`/clear` SessionStart `source` は 2.1.128 で reliable、過去 #49937 は fix 済み」を public で記録
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 5. 削減できるコード規模 (見積もり)
|
|
236
|
+
|
|
237
|
+
廃止対象:
|
|
238
|
+
- `src/cli/save-inflight.mjs` (~80 行) → 削除
|
|
239
|
+
- `src/cli/trim.mjs` の Claude path 部分 (~30 行) → 削除 (Codex path は維持)
|
|
240
|
+
- [.claude/commands/tl-trim.md](../.claude/commands/tl-trim.md) (~40 行) → 削除
|
|
241
|
+
- `src/baton.mjs` の `updateBatonMemo` 関数 (~10 行) → 削除
|
|
242
|
+
- `handoff_batons.memo_text` 列 (schema migration、コードへの影響は consumeBaton 戻り値変更のみ)
|
|
243
|
+
- `src/hook-entrypoints.test.mjs` 内 save-inflight test ケース (~30 行) → 削除
|
|
244
|
+
- `resume-context.mjs` の memo/thinking セクション (~30 行) → 削除
|
|
245
|
+
- `handoff-record.mjs` の memo/thinking projection (~50 行) → 削除
|
|
246
|
+
- [.claude/commands/tl.md](../.claude/commands/tl.md) の memo 4 項目要求 (~20 行) → 削除
|
|
247
|
+
|
|
248
|
+
代わりに追加:
|
|
249
|
+
- `session-start.mjs` の env / source 判定 (~15 行)
|
|
250
|
+
- `resume-context.mjs` の L3 refs framing (~30 行)
|
|
251
|
+
|
|
252
|
+
純減 ~245 行。
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 6. Codex 側との整合 (壊さない)
|
|
257
|
+
|
|
258
|
+
Codex 側 v0.3.25 の以下は本計画で **完全に無変更**:
|
|
259
|
+
|
|
260
|
+
- `codex-capture` / `codex-summarize` / `codex-resume` (Codex primary L1/L2/L3 path)
|
|
261
|
+
- `codex-resume --format handoff` (新規 Codex thread 用 prompt)
|
|
262
|
+
- `trim --execute --host codex` / `--preflight --host codex` (app-server `thread/rollback` + `thread/inject_items`)
|
|
263
|
+
- Codex Stop hook 90% auto-refresh
|
|
264
|
+
- restore-safety / host primitive audit diagnostics
|
|
265
|
+
- Codex skill ([codex/skills/throughline](../codex/skills/throughline)) の trim 機能 (= 機能自体は無変更、SKILL.md 内の `/tl-trim` 言及があれば 4 TODO で update)
|
|
266
|
+
- [src/codex-app-server.mjs](../src/codex-app-server.mjs), [src/codex-rollout-memory.mjs](../src/codex-rollout-memory.mjs)
|
|
267
|
+
- [src/trim-model.mjs](../src/trim-model.mjs) の Codex 関連 (`describeTrimHost('codex')`, `buildTrimPlan` の Codex source path)
|
|
268
|
+
|
|
269
|
+
`codex-resume --memo-stdin` は引き続きユーザーが stdin で memo を流す経路。Throughline DB の baton.memo_text には依存していない (= 列削除の影響なし)。
|
|
270
|
+
|
|
271
|
+
`/tl-trim` 削除に伴って Codex 経由でも slash command としての `/tl-trim` は使えなくなる。Codex 用 trim は `throughline trim --execute --host codex` を **CLI 直接呼ぶ**運用に統一 (Codex skill SKILL.md がそれを案内する)。
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 7. 進め方
|
|
276
|
+
|
|
277
|
+
1. **本計画 確定** (ユーザー A 採択済み、本書 update により方針固定)
|
|
278
|
+
2. **実装** (上記 TODO 順)
|
|
279
|
+
3. **テスト + 実機 smoke + commit**
|
|
280
|
+
4. **publish** (npm 0.4.0 として release)
|
|
281
|
+
|
|
282
|
+
実機 smoke 手順:
|
|
283
|
+
- 自動引継ぎ ON (デフォルト): /clear → 新セッションで curated memory 注入を確認
|
|
284
|
+
- 自動引継ぎ OFF: env を立てて /clear → 注入されないことを確認
|
|
285
|
+
- baton path: `/tl` を打って新 chat タブで開く → baton 経由で注入を確認
|
|
286
|
+
- Codex 側 regression: `npm test` で既存 Codex test がすべて pass することを確認、`throughline trim --execute --host codex` の CLI 動作も維持
|
package/package.json
CHANGED
package/src/baton.mjs
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* baton.mjs — 引き継ぎバトン管理
|
|
3
3
|
*
|
|
4
|
-
* バトン方式の設計 (docs/
|
|
5
|
-
* -
|
|
4
|
+
* バトン方式の設計 (docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md):
|
|
5
|
+
* - 新仕様では `/clear` 自動引継ぎがデフォルト ON。バトンは「/clear 自動引継ぎを
|
|
6
|
+
* 使わずに明示的に引き継ぎたい」ユーザーのための逃げ道。
|
|
7
|
+
* - ユーザーが旧セッションで `/tl` スラッシュコマンドを打つ → UserPromptSubmit hook が
|
|
6
8
|
* baton テーブルに (project_path, session_id, created_at) を INSERT OR REPLACE
|
|
7
|
-
* - 新セッションの SessionStart hook が baton
|
|
8
|
-
* TTL 1
|
|
9
|
-
*
|
|
10
|
-
* - 消費は atomic (BEGIN IMMEDIATE トランザクション内で SELECT + DELETE)
|
|
9
|
+
* - 新セッションの SessionStart hook が baton を atomic に消費 (BEGIN IMMEDIATE 内で
|
|
10
|
+
* SELECT + DELETE)。TTL 1 時間以内なら前任として merge、超過は破棄。
|
|
11
|
+
* - 注入する curated memory は L1 + L2 + L3 refs のみ (memo / thinking なし)。
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* 履歴: もともと VSCode 拡張で SessionStart payload の source が /clear 後も
|
|
14
|
+
* "startup" に潰される問題 (#49937) に対する明示意思マーカーとして導入。
|
|
15
|
+
* 2026-05-08 時点で Claude Code 2.1.128 で source='clear' は reliable に
|
|
16
|
+
* なったため auto path 中心の設計に変わったが、明示意思の signal として
|
|
17
|
+
* baton 仕組み自体は残す。詳細は docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md。
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -24,9 +25,7 @@ export const BATON_TTL_MS = 60 * 60 * 1000; // 1 時間
|
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* 現在セッション (= /tl を発動したセッション) を次回 SessionStart で merge 対象に指名する。
|
|
27
|
-
* 同 project_path の既存バトンがあれば session_id / created_at
|
|
28
|
-
* v7 で追加された memo_text は保持する(連続した /tl → save-inflight の順番で
|
|
29
|
-
* 呼ばれた場合に、再度 /tl を打った時点で古い memo が消えないようにする)。
|
|
28
|
+
* 同 project_path の既存バトンがあれば session_id / created_at を上書き。
|
|
30
29
|
*
|
|
31
30
|
* @param {import('node:sqlite').DatabaseSync} db
|
|
32
31
|
* @param {{ projectPath: string, sessionId: string, now?: number }} params
|
|
@@ -41,43 +40,17 @@ export function writeBaton(db, { projectPath, sessionId, now = Date.now() }) {
|
|
|
41
40
|
).run(projectPath, sessionId, now);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
/**
|
|
45
|
-
* 既存バトンの memo_text を更新する。バトンが存在しない場合は NOOP。
|
|
46
|
-
* /tl 発動後、現行セッションの Claude が `throughline save-inflight` CLI 経由で
|
|
47
|
-
* 呼び出す。memo_text は Markdown 形式の「次の一手 / 現在の方針 / 未解決 /
|
|
48
|
-
* 進行中 TODO」をまとめたテキスト。
|
|
49
|
-
*
|
|
50
|
-
* Windows 互換: ドライブレター(`C:` / `c:`)やパス区切りの差異で
|
|
51
|
-
* /tl 書き込み時と save-inflight 呼び出し時の project_path が一致しない
|
|
52
|
-
* ケースがあるため、SQLite の COLLATE NOCASE で大小無視で照合する。
|
|
53
|
-
*
|
|
54
|
-
* @param {import('node:sqlite').DatabaseSync} db
|
|
55
|
-
* @param {{ projectPath: string, memoText: string, now?: number }} params
|
|
56
|
-
* @returns {{ updated: boolean }}
|
|
57
|
-
*/
|
|
58
|
-
export function updateBatonMemo(db, { projectPath, memoText }) {
|
|
59
|
-
const result = db
|
|
60
|
-
.prepare(
|
|
61
|
-
`UPDATE handoff_batons SET memo_text = ? WHERE project_path = ? COLLATE NOCASE`,
|
|
62
|
-
)
|
|
63
|
-
.run(memoText, projectPath);
|
|
64
|
-
return { updated: (result.changes ?? 0) > 0 };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
43
|
/**
|
|
68
44
|
* 同 project_path のバトンを読み出して削除する (atomic)。
|
|
69
45
|
*
|
|
70
46
|
* 戻り値:
|
|
71
|
-
* - { sessionId, ageMs
|
|
72
|
-
* - { sessionId: null, skipReason: 'expired', ageMs }
|
|
47
|
+
* - { sessionId, ageMs } : バトン存在 かつ TTL 以内
|
|
48
|
+
* - { sessionId: null, skipReason: 'expired', ageMs } : TTL 超過で破棄
|
|
73
49
|
* - { sessionId: null, skipReason: 'missing' } : バトン無し
|
|
74
50
|
*
|
|
75
|
-
* memoText は /tl 後に save-inflight で書き込まれた in-flight メモ。
|
|
76
|
-
* 未保存なら null。
|
|
77
|
-
*
|
|
78
51
|
* @param {import('node:sqlite').DatabaseSync} db
|
|
79
52
|
* @param {{ projectPath: string, now?: number, ttlMs?: number }} params
|
|
80
|
-
* @returns {{ sessionId: string | null, ageMs?: number,
|
|
53
|
+
* @returns {{ sessionId: string | null, ageMs?: number, skipReason?: 'expired' | 'missing' }}
|
|
81
54
|
*/
|
|
82
55
|
export function consumeBaton(db, { projectPath, now = Date.now(), ttlMs = BATON_TTL_MS }) {
|
|
83
56
|
db.exec('BEGIN IMMEDIATE');
|
|
@@ -85,7 +58,7 @@ export function consumeBaton(db, { projectPath, now = Date.now(), ttlMs = BATON_
|
|
|
85
58
|
// Windows 互換: ドライブレターの大小差を吸収するため COLLATE NOCASE
|
|
86
59
|
const row = db
|
|
87
60
|
.prepare(
|
|
88
|
-
`SELECT session_id, created_at
|
|
61
|
+
`SELECT session_id, created_at FROM handoff_batons WHERE project_path = ? COLLATE NOCASE`,
|
|
89
62
|
)
|
|
90
63
|
.get(projectPath);
|
|
91
64
|
|
|
@@ -108,7 +81,6 @@ export function consumeBaton(db, { projectPath, now = Date.now(), ttlMs = BATON_
|
|
|
108
81
|
return {
|
|
109
82
|
sessionId: row.session_id,
|
|
110
83
|
ageMs,
|
|
111
|
-
memoText: row.memo_text ?? null,
|
|
112
84
|
};
|
|
113
85
|
} catch (err) {
|
|
114
86
|
try {
|
package/src/baton.test.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
-
import { writeBaton, consumeBaton,
|
|
4
|
+
import { writeBaton, consumeBaton, BATON_TTL_MS } from './baton.mjs';
|
|
5
5
|
|
|
6
6
|
function makeDb() {
|
|
7
7
|
const db = new DatabaseSync(':memory:');
|
|
@@ -9,8 +9,7 @@ function makeDb() {
|
|
|
9
9
|
CREATE TABLE handoff_batons (
|
|
10
10
|
project_path TEXT PRIMARY KEY,
|
|
11
11
|
session_id TEXT NOT NULL,
|
|
12
|
-
created_at INTEGER NOT NULL
|
|
13
|
-
memo_text TEXT
|
|
12
|
+
created_at INTEGER NOT NULL
|
|
14
13
|
);
|
|
15
14
|
`);
|
|
16
15
|
return db;
|
|
@@ -99,46 +98,10 @@ test('consumeBaton: scopes per project_path (does not cross-consume)', () => {
|
|
|
99
98
|
assert.equal(rows.length, 1);
|
|
100
99
|
});
|
|
101
100
|
|
|
102
|
-
test('
|
|
103
|
-
const db = makeDb();
|
|
104
|
-
writeBaton(db, { projectPath: '/proj', sessionId: 'S1', now: 1000 });
|
|
105
|
-
const result = updateBatonMemo(db, { projectPath: '/proj', memoText: '次の一手: X' });
|
|
106
|
-
assert.equal(result.updated, true);
|
|
107
|
-
const row = db.prepare('SELECT memo_text FROM handoff_batons').get();
|
|
108
|
-
assert.equal(row.memo_text, '次の一手: X');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('updateBatonMemo: is a NOOP when no baton exists (no throw)', () => {
|
|
112
|
-
const db = makeDb();
|
|
113
|
-
const result = updateBatonMemo(db, { projectPath: '/missing', memoText: 'hello' });
|
|
114
|
-
assert.equal(result.updated, false);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('writeBaton: preserves memo_text when same project_path is re-batoned', () => {
|
|
118
|
-
const db = makeDb();
|
|
119
|
-
writeBaton(db, { projectPath: '/proj', sessionId: 'S1', now: 1000 });
|
|
120
|
-
updateBatonMemo(db, { projectPath: '/proj', memoText: 'preserved memo' });
|
|
121
|
-
// 同じ project_path に再度 /tl を打っても memo は残る
|
|
122
|
-
writeBaton(db, { projectPath: '/proj', sessionId: 'S2', now: 2000 });
|
|
123
|
-
const row = db.prepare('SELECT * FROM handoff_batons').get();
|
|
124
|
-
assert.equal(row.session_id, 'S2');
|
|
125
|
-
assert.equal(row.created_at, 2000);
|
|
126
|
-
assert.equal(row.memo_text, 'preserved memo');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('consumeBaton: returns memoText when set', () => {
|
|
130
|
-
const db = makeDb();
|
|
131
|
-
writeBaton(db, { projectPath: '/proj', sessionId: 'S1', now: 1000 });
|
|
132
|
-
updateBatonMemo(db, { projectPath: '/proj', memoText: '中断メモ' });
|
|
133
|
-
const result = consumeBaton(db, { projectPath: '/proj', now: 1000 + 1000 });
|
|
134
|
-
assert.equal(result.sessionId, 'S1');
|
|
135
|
-
assert.equal(result.memoText, '中断メモ');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('consumeBaton: returns memoText=null when not set', () => {
|
|
101
|
+
test('consumeBaton: returns no memoText property in v8 (memo column dropped)', () => {
|
|
139
102
|
const db = makeDb();
|
|
140
103
|
writeBaton(db, { projectPath: '/proj', sessionId: 'S1', now: 1000 });
|
|
141
104
|
const result = consumeBaton(db, { projectPath: '/proj', now: 1000 + 1000 });
|
|
142
105
|
assert.equal(result.sessionId, 'S1');
|
|
143
|
-
assert.equal(result
|
|
106
|
+
assert.equal('memoText' in result, false, 'memoText property should be removed in v8');
|
|
144
107
|
});
|
package/src/cli/install.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { homedir } from 'node:os';
|
|
|
18
18
|
|
|
19
19
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
20
20
|
const SLASH_COMMANDS_SRC = join(PACKAGE_ROOT, '.claude', 'commands');
|
|
21
|
-
const SC_SLASH_COMMAND_FILES = ['tl.md', 'sc-detail.md'
|
|
21
|
+
const SC_SLASH_COMMAND_FILES = ['tl.md', 'sc-detail.md'];
|
|
22
22
|
const CODEX_SKILLS_SRC = join(PACKAGE_ROOT, 'codex', 'skills');
|
|
23
23
|
const CODEX_SKILL_NAMES = ['throughline'];
|
|
24
24
|
const CODEX_HOOKS_RELATIVE_PATH = ['.codex', 'hooks.json'];
|
package/src/cli/install.test.mjs
CHANGED
|
@@ -54,7 +54,7 @@ test('global install copies Throughline slash commands to ~/.claude/commands/',
|
|
|
54
54
|
const trim = join(home.dir, '.claude', 'commands', 'tl-trim.md');
|
|
55
55
|
assert.ok(existsSync(tl), 'tl.md should be installed globally');
|
|
56
56
|
assert.ok(existsSync(sc), 'sc-detail.md should be installed globally');
|
|
57
|
-
assert.ok(existsSync(trim), 'tl-trim.md should be installed
|
|
57
|
+
assert.ok(!existsSync(trim), 'tl-trim.md should NOT be installed (deprecated in v0.4.0)');
|
|
58
58
|
const tlBody = readFileSync(tl, 'utf8');
|
|
59
59
|
assert.match(tlBody, /Throughline/, 'tl.md content should be real');
|
|
60
60
|
const settings = JSON.parse(readFileSync(join(home.dir, '.claude', 'settings.json'), 'utf8'));
|
|
@@ -261,8 +261,6 @@ test('uninstall removes slash command files', async () => {
|
|
|
261
261
|
assert.ok(!existsSync(tl), 'uninstall should remove tl.md');
|
|
262
262
|
const sc = join(home.dir, '.claude', 'commands', 'sc-detail.md');
|
|
263
263
|
assert.ok(!existsSync(sc), 'uninstall should remove sc-detail.md');
|
|
264
|
-
const trim = join(home.dir, '.claude', 'commands', 'tl-trim.md');
|
|
265
|
-
assert.ok(!existsSync(trim), 'uninstall should remove tl-trim.md');
|
|
266
264
|
const codexSkill = join(home.dir, '.codex', 'skills', 'throughline', 'SKILL.md');
|
|
267
265
|
assert.ok(!existsSync(codexSkill), 'uninstall should remove Throughline Codex skill');
|
|
268
266
|
} finally {
|
package/src/db-schema.test.mjs
CHANGED
|
@@ -38,10 +38,10 @@ function indexNames(db) {
|
|
|
38
38
|
.map((row) => row.name);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
test('schema
|
|
41
|
+
test('schema v8 preserves Claude-facing tables, fields, and unique indexes', async () => {
|
|
42
42
|
await withIsolatedDb((db) => {
|
|
43
43
|
const version = db.prepare('PRAGMA user_version').get();
|
|
44
|
-
assert.equal(version.user_version,
|
|
44
|
+
assert.equal(version.user_version, 8);
|
|
45
45
|
|
|
46
46
|
assert.deepEqual(columnNames(db, 'sessions'), [
|
|
47
47
|
'session_id',
|
|
@@ -87,7 +87,6 @@ test('schema v7 preserves Claude-facing tables, fields, and unique indexes', asy
|
|
|
87
87
|
'project_path',
|
|
88
88
|
'session_id',
|
|
89
89
|
'created_at',
|
|
90
|
-
'memo_text',
|
|
91
90
|
]);
|
|
92
91
|
|
|
93
92
|
const indexes = indexNames(db);
|
package/src/db.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { join } from 'path';
|
|
|
9
9
|
|
|
10
10
|
const DB_DIR = join(homedir(), '.throughline');
|
|
11
11
|
const DB_PATH = join(DB_DIR, 'throughline.db');
|
|
12
|
-
const CURRENT_VERSION =
|
|
12
|
+
const CURRENT_VERSION = 8;
|
|
13
13
|
|
|
14
14
|
let _db = null;
|
|
15
15
|
|
|
@@ -202,6 +202,19 @@ function initSchema(db) {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// v7 → v8: handoff_batons から memo_text 列を drop。
|
|
206
|
+
// 新仕様 (docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md) で memo 廃止:
|
|
207
|
+
// - /clear 自動引継ぎ (SessionStart source='clear') + /tl baton (memo なし) の 2 経路に
|
|
208
|
+
// - 注入は L1 + L2 + L3 refs のみ
|
|
209
|
+
// - save-inflight CLI / updateBatonMemo 関数も併せて削除
|
|
210
|
+
// SQLite 3.35.0+ で DROP COLUMN サポート (Node.js v22.5+ 同梱版で利用可)。
|
|
211
|
+
if (version < 8) {
|
|
212
|
+
const batonCols = db.prepare('PRAGMA table_info(handoff_batons)').all();
|
|
213
|
+
if (batonCols.some((c) => c.name === 'memo_text')) {
|
|
214
|
+
db.exec('ALTER TABLE handoff_batons DROP COLUMN memo_text');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
205
218
|
if (version < CURRENT_VERSION) {
|
|
206
219
|
db.exec(`PRAGMA user_version = ${CURRENT_VERSION}`);
|
|
207
220
|
}
|
|
@@ -98,40 +98,6 @@ test('prompt-submit subprocess writes a /tl baton into an isolated DB', () => {
|
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
test('save-inflight subprocess stores memo on the current project baton', () => {
|
|
102
|
-
const home = makeTempHome();
|
|
103
|
-
const project = makeTempProject();
|
|
104
|
-
try {
|
|
105
|
-
const baton = runNode([join(REPO_ROOT, 'src/prompt-submit.mjs')], {
|
|
106
|
-
home,
|
|
107
|
-
cwd: project,
|
|
108
|
-
input: JSON.stringify({
|
|
109
|
-
session_id: 'old-session',
|
|
110
|
-
cwd: project,
|
|
111
|
-
prompt: '/tl',
|
|
112
|
-
}),
|
|
113
|
-
});
|
|
114
|
-
assert.equal(baton.status, 0, baton.stderr);
|
|
115
|
-
|
|
116
|
-
const memo = 'Next: keep the handoff precise';
|
|
117
|
-
const saved = runNode([join(REPO_ROOT, 'bin/throughline.mjs'), 'save-inflight'], {
|
|
118
|
-
home,
|
|
119
|
-
cwd: project,
|
|
120
|
-
input: memo,
|
|
121
|
-
});
|
|
122
|
-
assert.equal(saved.status, 0, saved.stderr);
|
|
123
|
-
assert.match(saved.stdout, /in-flight memo saved/);
|
|
124
|
-
|
|
125
|
-
const db = openDb(home);
|
|
126
|
-
const row = db.prepare('SELECT memo_text FROM handoff_batons').get();
|
|
127
|
-
assert.equal(row.memo_text, memo);
|
|
128
|
-
db.close();
|
|
129
|
-
} finally {
|
|
130
|
-
rmSync(project, { recursive: true, force: true });
|
|
131
|
-
rmSync(home, { recursive: true, force: true });
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
101
|
test('session-start subprocess consumes baton and injects inherited resume context', () => {
|
|
136
102
|
const home = makeTempHome();
|
|
137
103
|
const project = makeTempProject();
|
|
@@ -157,11 +123,6 @@ test('session-start subprocess consumes baton and injects inherited resume conte
|
|
|
157
123
|
(session_id, origin_session_id, turn_number, role, text, token_count, created_at)
|
|
158
124
|
VALUES ('old-session', 'old-session', 1, 'assistant', 'old assistant body', 4, 2)`,
|
|
159
125
|
).run();
|
|
160
|
-
db.prepare(
|
|
161
|
-
`UPDATE handoff_batons
|
|
162
|
-
SET memo_text = 'handoff memo'
|
|
163
|
-
WHERE project_path = ?`,
|
|
164
|
-
).run(project);
|
|
165
126
|
db.close();
|
|
166
127
|
|
|
167
128
|
const started = runNode([join(REPO_ROOT, 'src/session-start.mjs')], {
|
|
@@ -175,7 +136,6 @@ test('session-start subprocess consumes baton and injects inherited resume conte
|
|
|
175
136
|
});
|
|
176
137
|
|
|
177
138
|
assert.equal(started.status, 0, started.stderr);
|
|
178
|
-
assert.match(started.stdout, /handoff memo/);
|
|
179
139
|
assert.match(started.stdout, /old assistant body/);
|
|
180
140
|
|
|
181
141
|
const after = openDb(home);
|