@harnessforce/claude-code-plugin 0.1.0 → 0.3.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.
@@ -14,8 +14,9 @@ if (major >= 18) {
14
14
  // 繰り返し表示しないよう、session開始のhookだけが出す。
15
15
  if (process.argv[2] === "session-start") {
16
16
  process.stdout.write(
17
+ // biome-ignore lint/style/useTemplate: 古いNode.jsがES5として解析できる形を保つ。templateリテラルはES2015である。
17
18
  JSON.stringify({
18
- systemMessage: "Harnessforceのhookには18以上のNode.jsが必要です",
19
+ systemMessage: "Harnessforce hooks require Node.js 18 or later",
19
20
  }) + "\n"
20
21
  );
21
22
  }
@@ -1,85 +1,85 @@
1
1
  ---
2
2
  name: propose-improvements
3
- description: /harnessforce:tuneの分析結果から、人の介入、ループにできる繰り返し、MCP server、使い方(contextの圧縮、cacheの再利用、modelの使い分け)の改善の提案を作り、harnessforce tune recordで記録してから表示する。/harnessforce:tuneが「提案を作れます」と判定した対象にだけ使う。ファイルは書き換えない。
3
+ description: From the analysis results of /harnessforce:tune, make suggested improvements for human interventions, repeatable loops, MCP servers, and usage (context compaction, cache reuse, model choice), record them with harnessforce tune record, and then show them. Use it only for targets that /harnessforce:tune marks as "a suggestion can be made". Does not modify files.
4
4
  ---
5
5
 
6
- # 分析結果から改善の提案を作る
6
+ # Make improvement suggestions from the analysis results
7
7
 
8
- `/harnessforce:tune`の`summarize.mjs`が出した「提案の対象」のうち、「提案を作れます」の対象ごとに提案を作る。提案は利用者がそのまま適用できる形にし、記録してから表示する。
8
+ For each target marked "a suggestion can be made" in the "Targets for suggestions" printed by `summarize.mjs` of `/harnessforce:tune`, make a suggestion. Make each suggestion in a form the user can apply as it is, and record it before showing it.
9
9
 
10
- ## 書き換えない
10
+ ## Do not modify anything
11
11
 
12
- - 利用者のファイル、設定、repository、managed settingsを書き換えない。Edit、Writeのtool、`git apply`、`git commit`、branchの作成、`gh pr create`を使わない。差分とPull Requestの下書きは表示するだけである。
13
- - 書き込むのは、`harnessforce tune record`が`~/.harnessforce/tune/`へ書く記録だけである。一時fileも作らない。
12
+ - Do not modify the user's files, settings, repository, or managed settings. Do not use the Edit or Write tools, `git apply`, `git commit`, creating a branch, or `gh pr create`. The diff and the Pull Request draft are only shown.
13
+ - The only thing written is the record that `harnessforce tune record` writes to `~/.harnessforce/tune/`. Do not create temporary files either.
14
14
 
15
- ## 作る条件
15
+ ## Conditions for making a suggestion
16
16
 
17
- `summarize.mjs`の判定が正である。「データ不足」と「未計測のため提案しません」の対象には提案を作らない。判定の条件は次のとおりである。
17
+ The result of `summarize.mjs` is authoritative. Make no suggestion for a target marked "Not enough data" or "not measured, so no suggestion is made". The conditions are as follows.
18
18
 
19
- | 対象 | 提案を作る最小の条件 |
19
+ | Target | Minimum condition for a suggestion |
20
20
  | --- | --- |
21
- | すべて | 分析した範囲に10 session以上ある |
22
- | 人の介入 | その`intervention.kind`が、3 session以上で合計10回以上ある |
23
- | ループにできる繰り返し | その`loop.kind`が、合計3回以上ある |
24
- | MCP server | `configured`のsessionが10以上ある |
25
- | 使い方の`frequent_compaction`、`low_cache_reuse` | その`usage.kind`に当たるsessionが3以上ある |
26
- | 使い方の`model_choice` | `models`を持つsessionが10以上あり、分析した範囲の`models`の合計で、1つの`message.model`の値が`output_tokens`の合計の90%以上を占める |
21
+ | All | The analyzed range has at least 10 sessions |
22
+ | Human interventions | That `intervention.kind` appears in at least 3 sessions and at least 10 times in total |
23
+ | Repeatable loops | That `loop.kind` appears at least 3 times in total |
24
+ | MCP servers | At least 10 sessions have `configured` |
25
+ | `frequent_compaction` and `low_cache_reuse` of usage | At least 3 sessions match that `usage.kind` |
26
+ | `model_choice` of usage | At least 10 sessions have `models`, and in the total of `models` over the analyzed range, one `message.model` value accounts for at least 90% of the total `output_tokens` |
27
27
 
28
- - 未計測(`not_measured`)の値を根拠にしない。未計測を0として扱わない。
29
- - 使い方の提案の変更の種類は、`claude_md`、`rule`、`skill`、`agent`、`command`に限る(例: 肥大したCLAUDE.mdを分ける、探索をsubagentへ移してそのagentの`model`を指定する)。settingsの`model`など、他の変更の種類にしない。`harnessforce tune record`は他の変更の種類を拒否する。`claude_md`の変更は適用を検出しないため、前回の提案の前後にも現れない。
30
- - 使い方の値(`usage`)は端末だけの値であり、Harnessforceへ送られない。提案の根拠には使ってよい。
31
- - ハーネスの構成(skill、rule、agent、command、hook、MCP server、modelのversionごとの成果の差)を対象にした提案は作らない。
32
- - 調べても、利用者がそのまま適用できる変更が見つからなければ、その対象の提案を作らず、理由を1行で伝える。
28
+ - Do not use a value that was not measured (`not_measured`) as evidence. Do not treat "not measured" as 0.
29
+ - The change type of a usage suggestion is limited to `claude_md`, `rule`, `skill`, `agent`, and `command` (for example, split up a bloated CLAUDE.md, or move exploration to a subagent and specify that agent's `model`). Do not use another change type such as the `model` in settings. `harnessforce tune record` rejects other change types. A change to `claude_md` is not detected when applied, so it does not appear in the before and after of the previous suggestion either.
30
+ - Usage values (`usage`) stay on this device and are not sent to Harnessforce. You may use them as evidence for a suggestion.
31
+ - Do not make a suggestion about the harness configuration itself (differences in results between skills, rules, agents, commands, hooks, MCP servers, and model versions).
32
+ - If you investigate and find no change the user can apply as it is, make no suggestion for that target and say why in one line.
33
33
 
34
- ## 根拠を集める
34
+ ## Gather evidence
35
35
 
36
- - 対象の「根拠のsession」の`transcript_path`の記録を、提案に必要な部分だけ読む。人の介入なら、その種類のpromptと、直前のagentの応答を読む。ループなら、一巡の手順(Issueの取得、修正、test、push、Pull Requestの作成など)と、人が介入した時点を読む。MCP serverなら、呼び出しと失敗を読む。使い方なら、`frequent_compaction`は圧縮の前の指示と経緯、`low_cache_reuse`はcacheが再利用されなかった応答の前後、`model_choice`は本体が行っていた探索や定型の作業を読む。
37
- - 「前回の提案の前後」で「良くなった向きへ動いていません」と示された提案と同じ対象の提案を作るときは、その前後を根拠に示し、前回と同じ差分を繰り返さない。
38
- - 適用先の現在の定義(settings、skill、rule、agent、command、hook、`.mcp.json`、`~/.claude.json`の`mcpServers`、CLAUDE.md)を読み、差分を現在の内容に対して作る。
36
+ - Read only the parts you need of the record at the `transcript_path` of the target's evidence sessions. For human interventions, read the prompts of that kind and the agent response just before them. For loops, read the steps of one cycle (fetching the Issue, fixing, testing, pushing, creating the Pull Request, and so on) and the points where a person intervened. For MCP servers, read the calls and failures. For usage, read the instructions and the course of events before compaction for `frequent_compaction`, the responses before and after where the cache was not reused for `low_cache_reuse`, and the exploration and routine work done by the main agent for `model_choice`.
37
+ - When you make a suggestion for the same target as a suggestion that "Before and after the previous suggestion" shows as "Did not move in the improving direction", cite that before and after as evidence and do not repeat the same diff as last time.
38
+ - Read the current definition of the target (settings, skill, rule, agent, command, hook, `.mcp.json`, `mcpServers` in `~/.claude.json`, CLAUDE.md) and make the diff against the current content.
39
39
 
40
- ## 形式
40
+ ## Format
41
41
 
42
- 提案ごとに次を持たせる。
42
+ Give each suggestion the following.
43
43
 
44
- | 項目 | 内容 |
44
+ | Item | Content |
45
45
  | --- | --- |
46
- | 対象のカテゴリ | `intervention.kind=<kind>`、`loop.kind=<kind>`、`mcp_server=<識別子>`、`usage.kind=<kind>`のいずれか。`usage.kind`の`<kind>`は`frequent_compaction`、`low_cache_reuse`、`model_choice`のいずれか |
47
- | 根拠 | 期間、回数、該当したsessionの時刻と、記録の抜粋。抜粋は端末にだけ表示する |
48
- | 変更の種類 | `permissions`、`hook`、`skill`、`rule`、`agent`、`command`、`loop_prompt`、`mcp_config`、`claude_md`のいずれか |
49
- | 適用先 | 利用者のscope、repositoryのscope、managed settingsのいずれかと、ファイルのpath |
50
- | 変更 | そのまま適用できるunified diff。repositoryのscopeなら、branch名、title、本文、diffからなるPull Requestの下書き。managed settingsなら、Owner/Adminが貼り付ける差分 |
51
- | 期待する効果 | どのカテゴリの値が減るか、どの既存の指標(CIの失敗率、手戻り率、完了Issueあたりのcost)が変わるか |
52
- | 測り方 | 適用で変わるComponentVersion(種類と識別子)と、比較に使う指標 |
46
+ | Target category | One of `intervention.kind=<kind>`, `loop.kind=<kind>`, `mcp_server=<identifier>`, `usage.kind=<kind>`. The `<kind>` of `usage.kind` is one of `frequent_compaction`, `low_cache_reuse`, `model_choice` |
47
+ | Evidence | The period, the counts, the times of the matching sessions, and excerpts of the records. Excerpts are shown on this device only |
48
+ | Change type | One of `permissions`, `hook`, `skill`, `rule`, `agent`, `command`, `loop_prompt`, `mcp_config`, `claude_md` |
49
+ | Apply to | The user scope, the repository scope, or managed settings, and the file path |
50
+ | Change | A unified diff that can be applied as it is. For the repository scope, a Pull Request draft made of the branch name, the title, the body, and the diff. For managed settings, the diff that an Owner or Admin pastes |
51
+ | Expected effect | Which category value decreases, and which existing metric (CI failure rate, rework rate, cost per completed Issue) changes |
52
+ | How to measure | The ComponentVersion that changes when applied (kind and identifier), and the metric used for comparison |
53
53
 
54
- 変更の種類と、適用で変わるcomponentの対応は次のとおりである。
54
+ The change type and the component that changes when applied correspond as follows.
55
55
 
56
- | 変更の種類 | componentの`kind` | componentの識別子(`id`) | 期待する内容 |
56
+ | Change type | Component `kind` | Component identifier (`id`) | Expected content |
57
57
  | --- | --- | --- | --- |
58
- | `permissions` | `permissions` | `permissions` | 適用後のsettingsの`permissions`の値全体(`value`) |
59
- | `hook` | `hook` | hookのevent名 | 適用後のsettingsの`hooks`のそのeventの値(`value`) |
60
- | `skill` | `skill` | skillのdirectory名 | 適用後の`SKILL.md`の内容全体(`content`) |
61
- | `rule` | `rule` | `CLAUDE.md`か`rules/<path>` | 適用後のfileの内容全体(`content`) |
62
- | `agent` | `agent` | `agents/`からのpathの`.md`を除き`/`を`:`にした名前 | 適用後のfileの内容全体(`content`) |
63
- | `command` | `command` | `commands/`からのpathの`.md`を除き`/`を`:`にした名前 | 適用後のfileの内容全体(`content`) |
64
- | `loop_prompt` | 書くfileの形式に従い`command`か`skill` | 上の`command`か`skill`と同じ | 適用後のfileの内容全体(`content`) |
65
- | `mcp_config` | `mcp_server`(serverを削除する差分ではcomponentを持たない) | server名 | 適用後のserverの定義(`value`)。削除では適用後のfileの内容全体(`content`) |
66
- | `claude_md` | 無し | 無し | 適用後のCLAUDE.mdの内容全体(`content`) |
58
+ | `permissions` | `permissions` | `permissions` | The whole `permissions` value of the settings after applying (`value`) |
59
+ | `hook` | `hook` | The event name of the hook | The value of that event under `hooks` in the settings after applying (`value`) |
60
+ | `skill` | `skill` | The directory name of the skill | The whole content of `SKILL.md` after applying (`content`) |
61
+ | `rule` | `rule` | `CLAUDE.md` or `rules/<path>` | The whole content of the file after applying (`content`) |
62
+ | `agent` | `agent` | The path from `agents/` without `.md`, with `/` replaced by `:` | The whole content of the file after applying (`content`) |
63
+ | `command` | `command` | The path from `commands/` without `.md`, with `/` replaced by `:` | The whole content of the file after applying (`content`) |
64
+ | `loop_prompt` | `command` or `skill`, following the format of the file written | Same as `command` or `skill` above | The whole content of the file after applying (`content`) |
65
+ | `mcp_config` | `mcp_server` (a diff that deletes the server has no component) | The server name | The definition of the server after applying (`value`). For deletion, the whole content of the file after applying (`content`) |
66
+ | `claude_md` | None | None | The whole content of CLAUDE.md after applying (`content`) |
67
67
 
68
- componentの`source`は適用先のscope(`user`、`repository`、`local`、`managed`)とする。`.mcp.json`のserverは`repository`、`~/.claude.json`の最上位の`mcpServers`は`user`、その`projects`の下は`local`である。
68
+ The component `source` is the scope it applies to (`user`, `repository`, `local`, `managed`). A server in `.mcp.json` is `repository`, the top-level `mcpServers` of `~/.claude.json` is `user`, and the one under its `projects` is `local`.
69
69
 
70
- ## 許可を減らす提案
70
+ ## Suggestions to reduce permissions
71
71
 
72
- - 権限の確認への応答を減らす提案は、確認を求められたtoolと、その許可の内訳を根拠にする。許可するのは、根拠に現れたtoolとcommandの最小の範囲だけとする(例: `Bash(pnpm test:*)`、`Bash(git status)`、`mcp__github__get_issue`)。
73
- - すべてのコマンドを許可する規則を提案しない。`Bash`、`Bash(*)`、`Bash(:*)`、`*`のような、tool全体やすべてのcommandへの許可、`"defaultMode": "bypassPermissions"`、`--dangerously-skip-permissions`は、どの提案にも含めない。
72
+ - A suggestion to reduce answers to permission prompts takes the tools that asked for confirmation and the breakdown of their permissions as evidence. Allow only the smallest range of tools and commands that appeared in the evidence (for example `Bash(pnpm test:*)`, `Bash(git status)`, `mcp__github__get_issue`).
73
+ - Do not suggest a rule that allows all commands. Do not include in any suggestion a permission for a whole tool or all commands such as `Bash`, `Bash(*)`, `Bash(:*)`, or `*`, nor `"defaultMode": "bypassPermissions"` or `--dangerously-skip-permissions`.
74
74
 
75
- ## ループの提案
75
+ ## Suggestions for loops
76
76
 
77
- - ループにできる繰り返しの提案は、commandかskillの形式のprompt(`loop_prompt`)とし、止める条件を必ず含める。止める条件は、最大の繰り返し回数と、人が確認する時点(例: Pull Requestの作成前、同じ失敗が2回続いたとき)の両方とする。
78
- - promptは、根拠のsessionで人が介入していた時点を、止める条件か、agentが自分で行う手順のどちらかに置き換える。
77
+ - A suggestion for a repeatable loop is a prompt in the form of a command or a skill (`loop_prompt`), and it must include stopping conditions. The stopping conditions are both the maximum number of repetitions and the points where a person checks (for example before creating a Pull Request, or when the same failure happens twice in a row).
78
+ - In the prompt, replace each point where a person intervened in the evidence sessions with either a stopping condition or a step the agent does by itself.
79
79
 
80
- ## 記録してから表示する
80
+ ## Record, then show
81
81
 
82
- 提案を表示する前に、提案ごとに`harnessforce tune record`を実行し、stdinへUTF-8のJSONのobjectを1つ渡す。入力はheredocで渡し、fileに書かない。区切りの語は、入力に現れない語にする。
82
+ Before showing a suggestion, run `harnessforce tune record` for each suggestion and pass one UTF-8 JSON object on stdin. Pass the input with a heredoc and do not write it to a file. Use a delimiter word that does not appear in the input.
83
83
 
84
84
  ```sh
85
85
  harnessforce tune record <<'HF_TUNE_RECORD_7f3c'
@@ -87,19 +87,19 @@ harnessforce tune record <<'HF_TUNE_RECORD_7f3c'
87
87
  HF_TUNE_RECORD_7f3c
88
88
  ```
89
89
 
90
- | 項目 | 必須 | 内容 |
90
+ | Item | Required | Content |
91
91
  | --- | --- | --- |
92
- | `category` | ✓ | 対象のカテゴリ |
93
- | `change_type` | ✓ | 変更の種類 |
94
- | `scope` | ✓ | `user`、`repository`、`local`、`managed`のいずれか |
95
- | `path` | ✓ | 変更が書くfileの絶対path |
96
- | `project_root` | `scope`が`repository`か`local`なら必須 | 適用先のrepositoryのrootの絶対path。それ以外では渡さない |
97
- | `component` | 上の対応表でcomponentを持つ変更なら必須 | `kind`、`source`、`id` |
98
- | `content` | `component`の`kind`が`rule`、`skill`、`agent`、`command`の場合と、componentを持たない場合に必須 | 適用後の`path`のfileの内容全体 |
99
- | `value` | `component`の`kind`が`hook`、`permissions`、`mcp_server`の場合に必須 | 適用後のそのcomponentのsettingsの値(JSON) |
100
- | `evidence_session_ids` | ✓ | 根拠のsessionのsession ID(1件以上)。「根拠のsession」に表示されたものだけを使う |
101
- | `body` | ✓ | 提案の本文(markdown)。根拠、変更、期待する効果、測り方 |
102
-
103
- - `harnessforce tune record`が終了コード0で終わった提案だけを表示する。0以外で終わった提案は表示せず、stderrの文言を伝える。
104
- - 同じ提案は同じ入力で記録する。同じ`category`、`change_type`、`path`、適用後の内容なら、何回記録しても1件として数えられる。
105
- - 表示では、対象のカテゴリ、根拠、変更の種類、適用先、変更、期待する効果、測り方を、この順に示す。
92
+ | `category` | yes | The target category |
93
+ | `change_type` | yes | The change type |
94
+ | `scope` | yes | One of `user`, `repository`, `local`, `managed` |
95
+ | `path` | yes | The absolute path of the file the change writes |
96
+ | `project_root` | required when `scope` is `repository` or `local` | The absolute path of the root of the repository it applies to. Do not pass it otherwise |
97
+ | `component` | required for a change that has a component in the table above | `kind`, `source`, `id` |
98
+ | `content` | required when the `kind` of `component` is `rule`, `skill`, `agent`, or `command`, and when there is no component | The whole content of the file at `path` after applying |
99
+ | `value` | required when the `kind` of `component` is `hook`, `permissions`, or `mcp_server` | The settings value (JSON) of that component after applying |
100
+ | `evidence_session_ids` | yes | The session IDs of the evidence sessions (at least 1). Use only those shown in the evidence sessions |
101
+ | `body` | yes | The body of the suggestion (markdown): the evidence, the change, the expected effect, and how to measure |
102
+
103
+ - Show only the suggestions for which `harnessforce tune record` ended with exit code 0. Do not show a suggestion that ended with a non-zero code, and tell the user the message on stderr.
104
+ - Record the same suggestion with the same input. With the same `category`, `change_type`, `path`, and content after applying, it counts as one no matter how many times it is recorded.
105
+ - When showing, present the target category, the evidence, the change type, where to apply, the change, the expected effect, and how to measure, in this order.
@@ -3,9 +3,9 @@
3
3
  // Node.js 18で依存なしに動かすため、plainなESMで書く。
4
4
  import { spawnSync } from "node:child_process";
5
5
 
6
- const USAGE = "使い方: /harnessforce:tune [--all] [--no-send]";
6
+ const USAGE = "Usage: /harnessforce:tune [--all] [--no-send]";
7
7
  const HF_MISSING =
8
- "`harnessforce`が見つかりません。`/harnessforce:setup`でCLIを導入してください";
8
+ "`harnessforce` was not found. Install the CLI with `/harnessforce:setup`";
9
9
  const ALLOWED_FLAGS = new Set(["--all", "--no-send"]);
10
10
 
11
11
  const MINIMUM_SESSIONS = 10;
@@ -54,7 +54,7 @@ const median = (values) => {
54
54
  : (sorted[middle - 1] + sorted[middle]) / 2;
55
55
  };
56
56
 
57
- const secondsText = (value) => (value === null ? "—" : `${value.toFixed(1)}秒`);
57
+ const secondsText = (value) => (value === null ? "—" : `${value.toFixed(1)}s`);
58
58
 
59
59
  const newestFirst = (a, b) =>
60
60
  a.session.started_at < b.session.started_at
@@ -116,13 +116,13 @@ function mcpServers(sessions) {
116
116
 
117
117
  function evidenceLines(hits, unit) {
118
118
  const sorted = [...hits].sort(newestFirst);
119
- const lines = [" 根拠のsession(新しい順):"];
119
+ const lines = [" Evidence sessions (newest first):"];
120
120
  for (const { session, count } of sorted.slice(0, MAX_EVIDENCE))
121
121
  lines.push(
122
122
  ` ${session.session_id} ${session.started_at} ${session.repository ?? "-"} sendable=${session.sendable} ${count}${unit} ${session.transcript_path}`,
123
123
  );
124
124
  if (sorted.length > MAX_EVIDENCE)
125
- lines.push(` ほか${sorted.length - MAX_EVIDENCE}件`);
125
+ lines.push(` and ${sorted.length - MAX_EVIDENCE} more`);
126
126
  return lines;
127
127
  }
128
128
 
@@ -130,7 +130,7 @@ const missing = (parts) =>
130
130
  parts
131
131
  .filter(([amount]) => amount > 0)
132
132
  .map(([amount, unit]) => `${amount}${unit}`)
133
- .join("、");
133
+ .join(", ");
134
134
 
135
135
  const percent = (share) => `${(share * 100).toFixed(1)}%`;
136
136
 
@@ -165,13 +165,13 @@ function modelShares(sessions) {
165
165
 
166
166
  const sharesText = (shares) =>
167
167
  shares === null
168
- ? "なし"
168
+ ? "none"
169
169
  : [
170
- ...shares.main.map((m) => `本体 ${m.model} ${percent(m.share)}`),
170
+ ...shares.main.map((m) => `main ${m.model} ${percent(m.share)}`),
171
171
  ...shares.subagent.map(
172
172
  (m) => `subagent ${m.model} ${percent(m.share)}`,
173
173
  ),
174
- ].join("、");
174
+ ].join(", ");
175
175
 
176
176
  // 「作る条件」の`model_choice`: 分析した範囲の`models`の合計で、output_tokensが最も多いmodelとその割合。
177
177
  function topMainModel(sessions) {
@@ -190,7 +190,7 @@ function topMainModel(sessions) {
190
190
 
191
191
  const valueText = (value) =>
192
192
  value === null
193
- ? "値なし"
193
+ ? "no value"
194
194
  : typeof value === "number"
195
195
  ? String(Number(value.toFixed(3)))
196
196
  : sharesText(value);
@@ -203,17 +203,19 @@ function improvesBy(category) {
203
203
  function renderFollowups(followups) {
204
204
  if (!Array.isArray(followups) || followups.length === 0) return [];
205
205
  const lines = [
206
- "前回の提案の前後(前後の差(因果を示しません)。起点は適用を検出した時刻です):",
206
+ "Before and after the previous suggestion (Before/after difference (does not show causation). The starting point is the time the application was detected):",
207
207
  ];
208
208
  for (const f of followups) {
209
209
  lines.push(
210
- `- ${f.category}(${f.change_type} ${f.path})起点 ${f.origin}`,
211
- ` 前 ${f.before.from}〜${f.before.to}: ${f.before.sessions} session、${valueText(f.before.value)}`,
212
- ` 後 ${f.after.from}〜${f.after.to}: ${f.after.sessions} session、${valueText(f.after.value)}`,
210
+ `- ${f.category} (${f.change_type} ${f.path}) starting point ${f.origin}`,
211
+ ` Before ${f.before.from} to ${f.before.to}: ${f.before.sessions} session(s), ${valueText(f.before.value)}`,
212
+ ` After ${f.after.from} to ${f.after.to}: ${f.after.sessions} session(s), ${valueText(f.after.value)}`,
213
213
  );
214
- if (f.no_comparison_data) lines.push(" 比較データなし");
214
+ if (f.no_comparison_data) lines.push(" No comparison data");
215
215
  if (f.before_outside_range)
216
- lines.push(" 前の期間の一部が分析の範囲の外です");
216
+ lines.push(
217
+ " Part of the earlier period is outside the analysis range",
218
+ );
217
219
  const direction = improvesBy(f.category);
218
220
  const isImproved =
219
221
  direction === "decrease"
@@ -221,7 +223,7 @@ function renderFollowups(followups) {
221
223
  : f.after.value > f.before.value;
222
224
  if (direction !== undefined && !f.no_comparison_data && !isImproved)
223
225
  lines.push(
224
- " 良くなった向きへ動いていません。同じ対象の提案を作るときの根拠にします",
226
+ " Did not move in the improving direction. Use this as the basis when making a suggestion for the same target",
225
227
  );
226
228
  }
227
229
  lines.push("");
@@ -235,18 +237,18 @@ function usageTargets(sessions) {
235
237
  for (const kind of SESSION_USAGE_KINDS) {
236
238
  const category = `usage.kind=${kind}`;
237
239
  if (withUsage.length === 0) {
238
- lines.push(`- ${category}: 未計測のため提案しません`);
240
+ lines.push(`- ${category}: not measured, so no suggestion is made`);
239
241
  continue;
240
242
  }
241
243
  const hits = withUsage.filter((s) => s.usage.kinds.includes(kind));
242
244
  if (hits.length < USAGE_MIN_SESSIONS) {
243
245
  lines.push(
244
- `- ${category}: データ不足: 当たるsessionが${USAGE_MIN_SESSIONS}以上必要です(あと${USAGE_MIN_SESSIONS - hits.length} session)`,
246
+ `- ${category}: Not enough data: needs at least ${USAGE_MIN_SESSIONS} matching sessions (${USAGE_MIN_SESSIONS - hits.length} more session(s) needed)`,
245
247
  );
246
248
  continue;
247
249
  }
248
250
  lines.push(
249
- `- ${category}: 提案を作れます(${hits.length} session)`,
251
+ `- ${category}: a suggestion can be made (${hits.length} session(s))`,
250
252
  ...evidenceLines(
251
253
  hits.map((session) => ({
252
254
  session,
@@ -256,8 +258,8 @@ function usageTargets(sessions) {
256
258
  : Number(session.usage.cache_reuse_ratio.toFixed(3)),
257
259
  })),
258
260
  kind === "frequent_compaction"
259
- ? "回の自動圧縮"
260
- : "(cacheの再利用の割合)",
261
+ ? " auto-compaction(s)"
262
+ : " (cache reuse ratio)",
261
263
  ),
262
264
  );
263
265
  }
@@ -266,24 +268,24 @@ function usageTargets(sessions) {
266
268
  (s) => Array.isArray(s.usage.models) && s.usage.models.length > 0,
267
269
  );
268
270
  if (withUsage.length === 0) {
269
- lines.push(`- ${category}: 未計測のため提案しません`);
271
+ lines.push(`- ${category}: not measured, so no suggestion is made`);
270
272
  return lines;
271
273
  }
272
274
  if (withModels.length < MODEL_CHOICE_MIN_SESSIONS) {
273
275
  lines.push(
274
- `- ${category}: データ不足: modelsを持つsessionが${MODEL_CHOICE_MIN_SESSIONS}以上必要です(あと${MODEL_CHOICE_MIN_SESSIONS - withModels.length} session)`,
276
+ `- ${category}: Not enough data: needs at least ${MODEL_CHOICE_MIN_SESSIONS} sessions with models (${MODEL_CHOICE_MIN_SESSIONS - withModels.length} more session(s) needed)`,
275
277
  );
276
278
  return lines;
277
279
  }
278
280
  const top = topMainModel(withModels);
279
281
  if (top === undefined || top.share < MODEL_CHOICE_MIN_SHARE) {
280
282
  lines.push(
281
- `- ${category}: 提案しません: 1つのmodelが本体のoutput_tokensの${MODEL_CHOICE_MIN_SHARE * 100}%以上を占めていません${top ? `(最大 ${top.model} ${percent(top.share)})` : ""}`,
283
+ `- ${category}: not suggested: no single model accounts for ${MODEL_CHOICE_MIN_SHARE * 100}% or more of the main output_tokens${top ? ` (largest: ${top.model} ${percent(top.share)})` : ""}`,
282
284
  );
283
285
  return lines;
284
286
  }
285
287
  lines.push(
286
- `- ${category}: 提案を作れます(${withModels.length} session、${top.model} ${percent(top.share)})`,
288
+ `- ${category}: a suggestion can be made (${withModels.length} session(s), ${top.model} ${percent(top.share)})`,
287
289
  ...evidenceLines(
288
290
  withModels.map((session) => ({
289
291
  session,
@@ -291,7 +293,7 @@ function usageTargets(sessions) {
291
293
  session.usage.models.find((m) => m.model === top.model)
292
294
  ?.output_tokens ?? 0,
293
295
  })),
294
- ` output_tokens(${top.model})`,
296
+ ` output_tokens (${top.model})`,
295
297
  ),
296
298
  );
297
299
  return lines;
@@ -300,9 +302,9 @@ function usageTargets(sessions) {
300
302
  function render(output) {
301
303
  const { sessions } = output;
302
304
  const lines = [
303
- `分析したsession: ${output.session_count}件(analyzer ${output.analyzer_version}、parser ${output.parser_version})`,
305
+ `Sessions analyzed: ${output.session_count} (analyzer ${output.analyzer_version}, parser ${output.parser_version})`,
304
306
  "",
305
- "人の介入(回数、介入のあったsession数、待ち時間の中央値):",
307
+ "Human interventions (count, sessions with interventions, median wait time):",
306
308
  ];
307
309
  const interventions = INTERVENTION_KINDS.map((kind) => ({
308
310
  kind,
@@ -316,13 +318,13 @@ function render(output) {
316
318
  for (const { kind, result } of interventions)
317
319
  lines.push(
318
320
  result === undefined
319
- ? ` ${kind}: 未計測`
320
- : ` ${kind}: ${result.total}回、${result.hits.length} session、${secondsText(result.median)}`,
321
+ ? ` ${kind}: not measured`
322
+ : ` ${kind}: ${result.total} time(s), ${result.hits.length} session(s), ${secondsText(result.median)}`,
321
323
  );
322
324
 
323
325
  lines.push(
324
326
  "",
325
- "ループにできる繰り返し(回数、その間の介入、所要時間の中央値):",
327
+ "Repeatable loops (count, interventions during them, median duration):",
326
328
  );
327
329
  const loops = LOOP_KINDS.map((kind) => ({
328
330
  kind,
@@ -335,7 +337,7 @@ function render(output) {
335
337
  }));
336
338
  for (const { kind, result } of loops) {
337
339
  if (result === undefined) {
338
- lines.push(` ${kind}: 未計測`);
340
+ lines.push(` ${kind}: not measured`);
339
341
  continue;
340
342
  }
341
343
  const count = result.measured.reduce(
@@ -343,98 +345,98 @@ function render(output) {
343
345
  0,
344
346
  );
345
347
  lines.push(
346
- ` ${kind}: ${result.total}回、介入${count}回、${secondsText(result.median)}`,
348
+ ` ${kind}: ${result.total} time(s), ${count} intervention(s), ${secondsText(result.median)}`,
347
349
  );
348
350
  }
349
351
 
350
- lines.push("", "MCP server(設定されていたsession数、呼び出し、失敗):");
352
+ lines.push("", "MCP servers (sessions where configured, calls, failures):");
351
353
  const servers = mcpServers(sessions);
352
- if (servers.length === 0) lines.push(" (記録にMCP serverがありません)");
354
+ if (servers.length === 0) lines.push(" (no MCP servers in the records)");
353
355
  for (const s of servers)
354
356
  lines.push(
355
- ` ${s.server}: 設定されていたsession ${s.configured.length}件、${s.calls === undefined ? "呼び出し: 未計測" : `呼び出し${s.calls}回`}、${s.failures === undefined ? "失敗: 未計測" : `失敗${s.failures}回`}`,
357
+ ` ${s.server}: configured in ${s.configured.length} session(s), ${s.calls === undefined ? "calls: not measured" : `${s.calls} call(s)`}, ${s.failures === undefined ? "failures: not measured" : `${s.failures} failure(s)`}`,
356
358
  );
357
359
 
358
360
  const withUsage = sessions.filter((s) => s.usage);
359
- lines.push("", "使い方(当たるsession):");
361
+ lines.push("", "Usage (matching sessions):");
360
362
  for (const kind of SESSION_USAGE_KINDS)
361
363
  lines.push(
362
364
  withUsage.length === 0
363
- ? ` ${kind}: 未計測`
364
- : ` ${kind}: ${withUsage.filter((s) => s.usage.kinds.includes(kind)).length} session`,
365
+ ? ` ${kind}: not measured`
366
+ : ` ${kind}: ${withUsage.filter((s) => s.usage.kinds.includes(kind)).length} session(s)`,
365
367
  );
366
368
  lines.push(
367
- ` model(output_tokensの割合): ${sharesText(modelShares(withUsage))}`,
369
+ ` model (share of output_tokens): ${sharesText(modelShares(withUsage))}`,
368
370
  );
369
371
 
370
372
  lines.push("", ...renderFollowups(output.followups));
371
373
  if (!output.sufficient) {
372
374
  lines.push(
373
- `データ不足のため提案を作りません: 分析したsessionは${output.session_count}件です。提案には${MINIMUM_SESSIONS}件以上が必要です(あと${MINIMUM_SESSIONS - output.session_count}件)`,
375
+ `Not enough data, so no suggestions are made: ${output.session_count} session(s) analyzed. Suggestions need at least ${MINIMUM_SESSIONS} (${MINIMUM_SESSIONS - output.session_count} more needed)`,
374
376
  );
375
377
  return lines;
376
378
  }
377
379
 
378
- lines.push("提案の対象:");
380
+ lines.push("Targets for suggestions:");
379
381
  for (const { kind, result } of interventions) {
380
382
  const category = `intervention.kind=${kind}`;
381
383
  if (result === undefined) {
382
- lines.push(`- ${category}: 未計測のため提案しません`);
384
+ lines.push(`- ${category}: not measured, so no suggestion is made`);
383
385
  continue;
384
386
  }
385
387
  const lacking = missing([
386
- [INTERVENTION_MIN_SESSIONS - result.hits.length, " session"],
387
- [INTERVENTION_MIN_COUNT - result.total, "回"],
388
+ [INTERVENTION_MIN_SESSIONS - result.hits.length, " session(s)"],
389
+ [INTERVENTION_MIN_COUNT - result.total, " time(s)"],
388
390
  ]);
389
391
  if (lacking) {
390
392
  lines.push(
391
- `- ${category}: データ不足: ${INTERVENTION_MIN_SESSIONS} session以上で合計${INTERVENTION_MIN_COUNT}回以上が必要です(あと${lacking})`,
393
+ `- ${category}: Not enough data: needs at least ${INTERVENTION_MIN_SESSIONS} sessions and at least ${INTERVENTION_MIN_COUNT} times in total (${lacking} more needed)`,
392
394
  );
393
395
  continue;
394
396
  }
395
397
  lines.push(
396
- `- ${category}: 提案を作れます(${result.hits.length} session、${result.total}回)`,
397
- ...evidenceLines(result.hits, "回"),
398
+ `- ${category}: a suggestion can be made (${result.hits.length} session(s), ${result.total} time(s))`,
399
+ ...evidenceLines(result.hits, " time(s)"),
398
400
  );
399
401
  }
400
402
  for (const { kind, result } of loops) {
401
403
  const category = `loop.kind=${kind}`;
402
404
  if (result === undefined) {
403
- lines.push(`- ${category}: 未計測のため提案しません`);
405
+ lines.push(`- ${category}: not measured, so no suggestion is made`);
404
406
  continue;
405
407
  }
406
408
  if (result.total < LOOP_MIN_OCCURRENCES) {
407
409
  lines.push(
408
- `- ${category}: データ不足: 合計${LOOP_MIN_OCCURRENCES}回以上が必要です(あと${LOOP_MIN_OCCURRENCES - result.total}回)`,
410
+ `- ${category}: Not enough data: needs at least ${LOOP_MIN_OCCURRENCES} times in total (${LOOP_MIN_OCCURRENCES - result.total} more needed)`,
409
411
  );
410
412
  continue;
411
413
  }
412
414
  lines.push(
413
- `- ${category}: 提案を作れます(合計${result.total}回)`,
414
- ...evidenceLines(result.hits, "回"),
415
+ `- ${category}: a suggestion can be made (${result.total} time(s) in total)`,
416
+ ...evidenceLines(result.hits, " time(s)"),
415
417
  );
416
418
  }
417
419
  // unlistedは構成に無いserverをまとめたものであり、設定されていたsessionを持たないため対象にしない。
418
420
  for (const s of servers.filter((server) => server.server !== "unlisted")) {
419
421
  const category = `mcp_server=${s.server}`;
420
422
  if (s.calls === undefined && s.failures === undefined) {
421
- lines.push(`- ${category}: 未計測のため提案しません`);
423
+ lines.push(`- ${category}: not measured, so no suggestion is made`);
422
424
  continue;
423
425
  }
424
426
  if (s.configured.length < MCP_MIN_CONFIGURED_SESSIONS) {
425
427
  lines.push(
426
- `- ${category}: データ不足: 設定されていたsessionが${MCP_MIN_CONFIGURED_SESSIONS}件以上必要です(あと${MCP_MIN_CONFIGURED_SESSIONS - s.configured.length}件)`,
428
+ `- ${category}: Not enough data: needs at least ${MCP_MIN_CONFIGURED_SESSIONS} sessions where it was configured (${MCP_MIN_CONFIGURED_SESSIONS - s.configured.length} more needed)`,
427
429
  );
428
430
  continue;
429
431
  }
430
432
  lines.push(
431
- `- ${category}: 提案を作れます(設定されていたsession ${s.configured.length}件)`,
433
+ `- ${category}: a suggestion can be made (configured in ${s.configured.length} session(s))`,
432
434
  ...evidenceLines(
433
435
  s.configured.map(({ session, element }) => ({
434
436
  session,
435
437
  count: element.calls ?? 0,
436
438
  })),
437
- "回の呼び出し",
439
+ " call(s)",
438
440
  ),
439
441
  );
440
442
  }