peertable 0.8.22 → 0.8.25
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/LICENSE +21 -21
- package/README.ja.md +128 -128
- package/README.md +164 -164
- package/package.json +48 -47
- package/room/Dockerfile +7 -7
- package/room/client.mjs +17 -6
- package/room/message-bounds.mjs +55 -0
- package/skill/02_models.snapshot.md +103 -103
- package/skill/SKILL.md +265 -265
- package/skill/launchd/dev.kitepon.peertable-bridges.plist +20 -20
- package/skill/scripts/agent-pane-status.mjs +6 -0
- package/skill/scripts/alarm-bridge.mjs +28 -11
- package/skill/scripts/alarm-condition.mjs +7 -0
- package/skill/scripts/alarm-set.sh +22 -10
- package/skill/scripts/alarm-write.mjs +27 -0
- package/skill/scripts/archive-room-log.py +0 -0
- package/skill/scripts/bridge-record-live.mjs +0 -0
- package/skill/scripts/change-effort.sh +0 -0
- package/skill/scripts/change-seat.sh +0 -0
- package/skill/scripts/claude-dialog.mjs +4 -1
- package/skill/scripts/codex-dialog.mjs +18 -5
- package/skill/scripts/codex-parent-watch.sh +0 -0
- package/skill/scripts/doctor.sh +0 -0
- package/skill/scripts/ensure-all-bridges.sh +0 -0
- package/skill/scripts/ensure-bridge.sh +19 -6
- package/skill/scripts/ensure-codex-room-mcp.mjs +0 -0
- package/skill/scripts/ensure-project-runtime.sh +13 -0
- package/skill/scripts/ensure-room-mcp.mjs +0 -0
- package/skill/scripts/external-pane.mjs +0 -0
- package/skill/scripts/grok-seat-config.mjs +47 -0
- package/skill/scripts/kickoff-gate.mjs +0 -0
- package/skill/scripts/launch-seat.sh +74 -65
- package/skill/scripts/leave-seat.sh +0 -0
- package/skill/scripts/make-plan-input.mjs +0 -0
- package/skill/scripts/parent-join.sh +0 -0
- package/skill/scripts/parent-watch.mjs +2 -3
- package/skill/scripts/platform/windows/build-bridge-command.mjs +7 -2
- package/skill/scripts/resolve-seat-placement.mjs +0 -0
- package/skill/scripts/resume.sh +2 -9
- package/skill/scripts/runtime-contract.test.mjs +158 -0
- package/skill/scripts/runtime-digest.mjs +25 -0
- package/skill/scripts/seat-status-bridge.mjs +2 -2
- package/skill/scripts/seat-usage.mjs +8 -0
- package/skill/scripts/set-mission.sh +0 -0
- package/skill/scripts/setup.sh +3 -6
- package/skill/scripts/teardown.sh +0 -0
- package/skill/scripts/tmux-at.bash +22 -22
- package/skill/scripts/upgrade-team-assets.sh +4 -0
- package/skill/scripts/vendors/grok/pane-status.mjs +7 -0
- package/skill/scripts/wakeup-bridge.mjs +36 -10
- package/skill/templates/charter.md +20 -20
- package/skill/templates/mcp.json +5 -5
- package/skill/templates/member-standalone.md +60 -60
- package/skill/templates/member.md +159 -159
- package/skill/templates/parent.md +141 -141
- package/skill/templates/tasks.md +8 -8
|
@@ -98,11 +98,10 @@ brief_file=""
|
|
|
98
98
|
brief_max_bytes=65536
|
|
99
99
|
brief_completed=false
|
|
100
100
|
brief_dispatched=false
|
|
101
|
+
brief_sent=false
|
|
102
|
+
brief_event_cursor=""
|
|
101
103
|
seat_created=false
|
|
102
104
|
rollback_done=false
|
|
103
|
-
# ready を観測できないだけの不確実性は、投入後の真失敗と分ける。これは
|
|
104
|
-
# Aiterm から手動 dispatch できる空席として残し、席数へ成功着任とは数えない。
|
|
105
|
-
brief_not_ready=false
|
|
106
105
|
sock=""
|
|
107
106
|
sess=""
|
|
108
107
|
url=""
|
|
@@ -177,7 +176,7 @@ on_exit() {
|
|
|
177
176
|
local rollback_rc
|
|
178
177
|
trap - EXIT
|
|
179
178
|
cleanup_brief
|
|
180
|
-
if [ "$seat_created" = true ] && [ "$brief_completed" != true ] && [ "$
|
|
179
|
+
if [ "$seat_created" = true ] && [ "$brief_completed" != true ] && [ "$rollback_done" != true ]; then
|
|
181
180
|
rollback_done=true
|
|
182
181
|
if rollback_brief "$exit_rc"; then
|
|
183
182
|
:
|
|
@@ -214,6 +213,10 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
214
213
|
fi
|
|
215
214
|
fi
|
|
216
215
|
|
|
216
|
+
# 既存卓も正規着座入口の一回で現行generated assetへ収束させる。alarm輸送helperや
|
|
217
|
+
# member規約の更新を利用側の事前resume順序へ委ねない。
|
|
218
|
+
"$peertable_script_dir/upgrade-team-assets.sh" "$proj"
|
|
219
|
+
|
|
217
220
|
# **画面の文字列は「model が使えること」を意味しない。** 2026-08-11 実測: fable-5 の席は
|
|
218
221
|
# Claude の channels バナーが出たので下の着席判定を通ったが、その後の入力は全て
|
|
219
222
|
# model unavailable で 0 秒失敗し、席は一度も仕事をできなかった(room [11])。
|
|
@@ -241,7 +244,7 @@ case "$harness" in
|
|
|
241
244
|
exit 1
|
|
242
245
|
fi
|
|
243
246
|
cp "${HOME}/.grok/auth.json" "${grok_home}/auth.json"
|
|
244
|
-
|
|
247
|
+
node "$peertable_script_dir/grok-seat-config.mjs" "$proj" "${grok_home}/config.toml"
|
|
245
248
|
echo "grok preflight: GROK_HOME=${grok_home} ${grok_bin} --model ${model} --reasoning-effort ${effort} -p ping"
|
|
246
249
|
preflight_cmd=(env GROK_HOME="$grok_home" "$grok_bin" --model "$model" --reasoning-effort "$effort" -p "ping")
|
|
247
250
|
;;
|
|
@@ -346,7 +349,7 @@ if [ "$harness" = grok ]; then
|
|
|
346
349
|
fi
|
|
347
350
|
cp "${HOME}/.grok/auth.json" "${grok_home}/auth.json"
|
|
348
351
|
chmod 600 "${grok_home}/auth.json" 2>/dev/null || true
|
|
349
|
-
|
|
352
|
+
node "$peertable_script_dir/grok-seat-config.mjs" "$proj" "${grok_home}/config.toml"
|
|
350
353
|
fi
|
|
351
354
|
|
|
352
355
|
# 前の卓の残骸を回収してから、Aiterm の公開 agent launcher を唯一の席起動経路として呼ぶ。
|
|
@@ -435,9 +438,14 @@ print('dispatched' if cursor is not None and residue is not True
|
|
|
435
438
|
PY
|
|
436
439
|
)
|
|
437
440
|
case "$brief_receipt_state" in
|
|
438
|
-
dispatched)
|
|
441
|
+
dispatched)
|
|
442
|
+
brief_dispatched=true
|
|
443
|
+
brief_sent=true
|
|
444
|
+
brief_event_cursor=$(python3 -c 'import json,sys;print(json.loads(sys.argv[1]).get("event_cursor", ""))' "$launch_receipt")
|
|
445
|
+
;;
|
|
439
446
|
residue)
|
|
440
447
|
brief_in_composer=true
|
|
448
|
+
brief_event_cursor=$(python3 -c 'import json,sys;print(json.loads(sys.argv[1]).get("event_cursor", ""))' "$launch_receipt")
|
|
441
449
|
echo "SEAT_BRIEF_SUBMIT_RESIDUE: brief は入力欄に残り submit が落ちた。着席確認後に submit し直す" >&2
|
|
442
450
|
;;
|
|
443
451
|
*)
|
|
@@ -476,31 +484,29 @@ claude_trust_accepted=false
|
|
|
476
484
|
codex_dialog_helper="$peertable_script_dir/codex-dialog.mjs"
|
|
477
485
|
claude_dialog_helper="$peertable_script_dir/claude-dialog.mjs"
|
|
478
486
|
pass_codex_pane() {
|
|
479
|
-
local
|
|
480
|
-
|
|
481
|
-
[ -n "$
|
|
487
|
+
local keys key
|
|
488
|
+
keys=$(printf '%s' "$1" | node "$codex_dialog_helper" --keys || true)
|
|
489
|
+
[ -n "$keys" ] || return 1
|
|
490
|
+
sleep .3
|
|
482
491
|
while IFS= read -r key; do
|
|
483
492
|
[ -n "$key" ] || continue
|
|
484
493
|
tmux_at send-keys -t "$sess" "$key" || return 1
|
|
485
494
|
sleep .3
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
print("\n".join(a.get("keys") or []) if isinstance(a, dict) else "")' <<<"$json")
|
|
495
|
+
sleep .3
|
|
496
|
+
done <<<"$keys"
|
|
489
497
|
return 0
|
|
490
498
|
}
|
|
491
499
|
codex_pane_blocks_ready() {
|
|
492
500
|
printf '%s' "$1" | node "$codex_dialog_helper" --ready-ok
|
|
493
501
|
}
|
|
494
502
|
pass_claude_mcp_pane() {
|
|
495
|
-
local
|
|
496
|
-
|
|
497
|
-
[ -n "$
|
|
503
|
+
local keys key
|
|
504
|
+
keys=$(printf '%s' "$1" | node "$claude_dialog_helper" --keys || true)
|
|
505
|
+
[ -n "$keys" ] || return 1
|
|
498
506
|
while IFS= read -r key; do
|
|
499
507
|
[ -n "$key" ] || continue
|
|
500
508
|
tmux_at send-keys -t "$sess" "$key" || return 1
|
|
501
|
-
done
|
|
502
|
-
a=json.loads(sys.stdin.read() or "null")
|
|
503
|
-
print("\n".join(a.get("keys") or []) if isinstance(a, dict) else "")' <<<"$json")
|
|
509
|
+
done <<<"$keys"
|
|
504
510
|
return 0
|
|
505
511
|
}
|
|
506
512
|
while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
@@ -592,6 +598,11 @@ if [ "$room_ready" != true ]; then
|
|
|
592
598
|
exit 1
|
|
593
599
|
fi
|
|
594
600
|
echo "room ready: ${room}/${name}"
|
|
601
|
+
|
|
602
|
+
# ここから先のdialog処理・稼働状態・DM配送に必要なruntimeは、利用側へ順序を
|
|
603
|
+
# 委ねず正規着座入口が現行Peertable版へ収束させる。
|
|
604
|
+
PEERTABLE_CREDENTIAL_FILE="$credential_file" "$peertable_script_dir/ensure-project-runtime.sh" "$proj"
|
|
605
|
+
|
|
595
606
|
if [ "$harness" = codex ]; then
|
|
596
607
|
# member 登録後の最初の tool 呼び出しで MCP Allow が出る。出ている間だけ通し、沈黙5秒で抜ける。
|
|
597
608
|
codex_post_ready_deadline=$((SECONDS + 90))
|
|
@@ -611,14 +622,16 @@ if [ "$harness" = codex ]; then
|
|
|
611
622
|
done
|
|
612
623
|
fi
|
|
613
624
|
if [ "$brief_dispatched" = true ]; then
|
|
614
|
-
brief_completed=true
|
|
615
625
|
echo "briefed: ${sess}(Aiterm launch prompt)"
|
|
616
626
|
fi
|
|
617
627
|
|
|
618
628
|
if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
619
629
|
if [ "$brief_in_composer" != true ]; then
|
|
620
|
-
|
|
621
|
-
|
|
630
|
+
# TUIの入力可能判定とsubmitはAitermの公開 pty_send が所有する。ここで独自の
|
|
631
|
+
# prompt連続観測を重ねると、正常なTUI描画差だけでdispatch前に席を落とす。
|
|
632
|
+
if brief_send_receipt=$(node "$peertable_script_dir/aiterm-send.mjs" "$sess" "$brief_file"); then
|
|
633
|
+
brief_sent=true
|
|
634
|
+
brief_event_cursor=$(python3 -c 'import json,sys;print(json.loads(sys.argv[1]).get("event_cursor", ""))' "$brief_send_receipt")
|
|
622
635
|
echo "briefed: $sess(Aiterm pty_send)"
|
|
623
636
|
else
|
|
624
637
|
echo "LAUNCH_BRIEF_SEND_FAILED: Aiterm pty_sendでbriefをdispatchできない" >&2
|
|
@@ -645,14 +658,15 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
645
658
|
sleep 1
|
|
646
659
|
done
|
|
647
660
|
if [ "$brief_ready" != true ]; then
|
|
648
|
-
|
|
649
|
-
echo "LAUNCH_BRIEF_NOT_READY: brief を受け付ける入力
|
|
661
|
+
tmux_at capture-pane -t "$sess" -p >"$proj/.team/${name}-pane-on-fail.txt" 2>/dev/null || true
|
|
662
|
+
echo "LAUNCH_BRIEF_NOT_READY: brief を受け付ける入力promptを観測できない" >&2
|
|
663
|
+
echo "pane saved: $proj/.team/${name}-pane-on-fail.txt" >&2
|
|
664
|
+
exit 1
|
|
650
665
|
else
|
|
651
666
|
# prompt の描画とキー入力受理の境界を分ける。Codex のTUIが入力欄を
|
|
652
667
|
# mountした直後に paste と Enter を同一tickで受けると、Enterだけ落ちる。
|
|
653
668
|
sleep 1
|
|
654
669
|
|
|
655
|
-
brief_before=$(tmux_at capture-pane -S -1000 -t "$sess" -p 2>/dev/null || true)
|
|
656
670
|
# submit_residue の席は本文が composer に残っている。貼り直すと二重になるので Enter だけ打つ。
|
|
657
671
|
if [ "$brief_in_composer" != true ]; then
|
|
658
672
|
brief_buffer="peertable-brief-${name}-$$"
|
|
@@ -672,28 +686,46 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
672
686
|
exit 1
|
|
673
687
|
fi
|
|
674
688
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
689
|
+
brief_sent=true
|
|
690
|
+
echo "briefed: $sess"
|
|
691
|
+
fi
|
|
692
|
+
fi
|
|
693
|
+
fi
|
|
694
|
+
|
|
695
|
+
# 「promptを渡した」ではなく、全harness共通で実ターンが始まったことを成功条件にする。
|
|
696
|
+
# 1秒未満で完了するturnはAitermの完了正本で拾い、通常turnは現在paneのbusyランプで拾う。
|
|
697
|
+
if [ -n "$brief" ] && [ "$brief_sent" = true ]; then
|
|
698
|
+
command -v aiterm-wait >/dev/null 2>&1 || { echo "LAUNCH_AITERM_WAIT_MISSING: 実ターン開始を確認できない" >&2; exit 1; }
|
|
699
|
+
brief_active_deadline=$((SECONDS + 60))
|
|
700
|
+
brief_turn_observed=false
|
|
701
|
+
while [ $SECONDS -lt "$brief_active_deadline" ]; do
|
|
702
|
+
brief_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
703
|
+
if [ "$harness" = codex ] && pass_codex_pane "$brief_screen"; then
|
|
704
|
+
sleep 1
|
|
705
|
+
continue
|
|
706
|
+
fi
|
|
707
|
+
brief_pane_status=$(printf '%s' "$brief_screen" | node "$peertable_script_dir/agent-pane-status.mjs")
|
|
708
|
+
if [ "$brief_pane_status" = busy ]; then
|
|
709
|
+
brief_turn_observed=true
|
|
710
|
+
break
|
|
711
|
+
fi
|
|
712
|
+
if [ -n "$brief_event_cursor" ]; then
|
|
713
|
+
wait_receipt=$(aiterm-wait --session "$sess" --cursor "$brief_event_cursor" --timeout 0 2>/dev/null || true)
|
|
714
|
+
if printf '%s' "$wait_receipt" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{try{process.exit(JSON.parse(s).outcome==="done"?0:1)}catch{process.exit(1)}})'; then
|
|
715
|
+
brief_turn_observed=true
|
|
716
|
+
break
|
|
717
|
+
fi
|
|
718
|
+
fi
|
|
687
719
|
sleep 1
|
|
688
720
|
done
|
|
689
|
-
if [ "$
|
|
690
|
-
|
|
721
|
+
if [ "$brief_turn_observed" != true ]; then
|
|
722
|
+
tmux_at capture-pane -t "$sess" -p >"$proj/.team/${name}-pane-on-fail.txt" 2>/dev/null || true
|
|
723
|
+
echo "LAUNCH_BRIEF_TURN_NOT_STARTED: Aiterm dispatch後に${harness}の実ターン開始も完了も観測できない" >&2
|
|
724
|
+
echo "pane saved: $proj/.team/${name}-pane-on-fail.txt" >&2
|
|
691
725
|
exit 1
|
|
692
726
|
fi
|
|
693
727
|
brief_completed=true
|
|
694
|
-
echo "
|
|
695
|
-
fi
|
|
696
|
-
fi
|
|
728
|
+
echo "active: $sess(実ターン開始を観測)"
|
|
697
729
|
fi
|
|
698
730
|
|
|
699
731
|
# 席の本人性(pid / 起動時刻 / argv digest)を room 台帳の member 行へ登録する。
|
|
@@ -762,28 +794,5 @@ else
|
|
|
762
794
|
echo "SEAT_LEDGER_INCOMPLETE: 台帳の member 行に素性または本人性が欠けている(席は着席済み。doctor で確認)" >&2
|
|
763
795
|
fi
|
|
764
796
|
|
|
765
|
-
PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" alarm || echo "alarm-bridge の起動確認に失敗した(席は着席済み)" >&2
|
|
766
|
-
if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" seat-status; then
|
|
767
|
-
echo "seat-status-bridge: 起動確認済み"
|
|
768
|
-
else
|
|
769
|
-
echo "seat-status-bridge の起動確認に失敗した(席は着席済み)" >&2
|
|
770
|
-
fi
|
|
771
|
-
|
|
772
|
-
# Claude 席の新着は room client の notifications/claude/channel。TUI 配達は channels を持たない
|
|
773
|
-
# Codex / Grok 席だけ。Claude に立てると channel と二重配達になる。
|
|
774
|
-
if [ "$harness" = codex ] || [ "$harness" = grok ]; then
|
|
775
|
-
if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" wakeup; then
|
|
776
|
-
echo "wakeup-bridge: TUI配達 起動確認済み"
|
|
777
|
-
else
|
|
778
|
-
echo "SEAT_WAKEUP_BRIDGE_NOT_READY: Codex/Grok席の TUI 配達を準備できない" >&2
|
|
779
|
-
exit 1
|
|
780
|
-
fi
|
|
781
|
-
fi
|
|
782
|
-
|
|
783
797
|
if [ -z "$brief" ]; then brief_completed=true; fi
|
|
784
798
|
credential_persist=true
|
|
785
|
-
if [ "$brief_not_ready" = true ]; then
|
|
786
|
-
# 空席の後段セットアップ(identity / metadata / bridge)まで済ませたうえで、
|
|
787
|
-
# 呼び出し側にはready未確認を非0で返す。席はAiterm手動dispatchへ引き継ぐ。
|
|
788
|
-
exit 1
|
|
789
|
-
fi
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -330,9 +330,7 @@ async function catchUp() {
|
|
|
330
330
|
// 長寿命なのはbackground taskのloopだけで、端末sessionやNode processは常駐させない。
|
|
331
331
|
if (mode === '--poll') {
|
|
332
332
|
await catchUp()
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
333
|
+
} else {
|
|
336
334
|
const nextWindowMs = Number(process.env.PEERTABLE_PARENT_WATCH_WINDOW_MS ?? 55_000)
|
|
337
335
|
if (mode === '--next' && (!Number.isFinite(nextWindowMs) || nextWindowMs < 100)) {
|
|
338
336
|
console.error('PARENT_WATCH_WINDOW_INVALID')
|
|
@@ -411,3 +409,4 @@ for (;;) {
|
|
|
411
409
|
await sleep(Math.min(2000, Math.max(1, deadline - Date.now())))
|
|
412
410
|
}
|
|
413
411
|
}
|
|
412
|
+
}
|
|
@@ -28,9 +28,14 @@ export function buildWindowsBridgeLaunch({
|
|
|
28
28
|
const value = env[name]
|
|
29
29
|
if (value) statements.push(`$env:${name} = ${quotePowerShell(value)}`)
|
|
30
30
|
}
|
|
31
|
+
statements.push('$utf8 = [System.Text.UTF8Encoding]::new($false)')
|
|
32
|
+
statements.push('[Console]::InputEncoding = $utf8')
|
|
33
|
+
statements.push('[Console]::OutputEncoding = $utf8')
|
|
34
|
+
statements.push('$OutputEncoding = $utf8')
|
|
31
35
|
const command = [node, script, project, ...args].map(quotePowerShell).join(' ')
|
|
32
|
-
statements.push(`& ${command}
|
|
33
|
-
statements.push('
|
|
36
|
+
statements.push(`& ${command} 2>&1 | Out-File -FilePath ${quotePowerShell(log)} -Append -Encoding utf8`)
|
|
37
|
+
statements.push('$bridgeExit = $LASTEXITCODE')
|
|
38
|
+
statements.push('exit $bridgeExit')
|
|
34
39
|
const encoded = Buffer.from(statements.join('; '), 'utf16le').toString('base64')
|
|
35
40
|
const argv = ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', encoded]
|
|
36
41
|
return {
|
|
File without changes
|
package/skill/scripts/resume.sh
CHANGED
|
@@ -150,15 +150,8 @@ done < <(node -e '
|
|
|
150
150
|
' "$seats_file")
|
|
151
151
|
rm -f "$seats_file"
|
|
152
152
|
|
|
153
|
-
# Phase C:
|
|
154
|
-
|
|
155
|
-
if "$script_dir/ensure-bridge.sh" "$proj" "$kind"; then
|
|
156
|
-
echo "[実施] ${kind}-bridge: ready"
|
|
157
|
-
else
|
|
158
|
-
echo "[未実施] ${kind}-bridge: 起動を確認できなかった(ログ $proj/.team/${kind}-bridge.log)" >&2
|
|
159
|
-
exit 1
|
|
160
|
-
fi
|
|
161
|
-
done
|
|
153
|
+
# Phase C: runtime を一回の入口で現行版へ収束
|
|
154
|
+
"$script_dir/ensure-project-runtime.sh" "$proj"
|
|
162
155
|
|
|
163
156
|
# Phase D: fresh heartbeat の読み戻しと probe DM の配送 receipt 確認
|
|
164
157
|
RESUME_PROJ="$proj" RESUME_PROBE="$probe" RESUME_SCRIPT_DIR="$script_dir" node --input-type=module <<'NODE'
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import test from 'node:test'
|
|
9
|
+
|
|
10
|
+
import { keysForCodexPane } from './codex-dialog.mjs'
|
|
11
|
+
import { buildWindowsBridgeLaunch } from './platform/windows/build-bridge-command.mjs'
|
|
12
|
+
import { paneStatusTail } from './seat-usage.mjs'
|
|
13
|
+
import { classifyGrokPaneTail } from './vendors/grok/pane-status.mjs'
|
|
14
|
+
import { latticeTaskAvailable } from './alarm-condition.mjs'
|
|
15
|
+
import { boundedRecent, boundedUnread } from '../../room/message-bounds.mjs'
|
|
16
|
+
|
|
17
|
+
test('Grokの通信失敗はWaiting表示が残ってもbusyにしない', () => {
|
|
18
|
+
const tail = [
|
|
19
|
+
'Connection failed — reqwest error stream: error sending request.',
|
|
20
|
+
'Check your network and try again.',
|
|
21
|
+
'Waiting for response… 29s [stop]',
|
|
22
|
+
].join('\n')
|
|
23
|
+
assert.equal(classifyGrokPaneTail(tail), 'blocked')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('通信失敗の無いGrok応答待ちはbusy', () => {
|
|
27
|
+
assert.equal(classifyGrokPaneTail('Waiting for response… 12s [stop]'), 'busy')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('状態判定窓は折返しで14行より上へ出た通信失敗を含む', () => {
|
|
31
|
+
const pane = ['Connection failed — reqwest error stream', ...Array(20).fill('wrapped line'), 'Waiting for response…'].join('\n')
|
|
32
|
+
assert.ok(paneStatusTail(pane).includes('Connection failed'))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('Codexのroom許可とcommand許可は固定選択肢だけを通す', () => {
|
|
36
|
+
assert.deepEqual(keysForCodexPane([
|
|
37
|
+
'Allow the room MCP server to run tool "members"?',
|
|
38
|
+
'3. Always allow',
|
|
39
|
+
].join('\n')), { kind: 'mcp-allow', keys: ['Down', 'Down', 'Enter'] })
|
|
40
|
+
assert.deepEqual(keysForCodexPane([
|
|
41
|
+
'Would you like to run the following command?',
|
|
42
|
+
"2. Yes, and don't ask again for commands that start with Get-Content",
|
|
43
|
+
].join('\n')), { kind: 'command-approval', keys: ['Down', 'Enter'] })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('Codexの現在busy/idle composerはscrollbackの古い許可文より優先する', () => {
|
|
47
|
+
const stale = [
|
|
48
|
+
'Would you like to run the following command?',
|
|
49
|
+
"2. Yes, and don't ask again for commands that start with rg",
|
|
50
|
+
]
|
|
51
|
+
assert.equal(keysForCodexPane([...stale, '• Working (8s • esc to interrupt)', '› Ask Codex to do anything', 'gpt-5.6-terra high · ~/work'].join('\n')), null)
|
|
52
|
+
assert.equal(keysForCodexPane([...stale, '› Ask Codex to do anything', 'gpt-5.6-terra high · ~/work'].join('\n')), null)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('着座はAiterm dispatch前に独自prompt連続判定を重ねない', () => {
|
|
56
|
+
const source = readFileSync(new URL('./launch-seat.sh', import.meta.url), 'utf8')
|
|
57
|
+
const activeBranch = source.slice(
|
|
58
|
+
source.indexOf('if [ "$brief_in_composer" != true ]; then'),
|
|
59
|
+
source.indexOf(' else\n # Codex はヘッダを描いた後も MCP 初期化を続ける。'),
|
|
60
|
+
)
|
|
61
|
+
assert.ok(activeBranch.includes('aiterm-send.mjs'))
|
|
62
|
+
assert.ok(!activeBranch.includes('brief_ready_streak'))
|
|
63
|
+
assert.ok(source.includes('brief_turn_observed'))
|
|
64
|
+
assert.ok(source.includes('agent-pane-status.mjs'))
|
|
65
|
+
assert.ok(source.includes('aiterm-wait --session'))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('wakeup bridgeはpending DMが無くてもCodex既知dialogを巡回する', () => {
|
|
69
|
+
const source = readFileSync(new URL('./wakeup-bridge.mjs', import.meta.url), 'utf8')
|
|
70
|
+
assert.ok(source.includes('dialogSweepRunning'))
|
|
71
|
+
assert.ok(source.includes('await passKnownCodexDialog(member)'))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('円卓runtimeは3bridgeを固定順で一括収束する', () => {
|
|
75
|
+
const source = readFileSync(new URL('./ensure-project-runtime.sh', import.meta.url), 'utf8')
|
|
76
|
+
assert.ok(source.includes('for kind in alarm seat-status wakeup'))
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('bridge更新は版数だけでなくruntime source digestへ束縛する', () => {
|
|
80
|
+
const digest = spawnSync(process.execPath, [fileURLToPath(new URL('./runtime-digest.mjs', import.meta.url))], { encoding: 'utf8' })
|
|
81
|
+
assert.equal(digest.status, 0, digest.stderr)
|
|
82
|
+
assert.match(digest.stdout, /^[0-9a-f]{64}$/u)
|
|
83
|
+
const source = readFileSync(new URL('./ensure-bridge.sh', import.meta.url), 'utf8')
|
|
84
|
+
assert.ok(source.includes('peertable_runtime_digest'))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('Windows bridgeはUTF-8を明示してログへ書く', () => {
|
|
88
|
+
const launch = buildWindowsBridgeLaunch({ script: 'bridge.mjs', project: 'C:\\work', log: 'C:\\work\\bridge.log' })
|
|
89
|
+
const encodedAt = launch.argv.indexOf('-EncodedCommand')
|
|
90
|
+
const decoded = Buffer.from(launch.argv[encodedAt + 1], 'base64').toString('utf16le')
|
|
91
|
+
assert.ok(decoded.includes('[Console]::OutputEncoding = $utf8'))
|
|
92
|
+
assert.ok(decoded.includes('Out-File'))
|
|
93
|
+
assert.ok(decoded.includes('-Encoding utf8'))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('alarm writerは日本語noteをUTF-8 stdinから保存する', () => {
|
|
97
|
+
const dir = mkdtempSync(join(tmpdir(), 'peertable-alarm-'))
|
|
98
|
+
const out = join(dir, 'alarm.json')
|
|
99
|
+
try {
|
|
100
|
+
const input = ['script', 'yuna', 't04-integration が ready になったら工程を確認する', 'exit 0'].join('\0')
|
|
101
|
+
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./alarm-write.mjs', import.meta.url)), out], {
|
|
102
|
+
input: Buffer.from(input, 'utf8'),
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
})
|
|
105
|
+
assert.equal(result.status, 0, result.stderr)
|
|
106
|
+
const saved = JSON.parse(readFileSync(out, 'utf8'))
|
|
107
|
+
assert.equal(saved.note, 't04-integration が ready になったら工程を確認する')
|
|
108
|
+
} finally {
|
|
109
|
+
rmSync(dir, { recursive: true, force: true })
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('Lattice task alarmはready/activeだけで成立し未出現taskでは発火しない', () => {
|
|
114
|
+
const status = {
|
|
115
|
+
next_ready: [{ plan_key: 'p', task_id: 't04' }],
|
|
116
|
+
active_set: [{ plan_key: 'p', task_id: 't03' }],
|
|
117
|
+
}
|
|
118
|
+
assert.equal(latticeTaskAvailable(status, 't04', 'p'), true)
|
|
119
|
+
assert.equal(latticeTaskAvailable(status, 't03', 'p'), true)
|
|
120
|
+
assert.equal(latticeTaskAvailable(status, 't05', 'p'), false)
|
|
121
|
+
assert.equal(latticeTaskAvailable(status, 't04', 'other'), false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('Grok席configはClaude/Cursor互換を止めroom以外のproject MCPを無効化する', () => {
|
|
125
|
+
const dir = mkdtempSync(join(tmpdir(), 'peertable-grok-seat-'))
|
|
126
|
+
const output = join(dir, 'seat', 'config.toml')
|
|
127
|
+
try {
|
|
128
|
+
writeFileSync(join(dir, '.mcp.json'), JSON.stringify({ mcpServers: {
|
|
129
|
+
booth: { command: 'node' }, room: { command: 'node' }, extra: { command: 'node' },
|
|
130
|
+
} }))
|
|
131
|
+
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./grok-seat-config.mjs', import.meta.url)), dir, output], { encoding: 'utf8' })
|
|
132
|
+
assert.equal(result.status, 0, result.stderr)
|
|
133
|
+
const config = readFileSync(output, 'utf8')
|
|
134
|
+
assert.match(config, /\[compat\.claude\][\s\S]*mcps = false/u)
|
|
135
|
+
assert.match(config, /\[compat\.cursor\][\s\S]*hooks = false/u)
|
|
136
|
+
assert.match(config, /disabled_mcp_servers = \["booth","extra"\]/u)
|
|
137
|
+
assert.doesNotMatch(config, /"room"/u)
|
|
138
|
+
} finally {
|
|
139
|
+
rmSync(dir, { recursive: true, force: true })
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('room logは20件・UTF-8 12KB以内で古い分を明示省略する', () => {
|
|
144
|
+
const messages = Array.from({ length: 30 }, (_, index) => ({ seq: index + 1, body: `本文${index + 1}`.repeat(300) }))
|
|
145
|
+
const result = boundedRecent(messages, message => `[${message.seq}] ${message.body}`, 50)
|
|
146
|
+
assert.ok(Buffer.byteLength(result.text, 'utf8') <= 12_100)
|
|
147
|
+
assert.ok(result.requested === 20)
|
|
148
|
+
assert.match(result.text, /古い \d+ 件を出力上限のため省略/u)
|
|
149
|
+
assert.match(result.text, /\[30\]/u)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('room未読は返した位置までだけ進み残りを次回へ保つ', () => {
|
|
153
|
+
const messages = Array.from({ length: 6 }, (_, index) => ({ seq: index + 1, body: `未読${index + 1}`.repeat(500) }))
|
|
154
|
+
const first = boundedUnread(messages, () => true, message => `[${message.seq}] ${message.body}`, 4000)
|
|
155
|
+
assert.ok(first.omitted > 0)
|
|
156
|
+
assert.ok(first.consumedSeq < 6)
|
|
157
|
+
assert.match(first.text, /read_unreadを再実行/u)
|
|
158
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join, relative } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const root = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const files = []
|
|
9
|
+
function walk(dir) {
|
|
10
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
11
|
+
const path = join(dir, entry.name)
|
|
12
|
+
if (entry.isDirectory()) walk(path)
|
|
13
|
+
else if ((entry.name.endsWith('.mjs') || entry.name.endsWith('.sh')) && !entry.name.endsWith('.test.mjs')) files.push(path)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
walk(root)
|
|
17
|
+
files.sort((a, b) => relative(root, a).localeCompare(relative(root, b), 'en'))
|
|
18
|
+
const hash = createHash('sha256')
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
hash.update(relative(root, file).replaceAll('\\', '/'))
|
|
21
|
+
hash.update('\0')
|
|
22
|
+
hash.update(readFileSync(file))
|
|
23
|
+
hash.update('\0')
|
|
24
|
+
}
|
|
25
|
+
process.stdout.write(hash.digest('hex'))
|
|
@@ -27,7 +27,7 @@ import { execFileSync } from 'node:child_process'
|
|
|
27
27
|
import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
28
28
|
import { join } from 'node:path'
|
|
29
29
|
|
|
30
|
-
import { STOP_DECLARATION, attributeJobSession, combineSeatLamp, isPaneHarnessMissing, hasActiveDescendant, subtreeCpuSeconds, patrolTargets, classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, parsePaneTokenHint, resolveLatticeExecutable, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxArgv, tmuxPanePid } from './seat-usage.mjs'
|
|
30
|
+
import { STOP_DECLARATION, attributeJobSession, combineSeatLamp, isPaneHarnessMissing, hasActiveDescendant, subtreeCpuSeconds, patrolTargets, classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, paneStatusTail, parsePaneTokenHint, resolveLatticeExecutable, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxArgv, tmuxPanePid } from './seat-usage.mjs'
|
|
31
31
|
import { updateBridgeProgress } from './bridge-record-live.mjs'
|
|
32
32
|
|
|
33
33
|
const args = process.argv.slice(2)
|
|
@@ -111,7 +111,7 @@ function readSeat(member, previous, observedAt) {
|
|
|
111
111
|
}
|
|
112
112
|
const pane = tmux(target.socket, 'capture-pane', '-t', target.target, '-p')
|
|
113
113
|
if (pane === null) return { status: 'dead', busySince: null, paneTokenHint: null }
|
|
114
|
-
const tail = pane
|
|
114
|
+
const tail = paneStatusTail(pane)
|
|
115
115
|
const tentativeStatus = classifyPaneTail(tail)
|
|
116
116
|
// pane 自体は生きたまま中の CLI プロセスだけが停止する局面(Lattice pull run の accept hold 等)を
|
|
117
117
|
// 拾う。画面の残像だけでは idle に誤判定するため、idle と読めた時だけ実プロセスの stat を見る。
|
|
@@ -202,6 +202,14 @@ export const BLOCKED_MARKERS = [
|
|
|
202
202
|
|
|
203
203
|
export { isGrokPrivacyBanner }
|
|
204
204
|
|
|
205
|
+
// 承認画面や接続失敗は折返し・キュー表示で14行より上へ押し出される。現在のturnを
|
|
206
|
+
// 覆いながら、古いscrollback全体を状態に混ぜない固定窓として32行を読む。
|
|
207
|
+
export const PANE_STATUS_TAIL_LINES = 32
|
|
208
|
+
|
|
209
|
+
export function paneStatusTail(pane) {
|
|
210
|
+
return String(pane ?? '').split('\n').slice(-PANE_STATUS_TAIL_LINES).join('\n')
|
|
211
|
+
}
|
|
212
|
+
|
|
205
213
|
/**
|
|
206
214
|
* pane 末尾の生文字列から画面状態を判定する。判定順は busy → blocked → idle
|
|
207
215
|
* (承認プロンプト表示中は `esc to interrupt` が消えるので busy を先に見る)。
|
|
File without changes
|
package/skill/scripts/setup.sh
CHANGED
|
@@ -121,6 +121,8 @@ else
|
|
|
121
121
|
fi
|
|
122
122
|
# 目覚まし係の登録口は全モード共通で配る(member.md の待機作法が参照する)
|
|
123
123
|
cp "$repo/skill/scripts/alarm-set.sh" "$tdir/scripts/alarm-set.sh" && chmod +x "$tdir/scripts/alarm-set.sh"
|
|
124
|
+
cp "$repo/skill/scripts/alarm-write.mjs" "$tdir/scripts/alarm-write.mjs" && chmod +x "$tdir/scripts/alarm-write.mjs"
|
|
125
|
+
cp "$repo/skill/scripts/alarm-condition.mjs" "$tdir/scripts/alarm-condition.mjs" && chmod +x "$tdir/scripts/alarm-condition.mjs"
|
|
124
126
|
# member.md が attach 手順で参照する。配り漏れると席が attach できず停止する(実被弾 2026-08-22)
|
|
125
127
|
cp "$repo/skill/scripts/pull-attach-input.mjs" "$tdir/scripts/pull-attach-input.mjs" && chmod +x "$tdir/scripts/pull-attach-input.mjs"
|
|
126
128
|
# room MCP 定義は project root の .mcp.json が正(channels は --mcp-config を解決しない。決定44)
|
|
@@ -209,11 +211,6 @@ fi
|
|
|
209
211
|
# 4時間 HTTP 403 を撃ち続け、参加者一覧には点が1つも出なかった=起こしていないのと見分けがつかない)。
|
|
210
212
|
# AI は使わず席へも1バイト送らないので卓の作業を邪魔しない。**書けない時は常駐せずに死ぬ**ので、
|
|
211
213
|
# ここで壊れた常駐が黙って残ることは無い。setup-state.json を読むので、必ずその後で起こす。
|
|
212
|
-
"$repo/skill/scripts/ensure-
|
|
213
|
-
if "$repo/skill/scripts/ensure-bridge.sh" "$proj" seat-status; then
|
|
214
|
-
echo "seat-status-bridge: ready_at を確認した(ログ $tdir/seat-status-bridge.log・停止は teardown が行う)"
|
|
215
|
-
else
|
|
216
|
-
echo "seat-status-bridge: 起動を確認できなかった(ログ $tdir/seat-status-bridge.log)" >&2
|
|
217
|
-
fi
|
|
214
|
+
"$repo/skill/scripts/ensure-project-runtime.sh" "$proj"
|
|
218
215
|
|
|
219
216
|
echo "scaffold done: $tdir"
|
|
File without changes
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# sourced by launch-seat / leave-seat / teardown / ensure-bridge / change-seat
|
|
2
|
-
# POSIX は tmux -S <sock>。Windows psmux は -L <aiterm-ns>(-S は既定 namespace へ落ちる)。
|
|
3
|
-
_peertable_tmux_scripts=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
4
|
-
tmux_at() {
|
|
5
|
-
local prefix
|
|
6
|
-
prefix=$(node "$_peertable_tmux_scripts/tmux-socket.mjs" --prefix) || return 1
|
|
7
|
-
# shellcheck disable=SC2206
|
|
8
|
-
local -a conn=($prefix)
|
|
9
|
-
# tmux server は最初に立てた process の locale を一生使い、C/POSIX/未設定だと UTF-8 の
|
|
10
|
-
# send-keys/paste を破壊する(実被弾 2026-08-29: launchd の bridge 巡回が LANG 無しで
|
|
11
|
-
# server を立て、以後の日本語込み席起動コマンドが全て化けて Codex 席が着席不能になった)。
|
|
12
|
-
# launchd/cron 由来の呼び出しでも server が UTF-8 で立つよう、壊れた既定の時だけ注入する。
|
|
13
|
-
# 利用者が C/POSIX 以外を明示設定している場合はその選択を尊重して触らない(aiterm と同じ判定)。
|
|
14
|
-
local effective="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
15
|
-
if [ -z "$effective" ] || [[ "$effective" =~ ^(C|POSIX)$ ]]; then
|
|
16
|
-
local ctype="C.UTF-8"
|
|
17
|
-
[ "$(uname)" = "Darwin" ] && ctype="UTF-8"
|
|
18
|
-
env -u LC_ALL LC_CTYPE="$ctype" tmux "${conn[@]}" "$@"
|
|
19
|
-
else
|
|
20
|
-
command tmux "${conn[@]}" "$@"
|
|
21
|
-
fi
|
|
22
|
-
}
|
|
1
|
+
# sourced by launch-seat / leave-seat / teardown / ensure-bridge / change-seat
|
|
2
|
+
# POSIX は tmux -S <sock>。Windows psmux は -L <aiterm-ns>(-S は既定 namespace へ落ちる)。
|
|
3
|
+
_peertable_tmux_scripts=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
4
|
+
tmux_at() {
|
|
5
|
+
local prefix
|
|
6
|
+
prefix=$(node "$_peertable_tmux_scripts/tmux-socket.mjs" --prefix) || return 1
|
|
7
|
+
# shellcheck disable=SC2206
|
|
8
|
+
local -a conn=($prefix)
|
|
9
|
+
# tmux server は最初に立てた process の locale を一生使い、C/POSIX/未設定だと UTF-8 の
|
|
10
|
+
# send-keys/paste を破壊する(実被弾 2026-08-29: launchd の bridge 巡回が LANG 無しで
|
|
11
|
+
# server を立て、以後の日本語込み席起動コマンドが全て化けて Codex 席が着席不能になった)。
|
|
12
|
+
# launchd/cron 由来の呼び出しでも server が UTF-8 で立つよう、壊れた既定の時だけ注入する。
|
|
13
|
+
# 利用者が C/POSIX 以外を明示設定している場合はその選択を尊重して触らない(aiterm と同じ判定)。
|
|
14
|
+
local effective="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
15
|
+
if [ -z "$effective" ] || [[ "$effective" =~ ^(C|POSIX)$ ]]; then
|
|
16
|
+
local ctype="C.UTF-8"
|
|
17
|
+
[ "$(uname)" = "Darwin" ] && ctype="UTF-8"
|
|
18
|
+
env -u LC_ALL LC_CTYPE="$ctype" tmux "${conn[@]}" "$@"
|
|
19
|
+
else
|
|
20
|
+
command tmux "${conn[@]}" "$@"
|
|
21
|
+
fi
|
|
22
|
+
}
|
|
@@ -201,6 +201,10 @@ function main() {
|
|
|
201
201
|
const common = [
|
|
202
202
|
['.team/CLAUDE.md', 'skill/templates/charter.md', 0o644],
|
|
203
203
|
['.team/roles/parent.md', 'skill/templates/parent.md', 0o644],
|
|
204
|
+
['.team/scripts/alarm-set.sh', 'skill/scripts/alarm-set.sh', 0o755],
|
|
205
|
+
['.team/scripts/alarm-write.mjs', 'skill/scripts/alarm-write.mjs', 0o755],
|
|
206
|
+
['.team/scripts/alarm-condition.mjs', 'skill/scripts/alarm-condition.mjs', 0o755],
|
|
207
|
+
['.team/scripts/pull-attach-input.mjs', 'skill/scripts/pull-attach-input.mjs', 0o755],
|
|
204
208
|
]
|
|
205
209
|
const modeSpecific = state.mode === 'lattice'
|
|
206
210
|
? [
|
|
@@ -4,6 +4,13 @@ export function classifyGrokPaneTail(tail) {
|
|
|
4
4
|
|
|
5
5
|
if (isGrokPrivacyBanner(tail)) return 'blocked'
|
|
6
6
|
|
|
7
|
+
// Grok Build は通信失敗後も `Waiting for response` と `[stop]` を画面へ残し、
|
|
8
|
+
// 再試行中のように見せる。この状態を busy にすると「モデルが仕事を進めている」
|
|
9
|
+
// アクティブランプが点き続けるため、既知の接続失敗を先に blocked と判定する。
|
|
10
|
+
if (tail.includes('Connection failed — reqwest error')
|
|
11
|
+
|| tail.includes('Connection failed - reqwest error')
|
|
12
|
+
|| tail.includes('Check your network and try again.')) return 'blocked'
|
|
13
|
+
|
|
7
14
|
// 完了後も `stop [hooks: 1/3]` はステータス行へ残る。入力欄が戻った
|
|
8
15
|
// `Worked for ...` 行を先にidle扱いし、配達を永久保留しない。
|
|
9
16
|
if (/Worked for\s+\d+(?:m\d+)?s[\s\S]*?stop\s+\[hooks:\s*\d+\/\d+\]/u.test(tail)) return 'idle'
|