peertable 0.7.0 → 0.8.0
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 -124
- package/README.md +164 -160
- package/package.json +49 -49
- package/room/Dockerfile +7 -7
- package/room/client.mjs +14 -3
- package/room/server.mjs +13 -3
- package/skill/02_models.snapshot.md +103 -103
- package/skill/SKILL.md +264 -264
- package/skill/scripts/aiterm-launch.mjs +20 -1
- package/skill/scripts/alarm-set.sh +0 -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/codex-parent-watch.sh +0 -0
- package/skill/scripts/doctor.sh +22 -0
- package/skill/scripts/ensure-bridge.sh +0 -0
- package/skill/scripts/ensure-codex-room-mcp.mjs +0 -0
- package/skill/scripts/ensure-room-mcp.mjs +3 -7
- package/skill/scripts/external-pane.mjs +0 -0
- package/skill/scripts/kickoff-gate.mjs +0 -0
- package/skill/scripts/launch-seat.sh +0 -0
- 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/platform/windows/normalize-read-line.mjs +7 -0
- package/skill/scripts/remove-managed-room-mcp.mjs +70 -0
- package/skill/scripts/resolve-seat-placement.mjs +7 -2
- package/skill/scripts/resume.sh +4 -0
- package/skill/scripts/room-mcp-config.mjs +24 -0
- package/skill/scripts/seat-status-bridge.mjs +111 -2
- package/skill/scripts/seat-usage.mjs +72 -0
- package/skill/scripts/set-mission.sh +0 -0
- package/skill/scripts/setup.sh +0 -0
- package/skill/scripts/teardown.sh +16 -2
- package/skill/scripts/upgrade-team-assets.sh +59 -7
- package/skill/scripts/wakeup-bridge.mjs +35 -1
- 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 -149
- package/skill/templates/parent.md +143 -143
- package/skill/templates/tasks.md +8 -8
|
@@ -37,7 +37,26 @@ const result = await client.callTool({
|
|
|
37
37
|
},
|
|
38
38
|
})
|
|
39
39
|
await client.close()
|
|
40
|
-
if (result.isError)
|
|
40
|
+
if (result.isError) {
|
|
41
|
+
const text = result.content?.[0]?.text ?? ''
|
|
42
|
+
// aiterm v0.29.0からready gate失敗(起動時ダイアログ等でprompt未送信)は明示エラーになった。
|
|
43
|
+
// Peertableの席起動は「未送信→ダイアログ自動通過→着任指示を後段で貼る」を正規手順として
|
|
44
|
+
// 持っているので、sessionが残るこの失敗だけはnot_sent受領として続行する。
|
|
45
|
+
if (text.includes('initial_prompt=not_sent') && text.includes(`session_id: ${session_name}`)) {
|
|
46
|
+
console.log(JSON.stringify({
|
|
47
|
+
schema: 'aiterm.agent-launch-result.v1',
|
|
48
|
+
provider: harness,
|
|
49
|
+
harness: `${harness === 'claude' ? 'claude-code' : `${harness}-cli`}`,
|
|
50
|
+
session_id: session_name,
|
|
51
|
+
managed_completion: true,
|
|
52
|
+
event_cursor: null,
|
|
53
|
+
wait_command: null,
|
|
54
|
+
submit_residue: null,
|
|
55
|
+
}))
|
|
56
|
+
process.exit(0)
|
|
57
|
+
}
|
|
58
|
+
throw new Error(text || 'SEAT_AITERM_LAUNCH_FAILED')
|
|
59
|
+
}
|
|
41
60
|
const receipt = result.structuredContent ?? JSON.parse(result.content?.[0]?.text ?? '{}')
|
|
42
61
|
if (receipt?.schema !== 'aiterm.agent-launch-result.v1' || receipt.provider !== harness || receipt.session_id !== session_name) {
|
|
43
62
|
throw new Error('SEAT_AITERM_LAUNCH_RECEIPT_INVALID')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/skill/scripts/doctor.sh
CHANGED
|
@@ -166,6 +166,28 @@ for (const [name, judge] of [['wakeup', judgeWakeup], ['seat-status', judgeSeatS
|
|
|
166
166
|
line(result.level, result.text)
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// 4.5 端末別の装置可用性(silent absence禁止・2026-08-25 オーナー裁定):
|
|
170
|
+
// この端末で「動かない」なら動かないと表示する。黙ってMacだけで動く状態を作らない。
|
|
171
|
+
{
|
|
172
|
+
// job観測(アクティブランプへの預け仕事合成): tmux list-sessions がこの端末で成立するか
|
|
173
|
+
try {
|
|
174
|
+
const probe = spawnSync('tmux', ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' })
|
|
175
|
+
if (probe.error) throw probe.error
|
|
176
|
+
line('OK', `job観測: tmux list-sessions 実行可(ランプ合成は有効。jobは peer-<name>-job* セッションで走らせること)`)
|
|
177
|
+
} catch (error) {
|
|
178
|
+
line('NG', `job観測: tmux list-sessions が失敗——預け仕事のランプ合成はこの端末で無効(${String(error.message ?? error).split('\n')[0]})`)
|
|
179
|
+
}
|
|
180
|
+
// 読了ack: server members に read_seq 欄が観測できるか(席が一度もackしていない間は判定できない)
|
|
181
|
+
try {
|
|
182
|
+
const res = JSON.parse(execFileSync('curl', ['-s', `${url}/api/${encodeURIComponent(room)}/members`], { encoding: 'utf8' }))
|
|
183
|
+
const withAck = (res.members ?? []).filter(m => m.read_seq !== undefined).length
|
|
184
|
+
if (withAck > 0) line('OK', `読了ack: server対応を確認(read_seq保持 ${withAck} 席)`)
|
|
185
|
+
else line('判定不能', '読了ack: read_seq を持つ席がまだ無い(旧serverか、席が一度も read_unread→post していないか区別できない)')
|
|
186
|
+
} catch (error) {
|
|
187
|
+
line('NG', `読了ack: members を読めない(${String(error.message ?? error).split('\n')[0]})`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
169
191
|
// 5. Lattice 併用モードの工程正本
|
|
170
192
|
if (mode === 'lattice') {
|
|
171
193
|
const latticeCli = process.env.LATTICE_CLI || (typeof setup.lattice_cli === 'string' && setup.lattice_cli) || 'lattice'
|
|
File without changes
|
|
File without changes
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
closeSync, fsyncSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync,
|
|
5
5
|
} from 'node:fs'
|
|
6
6
|
import { join, resolve } from 'node:path'
|
|
7
|
+
import { expectedRoomMcp, isExpectedRoomMcp } from './room-mcp-config.mjs'
|
|
7
8
|
|
|
8
9
|
const fail = (code, message) => {
|
|
9
10
|
process.stderr.write(`${code}: ${message}\n`)
|
|
@@ -21,14 +22,9 @@ try { config = JSON.parse(readFileSync(file, 'utf8')) } catch {
|
|
|
21
22
|
}
|
|
22
23
|
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
23
24
|
fail('SEAT_ROOM_MCP_INVALID', `${file} のrootがobjectでない`)
|
|
24
|
-
const expected =
|
|
25
|
+
const expected = expectedRoomMcp(peertableRepo)
|
|
25
26
|
const current = config?.mcpServers?.room
|
|
26
|
-
if (current
|
|
27
|
-
&& Object.keys(current).sort().join(',') === 'args,command'
|
|
28
|
-
&& current.command === expected.command
|
|
29
|
-
&& Array.isArray(current.args)
|
|
30
|
-
&& current.args.length === 1
|
|
31
|
-
&& current.args[0] === expected.args[0]) process.exit(0)
|
|
27
|
+
if (isExpectedRoomMcp(current, expected)) process.exit(0)
|
|
32
28
|
|
|
33
29
|
if (ownership !== 'managed')
|
|
34
30
|
fail('SEAT_ROOM_MCP_STALE', '既存.mcp.jsonのroom serverをcurrent-tree clientへmergeする必要がある')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
const chunks = []
|
|
5
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
6
|
+
const value = Buffer.concat(chunks).toString('utf8')
|
|
7
|
+
process.stdout.write(value.endsWith('\r') ? value.slice(0, -1) : value)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
closeSync, fsyncSync, lstatSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync,
|
|
4
|
+
} from 'node:fs'
|
|
5
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
6
|
+
import { randomBytes } from 'node:crypto'
|
|
7
|
+
import { isPeertableRoomMcp } from './room-mcp-config.mjs'
|
|
8
|
+
|
|
9
|
+
const fail = (code, detail) => {
|
|
10
|
+
process.stderr.write(`${JSON.stringify({ schema: 'peertable.managed_room_mcp_remove_result.v1',
|
|
11
|
+
result: 'rejected', code, detail })}\n`)
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const [project] = process.argv.slice(2)
|
|
16
|
+
if (!project) fail('PEERTABLE_MANAGED_MCP_REMOVE_ARGS_INVALID', '<project>')
|
|
17
|
+
const file = resolve(project, '.mcp.json')
|
|
18
|
+
let stat
|
|
19
|
+
try { stat = lstatSync(file) } catch (error) {
|
|
20
|
+
if (error?.code === 'ENOENT') {
|
|
21
|
+
process.stdout.write(`${JSON.stringify({ schema: 'peertable.managed_room_mcp_remove_result.v1',
|
|
22
|
+
result: 'ok', action: 'already-absent' })}\n`)
|
|
23
|
+
process.exit(0)
|
|
24
|
+
}
|
|
25
|
+
fail('PEERTABLE_MANAGED_MCP_REMOVE_UNREADABLE', error.message)
|
|
26
|
+
}
|
|
27
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
28
|
+
fail('PEERTABLE_MANAGED_MCP_REMOVE_UNSAFE', `${file} がsymlinkまたはregular fileでない`)
|
|
29
|
+
|
|
30
|
+
let config
|
|
31
|
+
try { config = JSON.parse(readFileSync(file, 'utf8')) } catch (error) {
|
|
32
|
+
fail('PEERTABLE_MANAGED_MCP_REMOVE_INVALID', `${file}: JSONを読めない: ${error.message}`)
|
|
33
|
+
}
|
|
34
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)
|
|
35
|
+
|| !config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
|
|
36
|
+
fail('PEERTABLE_MANAGED_MCP_REMOVE_INVALID', `${file}: root/mcpServersがobjectでない`)
|
|
37
|
+
}
|
|
38
|
+
if (config.mcpServers.room === undefined) {
|
|
39
|
+
process.stdout.write(`${JSON.stringify({ schema: 'peertable.managed_room_mcp_remove_result.v1',
|
|
40
|
+
result: 'ok', action: 'room-already-absent' })}\n`)
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
if (!isPeertableRoomMcp(config.mcpServers.room))
|
|
44
|
+
fail('PEERTABLE_MANAGED_MCP_REMOVE_CONFLICT', `${file}: room blockがPeertable所有形でない`)
|
|
45
|
+
|
|
46
|
+
delete config.mcpServers.room
|
|
47
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers
|
|
48
|
+
if (Object.keys(config).length === 0) {
|
|
49
|
+
unlinkSync(file)
|
|
50
|
+
process.stdout.write(`${JSON.stringify({ schema: 'peertable.managed_room_mcp_remove_result.v1',
|
|
51
|
+
result: 'ok', action: 'file-deleted' })}\n`)
|
|
52
|
+
process.exit(0)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const temporary = join(dirname(file), `.${basename(file)}.teardown-${process.pid}-${randomBytes(6).toString('hex')}.tmp`)
|
|
56
|
+
let fd
|
|
57
|
+
try {
|
|
58
|
+
fd = openSync(temporary, 'wx', stat.mode & 0o777)
|
|
59
|
+
writeFileSync(fd, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
60
|
+
fsyncSync(fd)
|
|
61
|
+
closeSync(fd)
|
|
62
|
+
fd = undefined
|
|
63
|
+
renameSync(temporary, file)
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (fd !== undefined) closeSync(fd)
|
|
66
|
+
try { unlinkSync(temporary) } catch {}
|
|
67
|
+
fail('PEERTABLE_MANAGED_MCP_REMOVE_FAILED', error.message)
|
|
68
|
+
}
|
|
69
|
+
process.stdout.write(`${JSON.stringify({ schema: 'peertable.managed_room_mcp_remove_result.v1',
|
|
70
|
+
result: 'ok', action: 'room-removed-file-preserved' })}\n`)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// 02_models.md の役割→1位〜3位から、着席可能な harness / model / effort を解決する。
|
|
3
3
|
// 具体モデル名はここに持たない。台帳と順位表をその場で読む。
|
|
4
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
5
5
|
import { dirname, join, resolve } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
|
|
@@ -245,7 +245,12 @@ export function resolveSeatIdentity({
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
// realpath比較にする——symlink経由(例: ~/.claude/skills/peertable → 実体tree)で呼ばれると
|
|
249
|
+
// パス文字列比較が外れ、mainが走らず「exit 0・出力なし」で沈黙する(実被弾 2026-08-25:
|
|
250
|
+
// launch-seat.sh が SEAT_LAUNCH_HARNESS_UNSUPPORTED: harness= で落ち、原因が見えなかった)。
|
|
251
|
+
const isMain = process.argv[1] && (() => {
|
|
252
|
+
try { return realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url)) } catch { return false }
|
|
253
|
+
})()
|
|
249
254
|
if (isMain) {
|
|
250
255
|
const args = process.argv.slice(2)
|
|
251
256
|
let roles = ''
|
package/skill/scripts/resume.sh
CHANGED
|
@@ -40,6 +40,10 @@ if [ ! -f "$setup" ]; then
|
|
|
40
40
|
exit 1
|
|
41
41
|
fi
|
|
42
42
|
|
|
43
|
+
# resumeは同じroomを現行Peertable treeへ再接続する入口である。席が全て生存していて再起動0件でも、
|
|
44
|
+
# Peertable所有のgenerated assetとroot room MCPを先に現行版へ更新する。
|
|
45
|
+
"$script_dir/upgrade-team-assets.sh" "$proj"
|
|
46
|
+
|
|
43
47
|
seats_file="$proj/.team/resume-seats.json"
|
|
44
48
|
|
|
45
49
|
# Phase A: 到達確認・plan 再束縛・死記録掃除・再起動対象の抽出(node が JSON を書き出す)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
|
|
3
|
+
export function expectedRoomMcp(peertableRepo) {
|
|
4
|
+
return { command: 'node', args: [resolve(peertableRepo, 'room', 'client.mjs')] }
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isExpectedRoomMcp(current, expected) {
|
|
8
|
+
return Boolean(current && typeof current === 'object' && !Array.isArray(current)
|
|
9
|
+
&& Object.keys(current).sort().join(',') === 'args,command'
|
|
10
|
+
&& current.command === expected.command
|
|
11
|
+
&& Array.isArray(current.args)
|
|
12
|
+
&& current.args.length === 1
|
|
13
|
+
&& current.args[0] === expected.args[0])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isPeertableRoomMcp(current) {
|
|
17
|
+
return Boolean(current && typeof current === 'object' && !Array.isArray(current)
|
|
18
|
+
&& Object.keys(current).sort().join(',') === 'args,command'
|
|
19
|
+
&& current.command === 'node'
|
|
20
|
+
&& Array.isArray(current.args)
|
|
21
|
+
&& current.args.length === 1
|
|
22
|
+
&& typeof current.args[0] === 'string'
|
|
23
|
+
&& /(?:^|[\\/])room[\\/]client\.mjs$/u.test(current.args[0]))
|
|
24
|
+
}
|
|
@@ -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 { classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, parsePaneTokenHint, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxArgv, tmuxPanePid } from './seat-usage.mjs'
|
|
30
|
+
import { STOP_DECLARATION, combineSeatLamp, hasActiveDescendant, patrolTargets, classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, parsePaneTokenHint, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxArgv, tmuxPanePid } from './seat-usage.mjs'
|
|
31
31
|
|
|
32
32
|
const args = process.argv.slice(2)
|
|
33
33
|
const proj = args[0]
|
|
@@ -123,6 +123,39 @@ function readSeat(member, previous, observedAt) {
|
|
|
123
123
|
return { status, busySince, paneTokenHint: parsePaneTokenHint(tail) }
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// ---- 預け仕事(jobセッション)の観測(2026-08-25 オーナー裁定)----
|
|
127
|
+
// 席が常駐・長時間ジョブを `peer-<名前>-job*` の名前のtmuxセッションで走らせる取り決めを前提に、
|
|
128
|
+
// その実物を毎周期観測してランプへ合成する。登録・宣言は一切見ない——観測だけ。
|
|
129
|
+
// active判定は「画面内容が前周期から変わった」こと。出力を出さない計算中はidle表示になるが、
|
|
130
|
+
// それは観測できる事実の正直な表示であり、推測で点滅させない。
|
|
131
|
+
const jobPaneHash = new Map() // `${name}:${session}` -> { hash, changedAt }
|
|
132
|
+
let jobObserveFailureLogged = false
|
|
133
|
+
function observeJob(socket, name) {
|
|
134
|
+
const listed = tmux(socket, 'list-sessions', '-F', '#{session_name}')
|
|
135
|
+
if (listed === null) {
|
|
136
|
+
// 「jobなし」と「観測手段が壊れている」を同じ顔にしない(silent absence禁止・2026-08-25 オーナー裁定)。
|
|
137
|
+
// この端末でjob観測が成立しない場合、ランプ合成は席paneのみで動いていることを明示的に吠える。
|
|
138
|
+
if (!jobObserveFailureLogged) {
|
|
139
|
+
jobObserveFailureLogged = true
|
|
140
|
+
console.error('JOB_OBSERVE_UNAVAILABLE: tmux list-sessions が失敗するため、預け仕事のランプ合成はこの端末で無効(席paneのみで表示)。以後この警告は繰り返さない')
|
|
141
|
+
}
|
|
142
|
+
return { alive: false, active: false }
|
|
143
|
+
}
|
|
144
|
+
jobObserveFailureLogged = false
|
|
145
|
+
const sessions = listed.split('\n').filter(s => s.startsWith(`peer-${name}-job`))
|
|
146
|
+
if (sessions.length === 0) return { alive: false, active: false }
|
|
147
|
+
let active = false
|
|
148
|
+
for (const session of sessions) {
|
|
149
|
+
const pane = tmux(socket, 'capture-pane', '-t', session, '-p')
|
|
150
|
+
if (pane === null) continue
|
|
151
|
+
const hash = String(pane.length) + ':' + pane.slice(-256)
|
|
152
|
+
const key = `${name}:${session}`
|
|
153
|
+
const prev = jobPaneHash.get(key)
|
|
154
|
+
if (!prev || prev.hash !== hash) { jobPaneHash.set(key, { hash }); active = true }
|
|
155
|
+
}
|
|
156
|
+
return { alive: true, active }
|
|
157
|
+
}
|
|
158
|
+
|
|
126
159
|
async function send(name, observation, observedAt) {
|
|
127
160
|
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/members`, {
|
|
128
161
|
method: 'POST',
|
|
@@ -152,7 +185,6 @@ async function serverKeepsStatus() {
|
|
|
152
185
|
// 自己DM(席の自己規律)は保険として残るが、継続の保証はこの番犬が持つ。
|
|
153
186
|
// busy 2分未満は情報読取だけの正当な無宣言ターンがあるため起こさない(誤爆の代償は
|
|
154
187
|
// 待機宣言1ターンで、宣言後の episode は declared 判定で再起こしされない)。
|
|
155
|
-
const STOP_DECLARATION = /\[待機\]|\[監査提出\]|待機します|散会/u
|
|
156
188
|
const NUDGE_MIN_BUSY_MS = 120_000
|
|
157
189
|
const nudgedEpisodes = new Map() // name -> busySince(同一エピソード1回だけ)
|
|
158
190
|
async function nudgeIfDropped(name, busySince) {
|
|
@@ -179,6 +211,64 @@ async function nudgeIfDropped(name, busySince) {
|
|
|
179
211
|
}
|
|
180
212
|
}
|
|
181
213
|
|
|
214
|
+
// ---- claim巡回番犬(2026-08-25 オーナー設計)----
|
|
215
|
+
// 「activeなclaimを保有する席が、有効な待機宣言なしにidleでいる」ことを**周期的に**検査して起こす。
|
|
216
|
+
// 上のbusy→idle遷移番犬は2分未満のターンで作業を落とした席を構造的に見ない(実被弾 2026-08-25:
|
|
217
|
+
// 目覚ましで起床→1分未満で「再開します」と宣言だけしてidle化。以後どの装置も起こさなかった)。
|
|
218
|
+
// 待機宣言の有効性: 宣言より後にその席宛の名指しメッセージ(目覚まし・DM)が届いたら失効。
|
|
219
|
+
// 目覚ましで起こされた席の古い宣言文面を、寝ている根拠にしない(同実被弾: #105宣言→#106起床→就寝)。
|
|
220
|
+
// to:"all" は失効させない——親の待機宣言運用(起きても返信不要)と両立させるため。
|
|
221
|
+
const PATROL_INTERVAL_MS = 30_000
|
|
222
|
+
const PATROL_NAG_INTERVAL_MS = 300_000 // 条件が続く席へは5分間隔で再吠えする(1回きりにしない)
|
|
223
|
+
const patrolLastNag = new Map() // seat -> epoch_ms
|
|
224
|
+
const busyStartedAt = new Map() // seat -> epoch_ms(このbridgeプロセスが観測した最後のターン開始)
|
|
225
|
+
let lastPatrolAt = 0
|
|
226
|
+
async function patrolClaims() {
|
|
227
|
+
if (setup.mode !== 'lattice' || !setup.plan_key) return
|
|
228
|
+
const now = Date.now()
|
|
229
|
+
if (now - lastPatrolAt < PATROL_INTERVAL_MS) return
|
|
230
|
+
lastPatrolAt = now
|
|
231
|
+
let activeTasks
|
|
232
|
+
try {
|
|
233
|
+
// doctor.sh と同じ解決規則(LATTICE_CLI env → setup記録 → 既定 'lattice'、Windowsは .cmd 解決)
|
|
234
|
+
const latticeCli = process.env.LATTICE_CLI || (typeof setup.lattice_cli === 'string' && setup.lattice_cli) || 'lattice'
|
|
235
|
+
const latticeCommand = process.platform === 'win32' && !/\.(cmd|bat|exe)$/i.test(latticeCli) && existsSync(`${latticeCli}.cmd`)
|
|
236
|
+
? `${latticeCli}.cmd`
|
|
237
|
+
: latticeCli
|
|
238
|
+
const out = execFileSync(latticeCommand, ['todo', 'status', '--json'], { cwd: proj, encoding: 'utf8' })
|
|
239
|
+
activeTasks = (JSON.parse(out).active_set ?? []).map(t => t.task_id)
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.error(`seat-status-bridge: claim巡回がLatticeを読めない(今周期は検査しない): ${e.message.split('\n')[0]}`)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
if (!activeTasks.length) return
|
|
245
|
+
let messages
|
|
246
|
+
try {
|
|
247
|
+
messages = (await (await fetch(`${url}/api/${encodeURIComponent(room)}/messages`)).json()).messages ?? []
|
|
248
|
+
} catch { return }
|
|
249
|
+
const targets = patrolTargets({
|
|
250
|
+
activeTasks, messages, statusOf: seat => last.get(seat)?.status ?? null,
|
|
251
|
+
lastBusyStartAt: seat => busyStartedAt.get(seat) ?? null,
|
|
252
|
+
now, lastNag: patrolLastNag, nagIntervalMs: PATROL_NAG_INTERVAL_MS,
|
|
253
|
+
})
|
|
254
|
+
for (const { seat, task } of targets) {
|
|
255
|
+
patrolLastNag.set(seat, now)
|
|
256
|
+
try {
|
|
257
|
+
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/messages`, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: { 'Content-Type': 'application/json', ...(token ? { 'X-Peertable-Token': token } : {}) },
|
|
260
|
+
body: JSON.stringify({ from: 'alarm', to: seat,
|
|
261
|
+
body: `[継続] あなたがclaim中の工程 ${task} が着手中のまま、ターン終了後の待機宣言なしに席が停止している。作業を続行するか、外部待ちなら目覚まし条件を登録して [待機] を宣言してから終えること。` }),
|
|
262
|
+
})
|
|
263
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
264
|
+
console.error(`seat-status-bridge: claim巡回が ${seat} を起こした(${task} を保有したまま無宣言idle)`)
|
|
265
|
+
} catch (e) {
|
|
266
|
+
patrolLastNag.delete(seat)
|
|
267
|
+
console.error(`seat-status-bridge: claim巡回の起こしに失敗: ${e.message}`)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
182
272
|
// bridge 心拍(決定103)。server の bridge 台帳へ 30 秒ごとに送る。途絶=status_bridge_down、
|
|
183
273
|
// 403=server 側が bridge_auth_failed として観測する。旧 server(404)へは送り続けない
|
|
184
274
|
let lastBridgeBeatAt = 0
|
|
@@ -270,6 +360,23 @@ async function tick() {
|
|
|
270
360
|
const observation = readSeat(member, prev, observedAt)
|
|
271
361
|
// tmux 席を持たない member(親など)は一度も観測できないので送らない(deriveMissingSession が null を返す)
|
|
272
362
|
if (observation === null) { skipped++; continue }
|
|
363
|
+
{
|
|
364
|
+
const target = resolveSeatObservation(member, null) ?? resolveSeatObservation(member, defaultSocket())
|
|
365
|
+
if (target !== null) {
|
|
366
|
+
const job = observeJob(target.socket, member.name)
|
|
367
|
+
// 席paneの子孫プロセス(Codex内蔵background terminal等)の実働もjob稼働として合成する
|
|
368
|
+
if (!job.active && observation.status === 'idle') {
|
|
369
|
+
const panePid = tmuxPanePid(target.socket, target.target)
|
|
370
|
+
if (panePid) {
|
|
371
|
+
try {
|
|
372
|
+
const rows = execFileSync('ps', ['-axo', 'pid=,ppid=,pcpu='], { encoding: 'utf8' }).split('\n')
|
|
373
|
+
if (hasActiveDescendant(rows, Number(panePid))) job.active = true
|
|
374
|
+
} catch { /* psが失敗する端末では子孫観測なしで続行(named session観測は生きている) */ }
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
observation.status = combineSeatLamp(observation.status, job)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
273
380
|
const changed = !prev || prev.status !== observation.status
|
|
274
381
|
|| prev.busySince !== observation.busySince
|
|
275
382
|
// token表示は実行中に細かく増える。1k未満の差で8秒ごとにPOSTせず、表示精度に合う粒度で送る。
|
|
@@ -280,6 +387,7 @@ async function tick() {
|
|
|
280
387
|
await send(name, observation, observedAt)
|
|
281
388
|
last.set(name, { ...observation, at: now })
|
|
282
389
|
sent++
|
|
390
|
+
if ((observation.status === 'busy' || observation.status === 'blocked') && prev?.status !== 'busy' && prev?.status !== 'blocked') busyStartedAt.set(name, now)
|
|
283
391
|
if (prev?.status === 'busy' && observation.status === 'idle') await nudgeIfDropped(name, prev.busySince)
|
|
284
392
|
if (changed) console.error(`seat-status-bridge: ${name} → ${observation.status}${prev ? `(${prev.status} から)` : ''}`)
|
|
285
393
|
} catch (e) {
|
|
@@ -289,6 +397,7 @@ async function tick() {
|
|
|
289
397
|
}
|
|
290
398
|
// 0件でも0件と言う(条件付きログにしない。沈黙する失敗を作らない・決定58)
|
|
291
399
|
console.error(`seat-status-bridge: ${members.length} 席を見て ${sent} 件送った(tmux席を持たず観測対象外: ${skipped})`)
|
|
400
|
+
await patrolClaims()
|
|
292
401
|
return { attempted: sent + failed, failed }
|
|
293
402
|
}
|
|
294
403
|
|
|
@@ -280,3 +280,75 @@ export function parsePaneTokenHint(pane) {
|
|
|
280
280
|
}
|
|
281
281
|
return latest
|
|
282
282
|
}
|
|
283
|
+
|
|
284
|
+
// ---- claim巡回番犬の純関数(seat-status-bridge.mjsが使う。testはここからimportする)----
|
|
285
|
+
export const STOP_DECLARATION = /\[待機\]|\[監査提出\]|待機します|散会/u
|
|
286
|
+
|
|
287
|
+
export function patrolTargets({ activeTasks, messages, statusOf, lastBusyStartAt, now, lastNag, nagIntervalMs }) {
|
|
288
|
+
const owner = new Map() // task_id -> 最新のclaim発言者
|
|
289
|
+
for (const m of messages) {
|
|
290
|
+
const match = /^\[claim\]\s+(\S+)/u.exec(m.body ?? '')
|
|
291
|
+
if (match) owner.set(match[1], m.from)
|
|
292
|
+
}
|
|
293
|
+
const targets = []
|
|
294
|
+
for (const task of activeTasks) {
|
|
295
|
+
const seat = owner.get(task)
|
|
296
|
+
if (!seat || statusOf(seat) !== 'idle') continue
|
|
297
|
+
// 待機の判定は観測ベース(2026-08-25 オーナー裁定): 「最後のターン終了より後に待機宣言があるか」
|
|
298
|
+
// だけを見る。宛先による失効・TTLのようなメッセージ解釈の条件は持たない。
|
|
299
|
+
// ターンを終えたのに宣言せず黙った席(実被弾: 起こされて→落として→古い宣言の裏で就寝)は
|
|
300
|
+
// 宣言がターン終了より古いので確実に引っかかる。
|
|
301
|
+
let declTs = 0
|
|
302
|
+
for (const m of messages) {
|
|
303
|
+
if (m.from === seat && STOP_DECLARATION.test(m.body ?? '')) declTs = Date.parse(m.ts ?? '') || declTs
|
|
304
|
+
}
|
|
305
|
+
const busyStart = lastBusyStartAt(seat)
|
|
306
|
+
// 錨はターン「開始」(2026-08-25 実被弾 #120: 終了錨だと「宣言→数秒後にターン終了」の正常系が
|
|
307
|
+
// 常に古い判定を食らい誤爆する)。「最後のターンの開始より後に宣言した」=そのターンが宣言を
|
|
308
|
+
// 残したこと。起床ターンが無宣言で終わると、手持ちの宣言は前ターン開始より古くなり必ず捕まる。
|
|
309
|
+
// bridge再起動等でターン履歴が無い席は、宣言の存在だけで正当とみなす(誤爆しない安全側)
|
|
310
|
+
const declared = busyStart == null ? declTs > 0 : declTs >= busyStart - 10_000
|
|
311
|
+
if (declared) continue
|
|
312
|
+
if (now - (lastNag.get(seat) ?? 0) < nagIntervalMs) continue
|
|
313
|
+
if (!targets.some(t => t.seat === seat)) targets.push({ seat, task })
|
|
314
|
+
}
|
|
315
|
+
return targets
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
// ---- ランプ合成(2026-08-25 オーナー裁定: ドットは1個、席と預け仕事を合成する)----
|
|
320
|
+
// 点滅(busy) = 席のターン実行中 または jobセッションの画面が動いている
|
|
321
|
+
// 点灯(idle) = どのプロセスかは生きているが、何も動いていない
|
|
322
|
+
// 白抜き(dead/unknown相当) = 何も無い
|
|
323
|
+
// blockedは「存在するが承認待ちで詰まっている」の特殊表示としてそのまま通す。
|
|
324
|
+
export function combineSeatLamp(paneStatus, job) {
|
|
325
|
+
if (job?.active) return 'busy'
|
|
326
|
+
if (paneStatus === 'busy' || paneStatus === 'blocked') return paneStatus
|
|
327
|
+
if (paneStatus === 'idle' || job?.alive) return 'idle'
|
|
328
|
+
return paneStatus // 'dead' 等はそのまま
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// paneプロセスの子孫に実働(CPU消費)が居るかの判定素材。psの `pid ppid pcpu` 出力を受け取り、
|
|
332
|
+
// root配下の子孫(root自身は除く)に pcpu >= 閾値 が居れば true。席がCodex内蔵background terminal等、
|
|
333
|
+
// tmuxセッションを作らずに走らせた預け仕事を、プロセスツリーの実観測で拾う(2026-08-25 オーナー裁定
|
|
334
|
+
//「子プロセスも見る」)。nohup等でツリーから切り離された仕事はここでは見えない——それは
|
|
335
|
+
// peer-<name>-job* セッション慣例の側が受け持つ(二本立て)。
|
|
336
|
+
export function hasActiveDescendant(psRows, rootPid, { minCpu = 1.0 } = {}) {
|
|
337
|
+
const children = new Map()
|
|
338
|
+
const cpu = new Map()
|
|
339
|
+
for (const row of psRows) {
|
|
340
|
+
const m = /^\s*(\d+)\s+(\d+)\s+([\d.]+)/.exec(row)
|
|
341
|
+
if (!m) continue
|
|
342
|
+
const [pid, ppid, pcpu] = [Number(m[1]), Number(m[2]), Number(m[3])]
|
|
343
|
+
if (!children.has(ppid)) children.set(ppid, [])
|
|
344
|
+
children.get(ppid).push(pid)
|
|
345
|
+
cpu.set(pid, pcpu)
|
|
346
|
+
}
|
|
347
|
+
const queue = [...(children.get(rootPid) ?? [])]
|
|
348
|
+
while (queue.length) {
|
|
349
|
+
const pid = queue.pop()
|
|
350
|
+
if ((cpu.get(pid) ?? 0) >= minCpu) return true
|
|
351
|
+
queue.push(...(children.get(pid) ?? []))
|
|
352
|
+
}
|
|
353
|
+
return false
|
|
354
|
+
}
|
|
File without changes
|
package/skill/scripts/setup.sh
CHANGED
|
File without changes
|
|
@@ -43,6 +43,20 @@ did() { echo "teardown: [実施] $*"; }
|
|
|
43
43
|
skip() { echo "teardown: [スキップ] $*"; }
|
|
44
44
|
miss() { echo "teardown: [未実施] $*" >&2; fail=1; }
|
|
45
45
|
yes_() { [ "$1" = "True" ] || [ "$1" = "true" ]; }
|
|
46
|
+
platform=$(node -p 'process.platform')
|
|
47
|
+
normalize_read_line() {
|
|
48
|
+
if [ "$platform" = win32 ]; then
|
|
49
|
+
printf '%s' "$1" | node "$script_dir/platform/windows/normalize-read-line.mjs"
|
|
50
|
+
else
|
|
51
|
+
printf '%s' "$1"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
mcp_remove_action=not-managed
|
|
56
|
+
if yes_ "$added_mcp"; then
|
|
57
|
+
mcp_remove_result=$(node "$script_dir/remove-managed-room-mcp.mjs" "$proj") || exit "$?"
|
|
58
|
+
mcp_remove_action=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["action"])' "$mcp_remove_result")
|
|
59
|
+
fi
|
|
46
60
|
|
|
47
61
|
echo "teardown: mode=${mode}(archive=ログとstoreを残す/purge=痕跡ゼロ)"
|
|
48
62
|
|
|
@@ -188,6 +202,7 @@ elif [ -z "$run_refs" ]; then
|
|
|
188
202
|
skip "run landing(active runなし)"
|
|
189
203
|
else
|
|
190
204
|
while IFS= read -r run_ref; do
|
|
205
|
+
run_ref=$(normalize_read_line "$run_ref")
|
|
191
206
|
[ -n "$run_ref" ] || continue
|
|
192
207
|
if landing_report=$(cd "$proj" && "$lattice_cli" run landing --run "$run_ref" 2>&1); then
|
|
193
208
|
unlanded_count=""
|
|
@@ -316,8 +331,7 @@ rm -rf "$proj/.team"
|
|
|
316
331
|
did ".team/ 削除"
|
|
317
332
|
|
|
318
333
|
if yes_ "$added_mcp"; then
|
|
319
|
-
|
|
320
|
-
did ".mcp.json 削除"
|
|
334
|
+
did ".mcp.json のPeertable room MCP撤去(${mcp_remove_action})"
|
|
321
335
|
else
|
|
322
336
|
skip ".mcp.json(setup が作っていない)"
|
|
323
337
|
fi
|