peertable 0.3.8 → 0.3.10

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.
@@ -1,8 +1,9 @@
1
1
  #!/bin/bash
2
- # usage: .team/scripts/done.sh <task_id> [--evidence-from <隔離worktreeの証跡の絶対path>]
2
+ # usage: .team/scripts/done.sh <task_id> [--plan <plan_key>] [--evidence-from <隔離worktreeの証跡の絶対path>]
3
3
  # .team/scripts/done.sh --landing-run <run_ref>
4
- # evidence/<plan_key>/<task_id>.md(commit 済みであること)から記述子を作り lattice todo done を実行する。
5
- # plan key は環境変数 PEERTABLE_PLAN から取る。
4
+ # evidence/<plan_key>/<task_id>.md(commit 済みであること)から記述子を作り、同じ本文を
5
+ # Latticeのtest_resultへ渡して lattice todo done を実行する。
6
+ # plan key は --plan を優先し、省略時だけ環境変数 PEERTABLE_PLAN から取る。
6
7
  # 証跡を plan key で仕切るのは、task_id が campaign を跨いで再利用される(t1, t2, …)ため。
7
8
  # 平置きだと次の campaign の t1 が前の campaign の t1 の監査証跡を上書きで消す(2026-08-08 実測)。
8
9
  #
@@ -20,6 +21,33 @@
20
21
  # evidence verifier は descriptor.path の working tree 実在を見ず、object DB の blob と digest、
21
22
  # 読み出し時の `rev-list --all` 到達性を見る(mio が実 repo で確認・room [1016])。
22
23
  set -e
24
+
25
+ show_usage() {
26
+ cat <<'USAGE'
27
+ usage: done.sh <task_id> [--plan <plan_key>] [--evidence-from <隔離worktreeの証跡の絶対path>]
28
+ done.sh --landing-run <run_ref>
29
+
30
+ 完了処理:
31
+ --plan <plan_key> を指定する。省略時だけ PEERTABLE_PLAN を使う。
32
+ evidence/<plan>/<task>.md を commit 済みにして done.sh <task_id> を実行する。
33
+ wrapper が証跡から記述子を生成し、lattice todo doneで同じ本文を最終試験結果として canonical store へ記録する。
34
+ pull run の worktree で作業した場合だけ --evidence-from に同じrepoの絶対pathを渡す。
35
+ 未accept の intake がある場合は、先に run intake accept を完了させる。
36
+ USAGE
37
+ }
38
+
39
+ if [ "$#" = 0 ]; then
40
+ show_usage >&2
41
+ exit 2
42
+ fi
43
+ case "${1:-}" in
44
+ --help|-h)
45
+ [ "$#" = 1 ] || { echo "ERROR: helpには他の引数を付けないこと" >&2; exit 2; }
46
+ show_usage
47
+ exit 0
48
+ ;;
49
+ esac
50
+
23
51
  # `todo done` と run receipt の accept は別の正本を持つ。landing-only mode は accept の直後に
24
52
  # 同じ run ref を受け取り、受理済み receipt の着地だけを表示する。accept 自体はここへ吸収しない。
25
53
  if [ "${1:-}" = "--landing-run" ]; then
@@ -32,14 +60,14 @@ if [ "${1:-}" = "--landing-run" ]; then
32
60
  lattice_cli="${LATTICE_CLI:-$(command -v lattice 2>/dev/null || true)}"
33
61
  if [ -z "$lattice_cli" ] || [ ! -x "$lattice_cli" ]; then
34
62
  echo "着地状態を読めない: LATTICE_CLIが実行可能fileを指さない(${lattice_cli:-未設定})" >&2
35
- exit 0
63
+ exit 1
36
64
  fi
37
65
  landing_report=""
38
66
  landing_rc=0
39
67
  landing_report=$("$lattice_cli" run landing --run "$run_ref" 2>&1) || landing_rc=$?
40
68
  if [ "$landing_rc" != 0 ]; then
41
69
  echo "着地状態を読めない: run landing が rc=${landing_rc} で失敗: ${landing_report}" >&2
42
- exit 0
70
+ exit 1
43
71
  fi
44
72
  unlanded_count=""
