@tyhld/conductor 0.12.64 → 0.12.66

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/dist/headless.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * 同じ relayTick に分岐を詰め込むと壊れやすいので、独立したモジュールにする。
15
15
  * 共通の管制との通信(fetchControl, fetchCommand, patchStatus 等)は relay.ts から使う。
16
16
  */
17
- import { spawn } from 'node:child_process';
17
+ import { execFileSync, spawn } from 'node:child_process';
18
18
  import { createInterface } from 'node:readline';
19
19
  import { createWriteStream, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
20
20
  import path from 'node:path';
@@ -53,6 +53,23 @@ export const ACTIVE_JOB_PATH_LEGACY = path.join(ACTIVE_JOB_DIR, 'active-job.json
53
53
  export function activeJobPath(site) {
54
54
  return path.join(ACTIVE_JOB_DIR, `active-job-${site}.json`);
55
55
  }
56
+ /**
57
+ * 作業ディレクトリの git remote origin URL を返す。
58
+ * git リポジトリでない・origin が無い場合は null。
59
+ * ★パスの文字が同じでも別のリポなら止める(リポの同一性の照合に使う)。
60
+ */
61
+ export function resolveGitOrigin(workDir) {
62
+ try {
63
+ return execFileSync('git', ['-C', workDir, 'config', '--get', 'remote.origin.url'], {
64
+ encoding: 'utf8',
65
+ timeout: 5_000,
66
+ stdio: ['ignore', 'pipe', 'ignore'],
67
+ }).trim() || null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
56
73
  /** 実行中のジョブをファイルに書く(中継が死んでも拾い直せるように)。 */
57
74
  export function saveActiveJob(site, record) {
58
75
  try {
@@ -112,8 +129,12 @@ export function loadAndClearLegacyActiveJob() {
112
129
  }
113
130
  }
114
131
  /**
115
- * 手元の記録が自分(この中継)のものか4点照合する。
132
+ * 手元の記録が自分(この中継)のものか照合する。
133
+ * site, machine, workDir, gitOrigin を比べる。
116
134
  * 1点でも不一致なら理由を返す。すべて一致なら null。
135
+ *
136
+ * ★commandId は「この便が中央でまだ sent か」の確認(ネットワーク照合)で別に行う。
137
+ * ここではローカルの所有者照合のみ。
117
138
  */
118
139
  export function validateActiveJobOwnership(rec, cfg) {
119
140
  const mismatches = [];
@@ -126,10 +147,16 @@ export function validateActiveJobOwnership(rec, cfg) {
126
147
  if (rec.workDir !== undefined && rec.workDir !== cfg.workDir) {
127
148
  mismatches.push(`workDir: 記録=${rec.workDir} 自分=${cfg.workDir}`);
128
149
  }
129
- // 4点がそろわない古い記録(0.12.62 以前)は site/machine/workDir が undefined。
130
- // 照合できない = 安全に拾えない。
150
+ // ★git リポの同一性: パスが同じでも別のリポなら止める
151
+ if (rec.gitOrigin !== undefined) {
152
+ const currentOrigin = resolveGitOrigin(cfg.workDir);
153
+ if (currentOrigin !== null && rec.gitOrigin !== currentOrigin) {
154
+ mismatches.push(`gitOrigin: 記録=${rec.gitOrigin} 自分=${currentOrigin}`);
155
+ }
156
+ }
157
+ // 照合項目がそろわない古い記録は安全に拾えない。
131
158
  if (rec.site === undefined || rec.machine === undefined || rec.workDir === undefined) {
132
- mismatches.push('4点の照合項目が不足(0.12.62 以前の古い記録)');
159
+ mismatches.push('照合項目が不足(古い記録)');
133
160
  }
134
161
  return mismatches.length > 0 ? mismatches.join(', ') : null;
135
162
  }
@@ -740,6 +767,18 @@ export async function restoreHeadlessOrphans(cfg, state) {
740
767
  }]);
741
768
  return;
742
769
  }
770
+ // ★便IDの照合: 中央でまだ sent のまま残っている便か確かめる
771
+ const centralStatus = await fetchCommandStatus(cfg, saved.commandId);
772
+ if (centralStatus !== null && centralStatus !== 'sent') {
773
+ log(`WARN site=${cfg.site} id=${saved.commandId} 手元の記録の便は中央で ${centralStatus} → 再開しない`);
774
+ await sendCommandEvents(cfg, saved.commandId, [{
775
+ type: 'error',
776
+ text: `便IDの照合: 中央の状態が sent ではない(${centralStatus})→ 再開しない`.slice(0, EVENT_TEXT_MAX),
777
+ at: new Date().toISOString(),
778
+ attempt: saved.attempt,
779
+ }]);
780
+ return;
781
+ }
743
782
  log(`site=${cfg.site} id=${saved.commandId} 手元の記録から孤児を検出(中継再起動後の拾い直し)`);
744
783
  const pick = { id: saved.commandId, body: saved.body, site: cfg.site, sentAt: saved.startedAt };
745
784
  const attempt = saved.attempt + 1;
@@ -760,7 +799,7 @@ export async function restoreHeadlessOrphans(cfg, state) {
760
799
  return;
761
800
  }
762
801
  state.job = job;
763
- saveActiveJob(cfg.site, { commandId: pick.id, body: pick.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir });
802
+ saveActiveJob(cfg.site, { commandId: pick.id, body: pick.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir, gitOrigin: resolveGitOrigin(cfg.workDir) ?? undefined });
764
803
  markDelivered(state.relay, pick.id);
765
804
  state.relay.lastHeartbeatAt = null;
766
805
  noteReadiness(state.relay, 'delivered', pick.id);
@@ -802,7 +841,7 @@ export async function restoreHeadlessOrphans(cfg, state) {
802
841
  return;
803
842
  }
804
843
  state.job = job;
805
- saveActiveJob(cfg.site, { commandId: pick.id, body: pick.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir });
844
+ saveActiveJob(cfg.site, { commandId: pick.id, body: pick.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir, gitOrigin: resolveGitOrigin(cfg.workDir) ?? undefined });
806
845
  markDelivered(state.relay, pick.id);
807
846
  state.relay.lastHeartbeatAt = null;
808
847
  noteReadiness(state.relay, 'delivered', pick.id);
@@ -895,7 +934,7 @@ export async function headlessTick(cfg, state, hooks) {
895
934
  return;
896
935
  }
897
936
  state.job = job;
898
- saveActiveJob(cfg.site, { commandId: retry.cmd.id, body: retry.cmd.body, sessionId, attempt: retry.attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir });
937
+ saveActiveJob(cfg.site, { commandId: retry.cmd.id, body: retry.cmd.body, sessionId, attempt: retry.attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir, gitOrigin: resolveGitOrigin(cfg.workDir) ?? undefined });
899
938
  rs.lastHeartbeatAt = null;
900
939
  noteReadiness(rs, 'delivered', retry.cmd.id);
901
940
  await sendCommandEvents(cfg, retry.cmd.id, [{
@@ -951,7 +990,7 @@ export async function headlessTick(cfg, state, hooks) {
951
990
  return;
952
991
  }
953
992
  state.job = job;
954
- saveActiveJob(cfg.site, { commandId: cmd.id, body: cmd.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir });
993
+ saveActiveJob(cfg.site, { commandId: cmd.id, body: cmd.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir, gitOrigin: resolveGitOrigin(cfg.workDir) ?? undefined });
955
994
  markDelivered(rs, cmd.id);
956
995
  rs.lastHeartbeatAt = null;
957
996
  noteReadiness(rs, 'delivered', cmd.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.64",
3
+ "version": "0.12.66",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,9 +14,10 @@
14
14
  "Bash(git push --force:*)",
15
15
  "Bash(git push -f:*)",
16
16
  "Bash(git push --force-with-lease:*)",
17
- "Bash(git push * --force:*)",
18
- "Bash(git push * -f:*)",
19
- "Bash(git push * --force-with-lease:*)",
17
+ "Bash(git push * --force)",
18
+ "Bash(git push * -f)",
19
+ "Bash(git push * --force-with-lease)",
20
+ "Bash(git push * +*)",
20
21
  "Bash(rm -rf:*)",
21
22
  "Bash(rm -fr:*)",
22
23
  "Bash(rm -r -f:*)",
@@ -45,8 +46,8 @@
45
46
  "Bash(*TRUNCATE*)",
46
47
  "Bash(*rest/v1/rpc/exec_sql*)",
47
48
  "Bash(cat .env:*)",
48
- "Bash(cat .env.*:*)",
49
- "Bash(cat *.env:*)",
49
+ "Bash(cat .env.*)",
50
+ "Bash(cat *.env)",
50
51
  "Bash(printenv:*)",
51
52
  "Bash(echo $DATABASE_URL:*)",
52
53
  "Bash(echo $DIRECT_URL:*)",
@@ -999,6 +999,10 @@ RETIRED_DENY = frozenset({
999
999
  "Read(**/.config/gh/hosts.yml)", "Read(**/.npmrc)", "Read(**/*.env)", "Read(**/*.pem)",
1000
1000
  "Read(**/*.key)", "Read(**/credentials.json)", "Read(**/.credentials.json)",
1001
1001
  "Read(**/auth.json)", "Read(**/*login.json)",
1002
+ # ★便 07ee11a2: * と :* を混ぜた書き方は Claude Code が * をリテラル扱いして何も止めない。
1003
+ # 正しい形(* をワイルドカードとして展開)を正本に足したので、壊れた旧形を退役する。
1004
+ "Bash(git push * --force:*)", "Bash(git push * -f:*)", "Bash(git push * --force-with-lease:*)",
1005
+ "Bash(cat .env.*:*)", "Bash(cat *.env:*)",
1002
1006
  })
1003
1007
  retire_hit = [r for r in cur if r in RETIRED_DENY]
1004
1008
  if retire_hit: