@tyhld/conductor 0.12.91 → 0.12.92

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.
@@ -36,7 +36,7 @@ import os from 'node:os';
36
36
  import path from 'node:path';
37
37
  import { fileURLToPath } from 'node:url';
38
38
  import { CLAUDE_MIN_VERSION, compareVersions, isProcessAlive, parseClaudeVersion, resolveClaudeBin, } from './headless.js';
39
- import { defaultExec, fetchClaudeStable, npmBesideNode, pickCandidates } from './selfcheck.js';
39
+ import { defaultExec, fetchClaudeStable, npmBesideNode, pickCandidates, selfcheckDir } from './selfcheck.js';
40
40
  // ─────────────────────────────────────────────────────────────────────────────
41
41
  // 定数(★時間の値は設計 3ed9e2b2 の推奨値・管制の ROLLOUT_TIMES と同じ考え方)
42
42
  // ─────────────────────────────────────────────────────────────────────────────
@@ -55,6 +55,12 @@ export const RESTART_WAIT_MS = 120_000;
55
55
  export const RESTART_POLL_MS = 5_000;
56
56
  /** 戻しても直らなかった機で、自己診断を測り直す間隔。 */
57
57
  export const HELD_RECHECK_MS = 60 * 60 * 1000;
58
+ /**
59
+ * 自己診断が「人にしかできない操作(🔵)」だけで落ちているとき、測り直す間隔(便 4cf71dde)。
60
+ * ★人は直したあと何も打たない。この間隔で更新係が自己診断をやり直し、通れば先へ進む。
61
+ * 1回 約0.04ドルなので、待っているあいだも1時間に1回まで(戻しても直らない機の測り直しと同じ間隔)。
62
+ */
63
+ export const HUMAN_RECHECK_MS = 60 * 60 * 1000;
58
64
  /** 候補を届けられなかったとき、もう一度届けに行くまでの間隔。 */
59
65
  export const CANDIDATE_RETRY_MS = 24 * 60 * 60 * 1000;
60
66
  const SEMVER = /^\d+\.\d+\.\d+$/;