45
73
  if ! unlanded_count=$(printf '%s' "$landing_report" | python3 -c '
@@ -54,6 +82,8 @@ if not isinstance(report, dict):
54
82
  actual_schema = report.get("schema")
55
83
  if actual_schema != "lattice.run_landing_report.v1":
56
84
  sys.exit(f"run landing の schema が違う: {actual_schema}")
85
+ if not isinstance(report.get("landed"), bool):
86
+ sys.exit("run landing の landed が真偽値でない")
57
87
  receipts = report.get("accepted_receipts")
58
88
  if not isinstance(receipts, list):
59
89
  sys.exit("run landing に accepted_receipts 配列が無い")
@@ -63,29 +93,87 @@ for index, receipt in enumerate(receipts):
63
93
  print(sum(1 for receipt in receipts if not receipt["landed"]))
64
94
  ' 2>&1); then
65
95
  echo "着地状態を読めない: ${unlanded_count}" >&2
66
- exit 0
96
+ exit 1
67
97
  fi
68
98
  if [ "$unlanded_count" != 0 ]; then
69
99
  echo "未着地 ${unlanded_count}本: run ${run_ref} の受理済み成果が canonical default branch へ着地していない" >&2
70
100
  fi
101
+ # **「受理済みだが未着地」と「そもそも受理されていない」は別の完了軸である。**
102
+ # landing report は accepted receipt しか持たないので、accept 前で止まっている intake は
103
+ # ここでは 0 本=無言になる(2026-08-11 実測)。observe を併せて読み、別の軸として出す。
104
+ pending_report=""
105
+ pending_rc=0
106
+ pending_report=$("$lattice_cli" run observe --run "$run_ref" 2>&1) || pending_rc=$?
107
+ if [ "$pending_rc" != 0 ]; then
108
+ echo "未accept本数を読めない: run observe が rc=${pending_rc} で失敗: ${pending_report}" >&2
109
+ exit 1
110
+ fi
111
+ pending_count=""
112
+ if ! pending_count=$(printf '%s' "$pending_report" | python3 -c '
113
+ import json, sys
114
+ raw = sys.stdin.read()
115
+ try:
116
+ report = json.loads(raw)
117
+ except Exception as error:
118
+ sys.exit(f"run observe がJSONでない: {error}")
119
+ if not isinstance(report, dict):
120
+ sys.exit(f"run observe がobjectでない: {type(report).__name__}")
121
+ if report.get("schema") != "lattice.pull_run_observation.v1":
122
+ sys.exit(f"run observe の schema が違う: {report.get('schema')}")
123
+ intakes = report.get("intakes")
124
+ if not isinstance(intakes, list):
125
+ sys.exit("run observe に intakes 配列が無い")
126
+ pending = []
127
+ seen = set()
128
+ for index, intake in enumerate(intakes):
129
+ if not isinstance(intake, dict):
130
+ sys.exit(f"intakes[{index}] がobjectでない")
131
+ task_id = intake.get("task_id")
132
+ if not isinstance(task_id, str) or not task_id:
133
+ sys.exit(f"intakes[{index}] の task_id が空または文字列でない")
134
+ if task_id in seen:
135
+ sys.exit(f"intakes[{index}] の task_id が重複している: {task_id}")
136
+ seen.add(task_id)
137
+ if "accepted_head_sha" not in intake:
138
+ sys.exit(f"intakes[{index}] に accepted_head_sha が無い")
139
+ accepted_head_sha = intake.get("accepted_head_sha")
140
+ if accepted_head_sha is not None and not isinstance(accepted_head_sha, str):
141
+ sys.exit(f"intakes[{index}] の accepted_head_sha が文字列またはnullでない")
142
+ if not accepted_head_sha:
143
+ pending.append(task_id)
144
+ print(",".join(sorted(pending)))
145
+ ' 2>&1); then
146
+ echo "未accept本数を読めない: ${pending_count}" >&2
147
+ exit 1
148
+ fi
149
+ if [ -n "$pending_count" ]; then
150
+ echo "未accept: run ${run_ref} に受理されていない intake が在る(${pending_count})。着地以前に受理が済んでいない" >&2
151
+ fi
71
152
  exit 0
72
153
  fi
73
- # **引数の形を exact に要求する。** 緩く受けると、`--evidnce-from` のような typo が
74
- # 「option 無し」として通り、**canonical 側の同名証跡を黙って hash する**——別 file を
75
- # 受理させておいて green に見える(kanade の監査で実測・room [1029])。
76
- # 1引数(既定経路)か、3引数(`<task> --evidence-from <絶対path>`)だけを許す。
77
- case $# in
78
- 1) ;;
79
- 3) [ "$2" = "--evidence-from" ] || {
80
- echo "ERROR: 未知のoption: $2(使えるのは --evidence-from だけ)" >&2; exit 1; } ;;
81
- *) echo "ERROR: 引数の形が違う(usage: done.sh <task_id> [--evidence-from <絶対path>] | done.sh --landing-run <run_ref>)" >&2; exit 1 ;;
82
- esac
83
154
  t="$1"
84
155
  [ -n "$t" ] || { echo "ERROR: task_id が空" >&2; exit 1; }
156
+ shift
157
+ plan="${PEERTABLE_PLAN:-}"
85
158
  evidence_from=""
