@clubs.place/cli 0.1.17 → 0.1.23

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.
Files changed (53) hide show
  1. package/PROVENANCE.json +55 -48
  2. package/README.md +11 -0
  3. package/bin/clubs-nippo +0 -5
  4. package/bin/clubs-nippo-web.mjs +0 -5
  5. package/bin/clubs-nippo.mjs +1 -526
  6. package/bin/clubs.mjs +643 -0
  7. package/docs/events-schema.md +1 -1
  8. package/package.json +5 -8
  9. package/src/aggregate.mjs +32 -5
  10. package/src/aggregate.slots.test.mjs +23 -0
  11. package/src/autorun.mjs +0 -5
  12. package/src/autorun.test.mjs +0 -5
  13. package/src/cards.mjs +0 -5
  14. package/src/collect.mjs +0 -5
  15. package/src/config.mjs +10 -6
  16. package/src/generators/claude.mjs +0 -5
  17. package/src/generators/cli.mjs +0 -5
  18. package/src/generators/codex.mjs +0 -5
  19. package/src/generators/copilot.mjs +0 -5
  20. package/src/generators/gemini.mjs +0 -5
  21. package/src/generators/openai-compat.mjs +0 -5
  22. package/src/generators/openrouter.mjs +0 -5
  23. package/src/history.mjs +2 -5
  24. package/src/ingest.mjs +0 -5
  25. package/src/lemma.docHash.test.mjs +0 -5
  26. package/src/lemma.mjs +0 -5
  27. package/src/mask.mjs +0 -5
  28. package/src/mcp.mjs +0 -5
  29. package/src/narrative.blockRepos.test.mjs +48 -0
  30. package/src/narrative.mjs +47 -8
  31. package/src/profiles.mjs +110 -0
  32. package/src/profiles.test.mjs +66 -0
  33. package/src/prompt.mjs +99 -24
  34. package/src/prompt.test.mjs +87 -20
  35. package/src/remote-config.mjs +5 -6
  36. package/src/render.mjs +1 -5
  37. package/src/routines.mjs +61 -0
  38. package/src/routines.test.mjs +29 -0
  39. package/src/send.mjs +0 -5
  40. package/src/settings.mjs +0 -5
  41. package/src/sources/claude-code.mjs +0 -5
  42. package/src/sources/codex-cli.mjs +0 -5
  43. package/src/sources/copilot-cli.mjs +0 -5
  44. package/src/sources/cowork.mjs +0 -5
  45. package/src/sources/events-file.mjs +0 -5
  46. package/src/sources/gemini-cli.mjs +0 -5
  47. package/src/sources/hermes.mjs +0 -5
  48. package/src/store.mjs +0 -5
  49. package/src/tasks.mjs +0 -5
  50. package/src/tool-name.mjs +0 -5
  51. package/web/assets/qr.js +0 -5
  52. package/web/assets/sidebar.js +0 -5
  53. package/web/assets/twemoji.js +0 -5
package/src/prompt.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * LLM に渡すプロンプト。生成器(claude / openrouter)に依存しない。
8
3
  */