@@ -352,7 +358,10 @@ async function afterSwap(d, s) {
352
358
  const failed = r.failedKeys.join(',') || '不明';
353
359
  if (!s.rollback) {
354
360
  // ★前の版へ戻す(そのPCだけ)。
355
- addFailed(s, comp, s.to);
361
+ // ★人にしかできない操作だけで落ちたときは、版の不具合と決めつけない(落ちた版の印を付けない)。
362
+ // 人が直した後、管制がまだその版を返していれば、もう一度入れ替えて確かめられる(便 4cf71dde)。
363
+ if (!r.humanOnly)
364
+ addFailed(s, comp, s.to);
356
365
  const back = s.from ?? null;
357
366
  if (comp === 'claude-code' && back && compareVersions(back, CLAUDE_MIN_VERSION) < 0) {
358
367
  s.phase = 'held';
@@ -409,6 +418,49 @@ async function rollBack(d, s, back) {
409
418
  removeDrain(d, s);
410
419
  return afterSwap(d, s);
411
420
  }
421
+ /** 手元の自己診断の控え(last.json)のうち、更新係が読む物。読めなければ null。 */
422
+ export function readLastSelfcheck(home) {
423
+ try {
424
+ const j = JSON.parse(readFileSync(path.join(selfcheckDir(home), 'last.json'), 'utf8'));
425
+ if (j.result !== 'PASS' && j.result !== 'FAIL')
426
+ return null;
427
+ const failed = (j.items ?? []).filter((i) => i.ok === false);
428
+ const at = typeof j.finishedAt === 'string' ? Date.parse(j.finishedAt) : NaN;
429
+ return {
430
+ pass: j.result === 'PASS',
431
+ humanOnly: failed.length > 0 && failed.every((i) => i.failKind === 'human'),
432
+ finishedAt: Number.isFinite(at) ? at : null,
433
+ };
434
+ }
435
+ catch {
436
+ return null;
437
+ }
438
+ }
439
+ /**
440
+ * 最後の自己診断が「人にしかできない操作」だけで落ちているあいだは、人の手番を待つ(便 4cf71dde)。
441
+ * ★人が直しても何も打たなくてよいように、HUMAN_RECHECK_MS ごとに自己診断をやり直す。通れば待ちは終わり。
442
+ * 返り値 waiting=true =まだ待つ(入れ替えない・候補の自己診断を流さない)。
443
+ * ★仕組みの異常(fault)が混ざる FAIL は、ここでは待たない(今までどおり入れ替え・戻しで確かめる)。
444
+ */
445
+ function waitingForHuman(d, now) {
446
+ const last = readLastSelfcheck(d.home);
447
+ if (!last || last.pass || !last.humanOnly)
448
+ return { waiting: false, detail: '' };
449
+ const age = last.finishedAt === null ? Infinity : now.getTime() - last.finishedAt;
450
+ if (age < HUMAN_RECHECK_MS) {
451
+ const next = new Date((last.finishedAt ?? now.getTime()) + HUMAN_RECHECK_MS).toISOString();
452
+ return { waiting: true, detail: `自己診断が人の操作待ち(🔵)で落ちたままです。次の測り直し ${next}` };
453
+ }
454
+ const r = d.runSelfcheck('daily');
455
+ if (r.pass) {
456
+ d.log('人の操作待ちだった自己診断が通りました。先へ進みます');
457
+ return { waiting: false, detail: '' };
458
+ }
459
+ return {
460
+ waiting: true,
461
+ detail: `自己診断を測り直しましたが、まだ通りません(${r.failedKeys.join(',') || '不明'}${r.humanOnly ? '・人の操作待ち' : ''})`,
462
+ };
463
+ }
412
464
  // ─────────────────────────────────────────────────────────────────────────────
413
465
  // 1回ぶん
414
466
  // ─────────────────────────────────────────────────────────────────────────────
@@ -465,6 +517,9 @@ export async function runUpdater(d) {
465
517
  const why = !target.ready ? '管制はまだ準備中です' : target.halted ? '管制で自動更新が止められています' : 'この機は自動更新の対象ではありません';
466
518
  return done(d, s, 'skip', `${why}(${target.reason ?? '理由なし'})。何もしません`);
467
519
  }
520
+ // ③-2 人の操作待ち(🔵)で自己診断が落ちたままなら、測り直してから先へ進む(便 4cf71dde)。
521
+ // ★落ちたまま入れ替えると、同じ理由で落ちて戻し、版に疑いが付く(実物 2026-10-03 02:43Z・ノートPC)。
522
+ const human = waitingForHuman(d, now);
468
523
  // ④ どの部品を動かすか(★conductor の目標が今と違うあいだは Claude Code を動かさない)。
469
524
  const claudeNow = d.resolveClaude();
470
525
  let comp = null;
@@ -495,11 +550,18 @@ export async function runUpdater(d) {
495
550
  if (!comp || !want) {
496
551
  removeDrain(d, s);
497
552
  s.busySince = null;
553
+ if (human.waiting)
554
+ return done(d, s, 'wait-human', human.detail);
498
555
  // ★後続の機は候補を探さない・候補の自己診断を流さない(役は管制が決める・欄が無ければ今までどおり)。
499
556
  if (target.role !== 'follower')
500
557
  await reportCandidates(d, s, installed, claudeNow?.version ?? null);
501
558
  return done(d, s, 'none', `目標の版と同じです(conductor ${installed ?? '不明'}・Claude Code ${claudeNow?.version ?? '不明'})`);
502
559
  }
560
+ if (human.waiting) {
561
+ removeDrain(d, s);
562
+ s.busySince = null;
563
+ return done(d, s, 'wait-human', `${comp} ${want} へは入れ替えません。${human.detail}`);
564
+ }
503
565
  // ⑤ 全現場が空きか。
504
566
  const busy = busySites(d.home, d.isAlive);
505
567
  if (busy.length > 0) {
@@ -603,11 +665,14 @@ export function selfcheckViaEntry(home, exec, env) {
603
665
  catch {
604
666
  // 読めない=FAIL として扱う(迷ったら戻す側)
605
667
  }
606
- const failedKeys = (parsed.items ?? []).filter((x) => x.ok === false).map((x) => String(x.key));
668
+ const failedItems = (parsed.items ?? []).filter((x) => x.ok === false);
669
+ const failedKeys = failedItems.map((x) => String(x.key));
607
670
  const pass = r.status === 0 && parsed.result === 'PASS';
671
+ // ★読めなかった FAIL(項目が無い)は人の操作待ちとは言わない(迷ったら版を疑う側)。
672
+ const humanOnly = !pass && failedItems.length > 0 && failedItems.every((x) => x.failKind === 'human');
608
673
  if (!pass && failedKeys.length === 0)
609
674
  failedKeys.push(`selfcheck(終了 ${r.status})`);
610
- return { pass, failedKeys, sent: typeof parsed.sent?.sent === 'boolean' ? parsed.sent.sent : null };
675
+ return { pass, failedKeys, sent: typeof parsed.sent?.sent === 'boolean' ? parsed.sent.sent : null, humanOnly };
611
676
  };
612
677
  }
613
678
  /** `conductor autoupdate [--boot]` の入口。返り値は終了コード(★失敗でも 0=タイマーを止めない。使い方の誤りだけ 2)。 */
package/dist/selfcheck.js CHANGED
@@ -164,8 +164,9 @@ export function summarizeStream(stdout) {
164
164
  * init 行には `mcp_servers: [{ name, status }]` が載る。status は connected / pending /
165
165
  * needs-auth / failed など。このPCでは「claude.ai ai-kana」(アカウントのコネクタ)と、
166
166
  * 使っていない手元の登録「ai-kana」(いつも needs-auth)の2つが並ぶ。
167
- * ★1つでも connected なら接続済み。pending(つなぎかけで始まった)が在れば確定できない
168
- * (=要認証とは言わない)。connected も pending も無く needs-auth が在るときだけ要認証。
167
+ * ★1つでも connected なら接続済み(便が完了の知らせに使えるのは、どれか1つがつながっていれば足りる)。
168
+ * pending(つなぎかけ)・読めない状態が在れば確定できない(=要認証とは言わない)。
169
+ * connected も pending も無く needs-auth が在るときだけ要認証。
169
170
  */
170
171
  export function aiKanaMcpState(mcp) {
171
172
  if (mcp === null)
@@ -175,12 +176,88 @@ export function aiKanaMcpState(mcp) {
175
176
  return 'missing';
176
177
  if (mine.some((s) => s.status === 'connected'))
177
178
  return 'connected';
178
- if (mine.some((s) => s.status === 'pending'))
179
+ if (mine.some((s) => s.status === 'pending' || s.status === 'unknown'))
179
180
  return 'pending';
180
181
  if (mine.some((s) => /auth/i.test(s.status)))
181
182
  return 'needs-auth';
182
183
  return 'failed';
183
184
  }
185
+ /**
186
+ * MCP の状態が落ち着くのを待つ上限(★回数と時間の両方・便 4cf71dde)。
187
+ * ふつうは `claude mcp list` 1回(数秒)で落ち着く。上限まで落ち着かなければ PASS にも 🔵 にもしない(🟡)。
188
+ */
189
+ export const MCP_SETTLE_TRIES = 3;
190
+ export const MCP_SETTLE_WAIT_MS = 5_000;
191
+ export const MCP_LIST_TIMEOUT_MS = 45_000;
192
+ /**
193
+ * `claude mcp list` の出力を読む(★実機 2026-10-03・Claude Code 2.1.287 の形)。
194
+ * `claude.ai ai-kana: https://… - ✔ Connected` / `ai-kana: https://… (HTTP) - ! Needs authentication` /
195
+ * `名前: … - ✗ Failed to connect`。★知らない形の行は「読めない(unknown)」=落ち着いていない側に倒す。
196
+ */
197
+ export function parseMcpList(stdout) {
198
+ const out = [];
199
+ for (const raw of stdout.split('\n')) {
200
+ const line = raw.trim();
201
+ const m = line.match(/^(.+?): .* - (.+)$/);
202
+ if (!m)
203
+ continue;
204
+ const word = m[2];
205
+ const status = /connected/i.test(word) && !/not connected|disconnected/i.test(word)
206
+ ? 'connected'
207
+ : /auth/i.test(word)
208
+ ? 'needs-auth'
209
+ : /fail|error/i.test(word)
210
+ ? 'failed'
211
+ : /pending|connecting/i.test(word)
212
+ ? 'pending'
213
+ : 'unknown';
214
+ out.push({ name: m[1], status });
215
+ }
216
+ return out;
217
+ }
218
+ /**
219
+ * 便が実際に使う MCP の状態を、落ち着くまで確かめる(★便 4cf71dde の根治)。
220
+ *
221
+ * 【なぜ init 行だけでは足りないか(実物 2026-10-03・ノートPC)】
222
+ * 便(claude -p)が完了の知らせ・開発履歴に使っているのはアカウントのコネクタ「claude.ai ai-kana」
223
+ * (道具の名前 mcp__claude_ai_ai-kana__*)で、手元の登録「ai-kana」(User config・要認証)は使っていない。
224
+ * ところが常駐(systemd)から起こした claude -p の init 行には、コネクタがまだ1つも載らない
225
+ * (便の記録でも init は [ai-kana:needs-auth] だけ。コネクタは後から読み込まれ、便の中では使えている)。
226
+ * 人が端末から流すと載るが pending のまま。=init 行は「始まった瞬間」の写しで、落ち着いた状態ではない。
227
+ * そのため同じPC・同じ日に「つなぎかけ→PASS」と「要認証→🔵」に割れた。
228
+ * 【どう確かめるか】init 行で接続済みなら、それで足りる(追加の所要なし)。そうでなければ、同じ claude・
229
+ * 同じ環境で `claude mcp list`(コネクタも含めて1つずつつないで確かめる・型は使わない=費用なし)を流し、
230
+ * 落ち着いた状態(つながった/要認証/失敗)が出るまで、回数と時間の上限つきで待つ。
231
+ */
232
+ export async function settleMcp(d, claudeBin, cwd, init) {
233
+ const first = aiKanaMcpState(init);
234
+ if (first === 'connected')
235
+ return { state: 'connected', list: init, source: 'init', tries: 0, why: null };
236
+ if (!claudeBin) {
237
+ return { state: first === 'pending' ? 'unsettled' : first, list: init, source: init ? 'init' : 'none', tries: 0, why: 'claude が無いので確かめ直せません' };
238
+ }
239
+ const sleep = d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
240
+ const env = probeEnv(d, claudeBin);
241
+ let last = null;
242
+ let why = null;
243
+ for (let i = 1; i <= MCP_SETTLE_TRIES; i++) {
244
+ const r = d.exec(claudeBin, ['mcp', 'list'], { cwd, env, timeoutMs: MCP_LIST_TIMEOUT_MS });
245
+ const list = parseMcpList(r.stdout);
246
+ if (list.length > 0)
247
+ last = list;
248
+ const st = list.length > 0 ? aiKanaMcpState(list) : 'unknown';
249
+ if (st === 'connected' || st === 'needs-auth' || st === 'failed' || st === 'missing') {
250
+ return { state: st, list, source: 'mcp-list', tries: i, why: null };
251
+ }
252
+ why =
253
+ list.length === 0
254
+ ? `claude mcp list の一覧を読めません(終了 ${r.status}${r.error ? `・${r.error.slice(0, 80)}` : ''})`
255
+ : 'ai-kana の MCP が つなぎかけのまま';
256
+ if (i < MCP_SETTLE_TRIES)
257
+ await sleep(MCP_SETTLE_WAIT_MS);
258
+ }
259
+ return { state: 'unsettled', list: last ?? init, source: last ? 'mcp-list' : init ? 'init' : 'none', tries: MCP_SETTLE_TRIES, why };
260
+ }
184
261
  // ─────────────────────────────────────────────────────────────────────────────
185
262
  // 本体
186
263
  // ─────────────────────────────────────────────────────────────────────────────
@@ -456,6 +533,16 @@ function siteSettingsFor(distRoot) {
456
533
  return null;
457
534
  }
458
535
  }
536
+ /** 小さな試験と MCP の確かめで claude に渡す環境(★中継と同じ形・src/headless.ts)。 */
537
+ function probeEnv(d, claudeBin) {
538
+ const env = { ...d.env };
539
+ delete env.CLAUDECODE;
540
+ delete env.ANTHROPIC_API_KEY;
541
+ const binDir = path.dirname(claudeBin);
542
+ if (!(env.PATH ?? '').split(':').includes(binDir))
543
+ env.PATH = env.PATH ? `${binDir}:${env.PATH}` : binDir;
544
+ return env;
545
+ }
459
546
  /** ⑥ 小さな headless 試験を1回流す。 */
460
547
  function runHeadlessProbe(d, claudeBin, runDir) {
461
548
  const res = { summary: null, exitCode: null, outsideWrite: 'unknown', net: {}, probeRan: false, error: null };
@@ -475,12 +562,7 @@ function runHeadlessProbe(d, claudeBin, runDir) {
475
562
  }
476
563
  writeFileSync(path.join(work, '.claude', 'settings.json'), settings);
477
564
  writeFileSync(path.join(work, 'probe.sh'), probeScript(outsideFile), { mode: 0o755 });
478
- const env = { ...d.env };
479
- delete env.CLAUDECODE;
480
- delete env.ANTHROPIC_API_KEY;
481
- const binDir = path.dirname(claudeBin);
482
- if (!(env.PATH ?? '').split(':').includes(binDir))
483
- env.PATH = env.PATH ? `${binDir}:${env.PATH}` : binDir;
565
+ const env = probeEnv(d, claudeBin);
484
566
  const r = d.exec(claudeBin, [
485
567
  '-p', PROBE_PROMPT,
486
568
  '--output-format', 'stream-json',
@@ -683,23 +765,32 @@ export async function runSelfcheck(d, trigger) {
683
765
  else
684
766
  fail(headless, 'fault', 'tool の結果に台本の目印がありません(Bash が動いていない)');
685
767
  }
686
- // ③ MCP
687
- const mcpState = aiKanaMcpState(sum?.mcp ?? null);
688
- if (mcpState === 'connected')
689
- pass(central, 'ai-kana の MCP が接続済み');
690
- else if (mcpState === 'needs-auth') {
691
- fail(central, 'human', 'ai-kana の MCP が要認証です(完了の知らせを出せません)', {
768
+ // ③ MCP(★落ち着くまで確かめる・便が実際に使う登録を見る=settleMcp)
769
+ const mcp = await settleMcp(d, claude.bin, path.join(runDir, 'work'), sum?.mcp ?? null);
770
+ const seen = mcp.source === 'mcp-list' ? `claude mcp list で確かめた・${mcp.tries}回目` : mcp.source === 'init' ? '小さな試験の最初の行' : '一覧なし';
771
+ const mineOf = (st) => (mcp.list ?? []).filter((s) => /ai-kana/i.test(s.name) && s.status === st).map((s) => s.name);
772
+ if (mcp.state === 'connected') {
773
+ pass(central, `ai-kana の MCP が接続済み(${mineOf('connected').join('・')}/${seen})`);
774
+ const unused = mineOf('needs-auth');
775
+ if (unused.length > 0)
776
+ note(central, `要認証の登録 ${unused.join('・')} は、つながっている登録があるので便は使いません(直さなくて構いません)`);
777
+ }
778
+ else if (mcp.state === 'needs-auth') {
779
+ const names = mineOf('needs-auth');
780
+ fail(central, 'human', `ai-kana の MCP が要認証です(${names.join('・')}/${seen}・つながっている登録が無いので完了の知らせを出せません)`, {
692
781
  machine: d.machine,
693
- what: 'Ubuntu で claude を開き、/mcp → ai-kana → Authenticate を選んでブラウザでログインする',
782
+ what: `Ubuntu で claude を開き、/mcp → ${names[0] ?? 'ai-kana'} → Authenticate を選んでブラウザでログインする`,
694
783
  command: 'claude',
695
784
  });
696
785
  }
697
- else if (mcpState === 'pending')
698
- note(central, 'ai-kana の MCP は つなぎかけのまま試験が始まりました(接続済みか要認証かは確定できません)');
699
- else if (mcpState === 'missing')
700
- fail(central, 'fault', 'ai-kana の MCP が一覧にありません');
701
- else if (mcpState === 'failed')
702
- fail(central, 'fault', 'ai-kana の MCP につながりません(failed)');
786
+ else if (mcp.state === 'unsettled' || mcp.state === 'pending') {
787
+ // ★PASS にも 🔵 にもしない(確かめきれていない=🟡)。
788
+ fail(central, 'fault', `ai-kana の MCP の状態が、上限(${MCP_SETTLE_TRIES}回)まで待っても落ち着きませんでした(${mcp.why ?? 'つなぎかけのまま'})`);
789
+ }
790
+ else if (mcp.state === 'missing')
791
+ fail(central, 'fault', `ai-kana の MCP が一覧にありません(${seen})`);
792
+ else if (mcp.state === 'failed')
793
+ fail(central, 'fault', `ai-kana の MCP につながりません(failed・${seen})`);
703
794
  else
704
795
  note(central, 'MCP の状態を読めませんでした(小さな試験の最初の行が無い)');
705
796
  // ④ 書き込み
@@ -731,7 +822,7 @@ export async function runSelfcheck(d, trigger) {
731
822
  result: items.some((i) => i.ok === false) ? 'FAIL' : 'PASS',
732
823
  conductorVersion: version,
733
824
  claude: { bin: claude.bin, version: claude.version, loggedIn: claude.loggedIn, pin: lastClaudePinStatus() },
734
- mcp: sum?.mcp ?? null,
825
+ mcp: mcp.list,
735
826
  drain: readDrain().active,
736
827
  items,
737
828
  humanActions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.91",
3
+ "version": "0.12.92",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3586,6 +3586,7 @@ def _gate_command(command: str, depth: int = 0):
3586
3586
  # 【何のため】次の3件は、毎便の本文に「やらない」と書いてきたが、番人は通していた(棚卸し 8ceae57d の
3587
3587
  # 実測で allow)。番人で止められれば、便の本文からその行を外せる(書かなくても守られる)。
3588
3588
  # ① git add -A(--all・-A を含む短い旗のまとめ・git add . ・git add :/)
3589
+ # と git commit -a(--all・-am などのまとめ・便 4cf71dde で足した)
3589
3590
  # ② cd を && でほかの命令とつなげる/cd とリダイレクトを同じ命令の行に混ぜる
3590
3591
  # ③ リポ全体・ホーム全体を指す再帰の検索(grep -r・rg・ag・ack・find)
3591
3592
  # 【★確実にその形と言える物だけを止める】命令の1行目を、引用符を守って字句に割ってから見る
@@ -3600,6 +3601,8 @@ _DISCIPLINE_REDIRECTS = frozenset({'>', '>>', '<', '>&', '&>', '>|', '<>', '&>>'
3600
3601
 
3601
3602
  _DISC_GIT_ADD = ('git add はまとめて足さない(-A・--all・. は秘密や無関係の変更まで入る)。'
3602
3603
  '足すファイルを名指しで git add path/to/file(鉄則1)')
3604
+ _DISC_GIT_COMMIT_ALL = ('git commit -a・-am・--all はまとめて記録に入れない(着手前から変わっていた設定ファイルまで入る)。'
3605
+ 'ファイルを名指しで git add path/to/file してから git commit(鉄則1)')
3603
3606
  _DISC_CD = ('cd を && でほかの命令とつなげない・cd とリダイレクトを同じ行に混ぜない(許可の確認で止まる)。'
3604
3607
  'cd は作業のはじめに1回だけ単独で打ち、以後は絶対パスか git -C を使う(鉄則2)')
3605
3608
  _DISC_SEARCH = ('リポ全体・ホーム全体を再帰で検索しない(重く、秘密の置き場にも当たる)。'
@@ -3678,6 +3681,56 @@ def _disc_git_add(words):
3678
3681
  return None
3679
3682
 
3680
3683
 
3684
+ # git commit の短い旗のうち、値を取る物(-m 説明・-F ファイル・-C/-c 記録・-t 型)と、
3685
+ # 値を続けて書ける物(-u 形・-S 鍵)。★これらより後ろの字は旗ではなく値(`-ma` は説明 "a")。
3686
+ _COMMIT_VALUE_SHORT = frozenset('mFCct')
3687
+ _COMMIT_OPTVALUE_SHORT = frozenset('uS')
3688
+ _COMMIT_VALUE_LONG = frozenset({'--message', '--file', '--reuse-message', '--reedit-message',
3689
+ '--template', '--author', '--date', '--fixup', '--squash',
3690
+ '--cleanup', '--trailer', '--pathspec-from-file'})
3691
+
3692
+
3693
+ def _disc_git_commit_all(words):
3694
+ """git commit -a/--all/-am(追跡しているファイルの変更を全部まとめて記録に入れる形)。
3695
+
3696
+ ★便 4cf71dde:着手前から M だった .claude/settings.json が、空振り確認の `git commit -a` で
3697
+ 手元の記録に入った(ai-kana の2便が続けて踏んだ)。`git add -A` と同じ「まとめて入れる」形なので、
3698
+ 同じ扱いで止める(止める物を足すだけ)。
3699
+ """
3700
+ if not words or words[0].split('/')[-1] != 'git':
3701
+ return None
3702
+ i = 1
3703
+ while i < len(words) and words[i].startswith('-'):
3704
+ i += 2 if words[i] in ('-C', '-c') else 1
3705
+ if i >= len(words) or words[i] != 'commit':
3706
+ return None
3707
+ rest = words[i + 1:]
3708
+ k = 0
3709
+ while k < len(rest):
3710
+ a = rest[k]
3711
+ k += 1
3712
+ if a == '--':
3713
+ break
3714
+ if a.startswith('--'):
3715
+ if a == '--all':
3716
+ return _DISC_GIT_COMMIT_ALL
3717
+ if a in _COMMIT_VALUE_LONG:
3718
+ k += 1 # 値は次の語(`--message=…` の形は1語で終わる)
3719
+ continue
3720
+ if not a.startswith('-') or a == '-':
3721
+ continue
3722
+ for j, ch in enumerate(a[1:], 1):
3723
+ if ch == 'a':
3724
+ return _DISC_GIT_COMMIT_ALL
3725
+ if ch in _COMMIT_VALUE_SHORT:
3726
+ if j == len(a) - 1:
3727
+ k += 1 # `-m 説明` … 値は次の語
3728
+ break # `-mabc` … 残りは値
3729
+ if ch in _COMMIT_OPTVALUE_SHORT:
3730
+ break
3731
+ return None
3732
+
3733
+
3681
3734
  def _disc_repo_top(cwd):
3682
3735
  d = os.path.normpath(cwd) if cwd and os.path.isabs(cwd) else None
3683
3736
  while d:
@@ -3808,7 +3861,7 @@ def _discipline_scan(command: str, cwd=None):
3808
3861
  while k < len(words) and (_ASSIGN_RE.match(words[k]) or words[k].split('/')[-1] in _WRAPPERS):
3809
3862
  k += 1
3810
3863
  core = words[k:]
3811
- hit = _disc_git_add(core) or _disc_search(core, cwd)
3864
+ hit = _disc_git_add(core) or _disc_git_commit_all(core) or _disc_search(core, cwd)
3812
3865
  if hit:
3813
3866
  return hit
3814
3867
  return None
@@ -6,6 +6,7 @@
6
6
 
7
7
  【何を守っているか】
8
8
  (n) ★止まる形:git add -A/--all/./-Av/git -C 場所 add -A、
9
+ git commit -a/-am/--all/-qam/git -C 場所 commit -av(便 4cf71dde)、
9
10
  cd を && でつなぐ(前でも後ろでも・括弧の中でも)・cd とリダイレクトを同じ行に混ぜる、
10
11
  リポ全体・ホーム全体・/ を指す再帰の検索(grep -r・rg・find、行き先を省いた形も)
11
12
  (p) 止まってはいけない似た形:git add ファイル名・git add -- ファイル名・cd 単独・cd X; 命令・
@@ -44,6 +45,7 @@ os.mkdir(os.path.join(REPO, 'src'))
44
45
  SUB = os.path.join(REPO, 'src')
45
46
  HOME = os.path.expanduser('~')
46
47
  ADD = 'git ' + 'add'
48
+ COMMIT = 'git ' + 'commit'
47
49
 
48
50
 
49
51
  def decide(cmd, cwd=REPO):
@@ -76,6 +78,14 @@ NEGATIVE = [
76
78
  ('n22 リポの一番上を名指しした find', f'find {REPO} -name x'),
77
79
  ('n23 リポより広い所(リポの親)', f'find {os.path.dirname(REPO)} -name x'),
78
80
  ('n24 env 変数=… を前に付けても', f'GIT_TRACE=1 {ADD} -A'),
81
+ # ★便 4cf71dde:git commit -a は git add -A と同じ「まとめて入れる」形(ai-kana の2便が踏んだ)。
82
+ ('n25 git commit -a', f'{COMMIT} -a'),
83
+ ('n26 git commit -am 説明', f'{COMMIT} -am "x"'),
84
+ ('n27 git commit --all', f'{COMMIT} --all -m x'),
85
+ ('n28 git commit -m 説明 -a(後ろに置いても)', f'{COMMIT} -m "x" -a'),
86
+ ('n29 git -C 場所 commit -av', f'git -C /x commit -av'),
87
+ ('n30 git commit -qam 説明(短い旗のまとめ)', f'{COMMIT} -qam x'),
88
+ ('n31 命令 && git commit -am 説明', f'git status && {COMMIT} -am x'),
79
89
  ]
80
90
  for label, cmd in NEGATIVE:
81
91
  r = decide(cmd)
@@ -101,6 +111,14 @@ POSITIVE = [
101
111
  ('p16 下の場所から find .(リポ全体ではない)', 'find . -name x'),
102
112
  ('p17 grep(再帰でない)', 'grep -n foo src/a.ts'),
103
113
  ('p18 リダイレクトだけ(cd なし)', 'ls > /tmp/claude-1000/o'),
114
+ ('p19 git commit -m 説明(名指しで add した後)', f'{COMMIT} -m "fix: 直す"'),
115
+ ('p20 説明の中の -a は旗ではない', f'{COMMIT} -m "-a を止める"'),
116
+ ('p21 -ma は説明 "a"(-a ではない)', f'{COMMIT} -ma'),
117
+ ('p22 git commit --amend --no-edit', f'{COMMIT} --amend --no-edit'),
118
+ ('p23 git commit -F ファイル', f'{COMMIT} -F /tmp/claude-1000/msg.txt'),
119
+ ('p24 git commit --author 名前', f'{COMMIT} --author "-a <a@b>" -m x'),
120
+ ('p25 git commit -- ファイル名', f'{COMMIT} -m x -- src/a.ts'),
121
+ ('p26 git log --all(commit ではない)', 'git log --all --oneline'),
104
122
  ]
105
123
  for label, cmd in POSITIVE:
106
124
  cwd = SUB if label.startswith('p16') else REPO
@@ -108,6 +126,8 @@ for label, cmd in POSITIVE:
108
126
 
109
127
  print('── (m) 止めたときの文 ' + '─' * 44)
110
128
  check('(m) git add は名指しを案内', 'git add path/to/file' in decide(f'{ADD} -A')['reason'], True)
129
+ check('(m) git commit -a は「名指しで add してから commit」を案内',
130
+ all(w in decide(f'{COMMIT} -am x')['reason'] for w in ('git add path/to/file', 'してから git commit')), True)
111
131
  check('(m) cd は絶対パスか git -C を案内', 'git -C' in decide('cd x && ls')['reason'], True)
112
132
  check('(m) 検索は名指しの場所を案内', '名指し' in decide('find . -name x')['reason'], True)
113
133
  for label, cmd in [('git add', f'{ADD} -A'), ('cd', 'cd x && ls'), ('検索', 'find / -name x')]:
@@ -22,7 +22,7 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
22
22
  - ① そのファイルを**触らない・開かない・`git diff` の対象にしない**。「読めるようにする」方向へ回避しない(鉄則4)。
23
23
  - ② 差分は**対象を名指しして**見る。例 `git diff -- src docs`。全体で見たいときは除いて見る=`git diff -- . ':(exclude).npmrc'`。
24
24
  - ③ `M` と出ていても、**自分が触っていなければ触っていない**。★職人には本当に変わったかを確かめられない。`git status --porcelain -- <そのファイル>` の**表示だけ**を報告に書き、**中身は1文字も書かない**。
25
- - ④ **コミットに含めない**(`git add` は対象を名指しする=`git add -A` を使わない)。
25
+ - ④ **コミットに含めない**(`git add` は対象を名指しする=`git add -A` を使わない)。★`git commit -a`・`-am`・`--all` も同じ「まとめて全部を入れる」形なので使わない(番人が止める)。**ファイルを名指しで `git add` してから `git commit -m …`**(便 4cf71dde:着手前から `M` だった `.claude/settings.json` が、空振り確認の `git commit -a` で手元の記録に入った)。
26
26
  - ⑤ それで進めないときだけ、回避せず**人の手番として報告して止まる**。
27
27
  2. **cd は1回だけ** — cd は作業開始時に1回だけ実行する。以後 cd を `&&` で他コマンドと連結しない・リダイレクトと混ぜない。
28
28
  3. **持ち場の外に出ない** — 指示された作業ディレクトリの外を変更しない。
@@ -164,6 +164,7 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
164
164
  - 通る例: `rm -r /tmp/claude-1000/作業名` / `rm -r node_modules/.tmp-作業名`(★`-f` を付けない・自分で作った物だけ)
165
165
  - 止まる例: `rm -rf /tmp/claude-1000/作業名`(禁止の一覧)/ `rm -r "$DIR/x"`・`rm -r build/*`(番人)/ `rm -r .git`・`rm -r .claude/hooks`・`rm -r .tyhld`(番人=相対パスでも消せない)
166
166
  - ★**探すだけの道具の「探す言葉」に `rm …` と書いても、もう止まらない**(便 915694ac で直した)。鉄則や台本を調べるときの `grep -rn 'rm -rf .git' skills`・`git grep -n 'rm -r' -- skills`・`rg 'rm -rf /' docs` は通る。★囲みの外に書いた `rm` は今までどおり測るので、`grep -rn 'x' docs; rm -rf .git` のように**後ろで消す形は止まる**。
167
+ - **まとめて全部を記録に入れる `git add`・`git commit`**(`git add -A`・`git add .`・`git commit -a`・`git commit -am 説明`・`git commit --all`)→ 止まる。着手前から変わっていたファイル(例 `.claude/settings.json`)まで入り、番人がそれを記録から外す操作も止めるので取り返せない(便 4cf71dde)。**ファイルを名指しで `git add <ファイル>` してから `git commit -m 説明`**(鉄則1-④)。
167
168
  - **作業場所が変数のままの `git push`**(`git -C "$DIR" push`・行き先のブランチを確かめられない形)→ 止まる。**リポの中で、ブランチを名指しして** `git push origin <作業ブランチ名>`(★`-u` は付けない=鉄則7-⓪。上流の設定を書くと、囲いが `.git/config` を塞いでいるPCで止まる)。
168
169
 
169
170
  ## 便の終え方