86
- if [ "$#" = 3 ]; then
87
- evidence_from="$3"
88
- [ -n "$evidence_from" ] || { echo "ERROR: --evidence-from には証跡fileの絶対pathを渡すこと" >&2; exit 1; }
159
+ while [ "$#" -gt 0 ]; do
160
+ case "$1" in
161
+ --plan)
162
+ [ "$#" -ge 2 ] && [ -z "${plan_explicit:-}" ] || { echo "ERROR: --plan は一度だけ値付きで指定すること" >&2; exit 1; }
163
+ plan="$2"
164
+ plan_explicit=yes
165
+ shift 2
166
+ ;;
167
+ --evidence-from)
168
+ [ "$#" -ge 2 ] && [ -z "$evidence_from" ] || { echo "ERROR: --evidence-from は一度だけ値付きで指定すること" >&2; exit 1; }
169
+ evidence_from="$2"
170
+ shift 2
171
+ ;;
172
+ *) echo "ERROR: 未知のoption: $1(使えるのは --plan と --evidence-from だけ)" >&2; exit 1 ;;
173
+ esac
174
+ done
175
+ [ -n "$plan" ] || { echo "ERROR: 完了処理を続けられない: --plan または PEERTABLE_PLAN が必要" >&2; exit 1; }
176
+ if [ -n "$evidence_from" ]; then
89
177
  case "$evidence_from" in
