@tyhld/conductor 0.12.30 → 0.12.32
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/package.json +1 -1
- package/scripts/hooks/skill_sync_notify.py +103 -10
- package/scripts/hooks/test_policy.py +3 -1
- package/scripts/hooks/test_skill_sync_notify.py +230 -19
- package/scripts/update-guard.sh +30 -10
- package/systemd/conductor-build-runner.service +16 -0
- package/systemd/conductor-ear.service +16 -0
- package/systemd/conductor@.service +16 -0
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
python3 skill_sync_notify.py <直近の試みの控えのパス>
|
|
33
33
|
★控えの1行目が status=failed のときだけカードを出す。ok のときは何もしない。
|
|
34
34
|
"""
|
|
35
|
+
import json
|
|
35
36
|
import os
|
|
36
37
|
import sys
|
|
37
38
|
|
|
@@ -102,21 +103,54 @@ def parse_last(path):
|
|
|
102
103
|
return out
|
|
103
104
|
|
|
104
105
|
|
|
106
|
+
#: 入れ直しの1行(★取得先を明示する。ここを省くと社内の取得先を見に行って失敗する)。
|
|
107
|
+
REINSTALL = ('npm install -g @tyhld/conductor '
|
|
108
|
+
'--@tyhld:registry=https://registry.npmjs.org')
|
|
109
|
+
#: 配り直しの1行(★入っている配布物の中の台本を打つ。作業ツリーの台本ではない)。
|
|
110
|
+
RESYNC = 'APPLY=1 bash "$(npm root -g)/@tyhld/conductor/scripts/update-guard.sh"'
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def recovery_steps(reason):
|
|
114
|
+
"""うまくいかなかった理由に【合った】手順を返す(便 675e2d05)。
|
|
115
|
+
|
|
116
|
+
【なぜ理由ごとに分けるか(実地 2026-09-20・便 8692c586 の重大発見1)】
|
|
117
|
+
以前は理由によらず「update-guard を打ってください」の固定文だった。version-drift
|
|
118
|
+
(入っている版と公開の版が違う)のときにこれを打っても、配り元の確認でまた弾かれる=
|
|
119
|
+
同じ失敗を繰り返すだけで直らない。先に入れ直しが要る。
|
|
120
|
+
"""
|
|
121
|
+
if reason in ('version-drift', 'not-installed', 'incomplete'):
|
|
122
|
+
return [f'再開のしかた: この PC で次の順に打ってください。① {REINSTALL}',
|
|
123
|
+
f' ② {RESYNC}']
|
|
124
|
+
if reason == 'unverifiable':
|
|
125
|
+
return ['再開のしかた: 公開 npm に問い合わせられませんでした。'
|
|
126
|
+
'この PC の通信(回線・取得先の設定)を確かめてから、次を打ってください。',
|
|
127
|
+
f' {RESYNC}']
|
|
128
|
+
return [f'再開のしかた: この PC で次の1行を打ってください。 {RESYNC}']
|
|
129
|
+
|
|
130
|
+
|
|
105
131
|
def build_payload(rec, env=None):
|
|
106
|
-
"""カードの中身。★何が起きていて、何をすれば直るかを日本語で書く。
|
|
132
|
+
"""カードの中身。★何が起きていて、何をすれば直るかを日本語で書く。
|
|
133
|
+
|
|
134
|
+
★本文は【その回の控え(rec)の実際の値】だけから組み立てる。固定の版番号や、
|
|
135
|
+
もう済んだ操作の案内を焼き込まない(便 675e2d05・えふさん確定 2026-09-20)。
|
|
136
|
+
"""
|
|
107
137
|
env = os.environ if env is None else env
|
|
108
138
|
machine = env.get('CONDUCTOR_MACHINE') or os.uname().nodename
|
|
109
139
|
reason = rec.get('reason', '不明')
|
|
110
140
|
kind = notice_kind(rec)
|
|
111
|
-
|
|
141
|
+
lines = [
|
|
112
142
|
f'いま止まっているもの: この PC({machine})で、{kind["stopped"]}',
|
|
113
|
-
|
|
114
|
-
' cd ~/projects/conductor そのあと APPLY=1 bash scripts/update-guard.sh',
|
|
143
|
+
*recovery_steps(reason),
|
|
115
144
|
'★このカードは管制からは押せません(この PC での操作が要ります)。',
|
|
116
145
|
f'PC: {machine}/うまくいかなかった理由: {reason}',
|
|
117
146
|
f'最後に試した時刻: {rec.get("at", "不明")}',
|
|
118
|
-
f'
|
|
119
|
-
]
|
|
147
|
+
f'入っている版: {rec.get("version", "不明")}',
|
|
148
|
+
]
|
|
149
|
+
# ★公開の版は、控えに在るときだけ載せる。無い種類の知らせに「公開の版: 不明」と
|
|
150
|
+
# 書くと、読む人には【公開の版が読めなかった】ように見える(便 675e2d05)。
|
|
151
|
+
if rec.get('published'):
|
|
152
|
+
lines.append(f'公開の版: {rec["published"]}')
|
|
153
|
+
body = '\n'.join(lines)
|
|
120
154
|
return {
|
|
121
155
|
'site': env.get('CONDUCTOR_SITE') or machine,
|
|
122
156
|
'machine': machine,
|
|
@@ -131,19 +165,78 @@ def build_payload(rec, env=None):
|
|
|
131
165
|
}
|
|
132
166
|
|
|
133
167
|
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
#: 出した札の番号を覚えておく場所(鍵→カードID)。★秘密は入らない(番号だけ)。
|
|
169
|
+
CARD_STATE = os.path.join(os.path.expanduser('~'), '.tyhld', 'hooks', 'notice-cards.json')
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _load_cards(path=None):
|
|
173
|
+
"""覚えている札の番号。読めなければ空(覚えていない=今までどおりに振る舞う)。"""
|
|
174
|
+
try:
|
|
175
|
+
with open(path or CARD_STATE, encoding='utf-8') as f:
|
|
176
|
+
got = json.load(f)
|
|
177
|
+
return got if isinstance(got, dict) else {}
|
|
178
|
+
except Exception: # noqa: BLE001
|
|
179
|
+
return {}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _save_cards(cards, path=None):
|
|
183
|
+
"""覚え書きを書く。★書けなくても止まらない(知らせは本筋ではない)。"""
|
|
184
|
+
target = path or CARD_STATE
|
|
185
|
+
try:
|
|
186
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
187
|
+
tmp = target + '.tmp'
|
|
188
|
+
with open(tmp, 'w', encoding='utf-8') as f:
|
|
189
|
+
json.dump(cards, f, ensure_ascii=False)
|
|
190
|
+
os.replace(tmp, target)
|
|
191
|
+
except Exception: # noqa: BLE001
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _close(cfg, datalink, cards, key, path=None):
|
|
196
|
+
"""覚えている札を閉じる(best effort)。閉じたら覚え書きから外す。"""
|
|
197
|
+
card_id = cards.get(key)
|
|
198
|
+
if not card_id:
|
|
199
|
+
return False
|
|
200
|
+
try:
|
|
201
|
+
datalink.abandon(cfg, card_id)
|
|
202
|
+
except Exception: # noqa: BLE001
|
|
203
|
+
pass
|
|
204
|
+
cards.pop(key, None)
|
|
205
|
+
_save_cards(cards, path)
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def run(last_path, cfg=None, datalink=hook_datalink, state_path=None):
|
|
210
|
+
"""返り値: 出したカードのID / None(出さなかった・閉じただけ)。
|
|
211
|
+
|
|
212
|
+
★失敗の回 … 前に出した札を【閉じてから】出し直す。管制は同じ鍵の再登録を寿命の延長
|
|
213
|
+
として扱い、本文は最初のままになる(実地 2026-09-20・札に 4日前の版が出ていた)。
|
|
214
|
+
閉じてから出し直せば、本文は必ずその回の値になる。
|
|
215
|
+
★成功の回 … 出していた札を閉じる。直ったのに札が最大60分出たままにならない。
|
|
216
|
+
"""
|
|
136
217
|
rec = parse_last(last_path)
|
|
137
|
-
|
|
218
|
+
status = rec.get('status')
|
|
219
|
+
if status not in ('failed', 'ok'):
|
|
138
220
|
return None
|
|
139
221
|
cfg = datalink.load_config() if cfg is None else cfg
|
|
140
222
|
if cfg is None:
|
|
141
223
|
# 接続情報が無い機体。★控えには残っているので、黙って消えるわけではない。
|
|
142
224
|
return None
|
|
225
|
+
key = notice_kind(rec)['key']
|
|
226
|
+
cards = _load_cards(state_path)
|
|
227
|
+
|
|
228
|
+
if status == 'ok':
|
|
229
|
+
_close(cfg, datalink, cards, key, state_path)
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
# 失敗。★古い本文の札が残っていたら先に閉じる(上書きできない作りへの対処)。
|
|
233
|
+
_close(cfg, datalink, cards, key, state_path)
|
|
143
234
|
reg = datalink.register(cfg, build_payload(rec))
|
|
144
235
|
if not reg or not reg.get('id'):
|
|
145
236
|
return None
|
|
146
|
-
|
|
237
|
+
cards[key] = reg['id']
|
|
238
|
+
_save_cards(cards, state_path)
|
|
239
|
+
# ★待たない。押されるまで管制に出たままにする=直っていないことが見え続ける。
|
|
147
240
|
return reg['id']
|
|
148
241
|
|
|
149
242
|
|
|
@@ -1087,7 +1087,9 @@ check('同期モードは掃除・タイマー・pnpm 点検を飛ばす',
|
|
|
1087
1087
|
check('職人の鉄則は同期モードでも配る',
|
|
1088
1088
|
_ug.index('①-d 職人の鉄則スキル') < _ug.index('①-b・①-c・①-d2 は省略(同期モード)'), True)
|
|
1089
1089
|
check('配り損ねたら成功と偽らず管制へ知らせる',
|
|
1090
|
-
'
|
|
1090
|
+
'craftsman_notify' in _ug and 'skill_sync_notify.py' in _ug, True)
|
|
1091
|
+
# ★直った回は札を閉じる(便 675e2d05)。直っているのに札が最大60分出たままだった。
|
|
1092
|
+
check('直った回は管制の札を閉じる', 'craftsman_notify_recovered' in _ug, True)
|
|
1091
1093
|
# ★上書きの前に日時つきの控えを取る(えふさん確定 2026-09-16・便 bf030fee)。
|
|
1092
1094
|
# 中身の検査は test/craftsman-skill-sync.test.ts。ここでは決まりが消えていないことだけ見る。
|
|
1093
1095
|
check('上書きの前に控えを取る', 'craftsman_backup()' in _ug, True)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
【何を守っているか(便 29612ff1・えふさん確定 2026-09-16)】
|
|
8
8
|
「★更新失敗時に古いルールのまま黙って動かないこと。適用version/時刻を確認可能にする」
|
|
9
|
-
(a)
|
|
9
|
+
(a) 成功の控えでは【新しい札を1枚も出さない】(毎時のカードで管制を埋めない)
|
|
10
10
|
(b) 失敗の控えでは【必ず知らせる】=黙って進まない
|
|
11
11
|
(c) カードの中身に「いま止まっているもの」「再開のしかた」が入っている
|
|
12
12
|
=管制が知らせとして描ける形(ai-kana の hook-notice-text.ts が探す見出し語)
|
|
@@ -15,7 +15,16 @@
|
|
|
15
15
|
(e) 機体ごとに鍵が固定=毎時の失敗が同じ1枚に集約される(カードが増殖しない)
|
|
16
16
|
(f) 知らせに失敗しても例外を外へ出さない(★同期そのものは止めない)
|
|
17
17
|
(g) 控えが読めない・空・壊れていても落ちない
|
|
18
|
+
|
|
19
|
+
【★便 675e2d05 で足した3つ(実地 2026-09-20・えふさん確定)】
|
|
20
|
+
(h) 失敗の回は【前の札を閉じてから】出し直す=本文が必ずその回の値になる。
|
|
21
|
+
管制は同じ鍵の再登録を寿命の延長として扱い、本文は最初のまま残る(upsertHookRequest)。
|
|
22
|
+
実地では、直しても札に4日前の版が出たままだった。
|
|
23
|
+
(i) 成功の回は【出していた札を閉じる】=直ったのに札が最大60分出たままにならない。
|
|
24
|
+
(j) 再開の手順が【うまくいかなかった理由に合っている】=同じ失敗を繰り返させない。
|
|
25
|
+
版のずれに「配り直してください」とだけ言っても、配り元の確認でまた弾かれる。
|
|
18
26
|
"""
|
|
27
|
+
import json
|
|
19
28
|
import os
|
|
20
29
|
import sys
|
|
21
30
|
import tempfile
|
|
@@ -51,22 +60,49 @@ class FakeLink:
|
|
|
51
60
|
#: 「既定のまま」と「わざと None を返させる」を区別するための印。
|
|
52
61
|
DEFAULT = object()
|
|
53
62
|
|
|
54
|
-
def __init__(self, cfg=CFG, reg=DEFAULT):
|
|
63
|
+
def __init__(self, cfg=CFG, reg=DEFAULT, ids=None, abandon_raises=False):
|
|
55
64
|
self.cfg = cfg
|
|
56
65
|
self.reg = {'id': 'req-1'} if reg is FakeLink.DEFAULT else reg
|
|
57
|
-
self.
|
|
66
|
+
self.ids = list(ids) if ids else None # 連番で返したいとき
|
|
67
|
+
self.abandon_raises = abandon_raises
|
|
68
|
+
self.sent = [] # 送ったカードの中身
|
|
69
|
+
self.closed = [] # 閉じに行ったカードID
|
|
70
|
+
self.log = [] # ('close'|'register', …) の並び=★順序を見るため
|
|
58
71
|
|
|
59
72
|
def load_config(self, env=None):
|
|
60
73
|
return self.cfg
|
|
61
74
|
|
|
62
75
|
def register(self, cfg, payload):
|
|
63
76
|
self.sent.append(payload)
|
|
77
|
+
self.log.append(('register', payload['requestKey']))
|
|
78
|
+
if self.ids is not None:
|
|
79
|
+
return {'id': self.ids.pop(0)} if self.ids else None
|
|
64
80
|
return self.reg
|
|
65
81
|
|
|
82
|
+
def abandon(self, cfg, request_id):
|
|
83
|
+
self.closed.append(request_id)
|
|
84
|
+
self.log.append(('close', request_id))
|
|
85
|
+
if self.abandon_raises:
|
|
86
|
+
raise RuntimeError('通信断')
|
|
87
|
+
return {'ok': True}
|
|
88
|
+
|
|
66
89
|
# ★本物と同じ実装を借りる(sessionId が空にならないことを一緒に確かめるため)。
|
|
67
90
|
stable_session_id = staticmethod(sn.hook_datalink.stable_session_id)
|
|
68
91
|
|
|
69
92
|
|
|
93
|
+
def real_state_snapshot():
|
|
94
|
+
"""【本物】の覚え書きの中身(無ければ None)。★試験が実機を汚していないかを見るため。"""
|
|
95
|
+
try:
|
|
96
|
+
with open(sn.CARD_STATE, encoding='utf-8') as f:
|
|
97
|
+
return f.read()
|
|
98
|
+
except Exception: # noqa: BLE001
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
#: 試験を始める前の姿。終わりに同じであることを確かめる。
|
|
103
|
+
REAL_STATE_BEFORE = real_state_snapshot()
|
|
104
|
+
|
|
105
|
+
|
|
70
106
|
def write(tmp, name, text):
|
|
71
107
|
p = os.path.join(tmp, name)
|
|
72
108
|
with open(p, 'w', encoding='utf-8') as f:
|
|
@@ -74,30 +110,48 @@ def write(tmp, name, text):
|
|
|
74
110
|
return p
|
|
75
111
|
|
|
76
112
|
|
|
113
|
+
def state(tmp, name='cards.json'):
|
|
114
|
+
"""★札の覚え書きは必ずこの回だけの場所へ。
|
|
115
|
+
|
|
116
|
+
既定は $HOME/.tyhld/hooks/notice-cards.json =【本物】の置き場なので、試験で
|
|
117
|
+
そこへ書くと、実機に居もしないカードの番号が残り、次の同期が存在しない札を
|
|
118
|
+
閉じに行く。試験は本物の控えに1バイトも書かない。
|
|
119
|
+
"""
|
|
120
|
+
return os.path.join(tmp, name)
|
|
121
|
+
|
|
122
|
+
|
|
77
123
|
# ── (a)(b) 出す・出さない ────────────────────────────────────────────────────
|
|
78
124
|
print('\n【(a)(b) 成功では出さない・失敗では必ず出す】')
|
|
79
125
|
with tempfile.TemporaryDirectory() as tmp:
|
|
126
|
+
st = state(tmp)
|
|
80
127
|
link = FakeLink()
|
|
81
|
-
check('成功の控え: 知らせない',
|
|
128
|
+
check('成功の控え: 知らせない',
|
|
129
|
+
sn.run(write(tmp, 'ok.last', OK_LINE + '\n'), datalink=link, state_path=st), None)
|
|
82
130
|
check('成功の控え: 1件も送っていない', len(link.sent), 0)
|
|
131
|
+
check('成功の控え: 覚えが無ければ閉じにも行かない', len(link.closed), 0)
|
|
132
|
+
check('成功の控え: 覚え書きを作らない', os.path.exists(st), False)
|
|
83
133
|
|
|
84
134
|
link = FakeLink()
|
|
85
|
-
got = sn.run(write(tmp, 'ng.last', NG_LINE + '\n'), datalink=link)
|
|
135
|
+
got = sn.run(write(tmp, 'ng.last', NG_LINE + '\n'), datalink=link, state_path=st)
|
|
86
136
|
check('失敗の控え: 知らせた(カードIDが返る)', got, 'req-1')
|
|
87
137
|
check('失敗の控え: 1件だけ送った', len(link.sent), 1)
|
|
88
138
|
|
|
89
139
|
# ★控えが無い・空・壊れている=出さない(落ちない)=(g)
|
|
90
140
|
link = FakeLink()
|
|
91
|
-
check('控えが無い: 落ちずに何もしない',
|
|
92
|
-
|
|
93
|
-
check('
|
|
141
|
+
check('控えが無い: 落ちずに何もしない',
|
|
142
|
+
sn.run(os.path.join(tmp, 'none.last'), datalink=link, state_path=st), None)
|
|
143
|
+
check('控えが空: 落ちずに何もしない',
|
|
144
|
+
sn.run(write(tmp, 'e.last', ''), datalink=link, state_path=st), None)
|
|
145
|
+
check('控えが壊れている: 落ちずに何もしない',
|
|
146
|
+
sn.run(write(tmp, 'b.last', 'ゴミ\n'), datalink=link, state_path=st), None)
|
|
94
147
|
check('壊れた控えでは1件も送らない', len(link.sent), 0)
|
|
148
|
+
check('壊れた控えでは札も閉じない(出したままにする)', len(link.closed), 0)
|
|
95
149
|
|
|
96
150
|
# ── (c)(d)(e) カードの形 ────────────────────────────────────────────────────
|
|
97
151
|
print('\n【(c)(d)(e) カードの形】')
|
|
98
152
|
with tempfile.TemporaryDirectory() as tmp:
|
|
99
153
|
link = FakeLink()
|
|
100
|
-
sn.run(write(tmp, 'ng.last', NG_LINE + '\n'), datalink=link)
|
|
154
|
+
sn.run(write(tmp, 'ng.last', NG_LINE + '\n'), datalink=link, state_path=state(tmp))
|
|
101
155
|
p = link.sent[0]
|
|
102
156
|
q = p['question']
|
|
103
157
|
# (c) 管制が本文として取り出す見出し語(ai-kana: NOTICE_BODY_LABELS)。
|
|
@@ -109,7 +163,9 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
109
163
|
# 失敗の理由・時刻・版が読める=(2) 適用version/時刻を確認可能にする。
|
|
110
164
|
check_true('うまくいかなかった理由が載る', 'write-failed' in q)
|
|
111
165
|
check_true('最後に試した時刻が載る', '2026-09-16T11:00:00+0900' in q)
|
|
112
|
-
check_true('
|
|
166
|
+
check_true('入っている版が載る', '入っている版: 0.12.7' in q)
|
|
167
|
+
# ★控えに公開の版が無い種類では、その行を出さない(「不明」と書いて読む人を惑わせない)。
|
|
168
|
+
check('控えに無い「公開の版」は行ごと出さない', '公開の版' in q, False)
|
|
113
169
|
# (d) ボタンを1つも出さない形(kind=message かつ choices なし)。
|
|
114
170
|
check('種別は message', p['kind'], 'message')
|
|
115
171
|
check('選択肢を付けない(押せないボタンを置かない)', 'choices' in p, False)
|
|
@@ -119,7 +175,7 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
119
175
|
# (e) 鍵が機体ごとに固定。
|
|
120
176
|
check_true('鍵が付いている', p['requestKey'].startswith(sn.REQUEST_KEY_PREFIX + ':'))
|
|
121
177
|
link2 = FakeLink()
|
|
122
|
-
sn.run(write(tmp, 'ng2.last', NG_LINE + '\n'), datalink=link2)
|
|
178
|
+
sn.run(write(tmp, 'ng2.last', NG_LINE + '\n'), datalink=link2, state_path=state(tmp, 'b.json'))
|
|
123
179
|
check('2回目も同じ鍵(カードが増えない)', link2.sent[0]['requestKey'], p['requestKey'])
|
|
124
180
|
# ★控えの中身(パス)以外の余計な物を載せない=秘密は1文字も入らない。
|
|
125
181
|
check('本文にトークンは入らない', CFG['token'] in q, False)
|
|
@@ -128,9 +184,11 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
128
184
|
print('\n【★2種類目=配り元の配り損ね(便 4061dd71)】')
|
|
129
185
|
DIST_NG = ('2026-09-16T12:00:00+0900 status=failed reason=version-drift what=dist-source '
|
|
130
186
|
'version=0.12.6 published=0.12.8 root=/n/@tyhld/conductor')
|
|
187
|
+
DIST_OK = ('2026-09-16T13:00:00+0900 status=ok reason=verified what=dist-source '
|
|
188
|
+
'version=0.12.8 published=0.12.8 root=/n/@tyhld/conductor')
|
|
131
189
|
with tempfile.TemporaryDirectory() as tmp:
|
|
132
190
|
link = FakeLink()
|
|
133
|
-
sn.run(write(tmp, 'dist.last', DIST_NG + '\n'), datalink=link)
|
|
191
|
+
sn.run(write(tmp, 'dist.last', DIST_NG + '\n'), datalink=link, state_path=state(tmp))
|
|
134
192
|
p = link.sent[0]
|
|
135
193
|
q = p['question']
|
|
136
194
|
check_true('見出しが配り元の話になっている', '安全装置を配れていません' in p['summary'])
|
|
@@ -138,35 +196,181 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
138
196
|
check_true('本文に「再開のしかた」がある', '再開のしかた:' in q)
|
|
139
197
|
check_true('何を配れていないかが書いてある', '番人' in q and '権限ルール' in q)
|
|
140
198
|
check_true('うまくいかなかった理由が載る', 'version-drift' in q)
|
|
199
|
+
check_true('入っている版と公開の版が並ぶ', '入っている版: 0.12.6' in q and '公開の版: 0.12.8' in q)
|
|
141
200
|
check_true('鍵が種類ごとに分かれている', p['requestKey'].startswith('dist-source-unverified:'))
|
|
142
201
|
# ★鉄則の配り損ねとは別の鍵=2種類が同じ1枚に潰れない。
|
|
143
202
|
link2 = FakeLink()
|
|
144
|
-
sn.run(write(tmp, 'skill.last', NG_LINE + '\n'), datalink=link2)
|
|
203
|
+
sn.run(write(tmp, 'skill.last', NG_LINE + '\n'), datalink=link2, state_path=state(tmp, 'b.json'))
|
|
145
204
|
check('鉄則の配り損ねとは別の鍵', p['requestKey'] == link2.sent[0]['requestKey'], False)
|
|
146
205
|
# ★知らない種類は、従来どおり鉄則の配り損ねとして扱う(増やしても壊れない)。
|
|
147
206
|
link3 = FakeLink()
|
|
148
207
|
unknown = DIST_NG.replace('what=dist-source', 'what=まだ無い種類')
|
|
149
|
-
sn.run(write(tmp, 'unknown.last', unknown + '\n'), datalink=link3)
|
|
208
|
+
sn.run(write(tmp, 'unknown.last', unknown + '\n'), datalink=link3, state_path=state(tmp, 'c.json'))
|
|
150
209
|
check_true('知らない種類でも落ちずにカードは出る', bool(link3.sent))
|
|
151
210
|
check_true('知らない種類は従来の文面へ倒れる',
|
|
152
211
|
'職人の決まりが古いままです' in link3.sent[0]['summary'])
|
|
153
212
|
|
|
213
|
+
# ── (h) 本文は【いつもその回の値】=閉じてから出し直す(便 675e2d05)──────────
|
|
214
|
+
print('\n【(h) 失敗の回は前の札を閉じてから出し直す】')
|
|
215
|
+
DRIFT_OLD = ('2026-09-16T12:00:00+0900 status=failed reason=version-drift what=dist-source '
|
|
216
|
+
'version=0.12.6 published=0.12.8 root=/n/@tyhld/conductor')
|
|
217
|
+
DRIFT_NEW = ('2026-09-20T09:00:00+0900 status=failed reason=version-drift what=dist-source '
|
|
218
|
+
'version=0.12.6 published=0.12.31 root=/n/@tyhld/conductor')
|
|
219
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
220
|
+
st = state(tmp)
|
|
221
|
+
link = FakeLink(ids=['req-old', 'req-new'])
|
|
222
|
+
check('1回目の失敗: 札を出す',
|
|
223
|
+
sn.run(write(tmp, 'a.last', DRIFT_OLD + '\n'), datalink=link, state_path=st), 'req-old')
|
|
224
|
+
check('1回目は閉じに行かない(まだ何も出ていない)', link.closed, [])
|
|
225
|
+
|
|
226
|
+
check('2回目の失敗: 別のIDで出し直す',
|
|
227
|
+
sn.run(write(tmp, 'b.last', DRIFT_NEW + '\n'), datalink=link, state_path=st), 'req-new')
|
|
228
|
+
check('2回目は前の札を閉じた', link.closed, ['req-old'])
|
|
229
|
+
# ★順序が肝。出し直してから閉じると、閉じたのが新しい札になって何も残らない。
|
|
230
|
+
check('★閉じる→出し直す の順である', link.log[1:], [('close', 'req-old'),
|
|
231
|
+
('register', link.sent[1]['requestKey'])])
|
|
232
|
+
q2 = link.sent[1]['question']
|
|
233
|
+
check_true('本文がその回の値になっている', '公開の版: 0.12.31' in q2)
|
|
234
|
+
check('前の回の値が本文に残らない', '0.12.8' in q2, False)
|
|
235
|
+
check_true('時刻もその回のもの', '2026-09-20T09:00:00+0900' in q2)
|
|
236
|
+
check('覚えているのは最後に出した札だけ',
|
|
237
|
+
json.load(open(st, encoding='utf-8')), {'dist-source-unverified': 'req-new'})
|
|
238
|
+
|
|
239
|
+
# ★空振り: 閉じる手当てを外すと、前の札が管制に残ったまま=本文が古いまま延命される。
|
|
240
|
+
orig_close = sn._close
|
|
241
|
+
sn._close = lambda *a, **k: False
|
|
242
|
+
try:
|
|
243
|
+
link2 = FakeLink(ids=['req-x'])
|
|
244
|
+
sn.run(write(tmp, 'c.last', DRIFT_NEW + '\n'), datalink=link2, state_path=st)
|
|
245
|
+
check('★空振り: 閉じなくすると1枚も閉じられない', link2.closed, [])
|
|
246
|
+
finally:
|
|
247
|
+
sn._close = orig_close
|
|
248
|
+
|
|
249
|
+
# ── (i) 直ったら札を閉じる ──────────────────────────────────────────────────
|
|
250
|
+
print('\n【(i) 成功の回は出していた札を閉じる】')
|
|
251
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
252
|
+
st = state(tmp)
|
|
253
|
+
link = FakeLink(ids=['req-1'])
|
|
254
|
+
sn.run(write(tmp, 'ng.last', DRIFT_OLD + '\n'), datalink=link, state_path=st)
|
|
255
|
+
check('直る前: 札が1枚出ている', link.sent and len(link.sent), 1)
|
|
256
|
+
|
|
257
|
+
check('成功の控え: 新しい札は出さない',
|
|
258
|
+
sn.run(write(tmp, 'ok.last', DIST_OK + '\n'), datalink=link, state_path=st), None)
|
|
259
|
+
check('成功の控え: 出ていた札を閉じた', link.closed, ['req-1'])
|
|
260
|
+
check('成功の控え: 送ったのは最初の1枚だけ', len(link.sent), 1)
|
|
261
|
+
check('閉じた札は覚え書きから外れる', json.load(open(st, encoding='utf-8')), {})
|
|
262
|
+
|
|
263
|
+
# 2度目の成功では、もう閉じるものが無い=管制へ1回も触らない。
|
|
264
|
+
link2 = FakeLink()
|
|
265
|
+
sn.run(write(tmp, 'ok2.last', DIST_OK + '\n'), datalink=link2, state_path=st)
|
|
266
|
+
check('直ったあとの毎時の成功: 管制へ触らない', link2.log, [])
|
|
267
|
+
|
|
268
|
+
# ★種類ごとに別々に覚える=配り元が直っても鉄則の札は閉じない。
|
|
269
|
+
st2 = state(tmp, 'two.json')
|
|
270
|
+
link3 = FakeLink(ids=['req-dist', 'req-skill'])
|
|
271
|
+
sn.run(write(tmp, 'd.last', DRIFT_OLD + '\n'), datalink=link3, state_path=st2)
|
|
272
|
+
sn.run(write(tmp, 's.last', NG_LINE + '\n'), datalink=link3, state_path=st2)
|
|
273
|
+
link3.closed.clear()
|
|
274
|
+
sn.run(write(tmp, 'do.last', DIST_OK + '\n'), datalink=link3, state_path=st2)
|
|
275
|
+
check('配り元の成功で閉じるのは配り元の札だけ', link3.closed, ['req-dist'])
|
|
276
|
+
check('鉄則の札は覚えたまま(まだ直っていない)',
|
|
277
|
+
json.load(open(st2, encoding='utf-8')), {'craftsman-skill-stale': 'req-skill'})
|
|
278
|
+
|
|
279
|
+
# ── (j) 再開の手順が理由に合っている ────────────────────────────────────────
|
|
280
|
+
print('\n【(j) 再開の手順が、うまくいかなかった理由に合っている】')
|
|
281
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
282
|
+
def body_for(reason, what='dist-source'):
|
|
283
|
+
line = (f'2026-09-20T09:00:00+0900 status=failed reason={reason} what={what} '
|
|
284
|
+
'version=0.12.6 published=0.12.31 root=/n/@tyhld/conductor')
|
|
285
|
+
link = FakeLink()
|
|
286
|
+
sn.run(write(tmp, f'{reason}.last', line + '\n'), datalink=link,
|
|
287
|
+
state_path=state(tmp, f'{reason}.json'))
|
|
288
|
+
return link.sent[0]['question']
|
|
289
|
+
|
|
290
|
+
drift = body_for('version-drift')
|
|
291
|
+
# ★版がずれているときに配り直しだけを促すと、配り元の確認でまた弾かれる=直らない。
|
|
292
|
+
check_true('版ずれ: 入れ直しを先に案内する', '① npm install -g @tyhld/conductor' in drift)
|
|
293
|
+
check_true('版ずれ: 取得先まで書いてある(社内の取得先を見に行かせない)',
|
|
294
|
+
'--@tyhld:registry=https://registry.npmjs.org' in drift)
|
|
295
|
+
check_true('版ずれ: そのあと配り直しを案内する', '② APPLY=1 bash' in drift)
|
|
296
|
+
check_true('版ずれ: 入れ直しが配り直しより先に書いてある',
|
|
297
|
+
drift.index('npm install -g') < drift.index('APPLY=1'))
|
|
298
|
+
|
|
299
|
+
check_true('入っていない: 入れ直しから案内する', 'npm install -g' in body_for('not-installed'))
|
|
300
|
+
check_true('中身が欠けている: 入れ直しから案内する', 'npm install -g' in body_for('incomplete'))
|
|
301
|
+
|
|
302
|
+
net = body_for('unverifiable')
|
|
303
|
+
check_true('公開npmに聞けない: 先に通信を確かめさせる', '通信' in net)
|
|
304
|
+
check('公開npmに聞けない: 入れ直しは案内しない(入っている版は正しいかもしれない)',
|
|
305
|
+
'npm install -g' in net, False)
|
|
306
|
+
check_true('公開npmに聞けない: 配り直しの1行は載る', 'APPLY=1 bash' in net)
|
|
307
|
+
|
|
308
|
+
plain = body_for('write-failed', what='craftsman-skill')
|
|
309
|
+
check('書けなかった: 入れ直しは案内しない', 'npm install -g' in plain, False)
|
|
310
|
+
check_true('書けなかった: 配り直しの1行だけ', 'APPLY=1 bash' in plain)
|
|
311
|
+
|
|
312
|
+
# ★どの理由でも、打たせるのは【入っている配布物の台本】=作業ツリーの台本ではない。
|
|
313
|
+
for label, b in (('版ずれ', drift), ('通信不能', net), ('書込失敗', plain)):
|
|
314
|
+
check_true(f'{label}: 入っている配布物の台本を打たせる', 'npm root -g' in b)
|
|
315
|
+
check(f'{label}: 作業ツリーへ cd させない', 'cd ~/projects' in b, False)
|
|
316
|
+
|
|
317
|
+
# ── ★札の覚え書き(壊れていても・書けなくても止まらない)──────────────────
|
|
318
|
+
print('\n【★札の覚え書き】')
|
|
319
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
320
|
+
broken = write(tmp, 'broken.json', '{ こわれている')
|
|
321
|
+
link = FakeLink()
|
|
322
|
+
check('覚え書きが壊れている: 落ちずに新しく出す',
|
|
323
|
+
sn.run(write(tmp, 'ng.last', NG_LINE + '\n'), datalink=link, state_path=broken), 'req-1')
|
|
324
|
+
check('覚え書きが壊れている: 閉じに行かない(番号が分からない)', link.closed, [])
|
|
325
|
+
|
|
326
|
+
# 配列など、想定外の形が入っていても辞書として扱わない。
|
|
327
|
+
odd = write(tmp, 'odd.json', '[1, 2, 3]')
|
|
328
|
+
check('覚え書きが辞書でない: 空として扱う', sn._load_cards(odd), {})
|
|
329
|
+
|
|
330
|
+
# 書けない場所でも止まらない(親がファイル=ディレクトリを作れない)。
|
|
331
|
+
blocked = os.path.join(write(tmp, 'afile', 'x'), 'cards.json')
|
|
332
|
+
link2 = FakeLink()
|
|
333
|
+
check('覚え書きを書けない: それでも知らせる',
|
|
334
|
+
sn.run(write(tmp, 'ng2.last', NG_LINE + '\n'), datalink=link2, state_path=blocked),
|
|
335
|
+
'req-1')
|
|
336
|
+
|
|
337
|
+
# ★閉じるのに失敗しても、出し直しは続ける(古い札が残るより無い方がまし、ではなく
|
|
338
|
+
# ★新しい本文を必ず出す方を優先する)。
|
|
339
|
+
st = state(tmp, 'x.json')
|
|
340
|
+
link3 = FakeLink(ids=['req-1', 'req-2'], abandon_raises=True)
|
|
341
|
+
sn.run(write(tmp, 'a.last', NG_LINE + '\n'), datalink=link3, state_path=st)
|
|
342
|
+
check('閉じるのが失敗しても出し直す',
|
|
343
|
+
sn.run(write(tmp, 'b.last', NG_LINE + '\n'), datalink=link3, state_path=st), 'req-2')
|
|
344
|
+
|
|
345
|
+
# ★覚え書きに入るのは番号だけ(秘密は1文字も入らない)。
|
|
346
|
+
saved = open(st, encoding='utf-8').read()
|
|
347
|
+
check('覚え書きにトークンは入らない', CFG['token'] in saved, False)
|
|
348
|
+
check('覚え書きに入るのは鍵と番号だけ', json.loads(saved), {'craftsman-skill-stale': 'req-2'})
|
|
349
|
+
|
|
154
350
|
# ── (f) 知らせに失敗しても同期は止めない ────────────────────────────────────
|
|
155
351
|
print('\n【(f) 知らせに失敗しても同期を止めない】')
|
|
156
352
|
with tempfile.TemporaryDirectory() as tmp:
|
|
157
353
|
last = write(tmp, 'ng.last', NG_LINE + '\n')
|
|
354
|
+
st = state(tmp)
|
|
158
355
|
|
|
159
356
|
# 接続情報が無い機体。
|
|
160
|
-
check('接続情報が無い: 何もせず終わる',
|
|
357
|
+
check('接続情報が無い: 何もせず終わる',
|
|
358
|
+
sn.run(last, datalink=FakeLink(cfg=None), state_path=st), None)
|
|
161
359
|
# 管制が受け取らなかった。
|
|
162
|
-
check('登録が空返事: 何もせず終わる',
|
|
163
|
-
|
|
360
|
+
check('登録が空返事: 何もせず終わる',
|
|
361
|
+
sn.run(last, datalink=FakeLink(reg=None), state_path=st), None)
|
|
362
|
+
check('登録にIDが無い: 何もせず終わる',
|
|
363
|
+
sn.run(last, datalink=FakeLink(reg={}), state_path=st), None)
|
|
364
|
+
check('受け取られなかった札は覚えない', os.path.exists(st), False)
|
|
164
365
|
|
|
165
366
|
# ★main() は何があっても 0 を返す(update-guard.sh が止まらない)。
|
|
166
367
|
argv = sys.argv
|
|
167
368
|
sys.argv = ['skill_sync_notify.py', last]
|
|
168
369
|
orig_load, orig_reg = sn.hook_datalink.load_config, sn.hook_datalink.register
|
|
370
|
+
orig_state = sn.CARD_STATE
|
|
169
371
|
try:
|
|
372
|
+
# ★本物の置き場へ書かせない(試験が実機の覚え書きを汚さない)。
|
|
373
|
+
sn.CARD_STATE = state(tmp, 'main.json')
|
|
170
374
|
sn.hook_datalink.load_config = lambda env=None: CFG
|
|
171
375
|
|
|
172
376
|
def boom(cfg, payload):
|
|
@@ -176,6 +380,7 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
176
380
|
check('通信が落ちても exit 0', sn.main(), 0)
|
|
177
381
|
finally:
|
|
178
382
|
sn.hook_datalink.load_config, sn.hook_datalink.register = orig_load, orig_reg
|
|
383
|
+
sn.CARD_STATE = orig_state
|
|
179
384
|
sys.argv = argv
|
|
180
385
|
check('引数が無くても exit 0', (lambda: (sys.argv.__setitem__(slice(None), ['x']), sn.main())[1])(), 0)
|
|
181
386
|
sys.argv = argv
|
|
@@ -183,17 +388,23 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
183
388
|
# ── ★空振り確認 ─────────────────────────────────────────────────────────────
|
|
184
389
|
print('\n【★空振り確認】')
|
|
185
390
|
with tempfile.TemporaryDirectory() as tmp:
|
|
391
|
+
st = state(tmp)
|
|
186
392
|
# 「status を見ずに必ず出す」ようにしたら (a) が落ちる=(a) は本当に効いている。
|
|
187
393
|
orig_parse = sn.parse_last
|
|
188
394
|
sn.parse_last = lambda p: dict(orig_parse(p), status='failed')
|
|
189
395
|
try:
|
|
190
396
|
link = FakeLink()
|
|
191
|
-
sn.run(write(tmp, 'ok.last', OK_LINE + '\n'), datalink=link)
|
|
397
|
+
sn.run(write(tmp, 'ok.last', OK_LINE + '\n'), datalink=link, state_path=st)
|
|
192
398
|
check('★空振り: 判定を壊すと成功の控えでも出てしまう', len(link.sent), 1)
|
|
193
399
|
finally:
|
|
194
400
|
sn.parse_last = orig_parse
|
|
195
401
|
link = FakeLink()
|
|
196
|
-
check('★戻したあと: 成功では出ない',
|
|
402
|
+
check('★戻したあと: 成功では出ない',
|
|
403
|
+
sn.run(write(tmp, 'ok2.last', OK_LINE + '\n'), datalink=link, state_path=st), None)
|
|
404
|
+
|
|
405
|
+
# ★試験そのものが実機の覚え書きに触っていないこと(上の state() を1つでも外したら落ちる)。
|
|
406
|
+
# 実機ではこのファイルが在るのが普通なので、「無いこと」ではなく【変わっていないこと】を見る。
|
|
407
|
+
check('試験は本物の覚え書きを書き換えない', real_state_snapshot(), REAL_STATE_BEFORE)
|
|
197
408
|
|
|
198
409
|
print('\n' + '=' * 72)
|
|
199
410
|
if failures:
|
package/scripts/update-guard.sh
CHANGED
|
@@ -119,9 +119,11 @@ dist_record() {
|
|
|
119
119
|
fi
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
#
|
|
122
|
+
# 管制の札を、いまの控えに合わせる(★便 29612ff1 と同じ口・届かなくても止まらない)。
|
|
123
123
|
# ★確認のみのときは知らせない(見ただけでお客さまの画面にカードを出さない)。
|
|
124
|
-
|
|
124
|
+
# ★失敗の回は札を出し直し、成功の回は札を閉じる(便 675e2d05)。どちらも同じ呼び方で、
|
|
125
|
+
# 知らせる側が控えの status を見て決める=ここに判断を二重に書かない。
|
|
126
|
+
dist_notify() {
|
|
125
127
|
[[ "$APPLY" == "1" ]] || return 0
|
|
126
128
|
[[ -f "$DIST_NOTIFY" ]] || return 0
|
|
127
129
|
python3 "$DIST_NOTIFY" "$DIST_LAST" >/dev/null 2>&1 || true
|
|
@@ -138,14 +140,14 @@ if ! dist_source_resolve; then
|
|
|
138
140
|
esac
|
|
139
141
|
warn " 人の手番: npm install -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org"
|
|
140
142
|
dist_record failed "$DIST_REASON"
|
|
141
|
-
|
|
143
|
+
dist_notify
|
|
142
144
|
exit 0
|
|
143
145
|
fi
|
|
144
146
|
if ! dist_source_complete "$DIST_ROOT"; then
|
|
145
147
|
warn "配り元の中身が欠けています: $DIST_ROOT(★1バイトも配りません)"
|
|
146
148
|
DIST_REASON='incomplete'
|
|
147
149
|
dist_record failed incomplete
|
|
148
|
-
|
|
150
|
+
dist_notify
|
|
149
151
|
exit 0
|
|
150
152
|
fi
|
|
151
153
|
info " 配り元: $DIST_ROOT(版 $DIST_VERSION =公開 npm のいまの版)"
|
|
@@ -153,6 +155,9 @@ if [[ "$DIST_ROOT" != "$REPO_ROOT" ]]; then
|
|
|
153
155
|
info " ★この台本の置き場($REPO_ROOT)からは配りません。"
|
|
154
156
|
fi
|
|
155
157
|
dist_record ok verified
|
|
158
|
+
# ★直ったので、出ていた「安全装置を配れていません」の札を閉じる(便 675e2d05)。
|
|
159
|
+
# 以前は成功しても閉じに行かず、最大60分出たままだった(実地 2026-09-20・便 8692c586)。
|
|
160
|
+
dist_notify
|
|
156
161
|
|
|
157
162
|
# 配る中身は、すべて上で確かめた配り元から取る。
|
|
158
163
|
GUARD_SRC_DIR="$DIST_ROOT/scripts/hooks" # 番人の正本
|
|
@@ -369,32 +374,45 @@ craftsman_backup() {
|
|
|
369
374
|
return 0
|
|
370
375
|
}
|
|
371
376
|
|
|
372
|
-
#
|
|
373
|
-
|
|
377
|
+
# 管制の札を、いまの控えに合わせる(★届かなくても同期は止めない)。
|
|
378
|
+
# ★失敗の回は札を出し直し、成功の回は札を閉じる(便 675e2d05)。どちらも同じ呼び方で、
|
|
379
|
+
# 知らせる側が控えの status を見て決める=ここに判断を二重に書かない。
|
|
380
|
+
craftsman_notify() {
|
|
374
381
|
[[ -f "$CRAFTSMAN_NOTIFY" ]] || return 0
|
|
375
382
|
python3 "$CRAFTSMAN_NOTIFY" "$CRAFTSMAN_LAST" >/dev/null 2>&1 || true
|
|
376
383
|
}
|
|
377
384
|
|
|
385
|
+
# 直った回だけの呼び口。★確認のみ(APPLY!=1)のときは管制の札に触らない。
|
|
386
|
+
# 【なぜ分けるか】確認のみは「いまどうなっているかを見るだけ」の操作なので、それで
|
|
387
|
+
# お客さまの画面の札が消える形にしない(配り元の知らせ dist_notify と同じ流儀)。
|
|
388
|
+
# ★失敗の回は従来どおり確認のみでも知らせる(古い決まりのまま動いていることは見せ続ける)。
|
|
389
|
+
craftsman_notify_recovered() {
|
|
390
|
+
[[ "$APPLY" == "1" ]] || return 0
|
|
391
|
+
craftsman_notify
|
|
392
|
+
}
|
|
393
|
+
|
|
378
394
|
info "①-d 職人の鉄則スキル: $CRAFTSMAN_SKILL_DST"
|
|
379
395
|
CRAFTSMAN_VER="$(craftsman_skill_version)"
|
|
380
396
|
CRAFTSMAN_SHA="$(craftsman_skill_sha "$CRAFTSMAN_SKILL_SRC")"
|
|
381
397
|
if [[ ! -f "$CRAFTSMAN_SKILL_SRC" ]]; then
|
|
382
398
|
warn "鉄則の正本が見つかりません: $CRAFTSMAN_SKILL_SRC(★古い鉄則のまま動きます)"
|
|
383
399
|
craftsman_record failed src-missing "$CRAFTSMAN_VER" none
|
|
384
|
-
|
|
400
|
+
craftsman_notify
|
|
385
401
|
elif [[ ! -s "$CRAFTSMAN_SKILL_SRC" ]]; then
|
|
386
402
|
warn "鉄則の正本が空です: $CRAFTSMAN_SKILL_SRC(★古い鉄則のまま動きます)"
|
|
387
403
|
craftsman_record failed src-empty "$CRAFTSMAN_VER" none
|
|
388
|
-
|
|
404
|
+
craftsman_notify
|
|
389
405
|
elif cmp -s "$CRAFTSMAN_SKILL_SRC" "$CRAFTSMAN_SKILL_DST" 2>/dev/null; then
|
|
390
406
|
info " 最新です(版 ${CRAFTSMAN_VER} / 中身 ${CRAFTSMAN_SHA})"
|
|
391
407
|
craftsman_record ok already-current "$CRAFTSMAN_VER" "$CRAFTSMAN_SHA"
|
|
408
|
+
# ★配れているので、出ていた「決まりが古いままです」の札を閉じる(便 675e2d05)。
|
|
409
|
+
craftsman_notify_recovered
|
|
392
410
|
elif [[ "$APPLY" == "1" ]]; then
|
|
393
411
|
# ★消える物が在るなら、上書きの【前】に控えを取る。取れなければ上書きしない。
|
|
394
412
|
if ! craftsman_backup; then
|
|
395
413
|
warn "上書きの前の控えを取れませんでした: $CRAFTSMAN_BACKUP_DIR(★鉄則は上書きしません)"
|
|
396
414
|
craftsman_record failed backup-failed "$CRAFTSMAN_VER" "$CRAFTSMAN_SHA"
|
|
397
|
-
|
|
415
|
+
craftsman_notify
|
|
398
416
|
elif mkdir -p "$(dirname "$CRAFTSMAN_SKILL_DST")" 2>/dev/null \
|
|
399
417
|
&& cp -f "$CRAFTSMAN_SKILL_SRC" "$CRAFTSMAN_SKILL_DST" 2>/dev/null \
|
|
400
418
|
&& cmp -s "$CRAFTSMAN_SKILL_SRC" "$CRAFTSMAN_SKILL_DST" 2>/dev/null; then
|
|
@@ -403,10 +421,12 @@ elif [[ "$APPLY" == "1" ]]; then
|
|
|
403
421
|
fi
|
|
404
422
|
info " 更新しました(版 ${CRAFTSMAN_VER} / 中身 ${CRAFTSMAN_SHA}・この1台の全現場が共有します)"
|
|
405
423
|
craftsman_record ok applied "$CRAFTSMAN_VER" "$CRAFTSMAN_SHA"
|
|
424
|
+
# ★直ったので札を閉じる。以前は成功しても閉じに行かず、最大60分出たままだった。
|
|
425
|
+
craftsman_notify_recovered
|
|
406
426
|
else
|
|
407
427
|
warn "鉄則を配れませんでした: $CRAFTSMAN_SKILL_DST(★古い鉄則のまま動きます)"
|
|
408
428
|
craftsman_record failed write-failed "$CRAFTSMAN_VER" "$CRAFTSMAN_SHA"
|
|
409
|
-
|
|
429
|
+
craftsman_notify
|
|
410
430
|
fi
|
|
411
431
|
else
|
|
412
432
|
info " (予定)正本の内容へ揃えます(版 ${CRAFTSMAN_VER} / 中身 ${CRAFTSMAN_SHA})"
|
|
@@ -50,5 +50,21 @@ ExecStart=__CONDUCTOR_BIN__ build-runner
|
|
|
50
50
|
Restart=always
|
|
51
51
|
RestartSec=30
|
|
52
52
|
|
|
53
|
+
# ★メモリが足りなくなったときに、真っ先に止められないようにする(便 85a9fb64・メモリ対策B)。
|
|
54
|
+
# 【なぜ要るか(実測 2026-09-19・調査 5286ffd5)】機体のメモリが尽きたとき、カーネルは
|
|
55
|
+
# 「使っている量+落ちやすさの下駄」で止める相手を選ぶ。利用者の常駐は systemd の既定
|
|
56
|
+
# (DefaultOOMScoreAdjust=200)で下駄を履かされており、200 は「機体の全メモリの20%ぶん
|
|
57
|
+
# 余計に使っている」のと同じ重み=15.8GB の機体では約3.2GB の下駄になる。
|
|
58
|
+
# その結果、40MB しか使っていないこの常駐が「3.2GB のプロセス」と見なされ、
|
|
59
|
+
# 本当に重い相手(職人の build・lint が 1〜4.7GB)より先に止められた。実際 2026-09-19 に
|
|
60
|
+
# 常駐18本がまとめて止められ、配達が止まった。
|
|
61
|
+
# 【何をするか】下駄を外して素の値(0)に戻すだけ。特別扱いはしない。
|
|
62
|
+
# =重い相手が先に止められ、配達役は生き残る。★常駐自身が膨らんだ場合は
|
|
63
|
+
# ふつうに止められる(守りすぎて機体ごと固まる形にしない)。
|
|
64
|
+
# 【なぜ負の値にしないか(実測)】権限のない利用者のプロセスは、落ちやすさを
|
|
65
|
+
# 0 より下げられない(-100 を書くと Permission denied)。負の値を書くと常駐が
|
|
66
|
+
# 起動できなくなるので、下限である 0 にする。
|
|
67
|
+
OOMScoreAdjust=0
|
|
68
|
+
|
|
53
69
|
[Install]
|
|
54
70
|
WantedBy=default.target
|
|
@@ -59,5 +59,21 @@ ExecStart=__CONDUCTOR_BIN__ ear
|
|
|
59
59
|
Restart=always
|
|
60
60
|
RestartSec=10
|
|
61
61
|
|
|
62
|
+
# ★メモリが足りなくなったときに、真っ先に止められないようにする(便 85a9fb64・メモリ対策B)。
|
|
63
|
+
# 【なぜ要るか(実測 2026-09-19・調査 5286ffd5)】機体のメモリが尽きたとき、カーネルは
|
|
64
|
+
# 「使っている量+落ちやすさの下駄」で止める相手を選ぶ。利用者の常駐は systemd の既定
|
|
65
|
+
# (DefaultOOMScoreAdjust=200)で下駄を履かされており、200 は「機体の全メモリの20%ぶん
|
|
66
|
+
# 余計に使っている」のと同じ重み=15.8GB の機体では約3.2GB の下駄になる。
|
|
67
|
+
# その結果、40MB しか使っていないこの常駐が「3.2GB のプロセス」と見なされ、
|
|
68
|
+
# 本当に重い相手(職人の build・lint が 1〜4.7GB)より先に止められた。実際 2026-09-19 に
|
|
69
|
+
# 常駐18本がまとめて止められ、配達が止まった。
|
|
70
|
+
# 【何をするか】下駄を外して素の値(0)に戻すだけ。特別扱いはしない。
|
|
71
|
+
# =重い相手が先に止められ、配達役は生き残る。★常駐自身が膨らんだ場合は
|
|
72
|
+
# ふつうに止められる(守りすぎて機体ごと固まる形にしない)。
|
|
73
|
+
# 【なぜ負の値にしないか(実測)】権限のない利用者のプロセスは、落ちやすさを
|
|
74
|
+
# 0 より下げられない(-100 を書くと Permission denied)。負の値を書くと常駐が
|
|
75
|
+
# 起動できなくなるので、下限である 0 にする。
|
|
76
|
+
OOMScoreAdjust=0
|
|
77
|
+
|
|
62
78
|
[Install]
|
|
63
79
|
WantedBy=default.target
|
|
@@ -58,5 +58,21 @@ ExecStart=__CONDUCTOR_BIN__ start -s %i -t working -a "%i常駐"
|
|
|
58
58
|
Restart=always
|
|
59
59
|
RestartSec=5
|
|
60
60
|
|
|
61
|
+
# ★メモリが足りなくなったときに、真っ先に止められないようにする(便 85a9fb64・メモリ対策B)。
|
|
62
|
+
# 【なぜ要るか(実測 2026-09-19・調査 5286ffd5)】機体のメモリが尽きたとき、カーネルは
|
|
63
|
+
# 「使っている量+落ちやすさの下駄」で止める相手を選ぶ。利用者の常駐は systemd の既定
|
|
64
|
+
# (DefaultOOMScoreAdjust=200)で下駄を履かされており、200 は「機体の全メモリの20%ぶん
|
|
65
|
+
# 余計に使っている」のと同じ重み=15.8GB の機体では約3.2GB の下駄になる。
|
|
66
|
+
# その結果、40MB しか使っていないこの常駐が「3.2GB のプロセス」と見なされ、
|
|
67
|
+
# 本当に重い相手(職人の build・lint が 1〜4.7GB)より先に止められた。実際 2026-09-19 に
|
|
68
|
+
# 常駐18本がまとめて止められ、配達が止まった。
|
|
69
|
+
# 【何をするか】下駄を外して素の値(0)に戻すだけ。特別扱いはしない。
|
|
70
|
+
# =重い相手が先に止められ、配達役は生き残る。★常駐自身が膨らんだ場合は
|
|
71
|
+
# ふつうに止められる(守りすぎて機体ごと固まる形にしない)。
|
|
72
|
+
# 【なぜ負の値にしないか(実測)】権限のない利用者のプロセスは、落ちやすさを
|
|
73
|
+
# 0 より下げられない(-100 を書くと Permission denied)。負の値を書くと常駐が
|
|
74
|
+
# 起動できなくなるので、下限である 0 にする。
|
|
75
|
+
OOMScoreAdjust=0
|
|
76
|
+
|
|
61
77
|
[Install]
|
|
62
78
|
WantedBy=default.target
|