@harnessforce/claude-code-plugin 0.0.0-stage → 0.1.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-plugin/plugin.json +11 -0
- package/.mcp.json +8 -0
- package/LICENSE +201 -0
- package/README.md +122 -2
- package/commands/setup.md +83 -0
- package/commands/tune.md +39 -0
- package/hooks/hooks.json +127 -0
- package/package.json +26 -3
- package/scripts/harnessforce-hook-main.cjs +1510 -0
- package/scripts/harnessforce-hook.cjs +22 -0
- package/skills/propose-improvements/SKILL.md +105 -0
- package/skills/propose-improvements/scripts/summarize.mjs +472 -0
- package/skills/record-run/SKILL.md +66 -0
- package/skills/using-harnessforce/SKILL.md +54 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// hookのentry。18未満のNode.jsでも構文解析できるよう、ES5とCommonJSだけで書く(correlation.md「実行環境」)。
|
|
2
|
+
// 本体は組み込みのfetchを使うため、Node.js 18以上でだけ読み込む。
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
var major = Number(process.versions.node.split(".")[0]);
|
|
6
|
+
|
|
7
|
+
if (major >= 18) {
|
|
8
|
+
require("./harnessforce-hook-main.cjs");
|
|
9
|
+
} else {
|
|
10
|
+
process.stderr.write(
|
|
11
|
+
"harnessforce: session registration skipped (unsupported node)\n"
|
|
12
|
+
);
|
|
13
|
+
// 利用者に届く表示。文言はagent-coreのUNSUPPORTED_NODE_WARNINGと同じ値で、testで一致を確かめる。
|
|
14
|
+
// 繰り返し表示しないよう、session開始のhookだけが出す。
|
|
15
|
+
if (process.argv[2] === "session-start") {
|
|
16
|
+
process.stdout.write(
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
systemMessage: "Harnessforceのhookには18以上のNode.jsが必要です",
|
|
19
|
+
}) + "\n"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: propose-improvements
|
|
3
|
+
description: /harnessforce:tuneの分析結果から、人の介入、ループにできる繰り返し、MCP server、使い方(contextの圧縮、cacheの再利用、modelの使い分け)の改善の提案を作り、harnessforce tune recordで記録してから表示する。/harnessforce:tuneが「提案を作れます」と判定した対象にだけ使う。ファイルは書き換えない。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 分析結果から改善の提案を作る
|
|
7
|
+
|
|
8
|
+
`/harnessforce:tune`の`summarize.mjs`が出した「提案の対象」のうち、「提案を作れます」の対象ごとに提案を作る。提案は利用者がそのまま適用できる形にし、記録してから表示する。
|
|
9
|
+
|
|
10
|
+
## 書き換えない
|
|
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も作らない。
|
|
14
|
+
|
|
15
|
+
## 作る条件
|
|
16
|
+
|
|
17
|
+
`summarize.mjs`の判定が正である。「データ不足」と「未計測のため提案しません」の対象には提案を作らない。判定の条件は次のとおりである。
|
|
18
|
+
|
|
19
|
+
| 対象 | 提案を作る最小の条件 |
|
|
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%以上を占める |
|
|
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行で伝える。
|
|
33
|
+
|
|
34
|
+
## 根拠を集める
|
|
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)を読み、差分を現在の内容に対して作る。
|
|
39
|
+
|
|
40
|
+
## 形式
|
|
41
|
+
|
|
42
|
+
提案ごとに次を持たせる。
|
|
43
|
+
|
|
44
|
+
| 項目 | 内容 |
|
|
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(種類と識別子)と、比較に使う指標 |
|
|
53
|
+
|
|
54
|
+
変更の種類と、適用で変わるcomponentの対応は次のとおりである。
|
|
55
|
+
|
|
56
|
+
| 変更の種類 | componentの`kind` | componentの識別子(`id`) | 期待する内容 |
|
|
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`) |
|
|
67
|
+
|
|
68
|
+
componentの`source`は適用先のscope(`user`、`repository`、`local`、`managed`)とする。`.mcp.json`のserverは`repository`、`~/.claude.json`の最上位の`mcpServers`は`user`、その`projects`の下は`local`である。
|
|
69
|
+
|
|
70
|
+
## 許可を減らす提案
|
|
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`は、どの提案にも含めない。
|
|
74
|
+
|
|
75
|
+
## ループの提案
|
|
76
|
+
|
|
77
|
+
- ループにできる繰り返しの提案は、commandかskillの形式のprompt(`loop_prompt`)とし、止める条件を必ず含める。止める条件は、最大の繰り返し回数と、人が確認する時点(例: Pull Requestの作成前、同じ失敗が2回続いたとき)の両方とする。
|
|
78
|
+
- promptは、根拠のsessionで人が介入していた時点を、止める条件か、agentが自分で行う手順のどちらかに置き換える。
|
|
79
|
+
|
|
80
|
+
## 記録してから表示する
|
|
81
|
+
|
|
82
|
+
提案を表示する前に、提案ごとに`harnessforce tune record`を実行し、stdinへUTF-8のJSONのobjectを1つ渡す。入力はheredocで渡し、fileに書かない。区切りの語は、入力に現れない語にする。
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
harnessforce tune record <<'HF_TUNE_RECORD_7f3c'
|
|
86
|
+
{"category": "...", "change_type": "...", "scope": "...", "path": "...", "evidence_session_ids": ["..."], "body": "..."}
|
|
87
|
+
HF_TUNE_RECORD_7f3c
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| 項目 | 必須 | 内容 |
|
|
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
|
+
- 表示では、対象のカテゴリ、根拠、変更の種類、適用先、変更、期待する効果、測り方を、この順に示す。
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// `/harnessforce:tune`が使う。`harnessforce tune --json`を実行し、分析結果と、提案を作る条件の判定を表示する。
|
|
2
|
+
// 判定はimprovement-loop.md「作る条件」に従う。harnessforceの文言(stderr)と終了コードはそのまま渡す。
|
|
3
|
+
// Node.js 18で依存なしに動かすため、plainなESMで書く。
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const USAGE = "使い方: /harnessforce:tune [--all] [--no-send]";
|
|
7
|
+
const HF_MISSING =
|
|
8
|
+
"`harnessforce`が見つかりません。`/harnessforce:setup`でCLIを導入してください";
|
|
9
|
+
const ALLOWED_FLAGS = new Set(["--all", "--no-send"]);
|
|
10
|
+
|
|
11
|
+
const MINIMUM_SESSIONS = 10;
|
|
12
|
+
const INTERVENTION_MIN_SESSIONS = 3;
|
|
13
|
+
const INTERVENTION_MIN_COUNT = 10;
|
|
14
|
+
const LOOP_MIN_OCCURRENCES = 3;
|
|
15
|
+
const MCP_MIN_CONFIGURED_SESSIONS = 10;
|
|
16
|
+
const MAX_EVIDENCE = 20;
|
|
17
|
+
const USAGE_MIN_SESSIONS = 3;
|
|
18
|
+
const MODEL_CHOICE_MIN_SESSIONS = 10;
|
|
19
|
+
const MODEL_CHOICE_MIN_SHARE = 0.9;
|
|
20
|
+
// 使い方のうち、sessionごとに判定する種類。model_choiceは分析した範囲の合計で判定する。
|
|
21
|
+
const SESSION_USAGE_KINDS = ["frequent_compaction", "low_cache_reuse"];
|
|
22
|
+
// improvement-loop.md「前回の提案の前後」の「良くなった向き」。model_choiceは向きを持たない。
|
|
23
|
+
const IMPROVES_BY = {
|
|
24
|
+
"intervention.kind": "decrease",
|
|
25
|
+
"loop.kind": "decrease",
|
|
26
|
+
mcp_server: "decrease",
|
|
27
|
+
"usage.kind=frequent_compaction": "decrease",
|
|
28
|
+
"usage.kind=low_cache_reuse": "increase",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const INTERVENTION_KINDS = [
|
|
32
|
+
"approval",
|
|
33
|
+
"continue",
|
|
34
|
+
"ci_relay",
|
|
35
|
+
"review_relay",
|
|
36
|
+
"answer",
|
|
37
|
+
"other",
|
|
38
|
+
];
|
|
39
|
+
const LOOP_KINDS = [
|
|
40
|
+
"issue_to_pr",
|
|
41
|
+
"ci_fix",
|
|
42
|
+
"review_response",
|
|
43
|
+
"test_fix",
|
|
44
|
+
"lint_fix",
|
|
45
|
+
"dependency_update",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const median = (values) => {
|
|
49
|
+
if (values.length === 0) return null;
|
|
50
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
51
|
+
const middle = Math.floor(sorted.length / 2);
|
|
52
|
+
return sorted.length % 2 === 1
|
|
53
|
+
? sorted[middle]
|
|
54
|
+
: (sorted[middle - 1] + sorted[middle]) / 2;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const secondsText = (value) => (value === null ? "—" : `${value.toFixed(1)}秒`);
|
|
58
|
+
|
|
59
|
+
const newestFirst = (a, b) =>
|
|
60
|
+
a.session.started_at < b.session.started_at
|
|
61
|
+
? 1
|
|
62
|
+
: a.session.started_at > b.session.started_at
|
|
63
|
+
? -1
|
|
64
|
+
: a.session.session_id.localeCompare(b.session.session_id);
|
|
65
|
+
|
|
66
|
+
// 計測したsessionだけを合計する。1つも無ければundefined(未計測)とし、0と区別する。
|
|
67
|
+
function measure(sessions, pick, countField, medianField) {
|
|
68
|
+
const measured = sessions
|
|
69
|
+
.map((session) => ({ session, value: pick(session.report) }))
|
|
70
|
+
.filter(({ value }) => value?.measurement === "measured");
|
|
71
|
+
if (measured.length === 0) return undefined;
|
|
72
|
+
const hits = measured.filter(({ value }) => value[countField] > 0);
|
|
73
|
+
return {
|
|
74
|
+
total: measured.reduce((sum, { value }) => sum + value[countField], 0),
|
|
75
|
+
median: median(
|
|
76
|
+
measured
|
|
77
|
+
.map(({ value }) => value[medianField])
|
|
78
|
+
.filter((m) => typeof m === "number"),
|
|
79
|
+
),
|
|
80
|
+
hits: hits.map(({ session, value }) => ({
|
|
81
|
+
session,
|
|
82
|
+
count: value[countField],
|
|
83
|
+
})),
|
|
84
|
+
measured,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mcpServers(sessions) {
|
|
89
|
+
const byServer = new Map();
|
|
90
|
+
for (const session of sessions)
|
|
91
|
+
for (const element of session.report.mcp_servers ?? []) {
|
|
92
|
+
const entries = byServer.get(element.server) ?? [];
|
|
93
|
+
entries.push({ session, element });
|
|
94
|
+
byServer.set(element.server, entries);
|
|
95
|
+
}
|
|
96
|
+
return [...byServer.entries()]
|
|
97
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
98
|
+
.map(([server, entries]) => {
|
|
99
|
+
const configured = entries.filter(({ element }) => element.configured);
|
|
100
|
+
const sum = (field, measurement) => {
|
|
101
|
+
const measured = entries.filter(
|
|
102
|
+
({ element }) => element[measurement] === "measured",
|
|
103
|
+
);
|
|
104
|
+
return measured.length === 0
|
|
105
|
+
? undefined
|
|
106
|
+
: measured.reduce((total, { element }) => total + element[field], 0);
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
server,
|
|
110
|
+
configured,
|
|
111
|
+
calls: sum("calls", "measurement"),
|
|
112
|
+
failures: sum("failures", "failures_measurement"),
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function evidenceLines(hits, unit) {
|
|
118
|
+
const sorted = [...hits].sort(newestFirst);
|
|
119
|
+
const lines = [" 根拠のsession(新しい順):"];
|
|
120
|
+
for (const { session, count } of sorted.slice(0, MAX_EVIDENCE))
|
|
121
|
+
lines.push(
|
|
122
|
+
` ${session.session_id} ${session.started_at} ${session.repository ?? "-"} sendable=${session.sendable} ${count}${unit} ${session.transcript_path}`,
|
|
123
|
+
);
|
|
124
|
+
if (sorted.length > MAX_EVIDENCE)
|
|
125
|
+
lines.push(` ほか${sorted.length - MAX_EVIDENCE}件`);
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const missing = (parts) =>
|
|
130
|
+
parts
|
|
131
|
+
.filter(([amount]) => amount > 0)
|
|
132
|
+
.map(([amount, unit]) => `${amount}${unit}`)
|
|
133
|
+
.join("、");
|
|
134
|
+
|
|
135
|
+
const percent = (share) => `${(share * 100).toFixed(1)}%`;
|
|
136
|
+
|
|
137
|
+
// `models`(本体)と`subagent_models`の合計を分母にした、modelごとのoutput_tokensの割合。
|
|
138
|
+
// どちらかがnullのsessionは除く。分母が0ならnull。
|
|
139
|
+
function modelShares(sessions) {
|
|
140
|
+
const counted = sessions.filter(
|
|
141
|
+
(s) =>
|
|
142
|
+
s.usage && s.usage.models !== null && s.usage.subagent_models !== null,
|
|
143
|
+
);
|
|
144
|
+
const totals = (field) => {
|
|
145
|
+
const byModel = new Map();
|
|
146
|
+
for (const s of counted)
|
|
147
|
+
for (const m of s.usage[field])
|
|
148
|
+
byModel.set(m.model, (byModel.get(m.model) ?? 0) + m.output_tokens);
|
|
149
|
+
return byModel;
|
|
150
|
+
};
|
|
151
|
+
const main = totals("models");
|
|
152
|
+
const subagent = totals("subagent_models");
|
|
153
|
+
const denominator = [...main.values(), ...subagent.values()].reduce(
|
|
154
|
+
(total, v) => total + v,
|
|
155
|
+
0,
|
|
156
|
+
);
|
|
157
|
+
if (denominator === 0) return null;
|
|
158
|
+
const shares = (byModel) =>
|
|
159
|
+
[...byModel]
|
|
160
|
+
// harnessforce tune の表示(UTF-16 の code unit 順)と並びを揃える。
|
|
161
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
162
|
+
.map(([model, tokens]) => ({ model, share: tokens / denominator }));
|
|
163
|
+
return { main: shares(main), subagent: shares(subagent) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const sharesText = (shares) =>
|
|
167
|
+
shares === null
|
|
168
|
+
? "なし"
|
|
169
|
+
: [
|
|
170
|
+
...shares.main.map((m) => `本体 ${m.model} ${percent(m.share)}`),
|
|
171
|
+
...shares.subagent.map(
|
|
172
|
+
(m) => `subagent ${m.model} ${percent(m.share)}`,
|
|
173
|
+
),
|
|
174
|
+
].join("、");
|
|
175
|
+
|
|
176
|
+
// 「作る条件」の`model_choice`: 分析した範囲の`models`の合計で、output_tokensが最も多いmodelとその割合。
|
|
177
|
+
function topMainModel(sessions) {
|
|
178
|
+
const byModel = new Map();
|
|
179
|
+
for (const s of sessions)
|
|
180
|
+
for (const m of s.usage.models)
|
|
181
|
+
byModel.set(m.model, (byModel.get(m.model) ?? 0) + m.output_tokens);
|
|
182
|
+
const total = [...byModel.values()].reduce((sum, v) => sum + v, 0);
|
|
183
|
+
const [model, tokens] = [...byModel].sort(
|
|
184
|
+
([a, x], [b, y]) => y - x || a.localeCompare(b),
|
|
185
|
+
)[0] ?? [undefined, 0];
|
|
186
|
+
return model === undefined || total === 0
|
|
187
|
+
? undefined
|
|
188
|
+
: { model, tokens, share: tokens / total };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const valueText = (value) =>
|
|
192
|
+
value === null
|
|
193
|
+
? "値なし"
|
|
194
|
+
: typeof value === "number"
|
|
195
|
+
? String(Number(value.toFixed(3)))
|
|
196
|
+
: sharesText(value);
|
|
197
|
+
|
|
198
|
+
function improvesBy(category) {
|
|
199
|
+
return IMPROVES_BY[category] ?? IMPROVES_BY[category.split("=")[0]];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// improvement-loop.md「前回の提案の前後」
|
|
203
|
+
function renderFollowups(followups) {
|
|
204
|
+
if (!Array.isArray(followups) || followups.length === 0) return [];
|
|
205
|
+
const lines = [
|
|
206
|
+
"前回の提案の前後(前後の差(因果を示しません)。起点は適用を検出した時刻です):",
|
|
207
|
+
];
|
|
208
|
+
for (const f of followups) {
|
|
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)}`,
|
|
213
|
+
);
|
|
214
|
+
if (f.no_comparison_data) lines.push(" 比較データなし");
|
|
215
|
+
if (f.before_outside_range)
|
|
216
|
+
lines.push(" 前の期間の一部が分析の範囲の外です");
|
|
217
|
+
const direction = improvesBy(f.category);
|
|
218
|
+
const isImproved =
|
|
219
|
+
direction === "decrease"
|
|
220
|
+
? f.after.value < f.before.value
|
|
221
|
+
: f.after.value > f.before.value;
|
|
222
|
+
if (direction !== undefined && !f.no_comparison_data && !isImproved)
|
|
223
|
+
lines.push(
|
|
224
|
+
" 良くなった向きへ動いていません。同じ対象の提案を作るときの根拠にします",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
lines.push("");
|
|
228
|
+
return lines;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function usageTargets(sessions) {
|
|
232
|
+
const lines = [];
|
|
233
|
+
// usage を出力しない古い harnessforce の出力では、使い方を未計測として扱い提案を作らない。
|
|
234
|
+
const withUsage = sessions.filter((s) => s.usage);
|
|
235
|
+
for (const kind of SESSION_USAGE_KINDS) {
|
|
236
|
+
const category = `usage.kind=${kind}`;
|
|
237
|
+
if (withUsage.length === 0) {
|
|
238
|
+
lines.push(`- ${category}: 未計測のため提案しません`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const hits = withUsage.filter((s) => s.usage.kinds.includes(kind));
|
|
242
|
+
if (hits.length < USAGE_MIN_SESSIONS) {
|
|
243
|
+
lines.push(
|
|
244
|
+
`- ${category}: データ不足: 当たるsessionが${USAGE_MIN_SESSIONS}以上必要です(あと${USAGE_MIN_SESSIONS - hits.length} session)`,
|
|
245
|
+
);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
lines.push(
|
|
249
|
+
`- ${category}: 提案を作れます(${hits.length} session)`,
|
|
250
|
+
...evidenceLines(
|
|
251
|
+
hits.map((session) => ({
|
|
252
|
+
session,
|
|
253
|
+
count:
|
|
254
|
+
kind === "frequent_compaction"
|
|
255
|
+
? session.usage.compactions_auto
|
|
256
|
+
: Number(session.usage.cache_reuse_ratio.toFixed(3)),
|
|
257
|
+
})),
|
|
258
|
+
kind === "frequent_compaction"
|
|
259
|
+
? "回の自動圧縮"
|
|
260
|
+
: "(cacheの再利用の割合)",
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const category = "usage.kind=model_choice";
|
|
265
|
+
const withModels = withUsage.filter(
|
|
266
|
+
(s) => Array.isArray(s.usage.models) && s.usage.models.length > 0,
|
|
267
|
+
);
|
|
268
|
+
if (withUsage.length === 0) {
|
|
269
|
+
lines.push(`- ${category}: 未計測のため提案しません`);
|
|
270
|
+
return lines;
|
|
271
|
+
}
|
|
272
|
+
if (withModels.length < MODEL_CHOICE_MIN_SESSIONS) {
|
|
273
|
+
lines.push(
|
|
274
|
+
`- ${category}: データ不足: modelsを持つsessionが${MODEL_CHOICE_MIN_SESSIONS}以上必要です(あと${MODEL_CHOICE_MIN_SESSIONS - withModels.length} session)`,
|
|
275
|
+
);
|
|
276
|
+
return lines;
|
|
277
|
+
}
|
|
278
|
+
const top = topMainModel(withModels);
|
|
279
|
+
if (top === undefined || top.share < MODEL_CHOICE_MIN_SHARE) {
|
|
280
|
+
lines.push(
|
|
281
|
+
`- ${category}: 提案しません: 1つのmodelが本体のoutput_tokensの${MODEL_CHOICE_MIN_SHARE * 100}%以上を占めていません${top ? `(最大 ${top.model} ${percent(top.share)})` : ""}`,
|
|
282
|
+
);
|
|
283
|
+
return lines;
|
|
284
|
+
}
|
|
285
|
+
lines.push(
|
|
286
|
+
`- ${category}: 提案を作れます(${withModels.length} session、${top.model} ${percent(top.share)})`,
|
|
287
|
+
...evidenceLines(
|
|
288
|
+
withModels.map((session) => ({
|
|
289
|
+
session,
|
|
290
|
+
count:
|
|
291
|
+
session.usage.models.find((m) => m.model === top.model)
|
|
292
|
+
?.output_tokens ?? 0,
|
|
293
|
+
})),
|
|
294
|
+
` output_tokens(${top.model})`,
|
|
295
|
+
),
|
|
296
|
+
);
|
|
297
|
+
return lines;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function render(output) {
|
|
301
|
+
const { sessions } = output;
|
|
302
|
+
const lines = [
|
|
303
|
+
`分析したsession: ${output.session_count}件(analyzer ${output.analyzer_version}、parser ${output.parser_version})`,
|
|
304
|
+
"",
|
|
305
|
+
"人の介入(回数、介入のあったsession数、待ち時間の中央値):",
|
|
306
|
+
];
|
|
307
|
+
const interventions = INTERVENTION_KINDS.map((kind) => ({
|
|
308
|
+
kind,
|
|
309
|
+
result: measure(
|
|
310
|
+
sessions,
|
|
311
|
+
(r) => r.interventions?.[kind],
|
|
312
|
+
"count",
|
|
313
|
+
"wait_seconds_median",
|
|
314
|
+
),
|
|
315
|
+
}));
|
|
316
|
+
for (const { kind, result } of interventions)
|
|
317
|
+
lines.push(
|
|
318
|
+
result === undefined
|
|
319
|
+
? ` ${kind}: 未計測`
|
|
320
|
+
: ` ${kind}: ${result.total}回、${result.hits.length} session、${secondsText(result.median)}`,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
lines.push(
|
|
324
|
+
"",
|
|
325
|
+
"ループにできる繰り返し(回数、その間の介入、所要時間の中央値):",
|
|
326
|
+
);
|
|
327
|
+
const loops = LOOP_KINDS.map((kind) => ({
|
|
328
|
+
kind,
|
|
329
|
+
result: measure(
|
|
330
|
+
sessions,
|
|
331
|
+
(r) => r.loops?.[kind],
|
|
332
|
+
"occurrences",
|
|
333
|
+
"duration_seconds_median",
|
|
334
|
+
),
|
|
335
|
+
}));
|
|
336
|
+
for (const { kind, result } of loops) {
|
|
337
|
+
if (result === undefined) {
|
|
338
|
+
lines.push(` ${kind}: 未計測`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const count = result.measured.reduce(
|
|
342
|
+
(sum, { value }) => sum + value.interventions,
|
|
343
|
+
0,
|
|
344
|
+
);
|
|
345
|
+
lines.push(
|
|
346
|
+
` ${kind}: ${result.total}回、介入${count}回、${secondsText(result.median)}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
lines.push("", "MCP server(設定されていたsession数、呼び出し、失敗):");
|
|
351
|
+
const servers = mcpServers(sessions);
|
|
352
|
+
if (servers.length === 0) lines.push(" (記録にMCP serverがありません)");
|
|
353
|
+
for (const s of servers)
|
|
354
|
+
lines.push(
|
|
355
|
+
` ${s.server}: 設定されていたsession ${s.configured.length}件、${s.calls === undefined ? "呼び出し: 未計測" : `呼び出し${s.calls}回`}、${s.failures === undefined ? "失敗: 未計測" : `失敗${s.failures}回`}`,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const withUsage = sessions.filter((s) => s.usage);
|
|
359
|
+
lines.push("", "使い方(当たるsession):");
|
|
360
|
+
for (const kind of SESSION_USAGE_KINDS)
|
|
361
|
+
lines.push(
|
|
362
|
+
withUsage.length === 0
|
|
363
|
+
? ` ${kind}: 未計測`
|
|
364
|
+
: ` ${kind}: ${withUsage.filter((s) => s.usage.kinds.includes(kind)).length} session`,
|
|
365
|
+
);
|
|
366
|
+
lines.push(
|
|
367
|
+
` model(output_tokensの割合): ${sharesText(modelShares(withUsage))}`,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
lines.push("", ...renderFollowups(output.followups));
|
|
371
|
+
if (!output.sufficient) {
|
|
372
|
+
lines.push(
|
|
373
|
+
`データ不足のため提案を作りません: 分析したsessionは${output.session_count}件です。提案には${MINIMUM_SESSIONS}件以上が必要です(あと${MINIMUM_SESSIONS - output.session_count}件)`,
|
|
374
|
+
);
|
|
375
|
+
return lines;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
lines.push("提案の対象:");
|
|
379
|
+
for (const { kind, result } of interventions) {
|
|
380
|
+
const category = `intervention.kind=${kind}`;
|
|
381
|
+
if (result === undefined) {
|
|
382
|
+
lines.push(`- ${category}: 未計測のため提案しません`);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const lacking = missing([
|
|
386
|
+
[INTERVENTION_MIN_SESSIONS - result.hits.length, " session"],
|
|
387
|
+
[INTERVENTION_MIN_COUNT - result.total, "回"],
|
|
388
|
+
]);
|
|
389
|
+
if (lacking) {
|
|
390
|
+
lines.push(
|
|
391
|
+
`- ${category}: データ不足: ${INTERVENTION_MIN_SESSIONS} session以上で合計${INTERVENTION_MIN_COUNT}回以上が必要です(あと${lacking})`,
|
|
392
|
+
);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
lines.push(
|
|
396
|
+
`- ${category}: 提案を作れます(${result.hits.length} session、${result.total}回)`,
|
|
397
|
+
...evidenceLines(result.hits, "回"),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
for (const { kind, result } of loops) {
|
|
401
|
+
const category = `loop.kind=${kind}`;
|
|
402
|
+
if (result === undefined) {
|
|
403
|
+
lines.push(`- ${category}: 未計測のため提案しません`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (result.total < LOOP_MIN_OCCURRENCES) {
|
|
407
|
+
lines.push(
|
|
408
|
+
`- ${category}: データ不足: 合計${LOOP_MIN_OCCURRENCES}回以上が必要です(あと${LOOP_MIN_OCCURRENCES - result.total}回)`,
|
|
409
|
+
);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
lines.push(
|
|
413
|
+
`- ${category}: 提案を作れます(合計${result.total}回)`,
|
|
414
|
+
...evidenceLines(result.hits, "回"),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
// unlistedは構成に無いserverをまとめたものであり、設定されていたsessionを持たないため対象にしない。
|
|
418
|
+
for (const s of servers.filter((server) => server.server !== "unlisted")) {
|
|
419
|
+
const category = `mcp_server=${s.server}`;
|
|
420
|
+
if (s.calls === undefined && s.failures === undefined) {
|
|
421
|
+
lines.push(`- ${category}: 未計測のため提案しません`);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (s.configured.length < MCP_MIN_CONFIGURED_SESSIONS) {
|
|
425
|
+
lines.push(
|
|
426
|
+
`- ${category}: データ不足: 設定されていたsessionが${MCP_MIN_CONFIGURED_SESSIONS}件以上必要です(あと${MCP_MIN_CONFIGURED_SESSIONS - s.configured.length}件)`,
|
|
427
|
+
);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
lines.push(
|
|
431
|
+
`- ${category}: 提案を作れます(設定されていたsession ${s.configured.length}件)`,
|
|
432
|
+
...evidenceLines(
|
|
433
|
+
s.configured.map(({ session, element }) => ({
|
|
434
|
+
session,
|
|
435
|
+
count: element.calls ?? 0,
|
|
436
|
+
})),
|
|
437
|
+
"回の呼び出し",
|
|
438
|
+
),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
lines.push(...usageTargets(sessions));
|
|
442
|
+
return lines;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function run(args) {
|
|
446
|
+
if (!args.every((arg) => ALLOWED_FLAGS.has(arg))) {
|
|
447
|
+
process.stderr.write(`${USAGE}\n`);
|
|
448
|
+
return 2;
|
|
449
|
+
}
|
|
450
|
+
// Windowsのnpmのharnessforceは.cmdであり、shellを通さないと起動できない。引数は上で許した値だけである。
|
|
451
|
+
const hf = spawnSync("harnessforce", ["tune", "--json", ...new Set(args)], {
|
|
452
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
453
|
+
encoding: "utf8",
|
|
454
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
455
|
+
shell: process.platform === "win32",
|
|
456
|
+
});
|
|
457
|
+
if (hf.error?.code === "ENOENT") {
|
|
458
|
+
process.stderr.write(`${HF_MISSING}\n`);
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
const code = hf.status ?? 1;
|
|
462
|
+
let output;
|
|
463
|
+
try {
|
|
464
|
+
output = JSON.parse(hf.stdout);
|
|
465
|
+
} catch {
|
|
466
|
+
return code;
|
|
467
|
+
}
|
|
468
|
+
process.stdout.write(`${render(output).join("\n")}\n`);
|
|
469
|
+
return code;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
process.exitCode = run(process.argv.slice(2));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: record-run
|
|
3
|
+
description: HarnessforceのIssue(ENG-42のような識別子)に取り組む作業で、HarnessforceのMCP serverのtoolを使い、このsessionをRunとして登録し、Plan、Decision、Definition of Doneの充足を記録する。Issueに取り組み始めるとき、Issueを指定されずに変更をcommitする作業を始めるとき、方針を変えるとき、作業を終えるときに使う。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HarnessforceのIssueの作業を記録する
|
|
7
|
+
|
|
8
|
+
HarnessforceのMCP serverのtoolで、作業をIssueに結び付けて記録する。toolを初めて使うときは、ブラウザでのログインとWorkspaceの選択を求められる。
|
|
9
|
+
|
|
10
|
+
## 作業の開始時
|
|
11
|
+
|
|
12
|
+
利用者がIssueを示していなければ、先に下の「Issueを指定されていない場合」に従う。
|
|
13
|
+
|
|
14
|
+
1. contextにある`harnessforce session_id: <session_id>`の値を`session_id`として渡し、`start_run`でRunを登録する。この行はHarnessforceのpluginがsessionの開始時にcontextへ加える。複数あれば、最後に加えられたものを使う。入力は、Issueの識別子(`issue_identifier`)、`agent`(`claude_code`)、`session_id`とする。作業しているrepository、branch、commitが分かれば併せて渡す。返された`run_id`を終了時まで控えておく。contextにこの行が無い場合は、下の「session IDが無い場合」に従う。
|
|
15
|
+
2. `get_issue`で、Issue、Plan、Definition of Doneを確認する。nativeのIssueなら、下の「nativeのIssueの状態」に従う。Planは状態(現行か`proposed`か)で区別されている。
|
|
16
|
+
3. 現行のPlanも`proposed`のPlanも無ければ、作業の方針を`record_plan`で記録する。本文はmarkdownで書く。
|
|
17
|
+
|
|
18
|
+
## 作業の途中
|
|
19
|
+
|
|
20
|
+
- 方針を変えたとき、または選択肢から選んだときは、`record_decision`で記録する。題(`title`)、決めたこと(`decision`)、理由(`rationale`)を必ず書き、検討した選択肢があれば`alternatives`に書く。
|
|
21
|
+
|
|
22
|
+
## session IDが変わったとき
|
|
23
|
+
|
|
24
|
+
- contextの最後の`harnessforce session_id:`の値が、直前の`start_run`に渡した値と異なる(会話をforkしたとき、`/clear`したときなど)場合は、作業を続ける前に新しい値で`start_run`をもう1度呼び、返された`run_id`を以後の`complete_run`に使う。
|
|
25
|
+
|
|
26
|
+
## 作業の終了時
|
|
27
|
+
|
|
28
|
+
- `complete_run`に`run_id`と、`get_issue`が返したDefinition of Doneの項目ごとの充足(`item_id`と、満たしたなら`met`、満たしていないなら`unmet`)を渡す。Definition of Doneの項目が無い場合は、項目を渡さない。
|
|
29
|
+
|
|
30
|
+
## Issueを指定されていない場合
|
|
31
|
+
|
|
32
|
+
利用者がIssueを示さずに作業を始めたら、Issueを作るかを利用者に尋ねる。Issueを作るのは、利用者が承諾した後だけとする。
|
|
33
|
+
|
|
34
|
+
1. 次のすべてに当たる場合だけ尋ねる。
|
|
35
|
+
- 利用者がIssueの識別子を示していない。現在のbranch名が`ENG-42`、`eng-42-login`、`gh-45-fix`のような識別子を含み、`get_issue`でそのIssueを解決できたら(`gh-<番号>`は、そのrepositoryの`<owner>/<repo>#<番号>`として渡す)、そのIssueを示されたものとして「作業の開始時」に進み、尋ねない。`not_found`なら識別子が無い場合と同じに扱う。
|
|
36
|
+
- 作業が変更をcommitするものである。調べもの、質問への回答、計画だけの会話では尋ねない。
|
|
37
|
+
- contextに`harnessforce session_id:`の行がある。
|
|
38
|
+
- contextの最後の`harnessforce session_id:`の値ごとに、同じ値で尋ねるのは1回だけとする。利用者が断ったら、同じ値の間は再び尋ねない。forkや`/clear`で値が変わったら、改めて尋ねてよい。
|
|
39
|
+
2. 既存のIssueを先に示す。作業の要約から、Issueのtitleに現れそうな語を最大3つ選び、語ごとに`list_issues`を、`q`を`-status:done,canceled {語}`、`sort`を`updated`として1回ずつ呼ぶ。自由文は複数の語ではそのすべてを含むIssueにだけ一致するため、語を分けて呼ぶ。結果を合わせて重複を除き、更新の新しい順に最大5件を候補として示す。利用者が候補を選んだら、Issueを作らずにそのIssueで「作業の開始時」に進む。
|
|
40
|
+
3. 候補に当たるものが無ければ、作る案を示す。
|
|
41
|
+
- title: 作業の要約。80文字以内。
|
|
42
|
+
- 説明: 目的と完了の条件の箇条書き。
|
|
43
|
+
- Project: `list_projects`を`q`を`status:active`として呼び、1件ならそれを、2件以上なら利用者に選ばせる。0件ならIssueを作らず、その旨を伝える。
|
|
44
|
+
- titleと説明には、promptの本文、file path、command、secretを写さない。titleは一覧とURLに現れ、説明より広く読まれる。
|
|
45
|
+
- 外部のIssue管理ツールを使うチームには、「Harnessforceにだけ作られ、外部のツールには書き戻されません」と伝える。
|
|
46
|
+
4. 利用者が案を承諾した場合だけ、`create_issue`でIssueを作り、返されたidentifierで「作業の開始時」の`start_run`を呼ぶ。利用者が案を直したら、直した値で作る。承諾は会話の中の利用者の明示の返答とする。非対話の実行や利用者のいない自動の実行では、返答を得られないため作らない。
|
|
47
|
+
5. 作れない場合と失敗した場合:
|
|
48
|
+
- `create_issue`か、候補を選んだ後の`start_run`が`forbidden`(Viewerのtoken、閲覧のみのWorkspaceの`workspace_read_only`を含む)を返したら、作れない理由をそのまま伝え、同じsession IDでは再び尋ねない。
|
|
49
|
+
- `create_issue`を再試行しない。再試行するとIssueが2件作られうる。timeoutなどで結果が分からなければ、`list_issues`で案のtitleを自由文に検索し、titleが完全に一致し、作成時刻が呼び出しの後のIssueが見つかればそれを使う。見つからなければ、作れなかったことを伝える。
|
|
50
|
+
- `create_issue`が成功して`start_run`だけが失敗したら、作ったIssueのidentifierを伝え、Issueを作り直さない。
|
|
51
|
+
6. `create_project`、`create_milestone`、`create_cycle`、`create_objective`を自らの判断で呼ばない。目標日、所属、Priority、Assigneeも自ら変えない。これらは計画の変更であり、利用者が会話の中で明示に指示した場合だけ、指示どおりに呼ぶ。
|
|
52
|
+
|
|
53
|
+
## nativeのIssueの状態
|
|
54
|
+
|
|
55
|
+
- nativeのIssue(`get_issue`の`external`が`null`のIssue)で、作業を始めた時点の状態が`backlog`か`unstarted`なら、`start_run`の後に「状態を『進行中』にしますか」と尋ね、承諾した場合だけ`update_issue`で状態を`started`に変える。`update_issue`には、`get_issue`が返したIssueの`version`(`definition_of_done.version`ではない)と、変えない他の属性の現在の値を併せて渡す。
|
|
56
|
+
- 作業を終える時点で、利用者が仕事の完了を示していれば、`complete_run`の後に「状態を『完了』にしますか」と尋ね、承諾した場合だけ状態を`done`に変える。Pull Requestを開いただけで、mergeされていない場合は尋ねない。
|
|
57
|
+
- 外部由来のIssueの状態は変えない。状態の正本は外部のツールである。GitHub IssuesのIssueでは、利用者がPull Requestの本文を書く際の文面として`Closes #<番号>`を提案してよい。agentがPull RequestとIssueを自ら結び付ける操作の代わりにはしない。
|
|
58
|
+
- `update_issue`が`conflict`を返したら、`get_issue`で読み直して伝え、1回だけ尋ね直す。再び`conflict`なら、そのことを伝えて終える。
|
|
59
|
+
|
|
60
|
+
## session IDが無い場合
|
|
61
|
+
|
|
62
|
+
- contextに`harnessforce session_id:`の行が無ければ、session IDを推測して渡さず、`start_run`と`complete_run`を呼ばない。`get_issue`、`record_plan`、`record_decision`はRunに依らないため続ける。利用者には「Harnessforceのhookが動いていないため、Runを記録できません。`/harnessforce:setup`を実行してください」と1回だけ伝える。
|
|
63
|
+
|
|
64
|
+
## 記録の扱い
|
|
65
|
+
|
|
66
|
+
- agentが記録したPlan、Decision、充足は`proposed`として保存され、人が承認するまで確定しない。記録したことを、確定したこととして利用者へ伝えない。
|