90
178
  /*) ;;
91
179
  *) echo "ERROR: --evidence-from は絶対pathでなければならない: $evidence_from" >&2; exit 1 ;;
@@ -106,23 +194,177 @@ if [ "$#" = 3 ]; then
106
194
  }
107
195
  fi
108
196
 
109
- # descriptor の path は repo 内の相対(repo 外の絶対 path は --evidence が INVALID_ARGUMENTS で弾く)。
110
- # worktree でも canonical でも同じ相対 path に置く規約なので、この値は両者で一致する。
111
- f="evidence/$PEERTABLE_PLAN/$t.md"
197
+ f="evidence/$plan/$t.md"
112
198
  src="${evidence_from:-$f}"
113
199
  [ -f "$src" ] || { echo "ERROR: 証跡が見つからない: $src" >&2; exit 1; }
114
- oid=$(git hash-object -w "$src")
115
- digest=$(shasum -a 256 "$src" | cut -d' ' -f1)
116
- tmp=".ev-$t.json"
117
- # **失敗しても記述子を残さない。** `set -e` の下で `todo done` が落ちると、後段の `rm` へ
118
- # 到達せず repo `.ev-<task>.json` が残る(自分の負側 test で実測。TASK_NOT_FOUND の後に
119
- # untracked file が残った)。次に `git status` を撮った人が、それを誰かの作業中変更と読む。
120
- trap 'rm -f "$tmp"' EXIT
121
- printf '{"evidence_id":"ev-%s","repo_id":"self","path":"%s","git_blob_oid":"%s","content_digest":"%s","media_type":"text/markdown","anchor_digest":null}\n' "$t" "$f" "$oid" "$digest" > "$tmp"
122
- # **PATH の `lattice` へ黙って逸れない。** setup が解決した CLI を席 env の `LATTICE_CLI` で受け、
123
- # 無い時だけ PATH を使う(bridge の `--lattice` / teardown の `LATTICE_CLI` と同じ選択規律)。
124
- "${LATTICE_CLI:-lattice}" todo done --plan "$PEERTABLE_PLAN" --task "$t" --evidence "$tmp"
125
- rm -f "$tmp"
200
+
201
+ done_gate_cli="${LATTICE_CLI:-lattice}"
202
+ # 再試行では既にdoneのToDoへtodo doneを重ねない。
203
+ task_state_json=""
204
+ task_state_json=$("$done_gate_cli" todo show --plan "$plan" --task "$t" --json 2>&1) || {
205
+ echo "ERROR: 完了処理を続けられない: todo show が失敗: $task_state_json" >&2
206
+ exit 1
207
+ }
208
+ task_field() {
209
+ local field="$1"
210
+ printf '%s' "$task_state_json" | python3 -c '
211
+ import json
212
+ import sys
213
+
214
+ report = json.load(sys.stdin)
215
+ state = report.get("state")
216
+ task = report.get("task")
217
+ if not isinstance(state, dict) or not isinstance(task, dict):
218
+ raise SystemExit("todo show の state/task が不正")
219
+ field = sys.argv[1]
220
+ if field != "status":
221
+ raise SystemExit(f"未知のtask field: {field}")
222
+ value = state.get("status")
223
+ if not isinstance(value, str):
224
+ raise SystemExit(f"task fieldが文字列でない: {field}")
225
+ print(value)
226
+ ' "$field"
227
+ }
228
+ task_status=""
229
+ task_status=$(task_field status 2>&1) || {
230
+ echo "ERROR: 完了処理を続けられない: ToDo状態を読めない: $task_status" >&2
231
+ exit 1
232
+ }
233
+ already_done=no
234
+ case "$task_status" in
235
+ done) already_done=yes ;;
236
+ in-progress) ;;
237
+ *) echo "ERROR: 完了処理を続けられない: ToDoが完了可能状態でない: $task_status" >&2; exit 1 ;;
238
+ esac
239
+
240
+ # **receipt が未 accept のまま done を打たせない。** 実行層に載せた task の成果の正本は
241
+ # Lattice が撮った observed diff(accepted receipt)であって、ToDo の done ではない。
242
+ # 2026-08-11 実測: accept が `RUNTIME_CONFLICT_HOLD` で止まっているのに done は通り、
243
+ # 「ToDo は完了・成果はどこにも着地していない」状態が親の事後照合まで誰にも見えなかった
244
+ # (そして landing-only mode は receipt が無ければ 0 本=無言になる)。
245
+ # **実行層に載っていない task は素通しする**——pull run の利用は任意で、載っていない卓を止めない。
246
+ gate_runs=$("$done_gate_cli" run list --json 2>&1) || {
247
+ echo "ERROR: receipt の状態を読めない(run list が失敗): $gate_runs" >&2; exit 1;
248
+ }
249
+ gate_refs=$(printf '%s' "$gate_runs" | python3 -c '
250
+ import json, sys
251
+ plan = sys.argv[1]
252
+ raw = sys.stdin.read()
253
+ try:
254
+ report = json.loads(raw)
255
+ except Exception as error:
256
+ sys.exit(f"run list がJSONでない: {error}")
257
+ if not isinstance(report, dict):
258
+ sys.exit(f"run list がobjectでない: {type(report).__name__}")
259
+ if report.get("schema") != "lattice.run_list.v1":
260
+ sys.exit(f"run list の schema が違う: {report.get('schema')}")
261
+ runs = report.get("active_runs")
262
+ if not isinstance(runs, list):
263
+ sys.exit("run list に active_runs 配列が無い")
264
+ for index, run in enumerate(runs):
265
+ if not isinstance(run, dict):
266
+ sys.exit(f"active_runs[{index}] がobjectでない")
267
+ plan_key = run.get("plan_key")
268
+ selection = run.get("selection")
269
+ if not isinstance(plan_key, str) or not plan_key:
270
+ sys.exit(f"active_runs[{index}] の plan_key が空または文字列でない")
271
+ if not isinstance(selection, str) or not selection:
272
+ sys.exit(f"active_runs[{index}] の selection が空または文字列でない")
273
+ if plan_key == plan:
274
+ if selection != "pull":
275
+ sys.exit(f"active_runs[{index}] の対象planの selection がpullでない: {selection}")
276
+ run_ref = run.get("run_ref")
277
+ if not isinstance(run_ref, str) or not run_ref:
278
+ sys.exit(f"active_runs[{index}] の run_ref が空または文字列でない")
279
+ print(run_ref)
280
+ ' "$plan") || {
281
+ echo "ERROR: receipt の状態を読めない: $gate_refs" >&2; exit 1;
282
+ }
283
+ for gate_ref in $gate_refs; do
284
+ gate_obs=$("$done_gate_cli" run observe --run "$gate_ref" 2>&1) || {
285
+ echo "ERROR: receipt の状態を読めない(run observe が失敗): $gate_ref: $gate_obs" >&2; exit 1;
286
+ }
287
+ gate_state=$(printf '%s' "$gate_obs" | python3 -c '
288
+ import json, sys
289
+ task = sys.argv[1]
290
+ raw = sys.stdin.read()
291
+ try:
292
+ report = json.loads(raw)
293
+ except Exception as error:
294
+ sys.exit(f"run observe がJSONでない: {error}")
295
+ if not isinstance(report, dict):
296
+ sys.exit(f"run observe がobjectでない: {type(report).__name__}")
297
+ if report.get("schema") != "lattice.pull_run_observation.v1":
298
+ sys.exit(f"run observe の schema が違う: {report.get('schema')}")
299
+ intakes = report.get("intakes")
300
+ if not isinstance(intakes, list):
301
+ sys.exit("run observe に intakes 配列が無い")
302
+ entry = None
303
+ seen = set()
304
+ for index, intake in enumerate(intakes):
305
+ if not isinstance(intake, dict):
306
+ sys.exit(f"intakes[{index}] がobjectでない")
307
+ task_id = intake.get("task_id")
308
+ if not isinstance(task_id, str) or not task_id:
309
+ sys.exit(f"intakes[{index}] の task_id が空または文字列でない")
310
+ if task_id in seen:
311
+ sys.exit(f"intakes[{index}] の task_id が重複している: {task_id}")
312
+ seen.add(task_id)
313
+ if "accepted_head_sha" not in intake:
314
+ sys.exit(f"intakes[{index}] に accepted_head_sha が無い")
315
+ accepted_head_sha = intake.get("accepted_head_sha")
316
+ if accepted_head_sha is not None and not isinstance(accepted_head_sha, str):
317
+ sys.exit(f"intakes[{index}] の accepted_head_sha が文字列またはnullでない")
318
+ if task_id == task:
319
+ entry = intake
320
+ print("absent" if entry is None else ("accepted" if entry.get("accepted_head_sha") else "pending"))
321
+ ' "$t") || { echo "ERROR: receipt の状態を読めない: $gate_state" >&2; exit 1; }
322
+ if [ "$gate_state" = pending ]; then
323
+ echo "ERROR: receipt が未acceptのまま done は打てない: ${t} @ ${gate_ref}" >&2
324
+ echo " 先に受理させる: ${done_gate_cli} run intake accept --run ${gate_ref} --task ${t}" >&2
325
+ echo " (accept が hold で止まる場合、その理由の解消が完了条件であって、done は迂回路ではない)" >&2
326
+ exit 1
327
+ fi
328
+ done
329
+
330
+ # descriptor の path は repo 内の相対(repo 外の絶対 path は --evidence が INVALID_ARGUMENTS で弾く)。
331
+ # worktree でも canonical でも同じ相対 path に置く規約なので、この値は両者で一致する。
332
+ if [ "$already_done" = no ]; then
333
+ oid=$(git hash-object -w "$src")
334
+ digest=$(shasum -a 256 "$src" | cut -d' ' -f1)
335
+ tmp=".ev-$t.json"
336
+ test_result_tmp=".test-result-$plan-$t.md"
337
+ # **失敗しても記述子を残さない。** `set -e` の下で `todo done` が落ちると、後段の `rm` へ
338
+ # 到達せず repo に `.ev-<task>.json` が残る(自分の負側 test で実測。TASK_NOT_FOUND の後に
339
+ # untracked file が残った)。次に `git status` を撮った人が、それを誰かの作業中変更と読む。
340
+ trap 'rm -f "$tmp" "$test_result_tmp"' EXIT
341
+ printf '{"evidence_id":"ev-%s","repo_id":"self","path":"%s","git_blob_oid":"%s","content_digest":"%s","media_type":"text/markdown","anchor_digest":null}\n' "$t" "$f" "$oid" "$digest" > "$tmp"
342
+ cp "$src" "$test_result_tmp"
343
+ # **PATH の `lattice` へ黙って逸れない。** setup が解決した CLI を席 env の `LATTICE_CLI` で受け、
344
+ # 無い時だけ PATH を使う(bridge の `--lattice` / teardown の `LATTICE_CLI` と同じ選択規律)。
345
+ done_output=""
346
+ done_rc=0
347
+ done_output=$("$done_gate_cli" todo done --plan "$plan" --task "$t" --evidence "$tmp" --test-result "$test_result_tmp" 2>&1) || done_rc=$?
348
+ printf '%s\n' "$done_output"
349
+ [ "$done_rc" -eq 0 ] || exit "$done_rc"
350
+ rm -f "$tmp" "$test_result_tmp"
351
+ else
352
+ echo "todo done は既に記録済み: $t" >&2
353
+ fi
354
+
355
+ # todo done の直後(または再試行時の既存done)に、工程正本が本当に done か再確認する。
356
+ task_state_json=$("$done_gate_cli" todo show --plan "$plan" --task "$t" --json 2>&1) || {
357
+ echo "ERROR: 完了処理を続けられない: done後のToDo状態を読めない: $task_state_json" >&2
358
+ exit 1
359
+ }
360
+ task_status=$(task_field status 2>&1) || {
361
+ echo "ERROR: 完了処理を続けられない: done後の状態を読めない: $task_status" >&2
362
+ exit 1
363
+ }
364
+ [ "$task_status" = done ] || {
365
+ echo "ERROR: 完了処理を続けられない: todo done後も状態がdoneでない: $task_status" >&2
366
+ exit 1
367
+ }
126
368
 
127
369
  # 完了の定義は「repo 内の変更は push まで」。done を打つ瞬間はそれが成り立っていなければ
128
370
  # ならない唯一の時点で、かつ全員が必ず通る場所である。publish 経路の機械 gate は tarball
@@ -130,10 +372,111 @@ rm -f "$tmp"
130
372
  # 成果物が取り残される(2026-08-08 実測。卓の全員が立っていた穴で、親の監査が見つけた)。
131
373
  # 出すだけで止めない: push 既定でない repo も、まとめて push する運用も壊さないため。
132
374
  # upstream 未設定・git 管理外でも done.sh 自体は死なせない(set -e の下なので必ずガードする)。
133
- unpushed=$(git rev-list --count '@{u}..HEAD' 2>/dev/null || true)
134
- if [ -n "$unpushed" ] && [ "$unpushed" != 0 ]; then
375
+ upstream_ref=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)
376
+ [ -n "$upstream_ref" ] || {
377
+ echo "ERROR: 完了処理を続けられない: canonical upstream が無い" >&2
378
+ exit 1
379
+ }
380
+ unpushed=$(git rev-list --count "${upstream_ref}..HEAD" 2>/dev/null || true)
381
+ [ -n "$unpushed" ] || {
382
+ echo "ERROR: 完了処理を続けられない: canonical landing 状態を読めない" >&2
383
+ exit 1
384
+ }
385
+ if [ "$unpushed" != 0 ]; then
135
386
  echo "未push ${unpushed}本: この done の成果物はまだ upstream へ着地していない" >&2
387
+ echo "ERROR: 完了処理を続けられない: canonical landing 不足" >&2
388
+ exit 1
136
389
  fi
390
+ git merge-base --is-ancestor HEAD "$upstream_ref" || {
391
+ echo "ERROR: 完了処理を続けられない: HEAD が canonical upstream の祖先でない" >&2
392
+ exit 1
393
+ }
394
+
395
+ # pull run を使った task は、accept 済みで、かつこの task の receipt が
396
+ # canonical branch へ着地したことまで確認する。別 task の未着地だけでは
397
+ # この task の completed を止めない。
398
+ for gate_ref in $gate_refs; do
399
+ gate_obs=""
400
+ gate_obs=$("$done_gate_cli" run observe --run "$gate_ref" 2>&1) || {
401
+ echo "ERROR: 完了処理を続けられない: run observe が失敗: $gate_ref: $gate_obs" >&2
402
+ exit 1
403
+ }
404
+ gate_state=$(printf '%s' "$gate_obs" | python3 -c '
405
+ import json, sys
406
+ task = sys.argv[1]
407
+ report = json.load(sys.stdin)
408
+ if report.get("schema") != "lattice.pull_run_observation.v1":
409
+ raise SystemExit("run observe の schema が違う")
410
+ intakes = report.get("intakes")
411
+ if not isinstance(intakes, list):
412
+ raise SystemExit("run observe に intakes 配列が無い")
413
+ seen = set()
414
+ found = None
415
+ for intake in intakes:
416
+ if not isinstance(intake, dict) or not isinstance(intake.get("task_id"), str):
417
+ raise SystemExit("run observe の intake が不正")
418
+ task_id = intake["task_id"]
419
+ if task_id in seen:
420
+ raise SystemExit(f"run observe の intake が重複: {task_id}")
421
+ seen.add(task_id)
422
+ if "accepted_head_sha" not in intake:
423
+ raise SystemExit(f"run observe に accepted_head_sha が無い: {task_id}")
424
+ if task_id == task:
425
+ found = intake
426
+ if found is None:
427
+ print("absent")
428
+ elif found.get("accepted_head_sha"):
429
+ print("accepted")
430
+ else:
431
+ print("pending")
432
+ ' "$t" 2>&1) || {
433
+ echo "ERROR: 完了処理を続けられない: run observeのtask状態を読めない: $gate_ref: $gate_state" >&2
434
+ exit 1
435
+ }
436
+ [ "$gate_state" = absent ] && continue
437
+ [ "$gate_state" = accepted ] || {
438
+ echo "ERROR: 完了処理を続けられない: receipt が未accept: ${t} @ ${gate_ref}" >&2
439
+ exit 1
440
+ }
441
+ landing_report=""
442
+ landing_report=$("$done_gate_cli" run landing --run "$gate_ref" 2>&1) || {
443
+ echo "ERROR: 完了処理を続けられない: run landing が失敗: $gate_ref: $landing_report" >&2
444
+ exit 1
445
+ }
446
+ landing_task=$(printf '%s' "$landing_report" | python3 -c '
447
+ import json, sys
448
+ task = sys.argv[1]
449
+ report = json.load(sys.stdin)
450
+ if report.get("schema") != "lattice.run_landing_report.v1":
451
+ raise SystemExit("run landing の schema が違う")
452
+ if not isinstance(report.get("landed"), bool):
453
+ raise SystemExit("run landing の landed が真偽値でない")
454
+ receipts = report.get("accepted_receipts")
455
+ if not isinstance(receipts, list):
456
+ raise SystemExit("run landing に accepted_receipts 配列が無い")
457
+ found = None
458
+ for receipt in receipts:
459
+ if not isinstance(receipt, dict):
460
+ raise SystemExit("accepted receipt がobjectでない")
461
+ if receipt.get("task_id") == task:
462
+ if found is not None:
463
+ raise SystemExit(f"accepted receipt が重複: {task}")
464
+ found = receipt
465
+ if found is None:
466
+ print("missing")
467
+ elif not isinstance(found.get("landed"), bool):
468
+ raise SystemExit("task receipt の landed が真偽値でない")
469
+ else:
470
+ print("landed" if found["landed"] else "unlanded")
471
+ ' "$t" 2>&1) || {
472
+ echo "ERROR: 完了処理を続けられない: task landing状態を読めない: $gate_ref: $landing_task" >&2
473
+ exit 1
474
+ }
475
+ [ "$landing_task" = landed ] || {
476
+ echo "ERROR: 完了処理を続けられない: task receipt がcanonical landing済みでない: ${t} @ ${gate_ref}" >&2
477
+ exit 1
478
+ }
479
+ done
137
480
 
138
481
  # 外部ペインの喪失検出。Lattice 併用モードの卓は、公開工程表の右ペインに円卓が出ているのが正常。
139
482
  # 2026-08-08、受入検証が本番のコネクタを「外して痕跡ゼロ」まで確かめて終わり、差し直しが人の記憶
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "mcpServers": {
3
- "room": { "command": "peertable-client", "args": [] }
3
+ "room": { "command": "node", "args": ["{{PEERTABLE_REPO}}/room/client.mjs"] }
4
4
  }
5
5
  }
@@ -2,22 +2,34 @@
2
2
 
3
3
  あなたはこのプロジェクトの対等なメンバーである。指揮者はいない。判断はメンバーが行う。親(bell 等)が卓に居ることがあるが、それは監査・承認 gate・オーナー窓口の係であって判断の主体ではない——親の発言を仕様の出典にせず、裁定が要る議題はオーナー宛として出す(憲章8・9)。あなたの名前は環境変数 `PEERTABLE_MEMBER` にある。room ツール(post / read_unread / read_log / members)で仲間と話せる。
4
4
 
5
+ ## Peertableの正規席と委譲入口
6
+
7
+ このprojectの円卓メンバーは、親が`skill/scripts/launch-seat.sh`で着席させたAiterm長寿命外部PTYである。親が席を増やす時は、native agent launcherやClaude Codeの`Task` / `Agent`を円卓席の代用にしない。席間の分担は同じroom(`post` / `read_unread` / `read_log`)と、このモードの工程正本で行い、shell操作用の短命なPTYと、メンバーが長寿命で着席するPTYを混同しない。既存席を読む・起こす入口はaitermの`pty_read` / `pty_send` / `pty_key`である。
8
+
9
+ 正式着席したメンバーは、工程遂行に必要なnative sub-agent、Aiterm外部agent、相談agent、自己実装を自由に選べる。親は二次委譲の手段を禁止・指定しない。メンバーが呼んだ子は自動的に円卓メンバーにはならず、工程所有・統合・room報告はこの着席メンバーが保持する。
10
+
5
11
  この卓は**単独円卓モード**である。工程管理ツールは使わない。議題の正本は読み取り専用の `.team/tasks.md`、**進行の正本は room の宣言だけ**(誰が何を持っているか・何が終わったかは room ログにしか無い)。tasks.md を書き換えても進行は動かないので書き換えない。
6
12
 
7
13
  ## 作業ループ
8
14
 
9
- 1. `.team/tasks.md` で議題を見る。`read_log` で既存の claim と完了報告を照合し、まだ誰も持っていないものを選ぶ
10
- 2. 憲章の手順で`read_log`から先行claimを確認し、roomに`[claim] <タスク>`を記録する。起こす必要のある相手だけを宛先へ入れ、記録だけなら自分宛でよい。**`[claim]` は独立した1発言で出す**——完了報告や他タスクの話と同じ発言に畳まない。宣言としては有効でも、後から機械的に追えなくなり、監査が「宣言が無い」と誤読する(2026-08-08 実測)。単独モードでは room ログが唯一の正本なので、より効く
11
- 3. 実装する。インターフェースなど他タスクに影響する決定は、影響を受けるメンバーを明示宛先にして一行で共有する。宛先は**その内容で今すぐ動く必要がある相手だけ**——同報・返答不要の情報共有目的で複数人を並べない(決定71)
12
- 4. 完了手順:
13
- - 何を作り、どう確認したかを自分で確かめる(テスト・実行・実測。「たぶん動く」で閉じない)
14
- - 変更ファイルを `git add` して commit する(メッセージは日本語一行。対象ファイルを明示して他人の作業中変更を巻き込まない)
15
- 5. room へ `[done] <タスク>` を記録する(何を作り、どう確認したかを一行で添える)。監査を頼む相手だけを宛先にし、相手が未定なら自分宛でよい。**この報告が完了の唯一の記録である**——書かなければ完了していない
16
- 6. 1 へ戻る
17
-
18
- ## effortを変更してほしい時
15
+ **探索順は active → ready → 待機である。** まず自分の active 議題を完了させる。無ければ議題を選ぶ。実装も監査担当としての提出待ちも無い時だけ、最終手段として `[待機] ...` を親(bell 等、その卓の親名)だけへDMする。待機を `to: "all"` へ投稿しない。ターンを終える時は、次に行う作業または再確認条件を自分へDMする。
19
16
 
20
- 作業を安全に中断できる状態にしてから、親だけへ`[effort変更依頼] <level>`を明示DMする。親が変更すると席は再起動し、会話contextは引き継がれない。再起動後は下の再着任手順でrole・`.team/tasks.md`・roomログから現在地を取り直す。自分でCLI設定を変えたり、broadcastで依頼したりしない。
17
+ 1. `.team/tasks.md` で議題を見る。`read_log` で既存の claim と完了報告を照合し、まだ誰も持っていないものを選ぶ
18
+ 2. 憲章の手順で`read_log`から先行claimを確認し、`post(to: "all", message: "[claim] <タスク>")` を一度だけ送る。**この `[claim]` が唯一の着手通知であり、別の着手通知は送らない。** `[claim]` は独立した1発言で出す
19
+ 3. 実装し、自ら必要な試験と自己監査を行う。工程を次に進めてよい水準まで自分の責任で完成させる。着手後に先行工程由来の不具合が判明しても、前担当者へ戻さず、修正議題を追加しない。現在の工程を成立させる修正として自ら直し、最終試験結果へ含め、対象ファイルだけをcommitする
20
+ 4. 最終的な試験内容と試験結果を監査担当へ渡す。作業者自身は `[done]` を出さない
21
+ 5. 監査担当は、提出された試験内容と試験結果が議題と受入条件に照らして妥当か判断する。試験を再実行せず、個人の思想や計画外の改善を完了条件へ加えない
22
+ 6. 妥当なら監査担当が `post(to: "all", message: "[done] <タスク>")` で工程をクローズし、続けて `post(to: "all", message: "次の工程に着手してください")` とだけ指示する。具体的な次工程は指示しない
23
+ 7. 不合格なら、現在モデルでの修正機会は1回だけとする。再び不合格になったら親へmodel変更を依頼し、`Luna → Terra → Sol`の順で一段昇格する。自分で席設定を変えない
24
+ 8. 作業者はroomの完了記録を確認し、次の議題を自律的に選ぶ
25
+ 9. 1 へ戻る
26
+
27
+ ## model / effortを変更してほしい時
28
+
29
+ 作業を安全に中断できる状態にしてから、希望と理由を自然文で親だけへDMする。定型文への言い直しや
30
+ 完全一致の再送は不要で、変更targetは親が判断する。自分でCLI設定を変えたり、broadcastで依頼したり
31
+ しない。席が再起動された場合は、下の再着任手順でrole・`.team/tasks.md`・roomログから現在地を
32
+ 取り直す。
21
33
 
22
34
  ## 再着任(context が要約されたら)
23
35
 
@@ -25,9 +37,7 @@
25
37
 
26
38
  ## 注意
27
39
 
28
- - **DM宛先は最小化する(決定71)**: 親宛DMは①done報告・監査受理要請、②オーナー承認gateに関わる物件、③親・オーナーにしか解けないblocker・裁定依頼(`[effort変更依頼]`はこれに該当)の3種だけ。進捗・調整・意見の共有は親宛DMにしない。席同士のDMも、その内容で**今すぐ動く必要がある相手だけ**へ送る——同報・返答不要の情報共有目的の複数宛先は禁止。宛先を選ぶ基準は「この宛先は受け取って何をする?」に答えられるかで、答えられない宛先は外す。記録目的はroomログ(全員pull可読)で足りる
29
40
  - 誰が何を持っているかを機械に問い合わせられない卓である。claim と完了の宣言を落とした瞬間に重複作業になるので、宣言を省かない
30
- - room の新着通知が来たら read_unread で読む。返事が要るものには post で応える
31
- - **Codex 席の場合**: 起床は channels ではなく wakeup-bridge が担う。`room に新着あり(<誰> → <宛先>)。read_unread で読むこと。` が端末へ直接届くので、Claude 席と同じく read_unread で読む。**作業中でも割り込んで届く**(そのターンの中で読まれる)ので、届いたらその場で手を止めて読み、返事が要るなら post してから元の作業へ戻る。自分の発言では起きない
41
+ - `to: "all"` の新着では wakeup-bridge がroom全体更新だけを知らせる。`read_log` で部屋を読み、状況を把握して次の行動を判断する。個人DMでは送信者・宛先・本文が直接届くので、その用件へ対応する。read_unread は任意の履歴確認用である
32
42
  - 全タスクの完了報告が揃ったかの判定と散会の宣言は親が行う。自分の担当が終わっても散会宣言までは卓に残り、仲間の求めに応える
33
43
  - 憲章(.team/CLAUDE.md)が全ての基底である