peertable 0.8.55 → 0.8.57
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/README.ja.md +32 -33
- package/README.md +36 -37
- package/package.json +3 -1
- package/room/client.mjs +24 -33
- package/room/server.mjs +1 -1
- package/skill/SKILL.md +69 -94
- package/skill/scripts/aiterm-client.mjs +48 -0
- package/skill/scripts/aiterm-deliver.mjs +3 -1
- package/skill/scripts/change-seat.mjs +86 -0
- package/skill/scripts/change-seat.sh +3 -276
- package/skill/scripts/change-seat.test.mjs +129 -0
- package/skill/scripts/cli.mjs +77 -0
- package/skill/scripts/doctor.mjs +53 -0
- package/skill/scripts/doctor.sh +2 -213
- package/skill/scripts/ensure-all-bridges.mjs +42 -0
- package/skill/scripts/ensure-all-bridges.sh +3 -80
- package/skill/scripts/ensure-bridge.mjs +11 -0
- package/skill/scripts/ensure-bridge.sh +2 -95
- package/skill/scripts/ensure-codex-room-mcp.mjs +1 -1
- package/skill/scripts/ensure-project-runtime.mjs +85 -0
- package/skill/scripts/ensure-project-runtime.sh +2 -11
- package/skill/scripts/ensure-project-runtime.test.mjs +40 -0
- package/skill/scripts/install-skill.mjs +89 -0
- package/skill/scripts/install-skill.test.mjs +84 -0
- package/skill/scripts/launch-seat.mjs +163 -0
- package/skill/scripts/launch-seat.sh +3 -797
- package/skill/scripts/launch-seat.test.mjs +174 -0
- package/skill/scripts/leave-seat.mjs +44 -0
- package/skill/scripts/leave-seat.sh +3 -92
- package/skill/scripts/leave-seat.test.mjs +64 -0
- package/skill/scripts/legacy-entry.mjs +33 -0
- package/skill/scripts/platform/windows/resolve-lattice-command.mjs +12 -1
- package/skill/scripts/project-runtime.mjs +95 -0
- package/skill/scripts/project-runtime.test.mjs +29 -0
- package/skill/scripts/project-scaffold.mjs +179 -0
- package/skill/scripts/project-scaffold.test.mjs +129 -0
- package/skill/scripts/refresh-seat-identity.mjs +11 -8
- package/skill/scripts/remove-managed-room-mcp.mjs +9 -4
- package/skill/scripts/resume.sh +2 -225
- package/skill/scripts/room-api.mjs +28 -0
- package/skill/scripts/room-public-session.test.mjs +73 -0
- package/skill/scripts/runtime-contract.test.mjs +13 -146
- package/skill/scripts/runtime-launch-command.mjs +19 -0
- package/skill/scripts/runtime-launch-command.test.mjs +32 -0
- package/skill/scripts/seat-approval.mjs +24 -0
- package/skill/scripts/seat-approval.test.mjs +43 -0
- package/skill/scripts/seat-identity.mjs +2 -71
- package/skill/scripts/seat-launch-phase.mjs +11 -0
- package/skill/scripts/seat-observer.mjs +54 -0
- package/skill/scripts/seat-observer.test.mjs +58 -0
- package/skill/scripts/seat-session.mjs +17 -0
- package/skill/scripts/seat-status-bridge.mjs +33 -206
- package/skill/scripts/seat-usage.mjs +3 -334
- package/skill/scripts/setup.sh +3 -215
- package/skill/scripts/teardown.mjs +113 -0
- package/skill/scripts/teardown.sh +3 -410
- package/skill/scripts/teardown.test.mjs +62 -0
- package/skill/scripts/upgrade-team-assets.mjs +268 -0
- package/skill/scripts/upgrade-team-assets.sh +2 -282
- package/skill/scripts/wakeup-bridge.mjs +26 -32
- package/skill/scripts/wakeup-delivery.mjs +4 -14
- package/skill/templates/charter.md +1 -1
- package/skill/templates/member-standalone.md +1 -1
- package/skill/templates/member.md +3 -3
- package/skill/scripts/agent-pane-status.mjs +0 -6
- package/skill/scripts/aiterm-configure.mjs +0 -31
- package/skill/scripts/aiterm-launch.mjs +0 -64
- package/skill/scripts/aiterm-send.mjs +0 -22
- package/skill/scripts/claude-dialog.mjs +0 -26
- package/skill/scripts/codex-dialog.mjs +0 -67
- package/skill/scripts/platform/windows/build-bridge-command.mjs +0 -52
- package/skill/scripts/seat-input.mjs +0 -36
- package/skill/scripts/tmux-at.bash +0 -22
- package/skill/scripts/tmux-socket.mjs +0 -19
- package/skill/scripts/vendors/grok/pane-status.mjs +0 -30
|
@@ -1,277 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# **自然文の依頼を再解釈しない。** 依頼の意味・本人の意図・変更してよい局面を判断するのは親である AI で、
|
|
6
|
-
# この script が受け取るのは親が確定した target だけである(旧 change-effort.sh の
|
|
7
|
-
# 「本人→親の単独DMが `[effort変更依頼] <level>` と完全一致すること」という機械判定は廃止した。
|
|
8
|
-
# 明確な自然文の依頼を、同じ文面の再送を求めて拒否していたため)。
|
|
9
|
-
# この script が持つのは、親には出来ない外部境界の仕事だけである:
|
|
10
|
-
# 現在の素性の取得 / target の live catalog 検証 / 同値no-op / busy保護 / 再起動 /
|
|
11
|
-
# metadata の読み返し / room 履歴 / 失敗時の旧設定への1回だけの明示rollback。
|
|
12
|
-
#
|
|
13
|
-
# 会話contextは引き継がない。作業状態はroomログ・工程正本・gitから再着任で回収する。
|
|
14
|
-
set -eu
|
|
15
|
-
|
|
16
|
-
# token値はこの制御processや再起動する席へ継承しない。launch後のroom記録も席別file経由で行う。
|
|
17
|
-
unset PEERTABLE_POST_TOKEN
|
|
18
|
-
script_dir=$(cd "$(dirname "$0")" && pwd -P)
|
|
19
|
-
# shellcheck disable=SC1091
|
|
20
|
-
. "$script_dir/tmux-at.bash"
|
|
21
|
-
credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$script_dir/seat-credential.mjs}"
|
|
22
|
-
|
|
23
|
-
proj="${1:-}"; name="${2:-}"
|
|
24
|
-
shift 2 2>/dev/null || true
|
|
25
|
-
opt_harness=""; opt_model=""; opt_effort=""; parent="bell"; reason=""
|
|
26
|
-
while [ $# -gt 0 ]; do
|
|
27
|
-
case "$1" in
|
|
28
|
-
--harness|--vendor) opt_harness="${2:-}"; shift 2 || true ;; # --vendor は旧名互換
|
|
29
|
-
--model) opt_model="${2:-}"; shift 2 || true ;;
|
|
30
|
-
--effort) opt_effort="${2:-}"; shift 2 || true ;;
|
|
31
|
-
--parent) parent="${2:-}"; shift 2 || true ;;
|
|
32
|
-
--reason) reason="${2:-}"; shift 2 || true ;;
|
|
33
|
-
*) echo "SEAT_CHANGE_ARGS_INVALID: 不明な引数 $1" >&2; exit 2 ;;
|
|
34
|
-
esac
|
|
35
|
-
done
|
|
36
|
-
[ -n "$proj" ] && [ -n "$name" ] || {
|
|
37
|
-
echo "SEAT_CHANGE_ARGS_INVALID: usage: change-seat.sh <project_dir> <member> [--harness <harness>] [--model <model>] [--effort <effort>] [--parent <name>] [--reason <text>]" >&2
|
|
38
|
-
exit 2
|
|
39
|
-
}
|
|
40
|
-
[ -n "$opt_model" ] || [ -n "$opt_effort" ] || {
|
|
41
|
-
echo "SEAT_CHANGE_ARGS_INVALID: --model と --effort の少なくとも一方が要る" >&2; exit 2
|
|
42
|
-
}
|
|
43
|
-
case "$name:$parent" in
|
|
44
|
-
*[!A-Za-z0-9._:-]*) echo "SEAT_CHANGE_ARGS_INVALID: member/parent名に使えない文字がある" >&2; exit 2 ;;
|
|
45
|
-
esac
|
|
46
|
-
|
|
47
|
-
state="$proj/.team/setup-state.json"
|
|
48
|
-
[ -f "$state" ] || { echo "SEAT_CHANGE_STATE_MISSING: $state" >&2; exit 1; }
|
|
49
|
-
read -r room url <<EOF
|
|
50
|
-
$(python3 -c "import json;d=json.load(open('$state'));print(d['room'],d['server_url'])")
|
|
51
|
-
EOF
|
|
52
|
-
|
|
53
|
-
members=$(curl -sf "$url/api/$room/members") || {
|
|
54
|
-
echo "SEAT_CHANGE_ROOM_UNREACHABLE: membersを読めない" >&2; exit 1;
|
|
55
|
-
}
|
|
56
|
-
meta=$(printf '%s' "$members" | python3 -c '
|
|
57
|
-
import json,sys
|
|
58
|
-
name=sys.argv[1]
|
|
59
|
-
member=next((m for m in json.load(sys.stdin).get("members",[]) if m.get("name")==name),None)
|
|
60
|
-
if not member or (member.get("harness") or member.get("vendor")) not in ("claude","codex","grok") or not member.get("model"):
|
|
61
|
-
raise SystemExit(1)
|
|
62
|
-
roles=member.get("roles") or []
|
|
63
|
-
print("\t".join(((member.get("harness") or member.get("vendor")),member["model"],member.get("effort") or "",member.get("aiterm_session_id") or "",",".join(roles))))
|
|
64
|
-
' "$name") || { echo "SEAT_CHANGE_MEMBER_METADATA_MISSING: ${name} のharness/modelが要る" >&2; exit 1; }
|
|
65
|
-
IFS=$'\t' read -r old_harness old_model old_effort aiterm_session_id old_role <<EOF
|
|
66
|
-
$meta
|
|
67
|
-
EOF
|
|
68
|
-
|
|
69
|
-
harness="${opt_harness:-$old_harness}"
|
|
70
|
-
case "$harness" in
|
|
71
|
-
claude|codex|grok) ;;
|
|
72
|
-
*) echo "SEAT_CHANGE_HARNESS_UNSUPPORTED: harness=${harness}(claude / codex / grok のみ)" >&2; exit 2 ;;
|
|
73
|
-
esac
|
|
74
|
-
if [ "$harness" != "$old_harness" ] && { [ -z "$opt_model" ] || [ -z "$opt_effort" ]; }; then
|
|
75
|
-
echo "SEAT_CHANGE_ARGS_INVALID: harness変更には --model と --effort の明示指定が要る" >&2
|
|
76
|
-
exit 2
|
|
77
|
-
fi
|
|
78
|
-
model="${opt_model:-$old_model}"
|
|
79
|
-
effort="${opt_effort:-$old_effort}"
|
|
80
|
-
# effort を持たない席(CLI 既定で走っている席)へ model だけ渡すと、再起動で effort が確定してしまう。
|
|
81
|
-
# 既定値をここへ埋めない——launch-seat.sh と同じく「席を立てる時に決める」(オーナー裁定)。
|
|
82
|
-
[ -n "$effort" ] || {
|
|
83
|
-
echo "SEAT_CHANGE_EFFORT_UNKNOWN: ${name} の現在effortがmetadataに無い。--effort を明示する" >&2; exit 1
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if [ "$model" = "$old_model" ] && [ "$effort" = "$old_effort" ] && [ "$harness" = "$old_harness" ]; then
|
|
87
|
-
echo "SEAT_CHANGE_NOOP: ${name} は既に model=${model} / effort=${effort}(再起動しない)"
|
|
88
|
-
exit 0
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
sock="${PEERTABLE_TMUX_SOCKET:-${TMPDIR:-/tmp/}claude-tmux-sockets/claude.sock}"
|
|
92
|
-
sess="peer-$name"
|
|
93
|
-
tmux_at has-session -t "$sess" 2>/dev/null || {
|
|
94
|
-
echo "SEAT_CHANGE_SEAT_MISSING: ${sess}" >&2; exit 1;
|
|
95
|
-
}
|
|
96
|
-
screen=$(tmux_at capture-pane -t "$sess" -p -S -25 2>/dev/null) || {
|
|
97
|
-
echo "SEAT_CHANGE_SEAT_UNREADABLE: $sess" >&2; exit 1;
|
|
98
|
-
}
|
|
99
|
-
# busy の判定文字列は seat-status-bridge と同じ(Claude のステータス行にも Codex の `Working (…)` にも出る)
|
|
100
|
-
case "$screen" in
|
|
101
|
-
*"esc to interrupt"*) echo "SEAT_CHANGE_SEAT_BUSY: ${sess} は処理中。本人がidleになってから再実行する" >&2; exit 1 ;;
|
|
102
|
-
esac
|
|
103
|
-
|
|
104
|
-
# target の検証は live 面だけを使い、古くなる hardcode を足さない。
|
|
105
|
-
case "$harness" in
|
|
106
|
-
claude)
|
|
107
|
-
# Claude には非破壊で引ける model catalog が無い(`--help` の alias 例は catalog ではなく、
|
|
108
|
-
# 実際 2026-08-11 に `fable` は例に載ったまま live では unavailable だった)。
|
|
109
|
-
# よって **model 名は事前検証しない**——実 CLI の起動失敗と rollback が正式な検証境界である。
|
|
110
|
-
# effort は `--help` が live に列挙するので、そこから取る。
|
|
111
|
-
help_text=$(claude --help 2>/dev/null) || {
|
|
112
|
-
echo "SEAT_CHANGE_EFFORT_CATALOG_UNAVAILABLE: claude --help を読めない" >&2; exit 1;
|
|
113
|
-
}
|
|
114
|
-
levels=$(printf '%s' "$help_text" | python3 -c '
|
|
115
|
-
import re,sys
|
|
116
|
-
text=sys.stdin.read()
|
|
117
|
-
i=text.find("--effort")
|
|
118
|
-
m=re.search(r"\(([a-z0-9, ]+)\)", text[i:i+400]) if i>=0 else None
|
|
119
|
-
if not m: raise SystemExit(1)
|
|
120
|
-
print(" ".join(x.strip() for x in m.group(1).split(",") if x.strip()))
|
|
121
|
-
') || {
|
|
122
|
-
echo "SEAT_CHANGE_EFFORT_CATALOG_UNAVAILABLE: claude --help が effort の水準を列挙しない" >&2; exit 1;
|
|
123
|
-
}
|
|
124
|
-
case " $levels " in
|
|
125
|
-
*" $effort "*) ;;
|
|
126
|
-
*) echo "SEAT_CHANGE_EFFORT_UNSUPPORTED: claude は effort=${effort} を提供していない(live: ${levels})" >&2; exit 1 ;;
|
|
127
|
-
esac
|
|
128
|
-
;;
|
|
129
|
-
codex)
|
|
130
|
-
catalog=$(codex debug models 2>/dev/null) || {
|
|
131
|
-
echo "SEAT_CHANGE_MODEL_CATALOG_UNAVAILABLE: codex debug models" >&2; exit 1;
|
|
132
|
-
}
|
|
133
|
-
verdict=$(printf '%s' "$catalog" | python3 -c '
|
|
134
|
-
import json,sys
|
|
135
|
-
model,effort=sys.argv[1:3]
|
|
136
|
-
entry=next((m for m in json.load(sys.stdin).get("models",[]) if m.get("slug")==model),None)
|
|
137
|
-
if entry is None:
|
|
138
|
-
print("model"); raise SystemExit(0)
|
|
139
|
-
levels=[x.get("effort") for x in entry.get("supported_reasoning_levels",[])]
|
|
140
|
-
print("ok" if effort in levels else "effort")
|
|
141
|
-
' "$model" "$effort") || {
|
|
142
|
-
echo "SEAT_CHANGE_MODEL_CATALOG_UNAVAILABLE: codex debug models の出力を読めない" >&2; exit 1;
|
|
143
|
-
}
|
|
144
|
-
case "$verdict" in
|
|
145
|
-
model) echo "SEAT_CHANGE_MODEL_UNSUPPORTED: codex catalog に model=${model} が無い" >&2; exit 1 ;;
|
|
146
|
-
effort) echo "SEAT_CHANGE_EFFORT_UNSUPPORTED: codex/${model} は effort=${effort} をcatalogで提供していない" >&2; exit 1 ;;
|
|
147
|
-
esac
|
|
148
|
-
;;
|
|
149
|
-
grok)
|
|
150
|
-
catalog=$(grok models 2>/dev/null) || {
|
|
151
|
-
echo "SEAT_CHANGE_MODEL_CATALOG_UNAVAILABLE: grok models" >&2; exit 1;
|
|
152
|
-
}
|
|
153
|
-
if ! printf '%s' "$catalog" | python3 -c '
|
|
154
|
-
import re,sys
|
|
155
|
-
model=sys.argv[1]
|
|
156
|
-
models=[]
|
|
157
|
-
for line in sys.stdin:
|
|
158
|
-
match=re.match(r"^\s*[-*]\s+(\S+)", line)
|
|
159
|
-
if match: models.append(match.group(1))
|
|
160
|
-
raise SystemExit(0 if model in models else 1)
|
|
161
|
-
' "$model"; then
|
|
162
|
-
echo "SEAT_CHANGE_MODEL_UNSUPPORTED: grok catalog に model=${model} が無い" >&2
|
|
163
|
-
exit 1
|
|
164
|
-
fi
|
|
165
|
-
;;
|
|
166
|
-
esac
|
|
167
|
-
|
|
168
|
-
changes=""
|
|
169
|
-
[ "$harness" = "$old_harness" ] || changes="harness ${old_harness} → ${harness}"
|
|
170
|
-
[ "$model" = "$old_model" ] || {
|
|
171
|
-
[ -z "$changes" ] || changes="$changes / "
|
|
172
|
-
changes="${changes}model ${old_model} → ${model}"
|
|
173
|
-
}
|
|
174
|
-
if [ "$effort" != "$old_effort" ]; then
|
|
175
|
-
[ -z "$changes" ] || changes="$changes / "
|
|
176
|
-
changes="${changes}effort ${old_effort:-default} → ${effort}"
|
|
177
|
-
fi
|
|
178
|
-
|
|
179
|
-
credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" path "$proj" "$room" "$name") || {
|
|
180
|
-
echo "SEAT_CHANGE_CREDENTIAL_MISSING: ${name} のroom credentialを特定できない" >&2; exit 1
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if [ "$harness" = "$old_harness" ]; then
|
|
184
|
-
[ -n "$aiterm_session_id" ] || {
|
|
185
|
-
echo "SEAT_CHANGE_AITERM_SESSION_MISSING: ${name} にAiterm managed session_idが無い" >&2; exit 1
|
|
186
|
-
}
|
|
187
|
-
configure_args=("$aiterm_session_id")
|
|
188
|
-
[ -z "$opt_model" ] || configure_args+=(--model "$model")
|
|
189
|
-
[ -z "$opt_effort" ] || configure_args+=(--effort "$effort")
|
|
190
|
-
node "$script_dir/aiterm-configure.mjs" "${configure_args[@]}" >/dev/null || {
|
|
191
|
-
echo "SEAT_CHANGE_AITERM_CONFIGURE_FAILED: ${name} の設定は変更していない" >&2; exit 1
|
|
192
|
-
}
|
|
193
|
-
identity=$(python3 -c 'import json,sys;print(json.dumps({"name":sys.argv[1],"harness":sys.argv[2],"vendor":sys.argv[2],"model":sys.argv[3],"effort":sys.argv[4],"aiterm_session_id":sys.argv[5]}))' "$name" "$harness" "$model" "$effort" "$aiterm_session_id")
|
|
194
|
-
env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
|
|
195
|
-
"$url/api/$room/members" "$identity" >/dev/null || {
|
|
196
|
-
echo "SEAT_CHANGE_CHANGED_BUT_METADATA_FAILED: ${name} は設定済み、room metadataを同期できない" >&2; exit 1
|
|
197
|
-
}
|
|
198
|
-
change_method="同一sessionを維持"
|
|
199
|
-
else
|
|
200
|
-
leave="$script_dir/leave-seat.sh"
|
|
201
|
-
if ! "$leave" "$proj" "$name"; then
|
|
202
|
-
echo "SEAT_CHANGE_RESTART_PREPARE_FAILED: ${name} の旧席を安全に撤去できないため再起動しない" >&2
|
|
203
|
-
exit 1
|
|
204
|
-
fi
|
|
205
|
-
launch="$script_dir/launch-seat.sh"
|
|
206
|
-
[ -n "$old_role" ] || {
|
|
207
|
-
echo "SEAT_CHANGE_ROLE_MISSING: ${name} の role が無い(02_models の役割名が要る)" >&2
|
|
208
|
-
exit 1
|
|
209
|
-
}
|
|
210
|
-
brief="席設定が変更され(${changes})、席を再起動しました。.team/roles/member.mdと工程正本・roomログから再着任し、進行中taskを続けてください。"
|
|
211
|
-
if ! "$launch" "$proj" "$name" --roles "$old_role" --model "$model" --effort "$effort" "$brief"; then
|
|
212
|
-
echo "SEAT_CHANGE_RESTART_FAILED: ${changes}。旧設定(harness=${old_harness} / model=${old_model} / effort=${old_effort:-default})へrollbackする" >&2
|
|
213
|
-
rollback_brief="席設定の変更に失敗して旧設定へrollbackしました。.team/roles/member.mdと工程正本・roomログから再着任してください。"
|
|
214
|
-
if "$launch" "$proj" "$name" --roles "$old_role" --model "$old_model" --effort "$old_effort" "$rollback_brief"; then
|
|
215
|
-
echo "SEAT_CHANGE_ROLLED_BACK: ${name} は harness=${old_harness} / model=${old_model} / effort=${old_effort:-default} で再着席" >&2
|
|
216
|
-
else
|
|
217
|
-
echo "SEAT_CHANGE_ROLLBACK_FAILED: ${name} の席を手動で復旧する必要がある" >&2
|
|
218
|
-
fi
|
|
219
|
-
exit 1
|
|
220
|
-
fi
|
|
221
|
-
change_method="席を再起動"
|
|
222
|
-
fi
|
|
223
|
-
|
|
224
|
-
members_after=$(curl -sf "$url/api/$room/members") || {
|
|
225
|
-
echo "SEAT_CHANGE_CHANGED_BUT_UNVERIFIED: 席は再起動済み、membersを読めない" >&2; exit 1;
|
|
226
|
-
}
|
|
227
|
-
if ! printf '%s' "$members_after" | python3 -c '
|
|
228
|
-
import json,sys
|
|
229
|
-
name,harness,model,effort=sys.argv[1:5]
|
|
230
|
-
m=next((m for m in json.load(sys.stdin).get("members",[]) if m.get("name")==name),{})
|
|
231
|
-
raise SystemExit(0 if (m.get("harness") or m.get("vendor"))==harness and m.get("model")==model and m.get("effort")==effort else 1)
|
|
232
|
-
' "$name" "$harness" "$model" "$effort"; then
|
|
233
|
-
echo "SEAT_CHANGE_CHANGED_BUT_UNVERIFIED: 席は再起動済み、member metadataが harness=${harness} / model=${model} / effort=${effort} でない" >&2
|
|
234
|
-
exit 1
|
|
235
|
-
fi
|
|
236
|
-
|
|
237
|
-
body="[席設定変更] ${parent} が ${name} の ${changes} に変更(${change_method})"
|
|
238
|
-
[ -z "$reason" ] || body="${body}。理由: ${reason}"
|
|
239
|
-
history=$(node "$(dirname "$0")/post-message.mjs" --build-only "$parent" "$name" "$body")
|
|
240
|
-
history_response=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
|
|
241
|
-
"$url/api/$room/messages" "$history") || {
|
|
242
|
-
echo "SEAT_CHANGE_CHANGED_BUT_HISTORY_FAILED: ${name} は ${changes} で再着席済み、room履歴の記録に失敗" >&2
|
|
243
|
-
exit 1
|
|
244
|
-
}
|
|
245
|
-
history_seq=$(printf '%s' "$history_response" | python3 -c '
|
|
246
|
-
import json,sys
|
|
247
|
-
try:
|
|
248
|
-
seq=json.load(sys.stdin).get("seq")
|
|
249
|
-
except (ValueError, TypeError):
|
|
250
|
-
raise SystemExit(1)
|
|
251
|
-
print(seq)
|
|
252
|
-
raise SystemExit(0 if isinstance(seq, int) else 1)
|
|
253
|
-
') || {
|
|
254
|
-
echo "SEAT_CHANGE_CHANGED_BUT_HISTORY_FAILED: ${name} は ${changes} で再着席済み、room履歴POST応答のseqを読めない" >&2
|
|
255
|
-
exit 1
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
messages_after=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" GET \
|
|
259
|
-
"$url/api/$room/messages") || {
|
|
260
|
-
echo "SEAT_CHANGE_CHANGED_BUT_HISTORY_FAILED: ${name} は ${changes} で再着席済み、room履歴を読み返せない" >&2
|
|
261
|
-
exit 1
|
|
262
|
-
}
|
|
263
|
-
if ! printf '%s' "$messages_after" | python3 -c '
|
|
264
|
-
import json,sys
|
|
265
|
-
seq,parent,name,body=sys.argv[1:5]
|
|
266
|
-
try:
|
|
267
|
-
messages=json.load(sys.stdin).get("messages",[])
|
|
268
|
-
except (ValueError, TypeError):
|
|
269
|
-
raise SystemExit(1)
|
|
270
|
-
matched=next((m for m in messages if str(m.get("seq")) == seq), None)
|
|
271
|
-
raise SystemExit(0 if matched and matched.get("from") == parent and matched.get("to") == name and matched.get("body") == body else 1)
|
|
272
|
-
' "$history_seq" "$parent" "$name" "$body"; then
|
|
273
|
-
echo "SEAT_CHANGE_CHANGED_BUT_HISTORY_FAILED: ${name} は ${changes} で再着席済み、room履歴の読返しがtargetと一致しない" >&2
|
|
274
|
-
exit 1
|
|
275
|
-
fi
|
|
276
|
-
|
|
277
|
-
echo "SEAT_CHANGE_OK: ${name} ${changes}(parent=${parent})"
|
|
2
|
+
# 席変更は共通の公開API実装へ渡す。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
exec node "$(dirname "$0")/legacy-entry.mjs" change-seat "$@"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { changeSeat, validateTarget } from './change-seat.mjs'
|
|
7
|
+
|
|
8
|
+
const catalog = { claude: '--effort <level> (low, medium, high, max)',
|
|
9
|
+
codex: JSON.stringify({ models: [{ slug: 'model-a', supported_reasoning_levels: [{ effort: 'high' }, { effort: 'max' }] }] }),
|
|
10
|
+
grok: 'Available models:\n - grok-a\n * grok-b (default)' }
|
|
11
|
+
const catalogDependencies = { resolveCommand: (command, argv) => ({ command, argv }), execute: command => catalog[command] }
|
|
12
|
+
|
|
13
|
+
function fixture(t) {
|
|
14
|
+
const project = mkdtempSync(join(tmpdir(), 'peertable-change-'))
|
|
15
|
+
t.after(() => rmSync(project, { recursive: true, force: true }))
|
|
16
|
+
mkdirSync(join(project, '.team'))
|
|
17
|
+
writeFileSync(join(project, '.team', 'setup-state.json'), JSON.stringify({ room: 'room', server_url: 'http://example', mode: 'standalone' }))
|
|
18
|
+
let member = { name: 'alice', harness: 'claude', vendor: 'claude', model: 'old', effort: 'high', roles: ['実装者'], mission: '使命', aiterm_session_id: 'seat' }
|
|
19
|
+
const events = [], messages = []
|
|
20
|
+
const observation = { exists: true, state: 'idle', harness_alive: true, process_identity: { pid: 42, started_identity: 'old-start' } }
|
|
21
|
+
const dependencies = { ...catalogDependencies,
|
|
22
|
+
runScript: () => '/資格path',
|
|
23
|
+
api: {
|
|
24
|
+
members: async () => [{ ...member }],
|
|
25
|
+
request: async (path, options = {}) => {
|
|
26
|
+
events.push({ path, options })
|
|
27
|
+
if (path === 'members') { member = { ...member, ...options.body }; return member }
|
|
28
|
+
if (options.method === 'POST') {
|
|
29
|
+
const message = { ...options.body, seq: messages.length + 1 }
|
|
30
|
+
messages.push(message)
|
|
31
|
+
return message
|
|
32
|
+
}
|
|
33
|
+
return { messages }
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
aiterm: {
|
|
37
|
+
observe: async () => observation,
|
|
38
|
+
structured: async (tool, args) => {
|
|
39
|
+
events.push({ tool, args })
|
|
40
|
+
return { session_id: args.session_id, model: args.model ?? member.model, reasoning_effort: args.reasoning_effort ?? member.effort }
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
launchSeat: async options => {
|
|
44
|
+
events.push({ launch: options })
|
|
45
|
+
member = { ...member, model: options.model, harness: options.harness, vendor: options.harness, effort: options.effort }
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
return { project, dependencies, events, messages, observation, member: () => member, options: { project, name: 'alice' } }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('本人DMの定型検査をせず、model・effortの各変更を同じsessionへ渡し履歴を読む', async t => {
|
|
52
|
+
for (const target of [{ model: 'new' }, { effort: 'max' }, { model: 'new', effort: 'max' }]) {
|
|
53
|
+
const f = fixture(t)
|
|
54
|
+
const result = await changeSeat({ ...f.options, ...target, reason: '自然文の依頼を親が判断' }, f.dependencies)
|
|
55
|
+
assert.equal(result.status, 'changed')
|
|
56
|
+
assert.equal(f.events.filter(event => event.launch).length, 0)
|
|
57
|
+
assert.deepEqual(f.events.find(event => event.tool).args, { session_id: 'seat', ...(target.model ? { model: target.model } : {}), ...(target.effort ? { reasoning_effort: target.effort } : {}) })
|
|
58
|
+
assert.equal(f.member().model, target.model ?? 'old')
|
|
59
|
+
assert.equal(f.member().effort, target.effort ?? 'high')
|
|
60
|
+
assert.equal(f.messages.length, 1)
|
|
61
|
+
assert.match(f.messages[0].body, /自然文の依頼を親が判断/)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
test('同値はAPI変更なし、busy・未知harnessは席を触らない', async t => {
|
|
65
|
+
const f = fixture(t)
|
|
66
|
+
assert.equal((await changeSeat({ ...f.options, effort: 'high' }, f.dependencies)).status, 'unchanged')
|
|
67
|
+
f.observation.state = 'busy'
|
|
68
|
+
await assert.rejects(changeSeat({ ...f.options, effort: 'max' }, f.dependencies), { code: 'SEAT_CHANGE_SEAT_BUSY' })
|
|
69
|
+
await assert.rejects(changeSeat({ ...f.options, harness: 'unknown', model: 'x', effort: 'high' }, f.dependencies), { code: 'SEAT_CHANGE_HARNESS_UNSUPPORTED' })
|
|
70
|
+
assert.equal(f.events.length, 0)
|
|
71
|
+
})
|
|
72
|
+
test('公式catalogの未知値と取得失敗は固定値へ置き換えない', () => {
|
|
73
|
+
validateTarget('codex', 'model-a', 'max', catalogDependencies)
|
|
74
|
+
validateTarget('grok', 'grok-b', 'high', catalogDependencies)
|
|
75
|
+
assert.throws(() => validateTarget('claude', 'x', 'ultra', catalogDependencies), { code: 'SEAT_CHANGE_EFFORT_UNSUPPORTED' })
|
|
76
|
+
assert.throws(() => validateTarget('codex', 'missing', 'max', catalogDependencies), { code: 'SEAT_CHANGE_MODEL_UNSUPPORTED' })
|
|
77
|
+
assert.throws(() => validateTarget('codex', 'model-a', 'ultra', catalogDependencies), { code: 'SEAT_CHANGE_EFFORT_UNSUPPORTED' })
|
|
78
|
+
assert.throws(() => validateTarget('grok', 'missing', 'high', catalogDependencies), { code: 'SEAT_CHANGE_MODEL_UNSUPPORTED' })
|
|
79
|
+
assert.throws(() => validateTarget('claude', 'x', 'high', { ...catalogDependencies, execute: () => { throw new Error('catalog取得失敗') } }), /catalog取得失敗/)
|
|
80
|
+
})
|
|
81
|
+
test('configure失敗と応答不一致はmetadataを変更せず再起動もしない', async t => {
|
|
82
|
+
for (const fail of [true, false]) {
|
|
83
|
+
const f = fixture(t)
|
|
84
|
+
f.dependencies.aiterm.structured = async () => { if (fail) throw new Error('configure失敗'); return { session_id: 'other', reasoning_effort: 'max' } }
|
|
85
|
+
await assert.rejects(changeSeat({ ...f.options, effort: 'max' }, f.dependencies), fail ? /configure失敗/ : { code: 'SEAT_CHANGE_AITERM_RESULT_MISMATCH' })
|
|
86
|
+
assert.equal(f.member().effort, 'high')
|
|
87
|
+
assert.equal(f.events.length, 0)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
test('harness交代は役割とmissionを保って一度再着任し、履歴を一度残す', async t => {
|
|
91
|
+
const f = fixture(t)
|
|
92
|
+
await changeSeat({ ...f.options, harness: 'codex', model: 'model-a', effort: 'max' }, f.dependencies)
|
|
93
|
+
const launches = f.events.filter(event => event.launch)
|
|
94
|
+
assert.equal(launches.length, 1)
|
|
95
|
+
assert.equal(launches[0].launch.roles, '実装者')
|
|
96
|
+
assert.equal(launches[0].launch.mission, '使命')
|
|
97
|
+
assert.equal(f.messages.length, 1)
|
|
98
|
+
assert.equal(f.member().harness, 'codex')
|
|
99
|
+
})
|
|
100
|
+
test('既存席を止める前の失敗はそのまま返し、席を失った時だけ旧設定へ一度復旧する', async t => {
|
|
101
|
+
for (const stopped of [false, true]) {
|
|
102
|
+
const f = fixture(t)
|
|
103
|
+
let launches = 0
|
|
104
|
+
f.dependencies.launchSeat = async () => {
|
|
105
|
+
launches++
|
|
106
|
+
if (launches === 1) {
|
|
107
|
+
if (stopped) { f.observation.exists = false; f.observation.harness_alive = false }
|
|
108
|
+
throw new Error('着任失敗')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
await assert.rejects(changeSeat({ ...f.options, harness: 'codex', model: 'model-a', effort: 'max' }, f.dependencies), stopped ? { code: 'SEAT_CHANGE_ROLLED_BACK' } : /着任失敗/)
|
|
112
|
+
assert.equal(launches, stopped ? 2 : 1)
|
|
113
|
+
assert.equal(f.messages.length, 0)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
test('台帳・履歴の読返し不一致は変更成立を装わない', async t => {
|
|
117
|
+
for (const broken of ['metadata', 'history']) {
|
|
118
|
+
const f = fixture(t)
|
|
119
|
+
const request = f.dependencies.api.request
|
|
120
|
+
f.dependencies.api.request = async (path, options) => {
|
|
121
|
+
if (broken === 'metadata' && path === 'members') return {}
|
|
122
|
+
if (broken === 'history' && path === 'messages' && !options) return { messages: [] }
|
|
123
|
+
return request(path, options)
|
|
124
|
+
}
|
|
125
|
+
await assert.rejects(changeSeat({ ...f.options, effort: 'max' }, f.dependencies), {
|
|
126
|
+
code: broken === 'metadata' ? 'SEAT_CHANGE_CHANGED_BUT_UNVERIFIED' : 'SEAT_CHANGE_CHANGED_BUT_HISTORY_FAILED',
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npmが公開するPeertable操作入口。
|
|
3
|
+
import { skillCommand } from './install-skill.mjs'
|
|
4
|
+
import { setupProject, resumeProject } from './project-runtime.mjs'
|
|
5
|
+
import { launchSeat } from './launch-seat.mjs'
|
|
6
|
+
import { leaveSeat } from './leave-seat.mjs'
|
|
7
|
+
import { diagnoseProject } from './doctor.mjs'
|
|
8
|
+
import { teardownProject } from './teardown.mjs'
|
|
9
|
+
import { changeSeat } from './change-seat.mjs'
|
|
10
|
+
|
|
11
|
+
function projectOptions(args, { seat = false } = {}) {
|
|
12
|
+
const options = { project: args[0], phases: [] }
|
|
13
|
+
let index = 1
|
|
14
|
+
if (seat) options.name = args[index++]
|
|
15
|
+
const names = { '--room': 'room', '--url': 'url', '--tasks': 'tasks', '--plan': 'plan',
|
|
16
|
+
'--roles': 'roles', '--mission': 'mission', '--model': 'model', '--effort': 'effort', '--harness': 'harness', '--vendor': 'harness', '--brief': 'brief', '--parent': 'parent', '--reason': 'reason' }
|
|
17
|
+
for (; index < args.length; index++) {
|
|
18
|
+
const arg = args[index]
|
|
19
|
+
if (arg === '--json') continue
|
|
20
|
+
if (arg === '--no-probe') { options.probe = false; continue }
|
|
21
|
+
if (arg === '--repair') { options.repair = true; continue }
|
|
22
|
+
if (arg === '--purge') { options.purge = true; continue }
|
|
23
|
+
if (arg === '--archive') { options.purge = false; continue }
|
|
24
|
+
const key = names[arg]
|
|
25
|
+
if (!key && arg !== '--phase') throw new Error(`未対応の引数: ${arg}`)
|
|
26
|
+
const value = args[++index]
|
|
27
|
+
if (!value || value.startsWith('--')) throw new Error(`${arg}の値を指定してください`)
|
|
28
|
+
if (arg === '--phase') options.phases.push(value)
|
|
29
|
+
else options[key] = value
|
|
30
|
+
}
|
|
31
|
+
return options
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const [command, ...args] = process.argv.slice(2)
|
|
35
|
+
try {
|
|
36
|
+
if (command === 'install') {
|
|
37
|
+
const result = skillCommand(args)
|
|
38
|
+
console.log(JSON.stringify(result))
|
|
39
|
+
if (args.includes('--check') && result.status !== 'ready') process.exitCode = 1
|
|
40
|
+
} else if (command === 'diagnostics') {
|
|
41
|
+
const result = args[0] && !args[0].startsWith('--')
|
|
42
|
+
? await diagnoseProject(projectOptions(args)) : skillCommand(['--check', ...args])
|
|
43
|
+
console.log(JSON.stringify(result))
|
|
44
|
+
if (result.status !== 'ready') process.exitCode = 1
|
|
45
|
+
} else if (command === 'setup' || command === 'resume') {
|
|
46
|
+
console.log(JSON.stringify(await (command === 'setup' ? setupProject : resumeProject)(projectOptions(args))))
|
|
47
|
+
} else if (command === 'launch') {
|
|
48
|
+
console.log(JSON.stringify(await launchSeat(projectOptions(args, { seat: true }))))
|
|
49
|
+
} else if (command === 'change') {
|
|
50
|
+
console.log(JSON.stringify(await changeSeat(projectOptions(args, { seat: true }))))
|
|
51
|
+
} else if (command === 'leave') {
|
|
52
|
+
if (args.length !== 2) throw new Error('leaveにはprojectと席名を指定してください')
|
|
53
|
+
console.log(JSON.stringify(await leaveSeat(args[0], args[1])))
|
|
54
|
+
} else if (command === 'teardown') {
|
|
55
|
+
console.log(JSON.stringify(await teardownProject(projectOptions(args))))
|
|
56
|
+
} else if (!command || ['--help', '-h', 'help'].includes(command)) {
|
|
57
|
+
console.log(`Peertable
|
|
58
|
+
peertable install [--target claude|codex|grok|cursor] [--json]
|
|
59
|
+
peertable diagnostics [--target claude|codex|grok|cursor] [--json]
|
|
60
|
+
peertable diagnostics <project> [--repair]
|
|
61
|
+
peertable setup <project> --room <room> --url <URL> --tasks <file>
|
|
62
|
+
peertable setup <project> --room <room> --url <URL> --plan <plan> [--phase <id>]
|
|
63
|
+
peertable resume <project> [--plan <plan>] [--phase <id>] [--no-probe]
|
|
64
|
+
peertable launch <project> <name> --roles <roles> [--harness <harness>] [--model <model>] [--effort <effort>] [--brief <text>]
|
|
65
|
+
peertable leave <project> <name>
|
|
66
|
+
peertable change <project> <name> [--harness <harness>] [--model <model>] [--effort <effort>]
|
|
67
|
+
peertable teardown <project> [--purge]
|
|
68
|
+
|
|
69
|
+
installはskillを配置・更新します。対象AIは既存のホームディレクトリから検出します。
|
|
70
|
+
setup/resumeは明示したprojectの生成物とruntimeを更新し、実動作を確認します。`)
|
|
71
|
+
} else {
|
|
72
|
+
throw Object.assign(new Error(`未対応のコマンド: ${command}`), { code: 'PEERTABLE_COMMAND_UNKNOWN' })
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(JSON.stringify({ schema: 'peertable.error.v1', code: error.code ?? 'PEERTABLE_COMMAND_FAILED', message: error.message, ...(error.result ? { result: error.result } : {}) }))
|
|
76
|
+
process.exitCode = 1
|
|
77
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// 診断はPeertableの記録とAitermの公開観測を照合する。repairはbridgeだけを直す。
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { execFileSync } from 'node:child_process'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { AitermClient } from './aiterm-client.mjs'
|
|
6
|
+
import { RoomApi } from './room-api.mjs'
|
|
7
|
+
import { seatSessionId } from './seat-session.mjs'
|
|
8
|
+
import { bridgeRecordLive } from './bridge-record-live.mjs'
|
|
9
|
+
import { ensureBridge } from './ensure-project-runtime.mjs'
|
|
10
|
+
import { projectPath, readSetup, readJson } from './project-scaffold.mjs'
|
|
11
|
+
import { resolveLatticeInvocation } from './seat-usage.mjs'
|
|
12
|
+
|
|
13
|
+
export async function diagnoseProject(options) {
|
|
14
|
+
const project = projectPath(options.project)
|
|
15
|
+
const state = readSetup(project)
|
|
16
|
+
const api = new RoomApi(state)
|
|
17
|
+
const aiterm = new AitermClient()
|
|
18
|
+
const checks = []
|
|
19
|
+
const check = async (name, action) => {
|
|
20
|
+
try { const result = await action(); checks.push({ name, status: 'ok', ...result }); return result }
|
|
21
|
+
catch (error) { checks.push({ name, status: 'failed', message: error.message }); return null }
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
await check('room到達', () => api.request('summary'))
|
|
25
|
+
const listed = await check('member台帳', async () => ({ members: await api.members() }))
|
|
26
|
+
await check('Aitermと預け仕事の公開観測', async () => ({ sessions: (await aiterm.sessions(['PEERTABLE_MEMBER', 'PEERTABLE_ROOM'])).length }))
|
|
27
|
+
for (const member of listed?.members ?? []) {
|
|
28
|
+
if (member.delivery?.kind === 'parent_watch' || !seatSessionId(member)) continue
|
|
29
|
+
await check(`席 ${member.name}`, async () => {
|
|
30
|
+
if (!member.roles?.length || !member.model || !member.pid || !member.started_identity || !member.argv_digest) throw new Error('台帳の素性・本人性が不完全です')
|
|
31
|
+
const observed = await aiterm.observe(seatSessionId(member))
|
|
32
|
+
if (!observed.exists || observed.state === 'dead') throw new Error('席が停止しています。peertable resumeで再開してください')
|
|
33
|
+
const identity = observed.process_identity
|
|
34
|
+
if (!identity || identity.pid !== member.pid || identity.started_identity !== member.started_identity || identity.argv_digest !== member.argv_digest) throw new Error('台帳と現在のプロセス本人性が一致しません')
|
|
35
|
+
return { state: observed.state, session_id: observed.session_id }
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
for (const kind of ['alarm', 'seat-status', 'wakeup']) await check(`${kind} bridge`, async () => {
|
|
39
|
+
const path = join(project, '.team', `${kind}-bridge.json`)
|
|
40
|
+
const live = () => existsSync(path) && bridgeRecordLive(readJson(path))
|
|
41
|
+
let repaired = false
|
|
42
|
+
if (!live() && options.repair) { await ensureBridge(project, kind, { aiterm }); repaired = true }
|
|
43
|
+
if (!live()) throw new Error('起動記録が無いか、process・進捗が停止しています')
|
|
44
|
+
return { repaired, pid: readJson(path).pid }
|
|
45
|
+
})
|
|
46
|
+
checks.push({ name: '読了ack', status: (listed?.members ?? []).some(member => member.read_seq !== undefined) ? 'ok' : 'unknown', reason: 'memberのread_seqで判定' })
|
|
47
|
+
if (state.mode === 'lattice') await check('Lattice工程', async () => {
|
|
48
|
+
const invocation = resolveLatticeInvocation(process.env.LATTICE_CLI || state.lattice_cli || 'lattice', ['status', '--json'])
|
|
49
|
+
return JSON.parse(execFileSync(invocation.command, invocation.argv, { cwd: project, encoding: 'utf8' }))
|
|
50
|
+
})
|
|
51
|
+
return { schema: 'peertable.diagnostics.v1', status: checks.some(item => item.status === 'failed') ? 'failed' : 'ready', project, room: state.room, checks }
|
|
52
|
+
} finally { await aiterm.close() }
|
|
53
|
+
}
|