Rules for the session started with LIFE_ROLE=ops: it decides and does not measure (analysis commands need the person's OK).

Personal dotfiles for zsh, WezTerm, and global Codex / Claude Code Skills.
./init.sh
init.sh は idempotent。以下のいずれかをやったら再実行する:
skills/<name>/ を追加した (新 skill)skills/<name>/ を削除した (broken symlink が link_skills で自動 cleanup)zsh/, wezterm/, nvim/, git/, lazygit/, czg/, cz-git/, starship/ の構成を変えた./bootstrap.sh
Then run:
gh auth login
gh auth refresh -s project
git config --global ghq.root "$HOME/src"
mkdir -p "$HOME/.config/zsh" "$HOME/.config/wezterm" "$HOME/.config/codex/skills" "$HOME/.claude/skills"
./init.sh
既にあるマシンへ 追加分だけ 取り込むときは --no-upgrade を付ける。 付けないと brew bundle は古い formula をまとめて upgrade する。
git pull && brew bundle --file=./Brewfile --no-upgrade
./bootstrap.sh also installs Miniforge3 into ~/miniforge3 when missing. ./init.sh links zsh/env.zsh, which loads conda shell support without auto-activating base.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo "" >> "$HOME/.zprofile"
echo 'eval "$(/opt/homebrew/bin/brew shellenv zsh)"' >> "$HOME/.zprofile"
eval "$(/opt/homebrew/bin/brew shellenv zsh)"
# 公式以外の tap は Homebrew 7 から明示的な信頼が要る(~/.homebrew/trust.json, マシンごと)
brew trust olets/tap
# パッケージの一覧は Brewfile が正本(個別の brew install をここに増やさない)
brew bundle --file=./Brewfile
npm install -g git-cz czg cz-git
npx -y czg --api-key="sk-XXXX"
Clone this repo with ghq, then install:
gh auth login
gh auth refresh -s project
git config --global ghq.root "$HOME/src"
mkdir -p "$HOME/.config/zsh" "$HOME/.config/wezterm" "$HOME/.config/codex/skills" "$HOME/.claude/skills"
ghq get https://github.com/38kta-lab/dotfile
cd "$(ghq root)/github.com/38kta-lab/dotfile"
./install_miniforge.sh
./init.sh
gh auth refresh -s project is required on each machine where Codex or gh updates GitHub Projects, such as the Life project Status field.
# Codex CLI
npm install -g @openai/codex
mkdir -p ~/.config/codex
mv ~/.codex/* ~/.config/codex/
codex sign-in
agy) — Gemini CLI の後継Google は 2026-06-18 に個人 Google アカウント向けの Gemini CLI ログインを サーバ側で打ち切った(This client is no longer supported for Gemini Code Assist for individuals エラー)。後継の Antigravity CLI (agy) に移行する。
# 旧 gemini CLI が残っていれば削除
npm uninstall -g @google/gemini-cli 2>/dev/null; rm -rf ~/.gemini
# Antigravity CLI をインストール (Go 製 single binary、~/.local/bin/agy)
curl -fsSL https://antigravity.google/cli/install.sh | bash
# 初回起動でブラウザ認証 (要 Google アカウント。Pro 系モデルはサブスク必要)
agy
~/.local/bin は zsh/env.zsh で既に PATH 追加済み → 追加設定不要。agy / 非対話 agy -p "プロンプト" / モデル指定 agy --model <id> -p ... / 一覧 agy models / 継続 agy -c。Miniforge3 is installed under:
~/miniforge3
Install or verify it:
./install_miniforge.sh
Use a pinned Miniforge release when needed:
MINIFORGE_VERSION=25.11.0-0 ./install_miniforge.sh
The installer supports Apple Silicon and Intel macOS by selecting the matching installer from the official conda-forge/miniforge GitHub releases:
https://github.com/conda-forge/miniforge/releases
base should not auto-activate:
conda config --set auto_activate_base false
For per-repo environments, prefer environment.yml in that repo:
conda env create -f environment.yml
conda activate <env-name>
If an environment already exists:
conda env update -f environment.yml --prune
For Codex-assisted Calendar reads in the life repo, place the OAuth desktop client JSON at:
~/.config/life/google-calendar-credentials.json
Copy this file between personal Macs using a private secure channel. Do not commit it to git, and do not create a public/shared link.
On a new Mac:
mkdir -p "$HOME/.config/life"
mv "$HOME/Downloads/google-calendar-credentials.json" "$HOME/.config/life/google-calendar-credentials.json"
chmod 600 "$HOME/.config/life/google-calendar-credentials.json"
If the downloaded file has a client_secret_*.json name:
mkdir -p "$HOME/.config/life"
mv "$HOME/Downloads"/client_secret_*.json "$HOME/.config/life/google-calendar-credentials.json"
chmod 600 "$HOME/.config/life/google-calendar-credentials.json"
Do not copy this token between Macs:
~/.config/life/google-calendar-read-token.json
Generate that token separately on each Mac by running the Calendar reader from the life repo after activating its conda environment:
conda activate life
python scripts/google_calendar_read.py --format json
Global user Skills are managed in this repo under:
skills/
./init.sh links each directory under skills/ into both:
~/.config/codex/skills/
~/.claude/skills/
System Skills under ~/.config/codex/skills/.system/ are not managed here. Claude Code's auto memory under ~/.claude/projects/.../memory/ is also not managed here.
Use PRs with squash merge to keep main clean and reduce cross-machine conflicts. Rule of thumb: update main, but do not work directly on it.
Branch naming (one branch per machine):
work/<hostname> (example: work/kta38-mini-lab)Get <hostname> with:
hostname -s
Initial setup (first time on a machine):
HOST="$(hostname -s)"
git switch main
git pull --rebase
git switch -c "work/$HOST"
git push -u origin "work/$HOST"
Flow (manual):
# start work (every time)
git switch main
git pull --rebase
git switch work/<hostname>
git rebase main
# work + commit
git add -A
git commit -m "feat: ..."
git push -u origin work/<hostname>
Create a PR from work/<hostname> to main:
gh pr create --base main --head work/<hostname> --fill
Then Squash and merge it on GitHub, or with gh:
gh pr merge <PR_NUMBER> --squash
Merge commit (no squash):
gh pr merge <PR_NUMBER> --merge
Aliases (see zsh/alias.zsh):
winit # initial setup: create/push work/<hostname> branch
wmain # update main only (before switching)
wstart # start work: update main -> switch work/<hostname> -> rebase
wrebase # rebase current work branch onto main
prc # gh pr create --base main --head work/<hostname> --fill
prs # gh pr merge --squash
prm # gh pr merge --merge
Then on other machines:
git switch main
git pull --rebase
Notes:
work/<hostname> rebased onto main to avoid long-lived divergence.main.wezterm/keybinds.lua and linked to ~/.config/wezterm/keybinds.lua.skills/ and linked to both ~/.config/codex/skills/ and ~/.claude/skills/.~/.config/.czrc is not committed; see czrc/.czrc.example for a template.Keyboard
日本語 - ローマ字入力英字オフの時「英字」を入力Trackpad
MaxオンPointer
1つ大きくする#D05654#464758iCloud
オンDesktop
スタックを使用表示オプションを表示10種類名前36x36下から4番目Dock
左オンDefault browser
Chromehooks/register.ts 244 lines1import type { Register } from 'claude-code'
2
3// The ops session decides what to measure; the project sessions measure.
4// When ops runs an analysis itself, it has no control beside it and its
5// numbers leak into decisions unchecked. So in the ops session an analysis
6// command is held until the person allows it.
7const ANALYSIS_TOOLS = new Set([
8 // alignment and similarity search
9 'nucmer', 'promer', 'show-coords', 'delta-filter', 'dnadiff', 'mummer',
10 'blastn', 'blastp', 'blastx', 'tblastn', 'tblastx', 'makeblastdb', 'diamond', 'mmseqs',
11 'minimap2', 'bwa', 'bowtie2', 'hisat2',
12 // reads and variants
13 'samtools', 'bcftools', 'bedtools', 'mosdepth',
14 // assembly
15 'hifiasm', 'flye', 'canu', 'spades.py', 'metaMDBG', 'unicycler',
16 // annotation and profiles
17 'prodigal', 'prokka', 'bakta', 'dfast', 'exec_annotation', 'hmmsearch', 'hmmscan',
18 // phylogeny and alignment
19 'gtdbtk', 'iqtree', 'iqtree2', 'raxml-ng', 'FastTree', 'mafft', 'muscle', 'trimal',
20 // job submission
21 'sbatch', 'srun', 'vsub',
22])
23
24// Words that only wrap the command that follows them.
25const WRAPPERS = new Set(['sudo', 'time', 'nohup', 'exec', 'xargs', 'env', 'command', 'nice'])
26const ASKS_ONLY = /^(--?version|-v|--?help|-h)$/
27
28function strip(token: string): string {
29 return token.replace(/^[('"`$]+|[)'"`;]+$/g, '')
30}
31
32// The tools a shell command runs as commands (not names it only mentions).
33// Splits on the shell's own separators, quoted or not, so the command inside
34// `ssh host '...'` is split the same way.
35export function analysisTools(command: string): string[] {
36 const found: string[] = []
37 for (const segment of command.split(/\|\||&&|[;|\n]|\$\(|`/)) {
38 const tokens = segment.trim().split(/\s+/).map(strip).filter(Boolean)
39 let i = 0
40 while (i < tokens.length) {
41 const t = tokens[i]
42 if (WRAPPERS.has(t) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) { i++; continue }
43 if (t === 'ssh') { i++; while (i < tokens.length && tokens[i].startsWith('-')) i += 2; i++; continue }
44 if (t === 'conda' && tokens[i + 1] === 'run') { i += 2; while (i < tokens.length && tokens[i].startsWith('-')) i += 2; continue }
45 if (t === 'singularity' || t === 'apptainer') {
46 while (i < tokens.length && !tokens[i].endsWith('.sif')) i++
47 i++
48 continue
49 }
50 break
51 }
52 if (i >= tokens.length) continue
53 const name = tokens[i].split('/').pop() ?? ''
54 if (!ANALYSIS_TOOLS.has(name)) continue
55 const args = tokens.slice(i + 1)
56 if (args.length > 0 && args.every(a => ASKS_ONLY.test(a))) continue
57 found.push(name)
58 }
59 return [...new Set(found)]
60}
61
62// tasks.md: a line turned ✅ means a hub's Next Actions are now stale.
63// Rules.md says to fix the hub in the same turn; this tells the model so
64// right after the edit lands, which is when it can still act on it.
65const TASKS_PATH = /(^|\/)ideas\/task-review\/tasks\.md$/
66const NO_HUB = new Set(['事務'])
67
68// ✅ lines in `newText` beyond those already in `oldText`, counted as a
69// multiset so a second identical line still counts.
70export function newlyDone(oldText: string, newText: string): string[] {
71 const before = new Map<string, number>()
72 for (const l of oldText.split('\n').map(x => x.trim())) before.set(l, (before.get(l) ?? 0) + 1)
73 const out: string[] = []
74 for (const l of newText.split('\n').map(x => x.trim())) {
75 if (!l.startsWith('- ✅')) continue
76 const n = before.get(l) ?? 0
77 if (n > 0) before.set(l, n - 1)
78 else out.push(l)
79 }
80 return out
81}
82
83async function readText($: any, path: string): Promise<string | undefined> {
84 try {
85 return String(await $.fs.read(path))
86 } catch {
87 return undefined
88 }
89}
90
91function projects(lines: readonly string[]): string[] {
92 const tags = lines.flatMap(l => {
93 const m = l.match(/^- ✅ \[([^\]]+)\]/)
94 return m ? m[1].split('/') : []
95 })
96 return [...new Set(tags)].filter(t => !NO_HUB.has(t))
97}
98
99// Every line turned ✅ is also logged, with the minute, to a month file next
100// to tasks.md (done/YYYY-MM.md), so what was done when survives the morning
101// clean-up that deletes ✅ lines from tasks.md.
102function two(n: number): string {
103 return String(n).padStart(2, '0')
104}
105
106export function logLines(now: number, done: readonly string[]): { month: string; lines: string[] } {
107 const d = new Date(now)
108 const month = `${d.getFullYear()}-${two(d.getMonth() + 1)}`
109 const stamp = `${month}-${two(d.getDate())} ${two(d.getHours())}:${two(d.getMinutes())}`
110 return { month, lines: done.map(l => `- ${stamp} ${l.replace(/^- /, '')}`) }
111}
112
113async function logDone($: any, tasksPath: string, done: readonly string[]): Promise<void> {
114 const { month, lines } = logLines(await $.clock.now(), done)
115 const dir = tasksPath.replace(/[^/]*$/, '')
116 const path = `${dir}done/${month}.md`
117 let old = ''
118 try {
119 old = String(await $.fs.read(path))
120 } catch {
121 old = `# done ${month}\n\ntasks.md で ✅ にした行(ops-rules mod が ✅ の瞬間に書く)。\n\n`
122 }
123 await $.fs.write(path, old.replace(/\n*$/, '\n') + lines.join('\n') + '\n')
124}
125
126// ---- replies in Japanese ----------------------------------------------------
127// The person reads in Japanese. A rule in the system prompt asks for it every
128// turn; a turn whose answer still came out in English is followed, once, by an
129// automatic request to say the same thing again in Japanese.
130const JAPANESE: { id: string; text: string; scope: 'session' } = {
131 id: 'ops-rules:japanese',
132 text:
133 'user への返答は、作業の途中の一言も最終報告も、すべて日本語で書く。英語にするのは user が頼んだときだけ。' +
134 'コマンドの出力・コード・ファイル名が英語でも、説明の地の文は日本語で書く。長い作業の後の最終報告ほど英語になりやすいので、書き始める前に言語を確かめる。',
135 scope: 'session',
136}
137const RESTATE =
138 '(ops-rules 自動)直前の返答が英語でした。同じ内容を日本語で言い直してください。作業はやり直さず、ツールも使わないでください。'
139
140// Prose only: code blocks, inline code, URLs and paths say nothing about the
141// language of the reply. English when its words clearly outnumber the
142// Japanese characters; a short reply is never judged.
143export function looksEnglish(answer: string): boolean {
144 const prose = answer
145 .replace(/```[\s\S]*?```/g, ' ')
146 .replace(/`[^`\n]*`/g, ' ')
147 .replace(/https?:\/\/\S+/g, ' ')
148 .replace(/[\w.~-]*\/[\w.~/-]+/g, ' ')
149 const ja = (prose.match(/[\u3040-\u30ff\u3400-\u9fff]/g) ?? []).length
150 const words = (prose.match(/[A-Za-z]{2,}/g) ?? []).length
151 return words >= 25 && ja < words * 0.5
152}
153
154let restating = false
155
156const ALLOW = 'Allow once'
157const DENY = 'Deny'
158
159// The alert pane reads a mail's body with `alert_feed.py --body` to show it on
160// the terminal only. Run from the model's Bash, the body would land in the
161// model's context and go out with the next request, sensitive classes (人事・
162// 成績・査読) included. So the model may not run it, in any session. The pane's
163// own $.process.run is not a tool call and is not held here.
164export function readsMailBody(command: string): boolean {
165 return /alert_feed(\.py)?\b[^|;&\n]*--body\b/.test(command)
166}
167
168const MAIL_BODY_DENY =
169 'ops-rules: alert_feed.py --body is for the alert pane only (it shows a mail body on the terminal, sensitive ones included). ' +
170 'Do not run it, or read the body another way. To read a mail the person chose, use `alert_feed.py --show <id>`, which refuses sensitive classes.'
171
172export const register: Register = on => {
173 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
174 if (readsMailBody(e.command)) return { deny: MAIL_BODY_DENY }
175 const tools = analysisTools(e.command)
176 if (tools.length === 0) return next(e)
177 if ((await $.env.get('LIFE_ROLE')) !== 'ops') return next(e)
178 const what = tools.join(', ')
179 let answer: string
180 try {
181 answer = await $.ui.ask(
182 `ops-rules: the ops session is about to run an analysis (${what}). ops decides and does not measure. Allow it?`,
183 { header: 'ops', options: [DENY, ALLOW] },
184 )
185 } catch {
186 answer = DENY
187 }
188 if (answer === ALLOW) return next(e)
189 return {
190 deny:
191 `ops-rules: the person declined this analysis in the ops session (${what}). ` +
192 'Do not run it another way and do not hand it to another session on your own. ' +
193 'Tell the person what you wanted to measure and wait for their instruction.',
194 }
195 }).catch(($, e, next) => (next.called ? next(e) : { deny: 'ops-rules: its check failed, so the call was refused.' }))
196
197 on('prompt.compose', async ($, e, next) => {
198 const r = await next(e)
199 return { ...r, sections: [...r.sections.filter(x => x.id !== JAPANESE.id), JAPANESE] }
200 })
201
202 on('turn.complete', async ($, e, next) => {
203 const r = await next(e)
204 if (e.agentId !== undefined || e.reason !== 'answer') return r
205 if (restating) {
206 restating = false
207 return r
208 }
209 if (!looksEnglish(e.answer)) return r
210 restating = true
211 $.prompt.submit({ text: RESTATE }).catch(() => {
212 restating = false
213 })
214 return r
215 })
216
217 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
218 if (!TASKS_PATH.test(e.file_path)) return next(e)
219 // The edit's own strings say whether a ✅ was added, but an edit of part
220 // of a line carries only that part. The whole lines come from the file,
221 // read before and after; the strings stand in when it cannot be read.
222 const hint = newlyDone(e.old_string, e.new_string)
223 if (hint.length === 0) return next(e)
224 const before = await readText($, e.file_path)
225 const ran = await next(e)
226 if (ran.deny !== undefined || ran.isError) return ran
227 const after = before === undefined ? undefined : await readText($, e.file_path)
228 const whole = before !== undefined && after !== undefined ? newlyDone(before, after) : []
229 const done = whole.length > 0 ? whole : hint
230 try {
231 await logDone($, e.file_path, done)
232 } catch (err) {
233 $.ui.log(`ops-rules: could not log done lines: ${String(err)}`)
234 }
235 const pjs = projects(done)
236 if (pjs.length === 0) return ran
237 const hubs = pjs.map(p => `projects/active/${p}*.md`).join(', ')
238 const note =
239 `ops-rules: tasks.md で ✅ にした行があります(${done.join(' / ')})。` +
240 `Rules.md「記録の 3 層と tasks.md」により、このターンのうちに該当 hub(${hubs})の Next Actions から済んだ項目を外し、必要なら Current State を上書きしてください。`
241 return { ...ran, context: [...(ran.context ?? []), note] }
242 })
243}
244