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
package/src/resume-context.mjs
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* resume-context.mjs —
|
|
2
|
+
* resume-context.mjs — 引継ぎ注入テキストを組み立てる共有モジュール
|
|
3
3
|
*
|
|
4
4
|
* 呼び出し元:
|
|
5
|
-
* - session-start.mjs (
|
|
5
|
+
* - session-start.mjs (auto path / baton path どちらでも同じ注入)
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* - 注入順: ヘッダ
|
|
9
|
-
* - in-flight メモ: /tl 発動時に現行 Claude が書いた「次の一手 / 方針 / 未解決 / TODO」
|
|
10
|
-
* - 中断直前の思考: 最終ターンの assistant extended thinking (details kind='thinking')
|
|
7
|
+
* 設計 (docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md):
|
|
8
|
+
* - 注入順: ヘッダ + Reading Contract → L1 要約 → L2 本文 → L3 refs 一覧 → Continuation Instruction
|
|
11
9
|
* - 直近 N=20 ターンは bodies から L2 全文を注入
|
|
12
10
|
* - それ以前は skeletons から L1 要約のみ注入
|
|
11
|
+
* - L3 references は具体的な `throughline detail <時刻>` コマンドの一覧として注入
|
|
12
|
+
* (本文は埋め込まず、参照のみ)
|
|
13
|
+
* - memo / thinking は注入しない (= L2 全文があれば最後の assistant turn 自体に
|
|
14
|
+
* 「次に何をしようとしていたか」が含まれる)
|
|
13
15
|
* - 各行頭に [HH:MM:SS] 時刻プレフィックス(created_at ベース、DB 永続)
|
|
14
|
-
* - 末尾に /sc-detail <時刻> ガイドを追記
|
|
15
16
|
* - 現セッションのターンは注入しない(Claude Code 本体のコンテキストに既にあるため)
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
* L1/L2 を現在タスク用の作業文脈として読むよう誘導する。
|
|
17
|
+
* - フレーミング: 「過去の記憶」ではなく「現在進行中の作業」として読ませる
|
|
18
|
+
* (Codex 側 renderCodexRolloutMemoryPreview の写像)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { buildHandoffRecord, N_RECENT_L2 } from './handoff-record.mjs';
|
|
21
|
+
import { buildHandoffRecord, formatTime, N_RECENT_L2 } from './handoff-record.mjs';
|
|
22
22
|
|
|
23
23
|
const RESUME_HEADER_TEMPLATE = (turnCount) =>
|
|
24
24
|
`## Throughline: 中断した作業の再開(${turnCount} ターン分の文脈を保持)\n` +
|
|
25
25
|
`\n` +
|
|
26
26
|
`**前セッションで進行中だった作業を、この新セッションで引き継いでいます。以下が中断時点の状態です:**\n` +
|
|
27
|
-
`- 中断直前の in-flight メモ(前セッション末尾で Claude 自身が書いた「次の一手・方針・未解決・TODO」)\n` +
|
|
28
|
-
`- 中断直前の思考 (最終ターンの extended thinking)\n` +
|
|
29
27
|
`- 直近 ${N_RECENT_L2} ターンの会話本文 (L2)\n` +
|
|
30
28
|
`- それ以前の要約 (L1)\n` +
|
|
29
|
+
`- L3 (ツール入出力・思考) の参照一覧 (本文は別途取り出す)\n` +
|
|
31
30
|
`\n` +
|
|
32
31
|
`応答の冒頭でユーザーに「前の作業を ${turnCount} ターン分引き継ぎました」と報告してください。` +
|
|
33
32
|
`作業方針は前セッションのものを踏襲し、中断地点から自然に続行してください。`;
|
|
@@ -37,48 +36,38 @@ const ACTIVE_WORK_READING_CONTRACT =
|
|
|
37
36
|
`**読み方の契約:**\n` +
|
|
38
37
|
`- これは単なる過去ログではなく、現在進行中の作業を再開するための active work context です。\n` +
|
|
39
38
|
`- L2 は古い順に並んだ作業履歴です。後の発言・判断・TODO は前の仮説や作業方針を上書きし得ます。\n` +
|
|
40
|
-
`- すべての L2 行を現在も正しい事実として扱わず、最新の L2
|
|
41
|
-
`- 不足している tool output /
|
|
39
|
+
`- すべての L2 行を現在も正しい事実として扱わず、最新の L2 を優先して現在状態を推定してください。\n` +
|
|
40
|
+
`- 不足している tool output / 詳細根拠が必要なときだけ、L3 references の \`throughline detail <時刻>\` を使って取得してください。`;
|
|
42
41
|
|
|
43
42
|
const CONTINUATION_REMINDER =
|
|
44
|
-
'**再開指示:** 上記の L1 / L2
|
|
45
|
-
'最新の L2
|
|
43
|
+
'**再開指示:** 上記の L1 / L2 を、現在タスクに使う作業コンテキストとして扱ってください。' +
|
|
44
|
+
'最新の L2 から次の一手を決め、中断地点から続行してください。' +
|
|
46
45
|
'古い仮説や未完了 TODO は、後続の判断で上書きされ得ます。';
|
|
47
46
|
|
|
48
47
|
const NORMAL_HEADER = '## Throughline: セッション記憶';
|
|
49
48
|
|
|
50
|
-
const FOOTER_GUIDE =
|
|
51
|
-
'---\n' +
|
|
52
|
-
'**[Claude 向け — 記憶の使い方]**\n' +
|
|
53
|
-
'上の L1 要約や L2 本文を読んで「具体的なコマンドやツール出力、ファイル内容を確認したい」と感じたら、' +
|
|
54
|
-
'推測せずに **Bash ツールで `throughline detail <時刻>` を実行** して L3(ツール入出力・hook 出力・thinking)を取得してください。\n' +
|
|
55
|
-
'- 単一時刻: `throughline detail 14:23:05`\n' +
|
|
56
|
-
'- 時刻範囲: `throughline detail 14:23-14:30`\n' +
|
|
57
|
-
'\n' +
|
|
58
|
-
'返る内容: 指定ターンの L2 会話本文 + L3(tool_input / tool_output / system / thinking 別にグループ化)。\n' +
|
|
59
|
-
'ユーザーに「詳細を見せて」と言われた時だけでなく、**ユーザー発言の文脈が過去ターンに依存しているのに L1/L2 だけでは情報不足だと判断した時**に、Claude 自身の判断で呼び出して構いません。';
|
|
60
|
-
|
|
61
49
|
/**
|
|
62
|
-
* L1+L2 注入テキストを組み立てる。
|
|
50
|
+
* L1 + L2 + L3 references 注入テキストを組み立てる。
|
|
63
51
|
*
|
|
64
52
|
* @param {import('node:sqlite').DatabaseSync} db
|
|
65
53
|
* @param {{
|
|
66
54
|
* sessionId: string,
|
|
67
55
|
* isInheritance: boolean,
|
|
68
56
|
* excludeOriginId?: string | null,
|
|
69
|
-
* inflightMemo?: string | null,
|
|
57
|
+
* inflightMemo?: string | null, // 互換のため受け取るが新仕様では使用しない
|
|
70
58
|
* }} params
|
|
71
59
|
* @returns {string | null}
|
|
72
60
|
*/
|
|
73
61
|
export function buildResumeContext(
|
|
74
62
|
db,
|
|
75
|
-
{ sessionId, isInheritance, excludeOriginId = null, inflightMemo = null },
|
|
63
|
+
{ sessionId, isInheritance, excludeOriginId = null, inflightMemo: _ignoredMemo = null },
|
|
76
64
|
) {
|
|
65
|
+
// handoff-record は Codex 側でも使うので signature 維持。inflightMemo / latestThinking は
|
|
66
|
+
// 新仕様の注入テキストには使わない (L2 全文で十分という判断)。
|
|
77
67
|
const record = buildHandoffRecord(db, {
|
|
78
68
|
sessionId,
|
|
79
69
|
isInheritance,
|
|
80
70
|
excludeOriginId,
|
|
81
|
-
inflightMemo,
|
|
82
71
|
});
|
|
83
72
|
if (!record) return null;
|
|
84
73
|
|
|
@@ -88,20 +77,6 @@ export function buildResumeContext(
|
|
|
88
77
|
: NORMAL_HEADER;
|
|
89
78
|
const lines = [header];
|
|
90
79
|
|
|
91
|
-
if (record.memory.inflightMemo) {
|
|
92
|
-
lines.push('');
|
|
93
|
-
lines.push('### 中断直前の in-flight メモ(前セッションの Claude 自身による要約)');
|
|
94
|
-
lines.push(record.memory.inflightMemo);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (record.memory.latestThinking.length > 0) {
|
|
98
|
-
lines.push('');
|
|
99
|
-
lines.push('### 中断直前の思考 (最終ターンの extended thinking)');
|
|
100
|
-
for (const r of record.memory.latestThinking) {
|
|
101
|
-
lines.push(`[${r.time}] ${r.text}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
80
|
if (record.memory.l1Summaries.length > 0) {
|
|
106
81
|
lines.push('');
|
|
107
82
|
lines.push('### それ以前の要約 (L1)');
|
|
@@ -121,8 +96,16 @@ export function buildResumeContext(
|
|
|
121
96
|
}
|
|
122
97
|
}
|
|
123
98
|
|
|
124
|
-
|
|
125
|
-
|
|
99
|
+
if (record.references.l3.length > 0) {
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push('### L3 詳細参照 (本文は注入されていません)');
|
|
102
|
+
lines.push('必要なときだけ以下のコマンドで取得してください:');
|
|
103
|
+
for (const ref of record.references.l3) {
|
|
104
|
+
const time = formatTime(ref.createdAt);
|
|
105
|
+
lines.push(`- [${time}] ${ref.kind}: \`throughline detail ${time}\``);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
126
109
|
lines.push('');
|
|
127
110
|
lines.push(CONTINUATION_REMINDER);
|
|
128
111
|
|
|
@@ -65,7 +65,7 @@ function insertThinking(db, row) {
|
|
|
65
65
|
).run(row.session, row.origin, row.turn, row.text, row.createdAt, row.sourceId);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
test('buildResumeContext: inheritance output order is
|
|
68
|
+
test('buildResumeContext: inheritance output order is L1 -> L2 -> L3 refs -> reminder (no memo / no thinking)', () => {
|
|
69
69
|
const db = makeDb();
|
|
70
70
|
|
|
71
71
|
insertSkeleton(db, {
|
|
@@ -92,6 +92,7 @@ test('buildResumeContext: inheritance output order is memo -> thinking -> L1 ->
|
|
|
92
92
|
text: 'recent assistant body',
|
|
93
93
|
createdAt: 2100,
|
|
94
94
|
});
|
|
95
|
+
// thinking は DB に書かれても、新仕様では注入テキストに出ない
|
|
95
96
|
insertThinking(db, {
|
|
96
97
|
session: 'new',
|
|
97
98
|
origin: 'old',
|
|
@@ -104,32 +105,36 @@ test('buildResumeContext: inheritance output order is memo -> thinking -> L1 ->
|
|
|
104
105
|
const text = buildResumeContext(db, {
|
|
105
106
|
sessionId: 'new',
|
|
106
107
|
isInheritance: true,
|
|
107
|
-
inflightMemo
|
|
108
|
+
// inflightMemo は互換のため受け取れるが、新仕様では注入テキストに使わない
|
|
109
|
+
inflightMemo: '**Next**: keep going (should NOT appear)',
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
assert.ok(text);
|
|
111
113
|
assert.match(text, /^## Throughline: 中断した作業の再開/);
|
|
112
114
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
// 新仕様: memo / thinking セクションは注入されない
|
|
116
|
+
assert.ok(text.indexOf('### 中断直前の in-flight メモ') < 0, 'memo section should not be injected');
|
|
117
|
+
assert.ok(text.indexOf('### 中断直前の思考') < 0, 'thinking section should not be injected');
|
|
118
|
+
assert.ok(!text.includes('**Next**: keep going'), 'inflightMemo content should be ignored');
|
|
119
|
+
assert.ok(!text.includes('latest thinking block'), 'thinking text should not appear');
|
|
120
|
+
|
|
121
|
+
// 注入される順序: L1 → L2 → L3 refs → 再開指示
|
|
115
122
|
const l1Idx = text.indexOf('### それ以前の要約 (L1)');
|
|
116
123
|
const l2Idx = text.indexOf('### 現在進行中の作業履歴 (L2 / active work thread)');
|
|
117
|
-
const
|
|
124
|
+
const l3Idx = text.indexOf('### L3 詳細参照');
|
|
125
|
+
const reminderIdx = text.indexOf('**再開指示:**');
|
|
118
126
|
|
|
119
|
-
assert.ok(
|
|
120
|
-
assert.ok(thinkingIdx > memoIdx, 'thinking should follow in-flight memo');
|
|
121
|
-
assert.ok(l1Idx > thinkingIdx, 'L1 should follow thinking');
|
|
127
|
+
assert.ok(l1Idx > 0, 'L1 section should be present');
|
|
122
128
|
assert.ok(l2Idx > l1Idx, 'L2 should follow L1');
|
|
123
|
-
assert.ok(
|
|
129
|
+
assert.ok(l3Idx > l2Idx, 'L3 references should follow L2');
|
|
130
|
+
assert.ok(reminderIdx > l3Idx, 'continuation reminder should follow L3 refs');
|
|
124
131
|
|
|
125
|
-
assert.ok(text.includes('**Next**: keep going'));
|
|
126
|
-
assert.ok(text.includes('latest thinking block'));
|
|
127
132
|
assert.ok(text.includes('older L1 summary'));
|
|
128
133
|
assert.ok(text.includes('[user]: recent user body'));
|
|
129
134
|
assert.ok(text.includes('[assistant]: recent assistant body'));
|
|
135
|
+
// L3 refs は detail コマンドのみ (本文は注入しない)
|
|
136
|
+
assert.match(text, /throughline detail \d{2}:\d{2}:\d{2}/);
|
|
130
137
|
assert.match(text, /単なる過去ログではなく、現在進行中の作業/);
|
|
131
|
-
assert.match(text, /後の行ほど現在状態に近く/);
|
|
132
|
-
assert.match(text, /上記の L1 \/ L2 \/ thinking \/ in-flight メモを、現在タスクに使う作業コンテキスト/);
|
|
133
138
|
});
|
|
134
139
|
|
|
135
140
|
test('buildResumeContext: returns null when no memory rows or inflight memo exist', () => {
|
package/src/session-start.mjs
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* SessionStart hook — セッション登録 +
|
|
3
|
+
* SessionStart hook — セッション登録 + 引き継ぎ判定 + 注入
|
|
4
4
|
*
|
|
5
5
|
* stdin: { session_id, source, cwd, transcript_path, hook_event_name }
|
|
6
6
|
*
|
|
7
|
-
* 【引き継ぎ条件 (
|
|
8
|
-
*
|
|
9
|
-
* baton
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* 【引き継ぎ条件 (2 経路)】 docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md
|
|
8
|
+
*
|
|
9
|
+
* 1. baton path: ユーザーが旧セッションで `/tl` を打つと UserPromptSubmit hook が
|
|
10
|
+
* handoff_batons に session_id を書く。本 hook が TTL 1 時間以内に消費して
|
|
11
|
+
* 前任を merge + 引継ぎ stdout 注入。`source` 値関係なく発火。
|
|
12
|
+
* 2. auto path: `source='clear'` かつ env `THROUGHLINE_DISABLE_AUTO_HANDOFF` が
|
|
13
|
+
* `'1'` でない場合、同 project_path の最新 Claude unmerged session を
|
|
14
|
+
* 自動 merge して注入。
|
|
15
|
+
*
|
|
16
|
+
* 両方同時成立はしない (consumeBaton が先発、baton ありなら baton path、
|
|
17
|
+
* なければ source 判定)。env で OFF にしたユーザーは `/tl` を打ってから
|
|
18
|
+
* 新セッションスタートで baton path を使う。
|
|
12
19
|
*
|
|
13
20
|
* 役割:
|
|
14
21
|
* 1. sessions テーブルに新セッションを INSERT OR IGNORE
|
|
15
|
-
* 2.
|
|
16
|
-
* 3.
|
|
17
|
-
* 4.
|
|
22
|
+
* 2. baton path 判定 (consumeBaton + mergeSpecificPredecessor)
|
|
23
|
+
* 3. baton 無し かつ source='clear' かつ env disable 無し → auto path 判定
|
|
24
|
+
* 4. 合流成立なら curated memory (L1+L2+L3 refs) を「引き継ぎヘッダ」付きで stdout 注入
|
|
25
|
+
* 5. 判定結果を ~/.throughline/logs/inheritance-decision.log に記録
|
|
18
26
|
*/
|
|
19
27
|
|
|
20
28
|
import { getDb } from './db.mjs';
|
|
@@ -27,6 +35,37 @@ import { join, dirname } from 'node:path';
|
|
|
27
35
|
import { homedir } from 'node:os';
|
|
28
36
|
import { pathToFileURL } from 'node:url';
|
|
29
37
|
|
|
38
|
+
const ENV_DISABLE_AUTO_HANDOFF = 'THROUGHLINE_DISABLE_AUTO_HANDOFF';
|
|
39
|
+
|
|
40
|
+
function isAutoHandoffDisabled(env) {
|
|
41
|
+
return env[ENV_DISABLE_AUTO_HANDOFF] === '1';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 同 project_path の最新 Claude unmerged session を返す (auto path 用 predecessor)。
|
|
46
|
+
* Codex session (`codex:*`) と現セッション自身は除外。
|
|
47
|
+
*
|
|
48
|
+
* @param {import('node:sqlite').DatabaseSync} db
|
|
49
|
+
* @param {string} projectPath
|
|
50
|
+
* @param {string} currentSessionId
|
|
51
|
+
* @returns {{ session_id: string } | null}
|
|
52
|
+
*/
|
|
53
|
+
function findLatestClaudePredecessor(db, projectPath, currentSessionId) {
|
|
54
|
+
return (
|
|
55
|
+
db
|
|
56
|
+
.prepare(
|
|
57
|
+
`SELECT session_id FROM sessions
|
|
58
|
+
WHERE lower(project_path) = lower(?)
|
|
59
|
+
AND merged_into IS NULL
|
|
60
|
+
AND session_id != ?
|
|
61
|
+
AND session_id NOT LIKE 'codex:%'
|
|
62
|
+
ORDER BY updated_at DESC
|
|
63
|
+
LIMIT 1`,
|
|
64
|
+
)
|
|
65
|
+
.get(projectPath, currentSessionId) ?? null
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
30
69
|
function logDecision(entry) {
|
|
31
70
|
const path = join(homedir(), '.throughline', 'logs', 'inheritance-decision.log');
|
|
32
71
|
try {
|
|
@@ -57,11 +96,7 @@ export async function run() {
|
|
|
57
96
|
const db = getDb();
|
|
58
97
|
const now = Date.now();
|
|
59
98
|
|
|
60
|
-
// 0. VSCode
|
|
61
|
-
// Stop hook 側にも同じ呼び出しがあるが、Stop が発火しない環境(応答中断・IDE 挙動差)
|
|
62
|
-
// でも SessionStart は必ず走るので、新規プロジェクトでの自動起動を確実化する保険。
|
|
63
|
-
// 冪等性は ensureMonitorTaskFile 側で保証されており、Stop/UserPromptSubmit と重複呼び
|
|
64
|
-
// 出しされても安全。
|
|
99
|
+
// 0. VSCode 用 tasks.json 自動プロビジョニング (冪等)
|
|
65
100
|
try {
|
|
66
101
|
ensureMonitorTaskFile({ cwd: projectPath, env: process.env });
|
|
67
102
|
} catch (err) {
|
|
@@ -75,18 +110,40 @@ export async function run() {
|
|
|
75
110
|
VALUES (?, ?, 'active', ?, ?)`,
|
|
76
111
|
).run(session_id, projectPath, now, now);
|
|
77
112
|
|
|
78
|
-
// 2.
|
|
113
|
+
// 2. baton 消費
|
|
79
114
|
const baton = consumeBaton(db, { projectPath, now });
|
|
80
115
|
|
|
81
|
-
|
|
116
|
+
// 3. 引継ぎ判定
|
|
117
|
+
let mergeResult = { merged: false, skipReason: 'no_trigger' };
|
|
118
|
+
let triggeredPath = null;
|
|
119
|
+
const autoDisabled = isAutoHandoffDisabled(process.env);
|
|
120
|
+
|
|
82
121
|
if (baton.sessionId) {
|
|
83
|
-
//
|
|
122
|
+
// baton path
|
|
123
|
+
triggeredPath = 'baton';
|
|
84
124
|
const { target: predecessorId } = resolveMergeTarget(db, baton.sessionId);
|
|
85
125
|
mergeResult = mergeSpecificPredecessor(db, {
|
|
86
126
|
newSessionId: session_id,
|
|
87
127
|
predecessorId,
|
|
88
128
|
now,
|
|
89
129
|
});
|
|
130
|
+
} else if (source === 'clear' && !autoDisabled) {
|
|
131
|
+
// auto path: 同 project の最新 Claude unmerged session を自動 predecessor にする
|
|
132
|
+
triggeredPath = 'auto';
|
|
133
|
+
const predRow = findLatestClaudePredecessor(db, projectPath, session_id);
|
|
134
|
+
if (predRow?.session_id) {
|
|
135
|
+
const { target: predecessorId } = resolveMergeTarget(db, predRow.session_id);
|
|
136
|
+
mergeResult = mergeSpecificPredecessor(db, {
|
|
137
|
+
newSessionId: session_id,
|
|
138
|
+
predecessorId,
|
|
139
|
+
now,
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
mergeResult = { merged: false, skipReason: 'no_predecessor' };
|
|
143
|
+
}
|
|
144
|
+
} else if (source === 'clear' && autoDisabled) {
|
|
145
|
+
triggeredPath = 'auto-disabled';
|
|
146
|
+
mergeResult = { merged: false, skipReason: 'auto_handoff_disabled' };
|
|
90
147
|
}
|
|
91
148
|
|
|
92
149
|
logDecision({
|
|
@@ -94,22 +151,21 @@ export async function run() {
|
|
|
94
151
|
source: source ?? null,
|
|
95
152
|
session_id,
|
|
96
153
|
project_path: projectPath,
|
|
154
|
+
triggered_path: triggeredPath,
|
|
155
|
+
auto_handoff_disabled: autoDisabled,
|
|
97
156
|
baton_session_id: baton.sessionId ?? null,
|
|
98
157
|
baton_age_ms: baton.ageMs ?? null,
|
|
99
158
|
baton_skip_reason: baton.skipReason ?? null,
|
|
100
|
-
baton_has_memo: Boolean(baton.memoText),
|
|
101
159
|
merged: mergeResult.merged,
|
|
102
160
|
merge_skip_reason: mergeResult.skipReason ?? null,
|
|
103
161
|
predecessor_id: mergeResult.predecessorId ?? null,
|
|
104
162
|
});
|
|
105
163
|
|
|
106
|
-
//
|
|
107
|
-
// バトンに付いていた in-flight メモも併せて先頭セクションに注入する
|
|
164
|
+
// 4. 合流成立なら curated memory を stdout 注入 (L1 + L2 + L3 refs)
|
|
108
165
|
if (mergeResult.merged) {
|
|
109
166
|
const text = buildResumeContext(db, {
|
|
110
167
|
sessionId: session_id,
|
|
111
168
|
isInheritance: true,
|
|
112
|
-
inflightMemo: baton.memoText ?? null,
|
|
113
169
|
});
|
|
114
170
|
if (text) {
|
|
115
171
|
process.stdout.write(text + '\n');
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: 現セッションの context trim 計画を dry-run する (Throughline)
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Throughline の context trim は、現時点では dry-run のみ有効です。自動 `/rewind` や自動 rollback / inject は、host primitive の検証が完了するまで実行してはいけません。
|
|
6
|
-
|
|
7
|
-
まず、今この時点の「現在作業」を自分の文脈から思い出し、以下 4 項目を Markdown で整理してください。
|
|
8
|
-
|
|
9
|
-
- **次の一手**: 今まさに何をやろうとしていたか(1-3 文、具体的に)
|
|
10
|
-
- **現在の方針 / 仮説**: 追っているバグの原因、設計の方向性、調査中の観点など
|
|
11
|
-
- **未解決の疑問**: 判断保留中の論点
|
|
12
|
-
- **進行中 TODO**: 完了済みを除いた現行 TODO
|
|
13
|
-
|
|
14
|
-
その Markdown を stdin として、次のコマンドを Bash ツールで実行し、結果をユーザーにそのまま要約してください。
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
throughline trim --dry-run --host claude --memo-stdin $ARGUMENTS <<'EOF'
|
|
18
|
-
**次の一手**: ...
|
|
19
|
-
**現在の方針 / 仮説**: ...
|
|
20
|
-
**未解決の疑問**: ...
|
|
21
|
-
**進行中 TODO**:
|
|
22
|
-
- ...
|
|
23
|
-
EOF
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
出力には以下が含まれます。
|
|
27
|
-
|
|
28
|
-
- 現セッションで Throughline が捕捉している turn 数
|
|
29
|
-
- keep-recent 設定
|
|
30
|
-
- rollback 候補 turn 数
|
|
31
|
-
- host が自動 rollback / inject 対応かどうか
|
|
32
|
-
- rollback 後に戻す curated memory preview
|
|
33
|
-
- rollback 後に「今やっている作業」として戻す current-work memo
|
|
34
|
-
|
|
35
|
-
`$ARGUMENTS` には `--keep-recent 20`、`--all`、`--session <id>` を渡せます。
|
|
36
|
-
|
|
37
|
-
重要:
|
|
38
|
-
|
|
39
|
-
- dry-run の結果だけで自動 rollback しないでください。
|
|
40
|
-
- Claude の既存 `/tl` baton handoff は変更しません。
|
|
41
|
-
- L1/L2 だけでは「今やっている作業」として認識されにくいので、current-work memo を省略しないでください。
|
|
42
|
-
- `Throughline Trim Dry-run` の `Automatic execution allowed` が `no` の場合は、実行手順ではなく「現状は dry-run / 手動案内まで」と説明してください。
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* throughline save-inflight — /tl 発動後、現行 Claude が中断地点の in-flight メモを
|
|
3
|
-
* stdin 経由で書き込む CLI。
|
|
4
|
-
*
|
|
5
|
-
* 使い方 (Claude Code の Bash ツールから):
|
|
6
|
-
* throughline save-inflight <<'EOF'
|
|
7
|
-
* **次の一手**: ...
|
|
8
|
-
* **現在の方針**: ...
|
|
9
|
-
* **未解決の疑問**: ...
|
|
10
|
-
* **進行中 TODO**: ...
|
|
11
|
-
* EOF
|
|
12
|
-
*
|
|
13
|
-
* 動作:
|
|
14
|
-
* 1. stdin を UTF-8 で全部読む
|
|
15
|
-
* 2. 空なら exit 1 (§0 フォールバック禁止 — サイレント成功しない)
|
|
16
|
-
* 3. cwd に対応する handoff_batons 行の memo_text を UPDATE
|
|
17
|
-
* 4. バトン未登録なら updated=false で警告して exit 1
|
|
18
|
-
*
|
|
19
|
-
* 呼び出し元: [.claude/commands/tl.md] が Claude に実行させる
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { getDb } from '../db.mjs';
|
|
23
|
-
import { updateBatonMemo } from '../baton.mjs';
|
|
24
|
-
import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
25
|
-
import { join, dirname } from 'node:path';
|
|
26
|
-
import { homedir } from 'node:os';
|
|
27
|
-
|
|
28
|
-
function logInflight(entry) {
|
|
29
|
-
const path = join(homedir(), '.throughline', 'logs', 'inflight-memo.log');
|
|
30
|
-
try {
|
|
31
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
32
|
-
appendFileSync(path, JSON.stringify(entry) + '\n', 'utf8');
|
|
33
|
-
} catch (err) {
|
|
34
|
-
const msg = err instanceof Error ? err.message : 'unknown';
|
|
35
|
-
process.stderr.write(`[save-inflight:log] ${msg}\n`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function run() {
|
|
40
|
-
let memoText;
|
|
41
|
-
try {
|
|
42
|
-
memoText = readFileSync(0, 'utf8').trim();
|
|
43
|
-
} catch (err) {
|
|
44
|
-
const msg = err instanceof Error ? err.message : 'unknown';
|
|
45
|
-
process.stderr.write(`[save-inflight] failed to read stdin: ${msg}\n`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!memoText) {
|
|
51
|
-
process.stderr.write(
|
|
52
|
-
'[save-inflight] stdin was empty. Provide the in-flight memo via stdin (here-doc or pipe).\n',
|
|
53
|
-
);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const projectPath = process.cwd();
|
|
59
|
-
const db = getDb();
|
|
60
|
-
const { updated } = updateBatonMemo(db, { projectPath, memoText });
|
|
61
|
-
|
|
62
|
-
logInflight({
|
|
63
|
-
ts: new Date().toISOString(),
|
|
64
|
-
project_path: projectPath,
|
|
65
|
-
memo_length: memoText.length,
|
|
66
|
-
baton_updated: updated,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
if (!updated) {
|
|
70
|
-
process.stderr.write(
|
|
71
|
-
`[save-inflight] no baton found for ${projectPath}. ` +
|
|
72
|
-
`Run /tl first so the baton exists, then save-inflight can attach the memo.\n`,
|
|
73
|
-
);
|
|
74
|
-
process.exit(1);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
process.stdout.write(
|
|
79
|
-
`[throughline] in-flight memo saved (${memoText.length} chars) for next session\n`,
|
|
80
|
-
);
|
|
81
|
-
}
|