セッション名を見れば、
次に何をするか分かる
AIで作業は速くなった。いま一番遅いのは、読んで、分かって、決める人の側。そこを軽くするために入れた4つの工夫と、そのまま使える道具2つ。
速くなったのは、AIの側だけ
作業は AI が一気に進める。でもその後の「読む、分かる、決める、次を頼む」は人がやる。ここが詰まると、AI が速くても全体は速くならない。
困っていたこと
分からないまま「続けて」
できたこと、決めてほしいこと、前提が1つの文章に混ざっている。思っていたのと違う結果になって、最初からやり直しを何度もした。
数日後に「これ何のため?」
並べて走らせると目的を忘れる。新しいセッションで同じことをやらせて、結果が2つ。どっちを残すかで迷う。
どこを読めばいいか分からない
文章だけだと、しっかり読むところと読み流していいところの区別がつかない。
セッション名に状態を入れる
セッションの中身を読んで、名前を自動で書き換える mod を入れた。一覧を見れば、次に自分が何をするか分かる。1日の終わりは、確認待ちが残っていないかだけ見る。
名前の組み立て
優先順位は 状態、テーマ、作業。長すぎるときは具体作業から落とす。2時間以上あけて戻ると、応答の冒頭に「前回までの経緯」を3行で書かせる。
報告の形を決める
報告は毎回同じ4つの欄で返してもらう。読む場所が決まっているので、結論と選択肢だけ見ればいい。
確認が3件まで
報告の文章の中に、上の型で選択肢を並べる。
確認が4件以上
頭が追いつかないので、HTML の確認ページにしてもらう。前提を書き、A・B・C のボタンと自由記入欄を置く。
量が多い分析は、図と表にする
市場調査のように量が多いものは、チャットに文章で出させない。図、表、グラフ、概念図を使った HTML レポートにしてもらう。見て分かるものは、見て分かる形で。
それでも重いときは、解説動画
レポートが大きすぎるとき、切り口違いのレポートを何本もまとめて理解したいときは、自分用の解説動画を作ってもらう。Gemini TTS(Google の音声読み上げ)を使うと、抑揚のある自然な声になる。このページの冒頭の動画も、その作り方で作った。
まず動画
全体の流れと結論をつかむ。数分で済む。
次にレポート
気になったところだけ、図と表で細かく確かめる。
理解は省かない
AI に頼めば、大量のアウトプットはすぐ出てくる。でも鵜呑みにすると、ずれたものがすごい速さで大量にできる。1つずつ理解してから次を頼む。ここは省けない。省けないから、理解するところを AI に手伝わせる。
道具2つ(導入ファイル)
セットで使う前提。mod は報告の「確認してほしいこと」欄を見て状態を決めるので、報告ルールなしだと判定が甘くなる。ダウンロードするか、中身をコピーして下の名前で保存する。
~/.claude/output-styles/report-style.mdに保存- Claude Code で
/configを開き、Output style で「Report First」を選ぶ
--- name: Report First description: 結論から書き、確認事項は選択肢で答えられる形にする日本語の報告スタイル keep-coding-instructions: true --- <!-- 使い方 1. このファイルを ~/.claude/output-styles/report-style.md に置く 2. Claude Code で /config を開き、Output style に「Report First」を選ぶ (設定ファイルなら ~/.claude/settings.json に "outputStyle": "Report First") --> # 出力ルール これは応答の書き方の指定であり、コーディングの進め方には影響させない。 ## 語彙のルール - 略語や記号をその場で新造しない。 - 業界標準でない略語を使う場合は、その応答内の初出時に「正式名称(略語)」の形で必ず定義する。 - ファイル名・関数名・設定キー・パスは省略せず毎回フルネームで書く。 - 矢印や記号(→ ⇒ ✅ ❌ ⚠️ ★)で意味を代用しない。日本語の動詞と接続詞で書く。 - 標準の Markdown 見出しと箇条書きだけを使う。 ## 応答の固定フォーマット 作業を伴う応答は、必ず以下の4ブロックの順で書く。ブロックの見出しは毎回この文言のまま使う。 ### 結論 何をしたか、または何が分かったかを3行以内で書く。ここだけ読めば判断できる状態にする。 ### 変更したファイル パスと変更内容を1行ずつ列挙する。変更がない場合は「変更なし」と書く。 ### 確認してほしいこと こちらの判断が必要な点だけを書く。ない場合は「なし」と書き、質問を水増ししない。 各項目は、この欄だけ読めば答えられるように、次の型で書く。 ``` **1. 〈何を決めるか〉** - 【前提】: なぜ今この確認が要るのか。判断に必要な事実(数値・期限・影響)を1〜2文で。 - 【選択肢】: - A(推奨)〈案〉 - B 〈案〉 - C 〈案〉 - その他(自由記入) - 【推奨の理由】: 1行。 - 【決めないと】: 止まる作業や起きること。 ``` - いちばん大事なのは選択肢なので、いちばん目立たせる。 - 選択肢は2〜4個にし、必ず「推奨」を1つと「その他(自由記入)」を付ける。はい/いいえで済む確認も「A はい(推奨) B いいえ その他」の形にする。 - 項目の最後に「返事の例: 1A 2B 3その他: …」を1行添え、番号と記号だけで答えられるようにする。 - 4件以上になるときは、選択式の確認ページ(HTML。A・B・C のボタンと自由記入欄を付ける)にまとめ、この欄にはそのリンクと件数だけを書く。 ### 補足 読み飛ばしてよい詳細(試した手順、迷った点、代替案、作業中に分かったこと)をここにまとめる。 上の3ブロックに詳細を混ぜない。 質問への回答や短い相談など、作業を伴わない応答ではこのフォーマットを使わなくてよい。 その場合も結論を最初に書く。相手の判断を求めるときは「確認してほしいこと」の型で書く。 ### 前回までの経緯 時間を空けてセッションに戻ってきたと知らされたとき(session-status からの知らせ)は、 結論の前に「### 前回までの経緯」を置き、ここまでの流れと今待っている判断を3行以内で書く。 ## 量が多い内容 - 市場調査や分析など、文章で読むと重いものはチャットに長文で書かない。 - 図・表・グラフ・概念図を使った HTML レポートにして、チャットには結論とリンクだけを書く。 ## 説明の分量 - 前置きと復唱をしない。依頼内容を要約し直してから始めない。 - 1つの段落は3文以内にする。 - 同じ内容を「まとめ」として再掲しない。 ## 禁止 - 大げさな形容(「完全に」「劇的に」「根本的に」「大幅に」)を使わない。 - 実行していない作業を完了したかのように書かない。未実行なら未実行と書く。 - 推測を断定形で書かない。確認していないことは「未確認」と明示する。
- zip を展開して、フォルダ
session-status/を好きな場所に置く(設定ファイル2つも入っている) ~/.claude/settings.jsonの env に"CLAUDE_CODE_PLUGIN_DIRS": "フォルダの絶対パス"を足して、Claude Code を開き直す
// session-status: セッション名を「絵文字【状態】テーマ 具体作業|作業の種類」に自動で書き換える Claude Code の mod
//
// 例: 🟡【確認待ち】ブログ記事 骨子作成|記事作成
// ✅【完了】在庫レポート 集計修正|改修
//
// 状態は4つ。🔄【作業中】🟡【確認待ち】✅【完了】🔴【停止】
// - ターンが始まったら【作業中】
// - ターンが終わったら判定し直す。応答に「確認してほしいこと」欄があれば【確認待ち】、
// 「なし」なら【完了】、エラーや中断なら【停止】。決まらないときは Haiku に判定させる
// - テーマ・具体作業・作業の種類は Haiku が付ける(1ターンにつき1回呼ぶ)
// - 2時間以上あけて戻ってきたら、経緯の要約を Claude に渡して応答の冒頭に書かせる
//
// 名前を変えられるのは、デスクトップアプリのセッションと claude.ai(Remote Control)のセッション。
// どちらでもないセッション(ターミナルだけ、claude -p など)では何もしない。
// 報告スタイル report-style.md と組み合わせる前提(「確認してほしいこと」欄で状態を決めるため)。
//
// 置き方(フォルダの場所は自由。例として ~/.claude/mods/ に置く場合)
// ~/.claude/mods/session-status/
// .claude-plugin/plugin.json { "name": "session-status", "version": "0.1.0", "description": "セッション名に状態を入れる" }
// hooks/hooks.json { "modules": ["./register.js"] }
// hooks/register.js このファイル
// ~/.claude/settings.json の env に "CLAUDE_CODE_PLUGIN_DIRS": "<上のフォルダの絶対パス>" を足して、Claude Code を開き直す。
// 確かめるときは claude plugin validate ~/.claude/mods/session-status
// ---- 設定 ----------------------------------------------------------------
// 特定のフォルダの下だけで動かしたいときに書く(例: ['/my-project'])。空なら全部のセッションで動く。
const ONLY_DIRS = []
// 前回のターンからこの時間以上あいたら「前回までの経緯」を出させる。
const RECAP_GAP_MS = 2 * 60 * 60 * 1000
const STATUSES = ['確認待ち', '作業中', '完了', '停止']
const STATUS_MARKS = { 作業中: '🔄', 確認待ち: '🟡', 完了: '✅', 停止: '🔴' }
const MAX_TITLE = 60
const TITLE_SERVERS = [
// デスクトップアプリ。"self" で自分のセッション。
{ server: 'ccd_session_mgmt', probeArgs: { session_id: 'self' } },
// claude.ai の Remote Control。get_session で id を取る。
{ server: 'claude-code-remote', probeArgs: {} },
]
// ---- 規則で決める部分(AI を使わない) ------------------------------------
// 応答の「確認してほしいこと」欄を取り出す。欄が無ければ null。
function extractConfirmBlock(answer) {
const lines = String(answer || '').split('\n')
const start = lines.findIndex((l) => /^#{1,4}\s*確認してほしいこと/.test(l.trim()))
if (start < 0) return null
const body = []
for (const line of lines.slice(start + 1)) {
if (/^#{1,4}\s/.test(line.trim())) break
body.push(line)
}
return body.join('\n').trim()
}
// 別の AI やバックグラウンドのジョブに作業を頼んで、結果を待っている途中か。
const DELEGATED = [
/(Codex|バックグラウンド|ジョブ|サブエージェント)[^\n]{0,60}(始め|開始|進めて|作業中|動いて|実行中|走らせ|待って|待ち)/,
/(できたら|終わったら|完了したら|届いたら)[^\n]{0,40}(お見せ|確かめ|知らせ|報告)/,
]
function isDelegatedWork(answer) {
return DELEGATED.some((re) => re.test(String(answer || '')))
}
// 規則で決まる状態。決まらなければ null(Haiku の判定に任せる)。
function ruleStatus({ reason, isAborted, answer }) {
if (reason === 'error' || reason === 'refusal' || isAborted) return '停止'
const block = extractConfirmBlock(answer)
if (block !== null) {
const head = block.replace(/^[-*・\s]+/, '')
if (head === '' || head.startsWith('なし')) return isDelegatedWork(answer) ? '作業中' : '完了'
return '確認待ち'
}
if (/[??]\s*$/.test(String(answer || '').trim().slice(-200))) return '確認待ち'
if (isDelegatedWork(answer)) return '作業中'
return null
}
function clean(v, max) {
return String(v ?? '')
.replace(/[\u0000-\u001f\u007f]/g, '')
.replace(/[【】|]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, max)
}
// Haiku の返事から JSON を取り出す。壊れていれば null。
function parseJudgement(text) {
const m = String(text || '').match(/\{[\s\S]*\}/)
if (!m) return null
try {
const j = JSON.parse(m[0])
return {
subject: clean(j.subject, 24),
detail: clean(j.detail, 12),
work: clean(j.work, 10),
status: STATUSES.includes(j.status) ? j.status : null,
recap: clean(j.recap, 240),
subjectChanged: j.subject_changed === true,
}
} catch {
return null
}
}
// 「絵文字【状態】テーマ 具体作業|作業の種類」。長すぎるときは具体作業から落とす。
function buildTitle(status, subject, detail, work) {
const head = (STATUS_MARKS[status] || '') + '【' + status + '】' + (subject || '作業')
const extra = detail && detail !== work && !String(subject).includes(detail) ? ' ' + detail : ''
const tail = work ? '|' + work : ''
for (const full of [head + extra + tail, head + tail]) {
if (full.length <= MAX_TITLE) return full
}
return (head + tail).slice(0, MAX_TITLE - 1) + '…'
}
// ---- セッションごとの状態 --------------------------------------------------
let s = freshState()
function freshState() {
return {
enabled: true,
target: null,
probeTries: 0,
localId: '',
subject: '',
detail: '',
work: '',
status: '',
lastTitle: '',
firstPrompt: '',
lastPrompt: '',
recap: '',
lastTurnEndAt: 0,
locked: false,
busy: false,
busyAt: 0,
}
}
function resultText(r) {
return (r?.content || []).map((c) => (c && c.type === 'text' ? c.text : '')).join('\n')
}
// 名前を変える先を探す。セッション開始直後は接続がまだ無いことがあるので、数回まで探し直す。
async function ensureTarget($) {
if (s.target || s.probeTries >= 5) return s.target
s.probeTries += 1
for (const t of TITLE_SERVERS) {
try {
const r = await $.mcp.call(t.server, 'get_session', t.probeArgs)
if (r.isError) continue
if (t.server === 'claude-code-remote') {
const m = resultText(r).match(/"id"\s*:\s*"(session_[A-Za-z0-9]+)"/)
if (!m) continue
s.target = { server: t.server, sessionId: m[1] }
} else {
s.target = { server: t.server, sessionId: 'self' }
}
return s.target
} catch {
// その接続先が無いセッション。次を試す。
}
}
return null
}
async function setTitle($, title) {
if (!s.target || s.locked || !title || title === s.lastTitle) return
try {
const r = await $.mcp.call(s.target.server, 'set_session_title', { session_id: s.target.sessionId, title })
if (r.isError) {
// 手で付けた名前を守るため承認を断られた等。このセッションでは以後書き換えない。
s.locked = true
$.ui.log('セッション名の自動書き換えを止めました: ' + resultText(r).slice(0, 120))
return
}
s.lastTitle = title
} catch (err) {
$.ui.log('セッション名を変えられませんでした: ' + String(err).slice(0, 120))
}
}
async function judge($, input) {
const system = 'あなたは作業セッションの一覧に見出しを付ける係です。指示された JSON だけを返します。'
const prompt = [
'次の作業セッションについて、JSON を1つだけ返してください。見出しは「テーマ 具体作業|作業の種類」の形で組み立てます。',
'{"subject":"扱っているテーマ(20字以内。例: 在庫レポート、ブログ記事、ログイン画面)",',
' "subject_changed": false,',
' "detail":"今やっている具体的な作業(10字以内・無ければ空文字。例: 骨子作成、集計修正)",',
' "work":"作業の種類(8字以内。例: 調査、改修、記事作成、失敗調査)",',
' "status":"確認待ち / 作業中 / 完了 / 停止 のどれか",',
' "recap":"ここまでの流れと今待っている判断(3文・200字以内。時間を空けて戻った人が読み直さずに済むように)"}',
'',
'status の決め方: 回答がユーザーに判断・承認・回答を求めていれば 確認待ち。作業が終わり求めることが無ければ 完了。',
'続きを自分で進める途中、または別の AI やジョブに頼んで結果を待っている途中なら 作業中。失敗して止まったなら 停止。',
'現在のテーマがあるときは同じものを返す。直近の依頼が明らかに別の話題に移ったときだけ subject を変えて subject_changed を true にする。',
'',
'## 現在のテーマ・作業の種類',
s.subject ? s.subject + '|' + (s.work || '(未定)') : '(まだ無い)',
'## 最初の依頼',
s.firstPrompt.slice(0, 1200) || '(不明)',
'## 直近の依頼',
input.lastPrompt.slice(0, 1000) || '(不明)',
'## 直近の回答(末尾)',
input.answer.slice(-2500) || '(なし)',
].join('\n')
const r = await $.model.complete({ model: 'haiku', system, prompt, maxTokens: 600, timeoutMs: 30000 })
return r.isAnswered ? parseJudgement(r.text) : null
}
async function saveRecord($) {
await $.store.set('session:' + s.localId, {
subject: s.subject,
detail: s.detail,
work: s.work,
status: s.status,
firstPrompt: s.firstPrompt.slice(0, 500),
recap: s.recap,
updatedAt: new Date(await $.clock.now()).toISOString(),
})
}
// ---- フック ----------------------------------------------------------------
export function register(on) {
on('session.start', async ($, e, next) => {
s = freshState()
s.enabled = ONLY_DIRS.length === 0 || ONLY_DIRS.some((d) => e.cwd.includes(d))
if (s.enabled) {
s.localId = await $.session.id()
const saved = await $.store.get('session:' + s.localId)
if (saved && typeof saved === 'object') {
s.subject = saved.subject || ''
s.detail = saved.detail || ''
s.work = saved.work || ''
s.firstPrompt = saved.firstPrompt || ''
s.recap = saved.recap || ''
s.lastTurnEndAt = Date.parse(saved.updatedAt || '') || 0
}
}
return next(e)
})
on('prompt.submit', async ($, e, next) => {
if (!s.enabled || !['composer', 'bridge', 'sdk'].includes(e.origin.kind)) return next(e)
s.lastPrompt = e.text
if (!s.firstPrompt) s.firstPrompt = e.text.slice(0, 2000)
// 時間をあけて戻ってきたら、前回までの経緯を応答の冒頭に書かせる
const gap = s.lastTurnEndAt ? (await $.clock.now()) - s.lastTurnEndAt : 0
if (s.recap && gap >= RECAP_GAP_MS) {
s.lastTurnEndAt = 0
const hours = Math.max(2, Math.round(gap / 3600000))
const note =
'session-status より: ユーザーは前回の応答から約' + hours + '時間あけてこのセッションに戻ってきた。' +
'結論の前に「### 前回までの経緯」を置き、ここまでの流れと今待っている判断を3行以内で書く。' +
'材料(要約。会話と食い違うときは会話を正とする): ' + s.recap
return next({ ...e, context: [...(e.context ?? []), note] })
}
return next(e)
})
on('turn.start', async ($, e, next) => {
const result = await next(e)
if (s.enabled && s.subject) {
$.clock.after(0, async () => {
if (!(await ensureTarget($))) return
s.status = '作業中'
await setTitle($, buildTitle('作業中', s.subject, s.detail, s.work))
})
}
return result
})
on('turn.complete', async ($, e, next) => {
const result = await next(e)
if (!s.enabled || e.agentId !== undefined) return result
s.lastTurnEndAt = await $.clock.now()
const input = { reason: e.reason, isAborted: e.isAborted, answer: e.answer || '', lastPrompt: s.lastPrompt }
// ターンの終わりを待たせないよう、判定と書き換えは裏で行う
$.clock.after(0, async () => {
const now = await $.clock.now()
if (s.busy && now - s.busyAt < 2 * 60 * 1000) return
s.busy = true
s.busyAt = now
try {
if (!(await ensureTarget($))) return
const fixed = ruleStatus(input)
const j = await judge($, input)
if (j) {
if ((!s.subject || j.subjectChanged) && j.subject) {
s.subject = j.subject
s.work = ''
}
s.work = j.work || s.work
s.detail = j.detail
s.recap = j.recap || s.recap
}
s.status = fixed || (j && j.status) || '完了'
if (!s.subject) s.subject = clean(s.firstPrompt || input.lastPrompt, 20)
if (!s.subject) return
await setTitle($, buildTitle(s.status, s.subject, s.detail, s.work))
await saveRecord($)
} catch (err) {
$.ui.log('セッション状態の判定に失敗: ' + String(err).slice(0, 120))
} finally {
s.busy = false
}
})
return result
})
}
注意
- 名前を変えられるのは、デスクトップアプリと claude.ai(Remote Control)のセッションだけ。ターミナルだけのセッションでは何もしない
- 1ターンごとに Haiku を1回呼ぶ
- 自分の環境では毎日使っている。公開用に自分専用の部分を外した版なので、動かして確かめてから使ってほしい