@@ -81,10 +76,14 @@ const PROMPT_HEADER = `あなたはオーナーの AI アシスタント「{{AGE
81
76
  - 「[[REDACTED]]」はオーナーがマスクした箇所。そのまま [[REDACTED]] と書くこと(勝手に復元・推測しない)。
82
77
  - 各ブロックは「何をして・どこまで進めたか」が伝わる 2〜4文のナラティブで、要点だけに絞る。代表的な成果・判断・結果を1つずつ書き、細かな下位項目を列挙・網羅しない(読むのに疲れさせない)。専門用語は開いて書く。冗長な前置き・同義の言い換え・水増しはしない。目安は3〜5行。
83
78
  - 申し送りは箇条書きで書いてください。1行につき用件はひとつだけです。複数の用件を1行にまとめないでください。(例: 「PR #32のマージ判断」と「接続ページの実装着手」は別の行にする)
84
- - 継続中の用件は、前日と同じ表現をそのまま使ってください。{{CARRYOVER}}{{TERMS}}
79
+ - 申し送りに書くのは、今回はじめて出てきた用件だけです。すでに申し送った用件は、まだ済んでいなくても**もう一度書かないでください**(オーナーのタスクページに残っています)。新しい用件が無ければ『特にありません。』。{{HANDED_OFF}}{{TERMS}}
85
80
  - terms は**拾うだけ**です。プロジェクト名・システム名・略語・機能名など固有名詞的な語を、本文(workBlocks / handoff / insights / selfNote)に書いたとおり**原文の一部をそのまま**入れてください。一般名詞・動詞は拾わない——**ただし、その組織が機能名・概念名として使っている日本語の語は拾う**(例:「申し送り」「用語集」「繰越」「本物証明」のように、辞書では一般名詞でも、そのチームでは決まったものを指して繰り返し使われている語)。日本語の語も英字の語と同じだけ拾ってください。**人名も拾わない**(オーナー・同僚・AI の体名)——用語集は「何をなんと呼ぶか」を揃える場所で、人の名前はそこに要らない。ただしアカウント名・サービス名として使われている場合(例:「◯◯名義の投稿」)は、人ではなく対象なので拾ってよい。言い換え・要約・定義文は書かない(本文と1文字でも違う語は捨てられます)。**語であって文ではない**——文・箇条書きの一行・ファイル名・日付や見積りの入った見出し・図のラベル列は拾わない。無ければ空配列。
86
81
  - decisions も**拾うだけ**です。本文(workBlocks / handoff / insights / selfNote)の中から、**決まったことを述べている一文**を原文のまま抜き出してください。拾うのは「〜することにした」「〜で確定」「〜を採用」「〜に決めた」「〜で進める」のように決定を述べた文だけです。「〜したい」「〜を検討」「〜が良さそう」「〜すべき」「〜を提案」のような**まだ決まっていない文は入れないでください**。1件につき1文(複数の決定を1文にまとめない)、120字まで、1日あたり最大5件。迷ったら入れない。無ければ空配列。
87
- - insights(気づき・提案)は提案どまり。実行したとは書かない。無理に作らず、根拠が無ければ空配列。
82
+ - insights(気づき・提案)は提案どまり。実行したとは書かない。無理に作らず、根拠が無ければ空配列。1日0〜2件。
83
+ - insights のうち、**くり返し守る仕事の決まりとして置けるもの**には "rule": true を付けてください。true にしてよいのは、**いつ効くか(きっかけ:毎週月曜・マージの前・納品したら など)・どう確かめるか(OK がある・1件以上ある など)・当たったらどうするか(止める/知らせる/タスクを出す)**の3つが書けるものだけです。3つを desc に1文ずつ書き、type にルールの型(周期の知らせ/止めて通す/次を出す/照合して印/放置の監視)を入れてください。きっかけの無い方針・画面や作り方の決まり・一度きりの提案・作業の感想・機能の要望は false。迷ったら false。true の件は CLUBS のルールの候補としてチームに共有されます。
84
+ - insights の各件に、どこの話かを1つだけ付けてください。案件の話なら case に案件名を、案件に属さない話(進め方・開発の手順・営業など)なら area に分野を短く書き、もう片方は空文字。{{CASES}}{{DECIDED}}{{ROUTINES}}
85
+ - case を書いた件にだけ ask(その案件で決めること)を付けてください。オーナーが**はい/いいえで答えられる問い**を1文で、「〜するか」で終える(例:「掲示板の写真報告と役場の承認を、PUBLIC の標準機能にするか」)。選択肢や自分の答えは書かない。60字まで。area の件の ask は空文字(見出しがそのまま決定の文になる)。
86
+ - workBlocks の各行の repos には、その行のもとになったプロジェクト(ダイジェストの「[プロジェクト名]」)を書いてください。ダイジェストに無い名前は書かない。分からなければ空配列。
88
87
  - selfNote は敬体の一言(2文以内)。誇張しない。
89
88
  - 文体は必ずですます調(AI がオーナーに宛てて書く日報です)。text・handoff・selfNote すべて、文末を「〜しました/〜しています/〜です」にする。である調(〜した/〜である)や体言止めの多用は避ける。title は名詞句のままでよい。例: ×「週次レポートに反映した。」→ ○「週次レポートに反映しました。」
90
89
 
@@ -92,11 +91,11 @@ const PROMPT_HEADER = `あなたはオーナーの AI アシスタント「{{AGE
92
91
 
93
92
  {
94
93
  "workBlocks": [
95
- { "icon": "案件に合う絵文字1つ(例: 実装📦 / 連携🔀 / データ🗄️ / 為替💱 / メール📧 / 修正🐛 / 文書📄 / デプロイ🚀 / 調査🔎 / 会議🤝)", "title": "案件名(短く)", "tool": "使ったツール名", "text": "2〜4文の要点ナラティブ(代表的な成果と結果。列挙・網羅しない・3〜5行目安)", "metric": "集計の一行(例: 実行 11回 · 成功 11 / 失敗 0)" }
94
+ { "icon": "案件に合う絵文字1つ(例: 実装📦 / 連携🔀 / データ🗄️ / 為替💱 / メール📧 / 修正🐛 / 文書📄 / デプロイ🚀 / 調査🔎 / 会議🤝)", "title": "案件名(短く)", "tool": "使ったツール名", "text": "2〜4文の要点ナラティブ(代表的な成果と結果。列挙・網羅しない・3〜5行目安)", "metric": "集計の一行(例: 実行 11回 · 成功 11 / 失敗 0)", "repos": ["その行の作業をしたプロジェクト名(ダイジェストの [ ] の名前を一字一句そのまま・分からなければ空配列)"] }
96
95
  ],
97
96
  "handoff": { "title": "明日にまわすもの", "text": "申し送り。1行につき用件ひとつの箇条書き(行頭は - )。無ければ『特にありません。』" },
98
97
  "insights": [
99
- { "icon": "🔁", "title": "気づきの見出し", "desc": "根拠と提案", "save": "見込み効果(不明なら空文字)" }
98
+ { "icon": "🔁", "title": "気づきの見出し(言い切りの形)", "desc": "根拠と提案", "case": "案件名(案件の話のときだけ・それ以外は空文字)", "area": "分野(案件に属さないときだけ・それ以外は空文字)", "ask": "その案件で決めること(case のときだけ・はい/いいえで答えられる問い・〜するか)", "save": "見込み効果(不明なら空文字)", "rule": false, "because": "もとにした決定の文(次の一手のときだけ・それ以外は空文字)", "type": "提案の型(次の一手のときだけ・それ以外は空文字)" }
100
99
  ],
101
100
  "selfNote": "ひとこと",
102
101
  "terms": ["本文に出てきた固有名詞的な語(原文のまま)", "…"],
@@ -107,25 +106,98 @@ const PROMPT_HEADER = `あなたはオーナーの AI アシスタント「{{AGE
107
106
  `;
108
107
 
109
108
  /**
110
- * 前日から継続している用件を、**原文のまま**プロンプトに載せる。
109
+ * オーナーの進行中の案件名。提案の case はこの中から選ばせる(CLUBS 側で
110
+ * 案件と照らし合わせるので、表記がずれると結びつかない)。一覧が無ければ
111
+ * 見出しごと出さない——そのときの case は AI の見立てで、CLUBS 側で
112
+ * 一致しなければ札は出ない。
113
+ */
114
+ const casesSection = (cases) => {
115
+ const items = (Array.isArray(cases) ? cases : [])
116
+ .map((s) => String(s ?? "").trim())
117
+ .filter(Boolean);
118
+ if (!items.length) return "";
119
+ return [
120
+ "",
121
+ " case に書く案件名は、次の一覧から一字一句そのまま選んでください(当てはまらなければ area に分野を書く):",
122
+ ...items.map((s) => ` - ${s}`),
123
+ ].join("\n");
124
+ };
125
+
126
+ /**
127
+ * きのう CLUBS で決まったこと(本人 2026-10-04:もちの次の一手)。ここから続く
128
+ * 次の一手を insights に足させる。もとの決定は because に**一字一句**写させる——
129
+ * CLUBS が案件の判断に照らし直し、当たったときだけ「〇〇を決めたので」と出す。
130
+ * 決まったことが無ければ見出しごと(指示ごと)出さない。
131
+ */
132
+ const decidedSection = (decided) => {
133
+ const items = (Array.isArray(decided) ? decided : [])
134
+ .map((d) => ({
135
+ case: String(d?.case ?? "").trim(),
136
+ text: String(d?.text ?? "").trim(),
137
+ }))
138
+ .filter((d) => d.case && d.text);
139
+ if (!items.length) return "";
140
+ return [
141
+ "",
142
+ "- 下の「きのう CLUBS で決まったこと」から、続けてやるとよい**次の一手**があれば insights に最大3件まで足してください(上の 0〜2件とは別に数える)。case にその案件名を、because にもとの決定の文を**一字一句そのまま**書き、type には「周期の知らせ/止めて通す/次を出す/照合して印/放置の監視」から当てはまるものを1つ(多くは次を出す・当たらなければ空文字)。決定をなぞるだけのもの・ダイジェストでもう済んでいることは書かない。思いつかなければ足さない。",
143
+ " きのう CLUBS で決まったこと([案件名]決定の文):",
144
+ ...items.map((d) => ` - [${d.case}]${d.text}`),
145
+ ].join("\n");
146
+ };
147
+
148
+ /** くり返し1件の曜日の偏り(例:「・月 3」)。 */
149
+ const wdText = (weekdays) =>
150
+ (Array.isArray(weekdays) ? weekdays : [])
151
+ .map(([w, n]) => `${w} ${n}`)
152
+ .join("・");
153
+
154
+ /**
155
+ * この4週のくり返し(週に1回・月曜朝の日報づくりだけ・本人 2026-10-04)。
156
+ * CLUBS の MCP(collector-config の routines)と手元の履歴(localRoutines)。
157
+ * ここから、きっかけ・確かめ・動きが書けるものをルールの候補にさせる。
158
+ * どちらも無ければ見出しごと(指示ごと)出さない。
159
+ */
160
+ const routinesSection = (routines) => {
161
+ const clubs = (Array.isArray(routines?.clubs) ? routines.clubs : []).filter(
162
+ (r) => r?.what,
163
+ );
164
+ const projects = Array.isArray(routines?.local?.projects) ? routines.local.projects : [];
165
+ const tools = Array.isArray(routines?.local?.tools) ? routines.local.tools : [];
166
+ if (!clubs.length && !projects.length && !tools.length) return "";
167
+ const line = (label, days, weekdays) => {
168
+ const wd = wdText(weekdays);
169
+ return ` - ${label}:${days}日${wd ? `(${wd})` : ""}`;
170
+ };
171
+ return [
172
+ "",
173
+ "- 下の「この4週のくり返し」から、**同じきっかけでくり返している仕事**があれば、ルールの候補として insights に最大2件まで足してください(上の件数とは別に数える)。rule: true にし、desc に「いつ効くか・どう確かめるか・当たったらどうするか」を1文ずつ、type にルールの型を入れる。きっかけが書けないもの・たまたま重なっただけのものは出さない。思いつかなければ足さない。",
174
+ " この4週のくり返し(使った日数・曜日の偏り):",
175
+ ...clubs.map((r) => line(`CLUBS ${r.what}`, r.days, r.weekdays)),
176
+ ...projects.map((r) => line(`プロジェクト ${r.name}`, r.days, r.weekdays)),
177
+ ...tools.map((r) => line(`道具 ${r.name}`, r.days, r.weekdays)),
178
+ ].join("\n");
179
+ };
180
+
181
+ /**
182
+ * すでに申し送った未完了の用件を、**原文のまま**プロンプトに載せる——
183
+ * 同じものを申し送りにもう一度書かせないため。
111
184
  *
112
- * 繰越が同定されない主因は粒度である。1アイテムが「その日の申し送り全文」
113
- * (3〜5の別用件を含む1段落)になっていると、日ごとに必ず内容が変わるため
114
- * fingerprint は原理的に一致しない。上の「1行1用件」がそれを潰し、ここは
115
- * その次の問題=言い回しの揺れを潰す。**1行目が効かないとここは無意味**なので、
116
- * 優先度は箇条書きの指示が上。
185
+ * 以前は「前日から継続している用件」として渡し、同じ表現で書き写させていた
186
+ * (繰越の同定のため)。LLM は毎日律儀に書き写し、同じ一文が1か月日報に
187
+ * 載り続けた。済んでいないものは CLUBS のタスクページに残っているので、
188
+ * 日報でくり返す必要は無い(本人判断 2026-09-29)。
117
189
  *
118
190
  * 生成側への指示でしかなく、サーバー側の判定にも ✓ の発行にも影響しない
119
191
  * (カノン「規則の適用と ✓ の鋳造はコア専権」に抵触しない)。
120
192
  */
121
- const carryoverSection = (carryover) => {
122
- const items = (Array.isArray(carryover) ? carryover : [])
193
+ const handedOffSection = (handedOff) => {
194
+ const items = (Array.isArray(handedOff) ? handedOff : [])
123
195
  .map((s) => String(s ?? "").trim())
124
196
  .filter(Boolean);
125
197
  if (!items.length) return "";
126
198
  return [
127
199
  "",
128
- " 前日から継続している用件:",
200
+ " すでに申し送った用件(申し送りには書かない):",
129
201
  ...items.map((s) => ` - ${s}`),
130
202
  ].join("\n");
131
203
  };
@@ -167,7 +239,7 @@ const termsSection = (terms) => {
167
239
  };
168
240
 
169
241
  /*
170
- * decisions には `carryoverSection` / `termsSection` にあたる差し込みが無い。
242
+ * decisions には `handedOffSection` / `termsSection` にあたる差し込みが無い。
171
243
  * Phase 1 は**検出だけ**で、確定操作も出口(生成側への還流)も持たないため——
172
244
  * 還流できるのは人が確定したものだけで、確定する画面がまだ無い。
173
245
  * 出力 JSON に列が増えるだけなので、プロンプトは片方向で閉じている。
@@ -175,10 +247,13 @@ const termsSection = (terms) => {
175
247
 
176
248
  /**
177
249
  * ダイジェスト付きの完成プロンプト。
178
- * `carryover` は未完了の申し送り、`terms` は確定した代表表記
179
- * (どちらも CLUBS の collector-config が返す)。どちらも空なら見出しごと消える。
250
+ * `handedOff` は申し送り済みの未完了、`terms` は確定した代表表記、`cases` は進行中の案件名、
251
+ * `decided` はきのう決まったこと(どれも CLUBS の collector-config が返す)。どちらも空なら見出しごと消える。
180
252
  */
181
- export const buildPrompt = (activity, { carryover, terms } = {}) =>
253
+ export const buildPrompt = (activity, { handedOff, terms, cases, decided, routines } = {}) =>
182
254
  PROMPT_HEADER.replace("{{AGENT}}", activity.agentName)
183
- .replace("{{CARRYOVER}}", carryoverSection(carryover))
184
- .replace("{{TERMS}}", termsSection(terms)) + buildDigest(activity);
255
+ .replace("{{HANDED_OFF}}", handedOffSection(handedOff))
256
+ .replace("{{TERMS}}", termsSection(terms))
257
+ .replace("{{CASES}}", casesSection(cases))
258
+ .replace("{{DECIDED}}", decidedSection(decided))
259
+ .replace("{{ROUTINES}}", routinesSection(routines)) + buildDigest(activity);
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import test from "node:test";
7
2
  import assert from "node:assert/strict";
8
3
  import { buildPrompt } from "./prompt.mjs";
@@ -31,25 +26,27 @@ test("粒度の指示(1行1用件)が必ず入る", () => {
31
26
  assert.match(p, /1行につき用件はひとつだけです/);
32
27
  });
33
28
 
34
- test("継続中の用件が無いときは、その見出しごと出ない(プレースホルダも残らない)", () => {
29
+ test("申し送り済みの用件が無いときは、その見出しごと出ない(プレースホルダも残らない)", () => {
35
30
  const p = buildPrompt(activity);
36
- assert.doesNotMatch(p, /\{\{CARRYOVER\}\}/);
37
- assert.doesNotMatch(p, /前日から継続している用件/);
31
+ assert.doesNotMatch(p, /\{\{HANDED_OFF\}\}/);
32
+ assert.doesNotMatch(p, /すでに申し送った用件(/);
38
33
  });
39
34
 
40
- test("継続中の用件は原文のまま列挙される(言い回しを変えさせない)", () => {
41
- const carryover = ["PR #32のマージ判断", "接続ページの実装着手"];
42
- const p = buildPrompt(activity, { carryover });
43
- assert.match(p, /前日から継続している用件:/);
44
- for (const item of carryover) assert.ok(p.includes(`- ${item}`), item);
35
+ test("申し送り済みの用件は原文のまま「書かない」一覧として載る", () => {
36
+ const handedOff = ["PR #32のマージ判断", "接続ページの実装着手"];
37
+ const p = buildPrompt(activity, { handedOff });
38
+ assert.match(p, /すでに申し送った用件(申し送りには書かない):/);
39
+ assert.match(p, /もう一度書かないでください/);
40
+ assert.doesNotMatch(p, /前日と同じ表現をそのまま使って/);
41
+ for (const item of handedOff) assert.ok(p.includes(`- ${item}`), item);
45
42
  });
46
43
 
47
44
  test("空文字・空白だけの項目は落とす", () => {
48
- const p = buildPrompt(activity, { carryover: ["", " ", null] });
49
- assert.doesNotMatch(p, /前日から継続している用件/);
45
+ const p = buildPrompt(activity, { handedOff: ["", " ", null] });
46
+ assert.doesNotMatch(p, /すでに申し送った用件(/);
50
47
  });
51
48
 
52
- test("carryover の指定が無くても壊れない(tick 以外の手動実行)", () => {
49
+ test("handedOff の指定が無くても壊れない(tick 以外の手動実行)", () => {
53
50
  assert.doesNotThrow(() => buildPrompt(activity));
54
51
  assert.doesNotThrow(() => buildPrompt(activity, {}));
55
52
  });
@@ -96,12 +93,12 @@ test("文字列だけの terms でも壊れない(サーバ応答の形が痩
96
93
  assert.doesNotMatch(p, /\{\{TERMS\}\}/);
97
94
  });
98
95
 
99
- test("carryover と terms は同時に載る", () => {
96
+ test("handedOff と terms は同時に載る", () => {
100
97
  const p = buildPrompt(activity, {
101
- carryover: ["PR #32のマージ判断"],
98
+ handedOff: ["PR #32のマージ判断"],
102
99
  terms: [{ canonical: "MCP認証", variants: [] }],
103
100
  });
104
- assert.match(p, /前日から継続している用件:/);
101
+ assert.match(p, /すでに申し送った用件(申し送りには書かない):/);
105
102
  assert.match(p, /以下の表記は、この形で書いてください:/);
106
103
  });
107
104
 
@@ -139,8 +136,78 @@ test("件数と長さの上限がプロンプトにも書かれる(サーバ
139
136
  test("decisions には還流の差し込みが無い(Phase 1 は片方向)", () => {
140
137
  // 確定操作が無いので、生成側へ戻せる「確定した決定」は原理的に存在しない。
141
138
  const p = buildPrompt(activity, {
142
- carryover: ["継続の用件"],
139
+ handedOff: ["申し送り済みの用件"],
143
140
  terms: ["用語"],
144
141
  });
145
142
  assert.doesNotMatch(p, /\{\{DECISIONS\}\}/);
146
143
  });
144
+
145
+ // ── 提案の「決めること」と札(案件/分野)────────────────────
146
+
147
+ test("提案に case / area を書かせ、ask(はい/いいえの問い)は案件の件だけ", () => {
148
+ const p = buildPrompt(activity);
149
+ assert.match(p, /"ask":/);
150
+ assert.match(p, /"case":/);
151
+ assert.match(p, /"area":/);
152
+ assert.match(p, /はい/いいえで答えられる問い/);
153
+ assert.match(p, /選択肢や自分の答えは書かない/);
154
+ assert.match(p, /case を書いた件にだけ ask/);
155
+ assert.match(p, /area の件の ask は空文字/);
156
+ });
157
+
158
+ test("案件名の一覧があれば、そこから一字一句選ばせる", () => {
159
+ const p = buildPrompt(activity, { cases: ["MizuDAkO(西川町)", " ", "Lemma制約チェックデモ"] });
160
+ assert.match(p, /次の一覧から一字一句そのまま選んで/);
161
+ assert.ok(p.includes(" - MizuDAkO(西川町)"));
162
+ assert.ok(p.includes(" - Lemma制約チェックデモ"));
163
+ });
164
+
165
+ test("案件名の一覧が無いときは見出しもプレースホルダも出ない", () => {
166
+ const p = buildPrompt(activity);
167
+ assert.doesNotMatch(p, /\{\{CASES\}\}/);
168
+ assert.doesNotMatch(p, /次の一覧から一字一句/);
169
+ });
170
+
171
+ test("きのう決まったことがあれば、次の一手の指示と一覧が載る(本人 2026-10-04)", () => {
172
+ const p = buildPrompt(activity, {
173
+ decided: [
174
+ { case: "CLUBS改善", text: "AI の提案の型はルールの型に寄せる" },
175
+ { case: "", text: "案件名の無いものは載せない" },
176
+ { case: "CLUBS改善", text: "" },
177
+ ],
178
+ });
179
+ assert.doesNotMatch(p, /\{\{DECIDED\}\}/);
180
+ assert.match(p, /\*\*次の一手\*\*/);
181
+ assert.match(p, /because にもとの決定の文を\*\*一字一句そのまま\*\*/);
182
+ assert.ok(p.includes(" - [CLUBS改善]AI の提案の型はルールの型に寄せる"));
183
+ assert.ok(!p.includes("案件名の無いものは載せない"));
184
+ });
185
+
186
+ test("きのう決まったことが無ければ、次の一手の指示ごと出ない", () => {
187
+ for (const p of [buildPrompt(activity), buildPrompt(activity, { decided: [] })]) {
188
+ assert.doesNotMatch(p, /\{\{DECIDED\}\}/);
189
+ assert.doesNotMatch(p, /次の一手\*\*があれば/);
190
+ assert.doesNotMatch(p, /きのう CLUBS で決まったこと/);
191
+ }
192
+ });
193
+
194
+ test("くり返しがあれば、ルールの候補の指示と一覧が載る(週に1回)", () => {
195
+ const p = buildPrompt(activity, {
196
+ routines: {
197
+ clubs: [{ what: "clubs_leave:task_ideas", count: 4, days: 3, weekdays: [["月", 3]] }],
198
+ local: { projects: [{ name: "clubsx", days: 12, weekdays: [] }], tools: [] },
199
+ },
200
+ });
201
+ assert.doesNotMatch(p, /\{\{ROUTINES\}\}/);
202
+ assert.match(p, /同じきっかけでくり返している仕事/);
203
+ assert.ok(p.includes(" - CLUBS clubs_leave:task_ideas:3日(月 3)"));
204
+ assert.ok(p.includes(" - プロジェクト clubsx:12日"));
205
+ });
206
+
207
+ test("くり返しが無ければ指示ごと出ない・rule の基準は きっかけ・確かめ・動き", () => {
208
+ for (const p of [buildPrompt(activity), buildPrompt(activity, { routines: null })]) {
209
+ assert.doesNotMatch(p, /\{\{ROUTINES\}\}/);
210
+ assert.doesNotMatch(p, /この4週のくり返し/);
211
+ assert.match(p, /いつ効くか(きっかけ/);
212
+ }
213
+ });
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * CLUBS(clubsx)の collector-config を読む。
8
3
  *
@@ -59,7 +54,11 @@ export const fetchCollectorConfig = async ({
59
54
  10_000,
60
55
  );
61
56
  if (!res.ok) {
62
- return { ok: false, reason: `collector-config が ${res.status} を返しました` };
57
+ return {
58
+ ok: false,
59
+ status: res.status,
60
+ reason: `collector-config が ${res.status} を返しました`,
61
+ };
63
62
  }
64
63
  const body = await res.json();
65
64
  if (!body || typeof body !== 'object' || !body.schedule) {
package/src/render.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { hostname } from "node:os";
7
2
  import { REDACT_TOKEN } from "./mask.mjs";
8
3
 
@@ -119,6 +114,7 @@ const insights = (narrative) => {
119
114
  <td valign="top" style="padding-left:10px">
120
115
  <div style="font-size:13px;font-weight:800;color:${T.ink};line-height:1.35">${withRedactions(i.title)}</div>
121
116
  <div style="font-size:12px;color:${T.ink2};margin-top:2px;line-height:1.6">${withRedactions(i.desc)}</div>
117
+ ${i.case && i.ask ? `<div style="font-size:11.5px;font-weight:800;color:${T.brand2};line-height:1.5;margin-top:5px">この案件(${withRedactions(i.case)})で決めること:${withRedactions(i.ask)}</div>` : ""}
122
118
  ${i.save ? `<div style="font-family:${T.mono};font-size:11px;color:${T.ok};font-weight:600;margin-top:3px">▹ ${withRedactions(i.save)}</div>` : ""}
123
119
  </td>
124
120
  </tr>
@@ -0,0 +1,61 @@
1
+ import { readHistory } from "./history.mjs";
2
+
3
+ /**
4
+ * 手元の履歴(out/history.jsonl)から、この4週のくり返しを取り出す(本人
5
+ * 2026-10-04:ルールの候補は実際の仕事のくり返しから出す)。CLUBS の MCP の
6
+ * くり返し(collector-config の routines)と合わせて、月曜朝の日報づくりに渡す。
7
+ *
8
+ * 外へ出すのは**名前と日数だけ**(プロジェクト名・道具名)。ファイルは見ない。
9
+ */
10
+
11
+ const WEEKDAY = ["日", "月", "火", "水", "木", "金", "土"];
12
+
13
+ /** どの日にも使う道具(くり返しとして意味が無いので数えない)。 */
14
+ const BASIC_TOOLS = new Set([
15
+ "Bash", "Read", "Edit", "Write", "Grep", "Glob", "MultiEdit", "TodoWrite",
16
+ "ToolSearch", "LS", "NotebookEdit", "exec_command", "apply_patch", "shell",
17
+ ]);
18
+
19
+ const addDays = (day, n) =>
20
+ new Date(Date.parse(`${day}T00:00:00Z`) + n * 864e5).toISOString().slice(0, 10);
21
+
22
+ /** 名前 → { days, weekdays } を、日数の多い順に max 件(2日以上のものだけ)。 */
23
+ const tally = (rows, namesOf, max) => {
24
+ const acc = new Map();
25
+ for (const r of rows) {
26
+ const wd = new Date(`${r.date}T00:00:00Z`).getUTCDay();
27
+ for (const name of new Set(namesOf(r))) {
28
+ const a = acc.get(name) ?? { days: 0, wd: [0, 0, 0, 0, 0, 0, 0] };
29
+ a.days += 1;
30
+ a.wd[wd] += 1;
31
+ acc.set(name, a);
32
+ }
33
+ }
34
+ return [...acc.entries()]
35
+ .filter(([, a]) => a.days >= 2)
36
+ .sort((x, y) => y[1].days - x[1].days)
37
+ .slice(0, max)
38
+ .map(([name, a]) => ({
39
+ name,
40
+ days: a.days,
41
+ weekdays: a.wd
42
+ .map((n, i) => [WEEKDAY[i], n])
43
+ .filter(([, n]) => n >= 2)
44
+ .sort((x, y) => y[1] - x[1]),
45
+ }));
46
+ };
47
+
48
+ /** `to`(を含む)までの28日の履歴から、プロジェクトと道具のくり返し。 */
49
+ export const localRoutines = (to, history = readHistory(), max = 6) => {
50
+ const from = addDays(to, -27);
51
+ const rows = history.filter((r) => r.date >= from && r.date <= to);
52
+ return {
53
+ days: rows.length,
54
+ projects: tally(rows, (r) => (Array.isArray(r.projects) ? r.projects : []), max),
55
+ tools: tally(
56
+ rows,
57
+ (r) => Object.keys(r.toolCounts ?? {}).filter((t) => !BASIC_TOOLS.has(t)),
58
+ max,
59
+ ),
60
+ };
61
+ };
@@ -0,0 +1,29 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { localRoutines } from "./routines.mjs";
4
+
5
+ // 2026-09-14・21・28 は月曜。
6
+ const history = [
7
+ { date: "2026-09-14", projects: ["clubsx", "lemma"], toolCounts: { Bash: 9, mcp__clubs__clubs_leave: 2 } },
8
+ { date: "2026-09-21", projects: ["clubsx"], toolCounts: { Read: 3, mcp__clubs__clubs_leave: 1 } },
9
+ { date: "2026-09-28", projects: ["clubsx"], toolCounts: { Edit: 1 } },
10
+ { date: "2026-09-30", projects: ["lemma"], toolCounts: {} },
11
+ { date: "2026-08-01", projects: ["old"], toolCounts: {} },
12
+ { date: "2026-08-02", projects: ["old"], toolCounts: {} },
13
+ ];
14
+
15
+ test("4週のくり返し:2日以上だけ・日数の多い順・曜日の偏り・ふだんの道具は数えない・古い日は入れない", () => {
16
+ const r = localRoutines("2026-10-04", history);
17
+ assert.equal(r.days, 4);
18
+ assert.deepEqual(r.projects, [
19
+ { name: "clubsx", days: 3, weekdays: [["月", 3]] },
20
+ { name: "lemma", days: 2, weekdays: [] },
21
+ ]);
22
+ assert.deepEqual(r.tools, [{ name: "mcp__clubs__clubs_leave", days: 2, weekdays: [["月", 2]] }]);
23
+ });
24
+
25
+ test("古い履歴(projects の無い行)でも落ちない", () => {
26
+ const r = localRoutines("2026-10-04", [{ date: "2026-10-01" }, { date: "2026-10-02" }]);
27
+ assert.deepEqual(r.projects, []);
28
+ assert.deepEqual(r.tools, []);
29
+ });
package/src/send.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * 送信するのは「本文(マスク済)+集計数字+(あれば)署名」だけ。
8
3
  * activity.events / activity.prompts(生ログ由来)はここに渡ってこない。
package/src/settings.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { APP_HOME } from "./config.mjs";
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync, readdirSync, statSync } from "node:fs";
7
2
  import { join, basename } from "node:path";
8
3
  import { maskText } from "../mask.mjs";
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readdirSync, readFileSync, statSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { homedir } from "node:os";
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readdirSync, readFileSync, statSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { homedir } from "node:os";
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
7
2
  import { join, dirname, basename } from "node:path";
8
3
  import { homedir, platform } from "node:os";
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync } from "node:fs";
7
2
  import { maskText } from "../mask.mjs";
8
3
 
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readdirSync, readFileSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { homedir } from "node:os";
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { homedir } from "node:os";
package/src/store.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync, writeFileSync, readdirSync, mkdirSync, existsSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { APP_HOME } from "./config.mjs";
package/src/tasks.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { createHash } from "node:crypto";
package/src/tool-name.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * ツール名を人が読める形にする。
8
3
  *
package/web/assets/qr.js CHANGED
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * QR コード生成(依存ゼロ・バイトモード・誤り訂正レベル M)。
8
3
  *
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /* CLUBS Console の共通サイドバー。
7
2
  3画面(日報一覧・日報編集・設定)から読む。ビルドは無し・依存も無し。
8
3
  AI名は /api/settings の実データから入れる。固定値を出すと誰の日報か嘘になる。 */
@@ -1,8 +1,3 @@
1
- // clubs-nippo — AI業務日報 (CLUBS)
2
- // Copyright (c) 2026 FRAME00 Inc.
3
- // SPDX-License-Identifier: MIT
4
- // https://github.com/frame00/clubs-nippo
5
-
6
1
  /**
7
2
  * カードの主アイコン(Twemoji・インライン SVG)。
8
3
  *