peertable 0.8.18 → 0.8.20
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/room/client.mjs +1 -1
- package/skill/scripts/alarm-bridge.mjs +4 -1
- package/skill/scripts/bridge-record-live.mjs +8 -1
- package/skill/scripts/claude-dialog.mjs +23 -0
- package/skill/scripts/ensure-bridge.sh +4 -3
- package/skill/scripts/launch-seat.sh +18 -11
- package/skill/scripts/seat-status-bridge.mjs +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.20",
|
|
4
4
|
"description": "A round table of peer agents. No orchestrator at the head. Turn Claude Code, Codex, and Grok sessions into a team of equal, long-lived peers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/room/client.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { findModelsDoc, resolveSeatIdentity } from '../skill/scripts/resolve-sea
|
|
|
13
13
|
|
|
14
14
|
// client.mjs 側のハードコード版数。package.json の version と一致していることを
|
|
15
15
|
// diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
|
|
16
|
-
const MCP_VERSION = '0.8.
|
|
16
|
+
const MCP_VERSION = '0.8.20'
|
|
17
17
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
18
18
|
|
|
19
19
|
const USAGE = `usage:
|
|
@@ -22,6 +22,7 @@ import { promisify } from 'node:util'
|
|
|
22
22
|
|
|
23
23
|
import { resolvePostToken } from './seat-usage.mjs'
|
|
24
24
|
import { observePidCommand } from './seat-identity.mjs'
|
|
25
|
+
import { updateBridgeProgress } from './bridge-record-live.mjs'
|
|
25
26
|
|
|
26
27
|
const run = promisify(execFile)
|
|
27
28
|
const [proj, flag] = process.argv.slice(2)
|
|
@@ -57,9 +58,10 @@ if (!token) { console.error('ALARM_BRIDGE_TOKEN_MISSING: 書込トークンが
|
|
|
57
58
|
mkdirSync(alarmsDir, { recursive: true })
|
|
58
59
|
|
|
59
60
|
const self = observePidCommand(process.pid)
|
|
61
|
+
const startedAt = new Date().toISOString()
|
|
60
62
|
writeFileSync(record, JSON.stringify({
|
|
61
63
|
pid: process.pid, start_identity: self.started_identity, room, server_url: url,
|
|
62
|
-
started_at:
|
|
64
|
+
started_at: startedAt, ready_at: startedAt, last_progress_at: startedAt,
|
|
63
65
|
}) + '\n')
|
|
64
66
|
const cleanup = () => { try { unlinkSync(record) } catch {} process.exit(0) }
|
|
65
67
|
process.on('SIGTERM', cleanup)
|
|
@@ -117,5 +119,6 @@ async function tick() {
|
|
|
117
119
|
log(`alarm-bridge start: room=${room} project=${proj} pid=${process.pid}`)
|
|
118
120
|
for (;;) {
|
|
119
121
|
try { await tick() } catch (e) { log(`tick 失敗(続行): ${e.message}`) }
|
|
122
|
+
updateBridgeProgress(record)
|
|
120
123
|
await sleep(2000)
|
|
121
124
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Windows は pid を再利用する。alive な pid だけでは本人ではない
|
|
4
4
|
// (2026-08-21 実測: wakeup-bridge.json の pid が oracle-mcp に再利用され、
|
|
5
5
|
// 本物の配達は死んだまま ensure-bridge が起動済みと誤認した)。
|
|
6
|
-
import { readFileSync } from 'node:fs'
|
|
6
|
+
import { readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
7
7
|
import { resolve } from 'node:path'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
|
|
@@ -21,6 +21,13 @@ export function bridgeRecordLive(record, now = Date.now()) {
|
|
|
21
21
|
return (now - at) <= STALE_MS
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export function updateBridgeProgress(path, now = new Date()) {
|
|
25
|
+
const record = JSON.parse(readFileSync(path, 'utf8'))
|
|
26
|
+
const temporary = `${path}.${process.pid}.tmp`
|
|
27
|
+
writeFileSync(temporary, JSON.stringify({ ...record, last_progress_at: now.toISOString() }) + '\n')
|
|
28
|
+
renameSync(temporary, path)
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
const invokedDirectly = Boolean(process.argv[1])
|
|
25
32
|
&& resolve(fileURLToPath(import.meta.url)).toLowerCase() === resolve(process.argv[1]).toLowerCase()
|
|
26
33
|
if (invokedDirectly) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
export function keysForClaudeMcpPane(screen) {
|
|
6
|
+
const text = String(screen ?? '')
|
|
7
|
+
if (text.includes('New MCP server found in this project: room')) {
|
|
8
|
+
return { kind: 'single-mcp-consent', keys: ['Down', 'Enter'] }
|
|
9
|
+
}
|
|
10
|
+
if (/new MCP servers found in this project/iu.test(text) && text.includes('Enable selected')) {
|
|
11
|
+
const selected = text.split(/\r?\n/u).filter(line => /\[✔\]/u.test(line))
|
|
12
|
+
if (!selected.some(line => /\broom\b/u.test(line))) return null
|
|
13
|
+
return { kind: 'multi-mcp-consent', keys: [...selected.map(() => 'Down'), 'Enter'] }
|
|
14
|
+
}
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const isMain = Boolean(process.argv[1]) && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
19
|
+
if (isMain) {
|
|
20
|
+
const chunks = []
|
|
21
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
22
|
+
process.stdout.write(`${JSON.stringify(keysForClaudeMcpPane(Buffer.concat(chunks).toString('utf8')))}\n`)
|
|
23
|
+
}
|
|
@@ -25,6 +25,10 @@ fi
|
|
|
25
25
|
sock=$(node "$(dirname "$0")/tmux-socket.mjs")
|
|
26
26
|
room=$(node -e 'process.stdout.write(require(process.argv[1]).room)' "$team/setup-state.json")
|
|
27
27
|
session="peertable-${name}-${room}"
|
|
28
|
+
# **session が在るだけでは常駐が生きている証拠にならない**(中の node だけ死んで殻が残る)。
|
|
29
|
+
# pid/進捗の生存確認を通れずここへ来たsessionは、Windowsでlogを開いたままの可能性がある。
|
|
30
|
+
# log切詰めより先に畳み、EBUSYを発生させない。
|
|
31
|
+
tmux_at kill-session -t "$session" 2>/dev/null || true
|
|
28
32
|
# **死んだ記録をここで消す。** 残すと下の loop が前回の `ready_at` を読んで、
|
|
29
33
|
# 新しい bridge が1バイトも動いていない段階で success を返す——「起動していないのに
|
|
30
34
|
# 起動したと言う」=この supervisor が塞ぐはずの穴そのものになる。
|
|
@@ -41,9 +45,6 @@ for v in PEERTABLE_TMUX_SOCKET PEERTABLE_POST_TOKEN PEERTABLE_CREDENTIAL_FILE PE
|
|
|
41
45
|
eval "val=\${$v:-}"
|
|
42
46
|
[ -n "$val" ] && env_prefix="$env_prefix $v=$(printf '%q' "$val")"
|
|
43
47
|
done
|
|
44
|
-
# **session が在るだけでは常駐が生きている証拠にならない**(中の node だけ死んで殻が残る)。
|
|
45
|
-
# 上で pid 生存を確かめて here まで来た=生きていないので、殻は畳んでから立て直す。
|
|
46
|
-
tmux_at kill-session -t "$session" 2>/dev/null || true
|
|
47
48
|
if [ "$(node -p 'process.platform')" = "win32" ]; then
|
|
48
49
|
# psmuxが起動する正規shellはPowerShell 7。POSIXの`env`とMSYS pathを文字列のまま
|
|
49
50
|
# 渡すとnodeへ到達する前に終了するため、Windows nodeに各pathをargvとして変換させ、
|
|
@@ -474,6 +474,7 @@ codex_update_accepted=false
|
|
|
474
474
|
codex_trust_accepted=false
|
|
475
475
|
claude_trust_accepted=false
|
|
476
476
|
codex_dialog_helper="$peertable_script_dir/codex-dialog.mjs"
|
|
477
|
+
claude_dialog_helper="$peertable_script_dir/claude-dialog.mjs"
|
|
477
478
|
pass_codex_pane() {
|
|
478
479
|
local json key
|
|
479
480
|
json=$(printf '%s' "$1" | node "$codex_dialog_helper" || true)
|
|
@@ -489,6 +490,18 @@ print("\n".join(a.get("keys") or []) if isinstance(a, dict) else "")' <<<"$json"
|
|
|
489
490
|
codex_pane_blocks_ready() {
|
|
490
491
|
printf '%s' "$1" | node "$codex_dialog_helper" --ready-ok
|
|
491
492
|
}
|
|
493
|
+
pass_claude_mcp_pane() {
|
|
494
|
+
local json key
|
|
495
|
+
json=$(printf '%s' "$1" | node "$claude_dialog_helper" || true)
|
|
496
|
+
[ -n "$json" ] && [ "$json" != "null" ] || return 1
|
|
497
|
+
while IFS= read -r key; do
|
|
498
|
+
[ -n "$key" ] || continue
|
|
499
|
+
tmux_at send-keys -t "$sess" "$key" || return 1
|
|
500
|
+
done < <(python3 -c 'import json,sys
|
|
501
|
+
a=json.loads(sys.stdin.read() or "null")
|
|
502
|
+
print("\n".join(a.get("keys") or []) if isinstance(a, dict) else "")' <<<"$json")
|
|
503
|
+
return 0
|
|
504
|
+
}
|
|
492
505
|
while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
493
506
|
# Grok Build は初めて開く作業treeで、room MCPを初期化する前にworkspace trustを尋ねる。
|
|
494
507
|
# Peertableが正式に着席させるtreeなので、この既知文言だけを一度通す。未知の確認画面を
|
|
@@ -551,17 +564,11 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
551
564
|
# 同意が member 登録より前に出る。選択肢2(this and all future)だけを通す。
|
|
552
565
|
if [ "$harness" = claude ] && [ "$mcp_consent_accepted" != true ]; then
|
|
553
566
|
claude_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
fi
|
|
560
|
-
mcp_consent_accepted=true
|
|
561
|
-
room_ready_deadline=$((SECONDS + 90))
|
|
562
|
-
echo "claude room MCP consent: accepted"
|
|
563
|
-
;;
|
|
564
|
-
esac
|
|
567
|
+
if pass_claude_mcp_pane "$claude_screen"; then
|
|
568
|
+
mcp_consent_accepted=true
|
|
569
|
+
room_ready_deadline=$((SECONDS + 90))
|
|
570
|
+
echo "claude room MCP consent: accepted"
|
|
571
|
+
fi
|
|
565
572
|
fi
|
|
566
573
|
room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
|
|
567
574
|
if printf '%s' "$room_members" | python3 -c 'import json,sys; name=sys.argv[1]; members=json.load(sys.stdin).get("members",[]); raise SystemExit(0 if any(m.get("name") == name for m in members) else 1)' "$name"; then
|
|
@@ -28,6 +28,7 @@ import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from
|
|
|
28
28
|
import { join } from 'node:path'
|
|
29
29
|
|
|
30
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'
|
|
31
|
+
import { updateBridgeProgress } from './bridge-record-live.mjs'
|
|
31
32
|
|
|
32
33
|
const args = process.argv.slice(2)
|
|
33
34
|
const proj = args[0]
|
|
@@ -381,7 +382,8 @@ function recordReady() {
|
|
|
381
382
|
readyRecorded = true
|
|
382
383
|
const record = JSON.parse(readFileSync(pidPath, 'utf8'))
|
|
383
384
|
const temporary = `${pidPath}.${process.pid}.tmp`
|
|
384
|
-
|
|
385
|
+
const readyAt = new Date().toISOString()
|
|
386
|
+
writeFileSync(temporary, JSON.stringify({ ...record, ready_at: readyAt, last_progress_at: readyAt }) + '\n')
|
|
385
387
|
renameSync(temporary, pidPath)
|
|
386
388
|
}
|
|
387
389
|
|
|
@@ -484,6 +486,7 @@ let provenWritable = false
|
|
|
484
486
|
async function guardedTick() {
|
|
485
487
|
await beatBridge()
|
|
486
488
|
const { attempted, failed } = await tick() ?? NOTHING_ATTEMPTED
|
|
489
|
+
updateBridgeProgress(pidPath)
|
|
487
490
|
const decided = decideBridgeContinuation({ attempted, failed, provenWritable, failedTicks, limit: FAILED_TICK_LIMIT })
|
|
488
491
|
provenWritable = decided.provenWritable
|
|
489
492
|
failedTicks = decided.failedTicks
|