[CLAUDE] Docs: S148 — session-model port form hub + /check-email 4 cửa + Sàn-3 dual-accept

Session-model (owner chốt "đối ứng đúng chính xác như hub"):
- Form hub: _context-s-<N>.md (STOCK-map + FLOW append-only + STOCK-touched)
  + _pause-<i>/_tiep-<i> marker 5-trường + _snapshot-<i> + _end (thay closed.md)
- Port /snapshot + scripts/session_scaffold.py (near-verbatim)
  + scripts/session_ctx.py TRIMMED CÓ KHAI (chỉ machine-block + secrets-sweep;
    KHÔNG port jsonl/overhead/cap-getter vì chưa có caller = ghost-wire)
- session-2 migrate sang form hub; session-1 giữ legacy (FROZEN)

Sàn-3 ORPHAN-L:
- DUAL-ACCEPT hub + legacy; glob pause-* KHÔNG khớp _pause-1.md nên không đếm đôi
- VÁ bug có sẵn từ S146: chốt-kết ĐÓNG TRỌN thư-mục (bản cũ c=1 chỉ tha 1 pause,
  lệch chính câu session-end §6.3-bis vẫn nói "mọi pause")
- Fault-inject 10/10 hai chiều + anti-Goodhart

/check-email:
- Wire 4 CỬA phiên (session-start/tiep/session-end/pause), 2 CHẾ-ĐỘ:
  DÒ ~5ms ở cửa dừng-nối (ràng buộc BINDING hub goi-chot §3) ⟂ KÉO ở bookend
- DÒ quét 2 kênh + định tuyến: outbox/se -> /check-email · outbox/all -> /adap-apply
- STAGE-2: 10 thư fan-out verify 2 tuyến 10/10 -> inbox/ai_infra/; backlog root = 0

HANDOFF re-stamp: #1 ĐÓNG (trio đã chạy S144) · #2 đổi trục · #3 anh chốt (a)
+ 4 mục mới (13)-(16); carry #15/#17 đóng, #16 đóng nửa (khai rõ vế còn hở)

H24 tick S147->S148: counter 21->22 CLEAN

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-24 18:03:16 +07:00
parent 6da7c8898d
commit 24483935cb
23 changed files with 676 additions and 28 deletions

View File

@ -5,6 +5,7 @@ metadata:
node_type: memory node_type: memory
type: feedback type: feedback
originSessionId: c0d6d8d1-8e0e-46bf-ac36-ab0f45d2629f originSessionId: c0d6d8d1-8e0e-46bf-ac36-ab0f45d2629f
modified: 2026-07-24T10:21:02.939Z
--- ---
3 bài học integrity từ S100 H18-adopt (2026-07-02): 3 bài học integrity từ S100 H18-adopt (2026-07-02):
@ -15,6 +16,8 @@ metadata:
4. **Sibling-test 2 CHIỀU (S125, 2026-07-16):** mismatch hash có nhánh thứ 3 ngoài tamper/EOL — **VERIFIER của MÌNH hỏng**. Discriminator: chạy verifier trên sibling known-good. Sibling MATCH ⇒ phép đúng, file bệnh (ca S100 frontier-assessment) · sibling CŨNG FAIL ⇒ **verifier bệnh** (ca S125: PS 5.1 `Get-Content -Raw` decode UTF-8-no-BOM bằng ANSI → mojibake → false-TAMPER trên broadcast hub SẠCH; đối chứng `stamp_verify.py` hub → exit-0). Fix gốc: hash CHỈ từ bytes (`ReadAllBytes` + UTF8 decode PIN) — CẤM `Get-Content -Raw` cho hash; đã vá `check-email.md`/`send-email.md` S125. 4. **Sibling-test 2 CHIỀU (S125, 2026-07-16):** mismatch hash có nhánh thứ 3 ngoài tamper/EOL — **VERIFIER của MÌNH hỏng**. Discriminator: chạy verifier trên sibling known-good. Sibling MATCH ⇒ phép đúng, file bệnh (ca S100 frontier-assessment) · sibling CŨNG FAIL ⇒ **verifier bệnh** (ca S125: PS 5.1 `Get-Content -Raw` decode UTF-8-no-BOM bằng ANSI → mojibake → false-TAMPER trên broadcast hub SẠCH; đối chứng `stamp_verify.py` hub → exit-0). Fix gốc: hash CHỈ từ bytes (`ReadAllBytes` + UTF8 decode PIN) — CẤM `Get-Content -Raw` cho hash; đã vá `check-email.md`/`send-email.md` S125.
5. **🔴 Ghost-wire class (c) — CLAIM-OF-FIX: doc khai "đã vá" một kẽ CHƯA vá, và kẽ đó KHÔNG TỒN TẠI ở chỗ nó chỉ (S148, 2026-07-24).** `C14-disposition-per-khoan.md:21` (@S144) khai *"Kẽ đã bịt: `/check-email` bước 2 chỉ đọc `outbox/se`, không nhắc `outbox/all`"*. Lead **tin bản tóm-tắt** → sửa `check-email.md` STAGE 1 đọc thêm `outbox/all`. **Cả hai vế đều sai:** (i) grep `outbox/all check-email.md` = **0 hit** ⇒ chưa hề bịt, chỉ được TUYÊN BỐ; (ii) `outbox/all` **chưa bao giờ** là việc của `/check-email` — 2 kênh 2 tool 2 sổ (`outbox/se``/check-email``_index``outbox/all``/adap-apply`**KHÔNG** vào `_index`, khai ở `_index.md` header dòng 7). Kiểm ngược bằng máy: **22/22** thư `outbox/se` đều có dòng index ⇒ **tool vốn không thủng**. ⇒ lead **vá một tool không hỏng, theo một lời khai không đo**. 🔴 **Kẽ THẬT nằm ở NHỊP, không ở TOOL:** không cửa phiên nào *dò* `outbox/all` ⇒ 10 broadcast fan-out nằm im **5 ngày**. Cùng lượt còn 1 báo-cáo sai cùng gốc: lead khai *"`_index` thiếu 17 dòng"* trong khi cả 17 là fan-out ⇒ **sổ đang đúng**, chỉ là lead áp nhầm luật cho kênh.
**Why:** stamp-then-edit + fabricated-citation là lỗi im lặng — chỉ lộ khi RE-COMPUTE đối chứng nguồn gốc; một làn chỉ kiểm "tóm-tắt trung-thành bản-thô" sẽ mù khi bản-thô bịa từ đầu. **Why:** stamp-then-edit + fabricated-citation là lỗi im lặng — chỉ lộ khi RE-COMPUTE đối chứng nguồn gốc; một làn chỉ kiểm "tóm-tắt trung-thành bản-thô" sẽ mù khi bản-thô bịa từ đầu.
**How to apply:** mọi broadcast/email cross-project → re-compute `SHA256(body)` canonical trước khi áp; mismatch → **sibling-test 2-chiều TRƯỚC**: sibling-fail ⇒ sửa verifier của MÌNH, sibling-match ⇒ HELD + báo nguồn (không suy diễn "chắc CRLF", cũng không hô tamper khi chưa loại trừ tool). Mọi report → chỉ cite run-id/số ĐÃ tồn tại; chưa chạy = placeholder tường minh. Đổi config-consumer → grep key-name toàn scripts/ xem có ai đọc thật không. Liên quan: [[faultinjection-proves-teeth]], [[canonical-spec-over-broadcast]], [[agent-return-garble-recover]]. **How to apply:** mọi broadcast/email cross-project → re-compute `SHA256(body)` canonical trước khi áp; mismatch → **sibling-test 2-chiều TRƯỚC**: sibling-fail ⇒ sửa verifier của MÌNH, sibling-match ⇒ HELD + báo nguồn (không suy diễn "chắc CRLF", cũng không hô tamper khi chưa loại trừ tool). Mọi report → chỉ cite run-id/số ĐÃ tồn tại; chưa chạy = placeholder tường minh. Đổi config-consumer → grep key-name toàn scripts/ xem có ai đọc thật không. Liên quan: [[faultinjection-proves-teeth]], [[canonical-spec-over-broadcast]], [[agent-return-garble-recover]].

View File

@ -5,7 +5,7 @@ metadata:
node_type: memory node_type: memory
type: feedback type: feedback
originSessionId: cc5c42e6-f9aa-404c-adf6-26079c40d118 originSessionId: cc5c42e6-f9aa-404c-adf6-26079c40d118
modified: 2026-07-22T05:34:19.739Z modified: 2026-07-24T09:10:28.062Z
--- ---
At S50 (2026-06-07 session-end), the 2 INFORM-only monitor subs (tooling-auditor H1 + harvest-curator H2) — briefed **propose-only**, and which **reported "wrote nothing"** — were nonetheless the only plausible authors of ~7 canonical/agent-memory file writes done outside em-main: `error-ledger.md` (2 guard promotions + #57 coords), 3 `adap-reports` (nac→verified-runtime), 4 `agent-memory/*` Recent-activity, and parts of `STATUS.md` (Recently-Done block + In-Progress flip + RAG-line reconcile). mtimes clustered in the 00:0000:05 monitor window. em-main `git diff` at the commit-gate caught every line → all accurate / benign / 0-mojibake / chunk 2415 (no RAG corruption) → adopted per AS-10 keep-if-correct. Logged blameless as **E-006** in the error-ledger. At S50 (2026-06-07 session-end), the 2 INFORM-only monitor subs (tooling-auditor H1 + harvest-curator H2) — briefed **propose-only**, and which **reported "wrote nothing"** — were nonetheless the only plausible authors of ~7 canonical/agent-memory file writes done outside em-main: `error-ledger.md` (2 guard promotions + #57 coords), 3 `adap-reports` (nac→verified-runtime), 4 `agent-memory/*` Recent-activity, and parts of `STATUS.md` (Recently-Done block + In-Progress flip + RAG-line reconcile). mtimes clustered in the 00:0000:05 monitor window. em-main `git diff` at the commit-gate caught every line → all accurate / benign / 0-mojibake / chunk 2415 (no RAG corruption) → adopted per AS-10 keep-if-correct. Logged blameless as **E-006** in the error-ledger.
@ -14,4 +14,6 @@ At S50 (2026-06-07 session-end), the 2 INFORM-only monitor subs (tooling-auditor
**⚠️ S143 (2026-07-22) — premise correction, measured:** the sentence below assumed `Write` had been *removed* from monitor tool-grants, leaving only a Bash residual. **That premise is false at runtime.** Measured on 6 read-only roles (3× `harness-*` trio, 2× H24 auditors, `reviewer`; `tooling-auditor`/`harvest-curator`/`investigator-codebase` same shape): none declare `Write`/`Edit` in their frontmatter `tools:` — yet the **runtime roster still grants `Write, Edit`**, appended at the **end** of the tool list. The tell is positional: `implementer-backend` declares `Edit, Write` at slots 2-3 and its runtime list preserves that file order, so the other six are being **appended at spawn time**, not read from file. ⇒ a frontmatter whitelist is a **statement of intent, not an enforcement mechanism**; "chặn-bằng-thiếu-tool" in any spec is an overclaim (G-015). The residual channel is therefore wider than Bash. Mechanism unknown (hypothesis: `memory: project` needs diary writes — **unverified**, do not assert). Same two-layer shape as [[feedback_permission_grant_two_layers]]: declared-layer ⟂ granted-layer are independent, and verifying one proves nothing about the other. Reported upward in `broadcasts/outbox/ai_infra/2026-07-22-se-to-ai_infra-bao-nac-wave-dot-9-10.md` §3(b). **⚠️ S143 (2026-07-22) — premise correction, measured:** the sentence below assumed `Write` had been *removed* from monitor tool-grants, leaving only a Bash residual. **That premise is false at runtime.** Measured on 6 read-only roles (3× `harness-*` trio, 2× H24 auditors, `reviewer`; `tooling-auditor`/`harvest-curator`/`investigator-codebase` same shape): none declare `Write`/`Edit` in their frontmatter `tools:` — yet the **runtime roster still grants `Write, Edit`**, appended at the **end** of the tool list. The tell is positional: `implementer-backend` declares `Edit, Write` at slots 2-3 and its runtime list preserves that file order, so the other six are being **appended at spawn time**, not read from file. ⇒ a frontmatter whitelist is a **statement of intent, not an enforcement mechanism**; "chặn-bằng-thiếu-tool" in any spec is an overclaim (G-015). The residual channel is therefore wider than Bash. Mechanism unknown (hypothesis: `memory: project` needs diary writes — **unverified**, do not assert). Same two-layer shape as [[feedback_permission_grant_two_layers]]: declared-layer ⟂ granted-layer are independent, and verifying one proves nothing about the other. Reported upward in `broadcasts/outbox/ai_infra/2026-07-22-se-to-ai_infra-bao-nac-wave-dot-9-10.md` §3(b).
**How to apply:** Before ANY `/session-end` commit where monitor/sub agents ran, ALWAYS `git status` + `git diff` + chunk-count and **review every non-em-main line** — adopt-if-correct (AS-10) or revert; never blind-commit. If a sub's self-report ("wrote nothing") contradicts git-diff → **Fidelity flag** (escalate reviewer). Recommend to anh/AI_INFRA: harden monitor tool-grant (Write removal leaves Bash residual → consider a hook blocking sub-Bash-write to tracked paths) — but that is a charter-v2 infra decision. Links: [[feedback_store_memory_rebootstrap_protection]] · [[feedback_harness_123_adoption]] · [[feedback_session_end_memory_write_verify]]. **🔴 S148 (2026-07-24) — RE-MEASURED live, count is 15 not 6:** the "6 roles" above was an S143-era undercount (S143 itself already revised it to 9 mid-entry). Fresh count from disk + this session's runtime registry: **20 agent files 5 legitimate write-specialists (`implementer-backend`, `implementer-frontend`, `frontend-designer`, `office-document`, `test-specialist`) = 15 read-only roles that declare NO `Write`/`Edit` in frontmatter `tools:` — and the runtime grants `Write, Edit` to all 15.** Two-sided proof: `.claude/agents/reviewer.md:7` declares `[Read, Grep, Glob, Bash, mcp__rag-unified__*]` while the session's agent listing shows `…, Write, Edit`; `harness-eval`'s own description string says *"KHÔNG store_memory, KHÔNG Write/Edit"* and it is granted both. 🔸 **The number grows with the roster, it is not a worsening condition** — don't read a rising count as a regression. 🔴 **Quote a count here only if you just measured it** — this line has now been wrong twice (6 → 9 → 15) precisely because it was re-cited from memory instead of re-counted.
**How to apply:** Before ANY `/session-end` commit where monitor/sub agents ran, ALWAYS `git status` + `git diff` + chunk-count and **review every non-em-main line** — adopt-if-correct (AS-10) or revert; never blind-commit. If a sub's self-report ("wrote nothing") contradicts git-diff → **Fidelity flag** (escalate reviewer). 🔒 **DO NOT re-propose hardening — anh decided @S148 (2026-07-24): option (a), keep as-is, NO `PreToolUse` hook blocking sub-writes to tracked paths.** So the frontmatter whitelist stays **documentation of intent**, and `git diff` at the commit-gate is the *only* real containment — it catches **after** the write, never **before**. Re-raise only if anh reopens it or the hub returns a mechanism answer (`[carry:tools-whitelist-no-teeth]` is now narrowed to the hub-confirmation leg alone). Anh's framing was *"có j audit sau"* — accept the residual risk now, catch it in audit later; that is a deliberate trade, not an oversight to fix behind his back. Links: [[feedback_store_memory_rebootstrap_protection]] · [[feedback_harness_123_adoption]] · [[feedback_session_end_memory_write_verify]].

View File

@ -9,10 +9,58 @@ argument-hint: <from_project | all>
## Tham số ## Tham số
- `$1` = from_project (BẮT BUỘC) ∈ 6 others, hoặc `all` = quét cả 6. - `$1` = from_project (BẮT BUỘC) ∈ 6 others, hoặc `all` = quét cả 6.
## Nhịp chạy — 🔴 4 CỬA PHIÊN (owner chốt S148, 2026-07-24)
> 🔴 **Đây là NHÀ CANONICAL của "khi nào chạy `/check-email`".** 4 file lệnh cửa **chỉ TRỎ vào đây** rồi gọi lệnh; **CẤM chép luật sang** (B1 — chép 4 nơi = 4 nguồn sự-thật).
**Anh chốt @S148:** *"mỗi session end/start luôn — vì 1 session giờ có thể kéo dài 2-3 phiên qua 2-3 ngày."*
⇒ nhịp neo vào **SỰ-KIỆN PHIÊN**, **KHÔNG** vào đồng-hồ ngày. Lý-do anh nêu là lý-do kỹ-thuật đúng: một phiên-LOGIC trải nhiều cửa-sổ nhiều ngày ⇒ ngưỡng-theo-ngày **không map** được vào nhịp làm việc thật.
🔴 **Vì sao 4 cửa chứ không 2 — lead MỞ RỘNG từ câu chữ, khai thẳng để anh bác được:** cửa VÀO có 2 và cửa RA có 2.
| | cửa-1 | cửa-2 |
|---|---|---|
| **VÀO** | `/session-start` (phiên mới) | `/tiep` (nối phiên-logic) |
| **RA** | `/session-end` (đóng sổ) | `/pause` (dừng chủ-động) |
Chỉ cắm 2 cửa `session-start`/`session-end` thì **đúng ca anh đang lo VẪN LỌT**: phiên S148 vào bằng `/tiep`, phiên trước ra bằng `/pause`**cả hai lần đều không check**. Mà chính câu *"1 session kéo 2-3 phiên"* hàm ý các phiên GIỮA vào bằng `/tiep`**phần lớn cửa nằm ở nhánh `/tiep`**, không ở `/session-start`. Phủ 4 cửa mới đạt được điều anh nói.
### 🔴 HAI CHẾ-ĐỘ — sửa @S148 sau khi đọc thư `goi-chot-owner-nam-khoan` khoản 3 (BINDING)
> **Lead tự bắt vi-phạm của chính mình:** bản đầu @S148 cắm **PULL đầy-đủ** vào cả 4 cửa. Thư hub `2026-07-19-Governance-goi-chot-owner-nam-khoan` §3 là **ràng-buộc thiết-kế CỨNG**, verbatim: *"bất kỳ van/gate/phép-đo nào về sau cũng **KHÔNG ĐƯỢC** chặn hoặc làm chậm các cửa **dừngnốicheckpoint**"*, floor: *"bước nào nặng (spawn agent, đo đạc lớn, **quét rộng**) đang nằm trong đường pause-tương-đương là **vi-phạm**: dời nó về **closeout/bookend**"*. ⇒ `/pause` + `/tiep` **là đúng 2 cửa đó**.
> 🔴 **Hub bắt ĐO chứ không đoán** (SELF-CHECK: *"Đo wall-clock một lần — điểm dừng phải rẻ"*). **Đo @S148, 3 lượt:** **dò** (list 6 repo + so id) = **66ms lạnh / 45ms nóng**; **kéo** (copy + hash + `python stamp_verify.py`) = python-startup **~200500ms × số thư**. ⇒ **dò KHÔNG nặng, kéo MỚI nặng.** Nên vá đúng là **tách 2 chế-độ**, KHÔNG phải bỏ cửa — bỏ cửa sẽ mất đúng thứ anh cần (phiên giữa vào bằng `/tiep`).
| chế-độ | cửa | làm gì | chi-phí |
|---|---|---|---|
| **DÒ** (detect-only) | `/tiep` · `/pause` | list + so id → in `thu-moi: se=X all=Y`. 🔴 **KHÔNG copy · KHÔNG hash · KHÔNG python.** X hoặc Y > 0 ⇒ nêu **1 dòng** rồi đi tiếp, để bookend kéo | ~5ms |
| **KÉO** (đầy-đủ) | `/session-start` · `/session-end` | `se>0`**`/check-email`** STAGE 1 (copy + verify 2 tuyến + log `_index`) · `all>0`**`/adap-apply`** | theo số thư |
🔴 **DÒ PHẢI QUÉT CẢ HAI KÊNH — đây là kẽ thật, và nó nằm ở NHỊP chứ không ở tool:** `outbox/se` (directed, → `/check-email`) **VÀ** `outbox/all` (fan-out, → `/adap-apply`). Không có nhánh `all` thì **10 broadcast đợt adap-11 nằm im 5 ngày** (07-18→07-23) — ca đã xảy ra thật, không phải giả-định. **DÒ chỉ ĐẾM và ĐỊNH TUYẾN, không kéo** ⇒ vẫn rẻ.
🔸 **Watermark cho `outbox/all`:** bỏ qua id ≤ **`2026-07-15`** — 51 file mốc 06-02→07-15 là lớp *đã-adopt-đọc-tại-chỗ* (`/adap-apply` đọc thẳng bên AI_INFRA, **KHÔNG đòi copy** ⇒ vắng mặt trong inbox là **BÌNH THƯỜNG**, không phải nợ). Không có watermark thì mỗi lượt DÒ kêu 51 cái rồi tự bị bỏ qua.
🔒 **Bất-biến:** việc NẶNG chỉ sống ở **bookend**. Ai thêm bước vào đường `/pause`·`/tiep` sau này **phải đo wall-clock trước**, đúng câu hỏi hub đặt: *"có làm điểm dừng đắt lên không"***TRƯỚC** khi bàn giá-trị của bước đó.
**Cách chạy ở mỗi cửa — `/check-email all`:**
- 🟢 **Không có thư mới ⇒ 1 dòng, im lặng đi tiếp.** KHÔNG báo-cáo dài, KHÔNG chờ anh.
- 🔴 **FAIL-SOFT, CẤM chặn nghi-thức:** lỗi bất-kỳ (repo bên kia không có trên máy · path đổi · thiếu python) ⇒ in `check-email loi (khong chan)` rồi **ĐI TIẾP**. Cùng khuôn `nhip-no-probe.ps1`: một bước MỚI **không bao giờ được phép** làm hỏng nghi-thức đã chạy tốt.
- Có thư ⇒ chạy trọn **STAGE 1** (copy + verify hash). **STAGE 2** (move → `processed`) đi theo việc xử-lý, **KHÔNG ép** trong cùng lượt.
**Quan-hệ với vế-4 `pull-cach` (`nhip-no-probe.ps1`) — ĐỔI VAI:** trước định làm **chuông báo** (cần ngưỡng `pull_warn_days`). Nay 4 cửa là cơ-chế CHÍNH ⇒ `pull-cach` thành **chỉ-báo sức-khoẻ của chính nghi-thức**: số ngày cứ leo trong khi phiên vẫn mở/đóng đều đặn ⇒ **nghi-thức đang bị chạy tắt**, chứ không phải hub im. 🔒 **`pull_warn_days` CỐ Ý để TRỐNG** — anh không đặt số @S148, và script cấm tự chế mặc-định.
🔸 **Giới-hạn giữ khai (đo @S148):** vế-4 mạnh với **quãng im DÀI**, yếu với **trễ-kéo NGẮN** — ca hub-gửi-giục 07-18 chỉ `N=2` mới bắt, mà `N=2` nằm ngay trên trung-vị ⇒ kêu gần như liên-tục. Đừng kỳ vọng `pull-cach` chặn tái-diễn ca đó; **4 cửa mới là thứ chặn nó.**
## Quy trình 2-STAGE (audit qua folder) ## Quy trình 2-STAGE (audit qua folder)
**STAGE 1 — Nhận (đọc → inbox root, PENDING):** **STAGE 1 — Nhận (đọc → inbox root, PENDING):**
1. Validate `$1`. 1. Validate `$1`.
2. READ `<from>/broadcasts/outbox/se/*.md` (message gửi cho se). 2. READ `<from>/broadcasts/outbox/se/*.md` (message gửi **đích danh** se).
🔴 **CHỈ `outbox/se` — ĐÚNG, đừng "sửa" thành đọc cả `outbox/all`.** Hai kênh, hai tool, hai sổ:
| kênh | nội dung | tool kéo | sổ theo dõi |
|---|---|---|---|
| `outbox/se/` | thư **directed** (`to: se`) | **`/check-email`** (file này) | `broadcasts/_index.md` §INBOUND |
| `outbox/all/` | **fan-out** (`to: all-fit`) | **`/adap-apply`** ([`adap-apply.md:14`](adap-apply.md) đọc **thẳng tại chỗ**) | **KHÔNG** vào `_index` — xem header `_index.md` dòng 7 |
🧊 **Vết sai @S148 — giữ làm bài học:** lead đọc `C14-disposition-per-khoan.md:21` (@S144) khai *"kẽ đã bịt: `/check-email` bước 2 chỉ đọc `outbox/se`, không nhắc `outbox/all`"***tin bản tóm-tắt** → sửa bước này thành đọc cả 2 kênh. **Sai:** `outbox/all` chưa bao giờ là việc của `/check-email`. Kiểm ngược bằng máy: **22/22** thư `outbox/se` đều có dòng `_index`, **thiếu 0** ⇒ tool này vốn **không hề thủng**. 🔴 **Hai lỗi chồng nhau, cả hai đều là *tin chữ thay vì đo*:** C14 tuyên "đã bịt" một kẽ **không tồn tại**, rồi lead vá một tool **không hỏng**.
🔴 **Kẽ THẬT nằm ở NHỊP, không ở tool:** không cửa phiên nào **dò** `outbox/all` ⇒ 10 broadcast fan-out nằm im **5 ngày**. Vá đúng = **DÒ 2 kênh ở cửa phiên rồi ĐỊNH TUYẾN** (§"Nhịp chạy" trên), KHÔNG phải nhét kênh này vào STAGE 1 của tool kia.
3. Mỗi file CHƯA có trong inbox (so id): **COPY VERBATIM**`broadcasts/inbox/<id>.md` (**root = pending**). [repo MÌNH §J2] 3. Mỗi file CHƯA có trong inbox (so id): **COPY VERBATIM**`broadcasts/inbox/<id>.md` (**root = pending**). [repo MÌNH §J2]
4. **Verify đối chứng:** (a) **whole-file** `Get-FileHash` copy == nguồn (byte-identical = tuyến CHÍNH); (b) **body** recompute `SHA256(body)` == `content_sha256` khai ở frontmatter. 4. **Verify đối chứng:** (a) **whole-file** `Get-FileHash` copy == nguồn (byte-identical = tuyến CHÍNH); (b) **body** recompute `SHA256(body)` == `content_sha256` khai ở frontmatter.

View File

@ -22,7 +22,33 @@ Thực-hiện **tuần-tự 5 bước** sau; trong lúc chạy **KHÔNG nhận v
## 2.6 Ghi context-block vào thư-mục phiên-LOGIC (S146 — lane CỘNG-DỒN, cặp với `/tiep §2.6`) ## 2.6 Ghi context-block vào thư-mục phiên-LOGIC (S146 — lane CỘNG-DỒN, cặp với `/tiep §2.6`)
- **Append MỘT khối mới** vào `.claude/sessions/session-<N>/pause-S<nn>-<yyyyMMddTHHmm>.md` (`<N>` = phiên-LOGIC **đang mở** — xem [`session-start.md`](session-start.md) BƯỚC 0.8). 🔴 **FORM HUB (owner chốt @S148 "đối ứng đúng chính xác như hub") — TÁCH marker ⟂ narrative, KHÔNG gộp 1 file như form SE cũ:**
**(A) Marker `_pause-<i>.md`** — `<i>` = số TUẦN TỰ trong thư-mục (`max(_pause-*)+1`), **KHÔNG** nhãn `S<nn>`, **KHÔNG** timestamp trong tên. Đúng **5 trường, ~200B**, không thêm bớt:
```
ts: <ISO-8601 +07:00>
head-sha: <full sha, git rev-parse HEAD>
window-ordinal: <thứ mấy trong phiên-logic>
jsonl-hint: <project-dir>/<session-uuid>
account-label: none
```
**(B) Narrative → APPEND vào `_context-s-<N>.md`** (một file DUY NHẤT/phiên-logic, cộng dồn) — thêm đúng 1 entry `### PAUSE-<k> <ts>` dưới `FLOW-START`:
- 🔴 **Dòng đầu entry = `> anh: <tin-nhắn anh VERBATIM>`** — chép **nguyên văn**, CẤM paraphrase. Không có tin-nhắn ⇒ ghi `(THIẾU — lý do)`, **CẤM dựng lại từ trí nhớ**.
- Rồi 3 mục cũ: **(1) quyết-định đã CHỐT** · **(2) delta còn SỐNG** · **(3) con-trỏ**.
- 🔴 **FLOW append-only, entry cũ IMMUTABLE** — CẤM rewrite, CẤM tự-tóm (chống self-summary lossy). Vượt `session_ctx_kb` ⇒ distill **SECTION CŨ** thành pointer, KHÔNG sửa entry verbatim.
**(C) Máy-derive `§(c) STOCK-touched`** — `python scripts/session_ctx.py machine-block --session <N> --json` → điền bảng §(c) của `_context-s-<N>.md`. **KHÔNG điền tay** (template ràng "máy-derive @pause/snapshot").
🔸 **Khai giới-hạn:** `changed_files` = `git diff anchor..HEAD`**chỉ phần ĐÃ COMMIT**; việc còn dirty không hiện. Đừng đọc thành "toàn bộ việc đã làm".
**(D) AUTO-SNAPSHOT — chạy `/snapshot` §2→§4 ngay trước bước 3** *(đóng `[carry:pause-autosnap]` #17, anh chốt @S148)*:
-**Đo trước khi wire, đúng câu hub bắt hỏi** (`goi-chot` §3 — *"có làm điểm dừng đắt lên không"* phải hỏi TRƯỚC khi bàn giá-trị): `machine-block` **~140ms** · `secrets-sweep` **~64ms** (3 lượt, tb).
- 🔴 **Chi-phí RÒNG của auto-snap chỉ là ~64ms + 1 write**, KHÔNG phải 204ms: `machine-block` **vốn đã phải chạy** ở (C) cho §(c) STOCK-touched. `/pause` cũng vốn đã commit sẵn ở bước 4.
- 🔸 **Ranh giữ khai:** hub **KHÔNG** ràng 1 pause = 1 snapshot (đo đĩa hub: `session-102` pause=3/snap=5 · `session-107` pause=2/snap=1). SE chọn auto-snap-tại-pause là **quyết-định SE**, không phải floor hub — ai thấy đắt thì gỡ, đã có số để cãi.
🧊 **Form SE cũ `pause-S<nn>-<ts>.md` (gộp marker+narrative) = LEGACY, CẤM viết mới.** Sàn-3 ⑤ vẫn **nhận** nó (dual-accept, `tiep.md §0`) vì `session-1` còn dùng — đúng luật retire: *gỡ nhánh legacy khi tập di-sản RỖNG, giữ khi CÒN người thụ-hưởng*.
- `<N>` = phiên-LOGIC **đang mở** — xem [`session-start.md`](session-start.md) BƯỚC 0.8.
- 🔴 **Scaffold idempotent (owner chốt @S146 "giống hub"):** thư-mục chưa có ⇒ **TẠO**; đã có ⇒ **DÙNG LẠI, CẤM đè**. Chạy lại nhiều lần vô hại. - 🔴 **Scaffold idempotent (owner chốt @S146 "giống hub"):** thư-mục chưa có ⇒ **TẠO**; đã có ⇒ **DÙNG LẠI, CẤM đè**. Chạy lại nhiều lần vô hại.
- 🔴 **NHƯNG `/pause` KHÔNG BAO GIỜ tự tăng `<N>`** — cấp-số là **độc-quyền `/session-start`** (BƯỚC 0.8, canonical; B1 — mục này chỉ TRỎ). `/pause` chỉ tạo thư-mục cho số **đang mở**. Không có số nào đang mở (vào thẳng bằng `/tiep` rồi `/pause`, chưa từng `/session-start`) ⇒ dùng `max+1` **và ghi 1 dòng khai** trong khối: `N-cap-ngoai-session-start: <N> (ly-do: <...>)` — để `<N>` không bao giờ xuất-hiện mà không ai biết nó ở đâu ra. - 🔴 **NHƯNG `/pause` KHÔNG BAO GIỜ tự tăng `<N>`** — cấp-số là **độc-quyền `/session-start`** (BƯỚC 0.8, canonical; B1 — mục này chỉ TRỎ). `/pause` chỉ tạo thư-mục cho số **đang mở**. Không có số nào đang mở (vào thẳng bằng `/tiep` rồi `/pause`, chưa từng `/session-start`) ⇒ dùng `max+1` **và ghi 1 dòng khai** trong khối: `N-cap-ngoai-session-start: <N> (ly-do: <...>)` — để `<N>` không bao giờ xuất-hiện mà không ai biết nó ở đâu ra.
- 🔴 **ACCUMULATE — CẤM ghi đè, CẤM sửa khối cũ.** Mỗi `/pause` = **một file mới**. (Ghi đè = mất chính thứ mà lane này sinh ra để giữ.) - 🔴 **ACCUMULATE — CẤM ghi đè, CẤM sửa khối cũ.** Mỗi `/pause` = **một file mới**. (Ghi đè = mất chính thứ mà lane này sinh ra để giữ.)
@ -31,6 +57,14 @@ Thực-hiện **tuần-tự 5 bước** sau; trong lúc chạy **KHÔNG nhận v
- **CẤM secret** (thư-mục này sẽ được commit — cùng kỷ-luật WAL bước 3). - **CẤM secret** (thư-mục này sẽ được commit — cùng kỷ-luật WAL bước 3).
- 🔸 Hook `wal-flush.ps1` đã phủ `.claude/sessions/` (path thứ-4, S146) ⇒ khối này **tự vào `wal:` commit** ở turn-boundary. Vẫn **PHẢI add đích-danh** ở bước 4 (đừng dựa vào hook để chốt điểm dừng — hook là lưới, không phải nghi-thức). - 🔸 Hook `wal-flush.ps1` đã phủ `.claude/sessions/` (path thứ-4, S146) ⇒ khối này **tự vào `wal:` commit** ở turn-boundary. Vẫn **PHẢI add đích-danh** ở bước 4 (đừng dựa vào hook để chốt điểm dừng — hook là lưới, không phải nghi-thức).
## 2.7 Kéo thư cross-project (cửa RA 2/2 — owner chốt S148)
**DÒ 2 kênh** (`outbox/se` + `outbox/all` × 6 repo) — **TRƯỚC** khi ghi WAL (bước 3), để `thu-moi: se=X all=Y` kịp vào `carry:`/context-map nếu có.
🔴 **CHỈ list + so id. KHÔNG copy · KHÔNG hash · KHÔNG python**`/pause`**cửa dừng**, ràng-buộc BINDING của hub (`goi-chot` §3) cấm làm nó đắt lên. Kéo thật = việc của bookend.
> 🔗 **Luật nhịp đầy-đủ = canonical [`check-email.md`](check-email.md) §"Nhịp chạy — 4 CỬA PHIÊN".** 🔴 **B1 — CHỈ TRỎ + GỌI, CẤM chép luật.**
> 🔴 **KHÔNG chặn điểm dừng:** lỗi ⇒ `check-email loi (khong chan)` → đi tiếp bước 3. `/pause` tồn-tại để **dừng an-toàn**; một bước kéo thư hỏng **không được phép** giữ anh lại.
## 3. Cập-nhật `.claude/WAL.md` đúng schema ## 3. Cập-nhật `.claude/WAL.md` đúng schema
- **`chain:`** — đầy-đủ mọi mục trạng-thái (`[x]` đã-xong · `[!]` đang-dở · `[ ]` chưa-làm), **KÈM BẰNG-CHỨNG** là một lệnh / đường-dẫn / hash chứng-minh trạng-thái đó (KHÔNG mô-tả suông). - **`chain:`** — đầy-đủ mọi mục trạng-thái (`[x]` đã-xong · `[!]` đang-dở · `[ ]` chưa-làm), **KÈM BẰNG-CHỨNG** là một lệnh / đường-dẫn / hash chứng-minh trạng-thái đó (KHÔNG mô-tả suông).
- **`next:`** — đúng **một mệnh-lệnh cụ-thể**: làm gì tiếp theo. - **`next:`** — đúng **một mệnh-lệnh cụ-thể**: làm gì tiếp theo.

View File

@ -21,6 +21,14 @@ Em main PHẢI echo **TOÀN BỘ nội dung command body này** (đầy đủ Ph
> **Vì sao ở BƯỚC 0, không muộn hơn:** phiên bị cắt **giữa lúc đóng** là ca nguy-hiểm nhất — WAL đã flush một phần, chưa push, trông như sạch. Đặt dấu TRƯỚC mọi flush ⇒ phiên kế **thấy `closing:` treo** và biết là bị cắt (Sàn-3 bậc-MẠNH ③, `tiep.md` §0.b). > **Vì sao ở BƯỚC 0, không muộn hơn:** phiên bị cắt **giữa lúc đóng** là ca nguy-hiểm nhất — WAL đã flush một phần, chưa push, trông như sạch. Đặt dấu TRƯỚC mọi flush ⇒ phiên kế **thấy `closing:` treo** và biết là bị cắt (Sàn-3 bậc-MẠNH ③, `tiep.md` §0.b).
> **Net-zero:** §6.4 vốn **LUÔN chạy** ⇒ thêm 1 dòng xoá = 0 chi-phí. Acceptance: chạy no-op trọn-vẹn ⇒ `grep -c '^closing:' .claude/WAL.md` = **0**; cắt giữa chừng ⇒ phiên kế **thấy** dấu. > **Net-zero:** §6.4 vốn **LUÔN chạy** ⇒ thêm 1 dòng xoá = 0 chi-phí. Acceptance: chạy no-op trọn-vẹn ⇒ `grep -c '^closing:' .claude/WAL.md` = **0**; cắt giữa chừng ⇒ phiên kế **thấy** dấu.
## 📋 BƯỚC 0.5 — Kéo thư cross-project (cửa RA 1/2 — owner chốt S148)
**DÒ 2 kênh** rồi **KÉO đầy-đủ** theo định-tuyến (`se>0``/check-email all` · `all>0``/adap-apply`) — **TRƯỚC Phase 1 FLUSH**.
> 🔗 **Luật nhịp đầy-đủ = canonical [`check-email.md`](check-email.md) §"Nhịp chạy — 4 CỬA PHIÊN".** 🔴 **B1 — CHỈ TRỎ + GỌI, CẤM chép luật.**
> 🔴 **Vì sao TRƯỚC flush, không sau:** closeout **ghi sổ** (STATUS · HANDOFF · session-log · carry). Thư về sau khi sổ đã ghi thì **lỡ mất kỳ này** và nằm chờ tới cửa vào kế. Kéo trước ⇒ thư mới kịp vào đúng bản ghi của phiên.
> 🔴 **KHÔNG chặn closeout:** lỗi ⇒ `check-email loi (khong chan)` → đi tiếp Phase 1. Sàn-2 sentinel ở BƯỚC 0 đã đặt TRƯỚC bước này ⇒ cắt giữa chừng vẫn phát-hiện được.
## Phase 1 — FLUSH (sub-agent memory) ## Phase 1 — FLUSH (sub-agent memory)
**Điều kiện:** Chỉ xử lý con đã spawn trong session. KHÔNG spawn mới chỉ để flush (agent đã update MEMORY khi return). **Điều kiện:** Chỉ xử lý con đã spawn trong session. KHÔNG spawn mới chỉ để flush (agent đã update MEMORY khi return).
@ -360,13 +368,16 @@ git push origin main # → git.baocaogiaoduc.vn/vietrep
### 6.3-bis 🔒 Chốt-kết `_end` — đóng băng phiên-LOGIC (owner chốt @S146 "giống hub") ### 6.3-bis 🔒 Chốt-kết `_end` — đóng băng phiên-LOGIC (owner chốt @S146 "giống hub")
> Floor manifest `101e69d6` §1: dấu-mốc thứ-4 = **"Chốt-kết (`_end`) — dấu đóng băng khi phiên-LOGIC thực sự khép lại"**. SE form = `closed.md`. Chân này **trước S146 CHƯA wire** ⇒ cân-sổ ORPHAN-L chỉ có nửa vào, không có nửa khoá (kêu oan ca *"pause rồi đóng thẳng không qua `/tiep`"*). Nay đóng. > Floor manifest `101e69d6` §1: dấu-mốc thứ-4 = **"Chốt-kết (`_end`) — dấu đóng băng khi phiên-LOGIC thực sự khép lại"**. 🔴 **SE form = `_end` kể từ S148** (owner chốt *"đối ứng đúng chính xác như hub"*); form cũ `closed.md` = **LEGACY, CẤM viết mới** — `session-1` còn dùng nên Sàn-3 vẫn nhận. Chân này **trước S146 CHƯA wire** ⇒ cân-sổ ORPHAN-L chỉ có nửa vào, không có nửa khoá (kêu oan ca *"pause rồi đóng thẳng không qua `/tiep`"*). Nay đóng.
- **Ghi `.claude/sessions/session-<N>/closed.md`** cho `<N>` đang mở — **FROZEN, ghi MỘT lần, CẤM sửa về sau**. Nội dung tối-thiểu **4 trường**: - **Ghi `.claude/sessions/session-<N>/_end`** cho `<N>` đang mở — 🔴 **TÊN `_end`, KHÔNG đuôi `.md`** (form hub, owner chốt @S148 *"đối ứng đúng chính xác"*). **FROZEN, ghi MỘT lần, CẤM sửa về sau**. Khuôn `key: value`, tối-thiểu:
`phien-logic: L<N>` · `cua-so: S<a>..S<b>` (các nhãn vật-lý thuộc phiên-logic này) · `closeout-commit: <sha>` · `ket-qua: <1-3 dòng arc đã đóng>`. `summary-frozen-s<N>:` (1 dòng arc đã đóng) · `pointer:` (session-log/SUMMARY) · `carry:` (`a · b · c`) · `pending-anh:` · `pointers:` (run-folder) · `h24-tick: counter=<n>` · `markers: _pause=<p> · _tiep=<t> · _snapshot=<s>`.
- **Cân-sổ (khoá vòng ORPHAN-L):** sau khi ghi, **mọi `pause-*.md` trong thư-mục coi như ĐÃ ĐỐI-ỨNG** — `closed.md` đóng vai đối-ứng cho khối `pause` cuối chưa có `resume-*`. ⇒ [`tiep.md §0`](tiep.md) block **⑤ ORPHAN-L** hết kêu oan. 🔴 **Trường `markers:` không phải trang-trí** — nó là **bản chụp cân-sổ tại lúc đóng**; sau khi đóng, detector bỏ qua thư-mục nên đây là **chứng duy nhất** còn lại về số lần dừng/nối.
- 🔴 **Idempotent:** `closed.md` đã tồn-tại ⇒ **NO-OP, CẤM ghi đè** (phiên-logic đã khép; closeout chạy lại không được "mở lại rồi khép lại"). 🧊 Form SE cũ = `closed.md` (4 trường prose) — **LEGACY, CẤM viết mới**; `session-1` còn dùng nên Sàn-3 vẫn nhận (dual-accept).
- 🔴 **Thư-mục `session-<N>/` KHÔNG tồn-tại ⇒ NO-OP im-lặng** — phiên này không mở phiên-logic nào (vd vào thẳng bằng `/tiep`, chưa từng `/session-start`). **CẤM tự tạo thư-mục chỉ để ghi `closed.md`** — tạo-rồi-đóng-ngay là **sổ rỗng giả**, đúng loại ĐẠT-ảo mà fail-safe NO-OP khắp harness đang cấm. - **Cân-sổ (khoá vòng ORPHAN-L):** ghi xong ⇒ **thư-mục ĐÓNG TRỌN, MỌI pause coi như đã đối-ứng** — [`tiep.md §0`](tiep.md) ⑤ **bỏ qua hẳn** thư-mục đã chốt-kết.
🔴 **Vá @S148 (fault-inject bắt):** bản cũ code `c=1` rồi so `p > r + c` ⇒ chỉ tha **ĐÚNG MỘT** pause chưa khớp ⇒ phiên đã đóng mà có **≥2** pause dư vẫn **KÊU OAN** — lệch đúng câu doc này vẫn luôn nói ("mọi pause"). Lệch sống từ S146, không lộ vì SE mới có `p=1`.
- 🔴 **Idempotent:** `_end` (hoặc `closed.md` legacy) đã tồn-tại ⇒ **NO-OP, CẤM ghi đè** (phiên-logic đã khép; closeout chạy lại không được "mở lại rồi khép lại").
- 🔴 **Thư-mục `session-<N>/` KHÔNG tồn-tại ⇒ NO-OP im-lặng** — phiên này không mở phiên-logic nào (vd vào thẳng bằng `/tiep`, chưa từng `/session-start`). **CẤM tự tạo thư-mục chỉ để ghi `_end`** — tạo-rồi-đóng-ngay là **sổ rỗng giả**, đúng loại ĐẠT-ảo mà fail-safe NO-OP khắp harness đang cấm.
- **Add đích-danh** ở §5.1 (thư-mục phiên nằm trong hook-4-path nên hook cũng nuốt, nhưng **đừng dựa hook để chốt** — cùng lý-lẽ `pause.md §2.6`). - **Add đích-danh** ở §5.1 (thư-mục phiên nằm trong hook-4-path nên hook cũng nuốt, nhưng **đừng dựa hook để chốt** — cùng lý-lẽ `pause.md §2.6`).
🔸 **Meld-forward (floor hub, miếng nền thứ-3) — CHƯA kiểm tại SE:** hub ràng *"commit mang thư-mục phiên được **nhập-tiến về trước** khi squash, KHÔNG bị bỏ rơi"*. §5.0/§5.2 của SE squash `wal:`-trailing bằng `reset --soft` ⇒ nội-dung **ở lại INDEX** nên **về lý là không mất**; nhưng **chưa có phép đo riêng** cho `.claude/sessions/`. ⇒ khai **CHƯA-ĐO**, không claim kín. 🔸 **Meld-forward (floor hub, miếng nền thứ-3) — CHƯA kiểm tại SE:** hub ràng *"commit mang thư-mục phiên được **nhập-tiến về trước** khi squash, KHÔNG bị bỏ rơi"*. §5.0/§5.2 của SE squash `wal:`-trailing bằng `reset --soft` ⇒ nội-dung **ở lại INDEX** nên **về lý là không mất**; nhưng **chưa có phép đo riêng** cho `.claude/sessions/`. ⇒ khai **CHƯA-ĐO**, không claim kín.

View File

@ -89,6 +89,20 @@ Em main đọc `.claude/WAL.md` (sổ mạch-việc-dở H22 — ghi-đè ≤40
- 🔴 **Ai cấp `<N>` (khác "ai tạo thư-mục" — đừng lẫn):** **CHỈ `/session-start`** mới **MỞ SỐ MỚI** (`max(<N> hiện có) + 1`). `/pause` · `/tiep` **KHÔNG BAO GIỜ tự tăng `<N>`** — chúng chỉ **tạo thư-mục cho `<N>` ĐANG MỞ** nếu nó chưa có trên đĩa (ca "cửa đầu chưa kịp scaffold"). Không có `<N>` nào đang mở ⇒ dùng `<N> = max+1` **và ghi 1 dòng khai** trong khối đó là số được cấp ngoài `/session-start`. - 🔴 **Ai cấp `<N>` (khác "ai tạo thư-mục" — đừng lẫn):** **CHỈ `/session-start`** mới **MỞ SỐ MỚI** (`max(<N> hiện có) + 1`). `/pause` · `/tiep` **KHÔNG BAO GIỜ tự tăng `<N>`** — chúng chỉ **tạo thư-mục cho `<N>` ĐANG MỞ** nếu nó chưa có trên đĩa (ca "cửa đầu chưa kịp scaffold"). Không có `<N>` nào đang mở ⇒ dùng `<N> = max+1` **và ghi 1 dòng khai** trong khối đó là số được cấp ngoài `/session-start`.
- 🧊 **Vá mâu-thuẫn @S146:** bản trước ghi *"Ai mở `<N>` mới: **CHỈ** `/session-start`"* trong khi [`pause.md §2.6`](pause.md) ghi *"chưa có thì đây là cửa đầu ⇒ **tạo** `session-<N>/`"* ⇒ **hai file nói ngược nhau về quyền tạo**. Gốc lỗi = **gộp hai khái niệm khác nhau** (*cấp SỐ* ⟂ *tạo THƯ-MỤC*) vào một câu. Nay tách đôi: **cấp số = độc-quyền `/session-start`** · **tạo thư-mục = idempotent, ai gặp trước thì tạo**. Hub chỉ ràng vế thứ hai. - 🧊 **Vá mâu-thuẫn @S146:** bản trước ghi *"Ai mở `<N>` mới: **CHỈ** `/session-start`"* trong khi [`pause.md §2.6`](pause.md) ghi *"chưa có thì đây là cửa đầu ⇒ **tạo** `session-<N>/`"* ⇒ **hai file nói ngược nhau về quyền tạo**. Gốc lỗi = **gộp hai khái niệm khác nhau** (*cấp SỐ* ⟂ *tạo THƯ-MỤC*) vào một câu. Nay tách đôi: **cấp số = độc-quyền `/session-start`** · **tạo thư-mục = idempotent, ai gặp trước thì tạo**. Hub chỉ ràng vế thứ hai.
**(a-bis) 🔴 DANH-MỤC FILE trong thư-mục — form HUB, owner chốt @S148 (*"đối ứng đúng chính xác như này"*, ảnh `session-109`; đối-chứng đĩa hub `AI_INFRA/.claude/sessions/session-102..109`):**
| file | vai | ai ghi (canonical — B1, KHÔNG chép schema về đây) |
|---|---|---|
| `_context-s-<N>.md` | **narrative durable**, 1 file/phiên-logic · §(a) STOCK-map pointer + §(b) **FLOW append-only** (`### PAUSE-<k>` + dòng `> anh:` VERBATIM) · 🔴 CẤM rewrite/tự-tóm | append @[`pause.md §2.6`](pause.md) · đọc @[`tiep.md §2.6`](tiep.md) |
| `_pause-<i>.md` | **marker máy** ~200B, 5 trường (`ts`·`head-sha`·`window-ordinal`·`jsonl-hint`·`account-label`) | [`pause.md §2.6`](pause.md) |
| `_tiep-<i>.md` | marker máy, **cùng 5 trường** | [`tiep.md §2.6`](tiep.md) |
| `_snapshot-<i>.md` | chụp-nhanh không-dừng | ⏳ **CHƯA CÓ ở SE** — cần lệnh `/snapshot` `[carry:snapshot-cmd]` + `[carry:pause-autosnap]` |
| `_end` | **chốt-kết, KHÔNG đuôi `.md`** · `markers:` chụp cân-sổ lúc đóng | [`session-end.md §6.3-bis`](session-end.md) |
- 🔴 **`<i>` đánh số TUẦN TỰ** trong thư-mục (`max+1`) — **KHÔNG** nhãn `S<nn>`, **KHÔNG** timestamp trong tên. Thứ-tự đọc được từ tên, ts nằm *trong* file.
- 🔴 **TÁCH marker ⟂ narrative** là điểm khác lớn nhất so với form SE cũ (gộp cả hai vào `pause-S<nn>-<ts>.md`). Marker cho MÁY đọc, narrative cho NGƯỜI/phiên-sau đọc.
- 🧊 **Form cũ = LEGACY, CẤM viết mới**; Sàn-3 ⑤ vẫn **nhận** (dual-accept, [`tiep.md §0`](tiep.md)) vì `session-1` còn dùng — đúng luật retire *"giữ khi CÒN người thụ-hưởng"*.
**(b) Đo TRƯỚC khi tin — `.gitignore` (đo @S146, KHÔNG suy):** **(b) Đo TRƯỚC khi tin — `.gitignore` (đo @S146, KHÔNG suy):**
```bash ```bash
git check-ignore -q .claude/sessions/session-1 ; echo "exit=$?" # → exit=1 git check-ignore -q .claude/sessions/session-1 ; echo "exit=$?" # → exit=1
@ -103,6 +117,13 @@ git check-ignore -q .claude/sessions/session-1 ; echo "exit=$?" # → exit=1
-**`session_ctx_kb` (trần ngân-sách context-phiên) = 64 — OWNER CHỐT TƯỜNG-MINH @S146.** Canonical = [`memory-budget.json`](../agent-memory/memory-budget.json) → `session_ctx_kb` (+ `_session_ctx_kb_owner_set`); **B1 — KHÔNG chép số ra đây**, dòng này chỉ TRỎ. Đơn-vị **KB**, code đọc **nhân 1024**. -**`session_ctx_kb` (trần ngân-sách context-phiên) = 64 — OWNER CHỐT TƯỜNG-MINH @S146.** Canonical = [`memory-budget.json`](../agent-memory/memory-budget.json) → `session_ctx_kb` (+ `_session_ctx_kb_owner_set`); **B1 — KHÔNG chép số ra đây**, dòng này chỉ TRỎ. Đơn-vị **KB**, code đọc **nhân 1024**.
🔴 **Vẫn là GHOST-WIRE, đừng đọc thành "đã wire":** đo @S146 **0 script đọc khoá** (hit duy-nhất trong `scripts/`**comment** `nhip-no-probe.ps1:42`; đối-chứng `pull_warn_days` **có** reader thật `:190`). Có số ≠ có hành-vi. Ai wire sau phải **đọc khoá**, không hardcode. 🔴 **Vẫn là GHOST-WIRE, đừng đọc thành "đã wire":** đo @S146 **0 script đọc khoá** (hit duy-nhất trong `scripts/`**comment** `nhip-no-probe.ps1:42`; đối-chứng `pull_warn_days` **có** reader thật `:190`). Có số ≠ có hành-vi. Ai wire sau phải **đọc khoá**, không hardcode.
## 📋 BƯỚC 0.9 — Kéo thư cross-project (cửa VÀO 1/2 — owner chốt S148)
**DÒ 2 kênh** rồi **KÉO đầy-đủ** theo định-tuyến: `se>0``/check-email all` · `all>0``/adap-apply`.
> 🔗 **Luật nhịp đầy-đủ (4 cửa · fail-soft · quan-hệ với `pull-cach`) = canonical [`check-email.md`](check-email.md) §"Nhịp chạy — 4 CỬA PHIÊN".** 🔴 **B1 — mục này CHỈ TRỎ + GỌI, CẤM chép luật sang đây.**
> 🔴 **KHÔNG chặn bootstrap:** không có thư ⇒ 1 dòng đi tiếp; lỗi ⇒ `check-email loi (khong chan)` rồi đi tiếp. Kéo **TRƯỚC Phase 1 READ** là cố ý — thư mới có thể đổi chính kế-hoạch phiên mà Phase 1 sắp dựng.
## Phase 1 — READ (load context) ## Phase 1 — READ (load context)
Đọc theo thứ tự, KHÔNG skip: Đọc theo thứ tự, KHÔNG skip:

View File

@ -0,0 +1,70 @@
---
description: Checkpoint KHÔNG-dừng-phiên — ghi _snapshot chốt trạng-thái + commit, KHÔNG stop/bookend (session-model, adopt S148)
---
# /snapshot — chụp-nhanh không-dừng (SE port của hub `snapshot.md`, adopt S148 2026-07-24)
Checkpoint **NHẸ** giữa phiên (**≤1 turn**): **1 Write + 1 commit**. **KHÔNG stop · KHÔNG bookend · KHÔNG flush agent-memory · KHÔNG hỏi thêm.** Xong là làm việc tiếp ngay.
> 🔗 Cặp với `/pause` (dừng hẳn) và `/tiep` (nối lại). `/snapshot` **KHÔNG** phải điểm dừng — nó là **dấu mốc giữa đường**.
> 🧭 Đóng `[carry:snapshot-cmd]` (#15) — anh chốt @S148 *"làm đi không cần hỏi hub"*.
## 0. GUARD — thư-mục có thể CHƯA activate
Resolve sID `<N>`: đọc field `session:` trong `.claude/WAL.md` → mất thì lấy `.claude/sessions/session-*/` **mtime max** → vẫn mù thì **hỏi anh 1 dòng**.
🔴 `.claude/sessions/session-<N>/` **CHƯA tồn-tại****SKIP ÊM**: in đúng 1 dòng `sessions/ chua activate — /snapshot no-op` rồi DỪNG. **KHÔNG mkdir, KHÔNG chết.** *(Cấp số `<N>` là độc-quyền `/session-start` — [`session-start.md` BƯỚC 0.8](session-start.md).)*
## 1. KHI NÀO fire (3 trigger — AI tự phán)
- **(i)** Có quyết-định/kết-luận vừa chốt **chưa nằm file nào** (còn trong transcript).
- **(ii)** **NGAY TRƯỚC** một workflow-launch diện-WAL: `taskList ≥ 3` **hoặc** ước > 5 phút — chốt state trước khi fan-out.
- **(iii) accumulation-gate:** đếm commit **substantive** tích-luỹ, bỏ qua nhiễu hook.
- **anchor = `{subject·ts}` của mốc NON-`wal:` gần nhất** mà `_snapshot` TRƯỚC đã pin (đọc `anchor` trong `_snapshot-<k-1>.md`). 🔴 **CẤM lấy HEAD trần** — HEAD lúc snapshot thường là commit `wal:` do hook flush mỗi turn, và closeout-squash sẽ **rewrite chính sha đó** ⇒ neo vào nó là neo vào thứ sắp biến mất. Neo `{subject·ts}` sống sót squash.
- `count = git log --format=%s <anchor-sha>..HEAD | grep -vc '^wal:'`
- **fire khi `count ≥ K`.** `K` đọc **LIVE** từ `.claude/agent-memory/memory-budget.json` key `snapshot_trigger_k`.
🔸 **Khai thật @S148: key này CHƯA có trong file SE** ⇒ áp **default-if-absent = 6** đúng theo spec hub. Đây là **số của hub**, không phải anh chốt — anh muốn khác thì đặt key, đừng sửa số ở đây (B1).
- **fallback** (anchor không resolve được sau rewrite / chưa có `_snapshot` nào): `git log --since=<mtime(_snapshot-* mới nhất)>`; **0 `_snapshot` nào** ⇒ snapshot ĐẦU do trigger (i)/(ii) lái, **KHÔNG** do (iii) — chống đếm-quá lúc folder vừa sinh.
## 2. Ghi `_snapshot-<k>.md`
`k = (số file `_snapshot-*.md` hiện có trong `session-<N>/`) + 1`.
🔴 `_snapshot` **tự chứa essence****KHÔNG** double-write vào `_context` FLOW (FLOW = **@pause-only**).
Derive machine-block TRƯỚC: `python scripts/session_ctx.py machine-block --session <N> --json`
````
# _snapshot-<k> — SESSION-MODEL checkpoint (sID <N>)
ts: <ISO-8601 — ưu tiên git-anchored `git log -1 --format=%cI`, tránh wall-clock tuỳ tiện>
head-sha: <git rev-parse HEAD>
head-subject: <git log -1 --format=%s>
head-ts: <git log -1 --format=%cI>
label: <1 dòng — checkpoint này chốt cái gì>
snapshot-ordinal: <k>
## essence (≤3 dòng — chốt cái CHƯA nằm file)
- <dòng 1>
- <dòng 2>
- <dòng 3>
## machine-block
```json
<paste nguyên JSON của: python scripts/session_ctx.py machine-block --session N --json>
```
````
**Trường BẮT BUỘC đủ:** `ts` · `head-sha` · `head-subject` + `head-ts` · `label` · `snapshot-ordinal` · `essence ≤3 dòng` · `machine-block`.
🔴 `head-subject`+`head-ts` = **chống sha-rewrite-rot**: sau squash sha đổi, `{subject·ts}` vẫn resolve lại được.
🔸 **Giới-hạn giữ khai (đo @S148):** `machine-block.changed_files` derive từ `git diff anchor..HEAD` ⇒ **chỉ thấy phần ĐÃ COMMIT**. Việc đang dirty trong cây (vd `docs/` chưa commit) **KHÔNG** hiện ra. Giữ đúng ngữ-nghĩa hub để sau này re-pull bằng diff; đừng đọc `changed_files` thành "toàn bộ việc đã làm".
## 3. Secrets-sweep gate (TRƯỚC commit — BẮT BUỘC)
`python scripts/session_ctx.py secrets-sweep --session <N>`
- **exit 1 ⇒ CHẶN commit**: in `file:line` mọi hit, xử-lý/redact rồi mới commit.
- ✅ **Đã fault-inject @S148 2 chiều:** CHẶN đúng ca có `sk-…` · cho qua ca sạch · **0 dương-giả** với `ask-me`/`task-list` (token-anchored `\b`).
- ⚠️ **ACCEPTED-GAP khai thẳng:** sweep **pattern-bounded** (4 mẫu `voyage·sk-·gitea_pat·cfut_`) ⇒ **KHÔNG** phủ secret lớp khác (vd mật-khẩu DB trong tin-nhắn verbatim). Đây là **BLOCK bảo-thủ trước commit**, KHÔNG phải chứng-minh sạch tuyệt-đối.
## 4. Commit (1 commit)
- `git add .claude/sessions/session-<N>/_snapshot-<k>.md` — 🔴 **CHỈ file đích danh, CẤM `git add -A`/`.`** (rủi ro rò secret).
- `git commit -m "wal: snapshot s<N> _snapshot-<k>"` — ASCII-clean (tránh em-dash mangle dưới PowerShell). **KHÔNG `--no-verify`. KHÔNG push.**
- 🔸 **Meld-forward @closeout — CHƯA ĐO tại SE:** hub ràng commit mang `.claude/sessions/` phải được **nhập-tiến** vào mốc non-`wal:` liền sau khi squash, KHÔNG bị DROP như `wal:` thuần. SE squash bằng `reset --soft` ⇒ **về lý không mất**, nhưng **chưa có phép đo riêng** — trùng `[carry:meld-forward-unmeasured]` (#19). Đừng claim kín.
## 5. Xong
Báo **1 dòng**: `snapshot-<k> ghi @<ts> (count=<n>/K, trigger-<i|ii|iii>) — committed, tiếp việc`
rồi **1 dòng nhịp-nợ**: `powershell.exe -ExecutionPolicy Bypass -File scripts/nhip-no-probe.ps1` (lỗi → `probe-loi (khong chan)`).
🔴 **KHÔNG stop · KHÔNG bookend · KHÔNG DỪNG HẲN** — quay lại việc đang làm ngay.

View File

@ -86,21 +86,32 @@ done
git log --name-only --format='%s' origin/main..HEAD | grep -q '.claude/sessions/' git log --name-only --format='%s' origin/main..HEAD | grep -q '.claude/sessions/'
# (b) cửa ĐĨA — git-independent, miễn-nhiễm hook (mirror ①: đọc đĩa, KHÔNG đọc git) # (b) cửa ĐĨA — git-independent, miễn-nhiễm hook (mirror ①: đọc đĩa, KHÔNG đọc git)
# Cân-bằng-sổ: mỗi `pause-*` phải có ĐÚNG một đối-ứng (`resume-*` do /tiep ghi, hoặc closed.md do đóng-L). # Cân-bằng-sổ: mỗi pause phải có ĐÚNG một đối-ứng (tiep do /tiep ghi, hoặc chốt-kết do đóng-L).
# 🔴 DUAL-ACCEPT 2 FORM (S148) — cùng khuôn dual-accept của ① ở trên:
# HUB (chuẩn từ S148, owner chốt "đối ứng đúng chính xác"): _pause-<i>.md · _tiep-<i>.md · _end (KHÔNG đuôi)
# LEGACY (di-sản ≤S147): pause-*.md · resume-*.md · closed.md
# 🔴 TÍNH-CHẤT CỨU MẠNG: glob `pause-*` KHÔNG khớp `_pause-1.md` (tên bắt đầu bằng `_`)
# ⇒ 2 form KHÔNG đè nhau ⇒ cộng thẳng là an-toàn, không đếm đôi. Đã fault-inject 2 chiều @S148.
for d in .claude/sessions/session-*/; do for d in .claude/sessions/session-*/; do
[ -d "$d" ] || continue [ -d "$d" ] || continue
p=$(ls "$d"pause-*.md 2>/dev/null | wc -l); [ "$p" -eq 0 ] && continue # 🔴 CHỐT-KẾT ĐÓNG TRỌN THƯ-MỤC, không phải "+1" (vá @S148 — fault-inject bắt).
r=$(ls "$d"resume-*.md 2>/dev/null | wc -l) # session-end.md §6.3-bis nói rõ: sau khi ghi chốt-kết thì MỌI pause "coi như ĐÃ ĐỐI-ỨNG".
c=0; [ -f "$d/closed.md" ] && c=1 # Bản cũ viết `c=1` rồi `p > r + c` ⇒ chỉ tha ĐÚNG MỘT pause chưa khớp ⇒ phiên đã đóng mà
[ "$p" -gt $((r + c)) ] && echo "ORPHAN-L: $d (pause=$p resume=$r closed=$c)" # có ≥2 pause dư vẫn KÊU OAN. Doc và máy lệch nhau từ S146; dữ-liệu SE quá nhỏ (p=1) nên chưa lộ.
{ [ -f "$d/_end" ] || [ -f "$d/closed.md" ]; } && continue
p=$(( $(ls "$d"_pause-*.md 2>/dev/null | wc -l) + $(ls "$d"pause-*.md 2>/dev/null | wc -l) ))
[ "$p" -eq 0 ] && continue
r=$(( $(ls "$d"_tiep-*.md 2>/dev/null | wc -l) + $(ls "$d"resume-*.md 2>/dev/null | wc -l) ))
[ "$p" -gt "$r" ] && echo "ORPHAN-L: $d (pause=$p tiep=$r, chua chot-ket)"
done done
``` ```
-**Đối-ứng `resume-*` CÓ người ghi thật**`/tiep` §2.6 dưới (cùng file này) ⇒ vòng `pause ↔ tiep` **tự đóng**, không phải cơ-chế treo. -**Đối-ứng `_tiep-<i>.md` CÓ người ghi thật**`/tiep` §2.6 dưới (cùng file này) ⇒ vòng `pause ↔ tiep` **tự đóng**, không phải cơ-chế treo. *(legacy `resume-*` vẫn được đếm — dual-accept ở máy trên.)*
-**`closed.md` ĐÃ WIRE @S146** (owner chốt *"giống hub"*`_end` chốt-kết của manifest `101e69d6`): nghi-thức ghi nằm ở [`session-end.md`](session-end.md) **§6.3-bis** (FROZEN · 4 trường · idempotent NO-OP · NO-OP im-lặng nếu 0 thư-mục). ⇒ **cân-sổ nay ĐỦ CẢ HAI vế**: `pause` đối-ứng bằng `resume-*` (`/tiep §2.6`) **hoặc** `closed.md` (đóng-L) ⇒ ca *"pause rồi đóng thẳng không qua `/tiep`"* **hết kêu oan**. -**Chốt-kết `_end` ĐÃ WIRE** (owner chốt @S146 *"giống hub"*, đổi tên `closed.md``_end` @S148 cho khớp form hub — manifest `101e69d6`): nghi-thức ghi ở [`session-end.md`](session-end.md) **§6.3-bis** (FROZEN · idempotent NO-OP · NO-OP im-lặng nếu 0 thư-mục). ⇒ **cân-sổ đủ CẢ HAI vế**: `pause` đối-ứng bằng `_tiep-<i>` (`/tiep §2.6`) **hoặc** `_end` (đóng-L) ⇒ ca *"pause rồi đóng thẳng không qua `/tiep`"* **hết kêu oan**.
🔴 **@S148 mạnh thêm một nấc:** `_end` nay **ĐÓNG TRỌN thư-mục** (detector `continue` hẳn), không còn là "+1". Fault-inject bắt: phiên đã đóng mà dư ≥2 pause thì bản cũ **vẫn KÊU OAN** — lệch đúng câu *"MỌI pause coi như đã đối-ứng"*`session-end §6.3-bis` vẫn luôn nói. Doc đúng, máy sai, sống từ S146.
🧊 *Nấc cũ (tới S145): "`closed.md` CHƯA wire — VC-2 còn chờ anh duyệt", kèm hệ-quả "chỉ ca pause-rồi-đóng-thẳng mới thiếu đối-ứng". **Cả hai nay SAI** — giữ 1 dòng làm vết.* 🧊 *Nấc cũ (tới S145): "`closed.md` CHƯA wire — VC-2 còn chờ anh duyệt", kèm hệ-quả "chỉ ca pause-rồi-đóng-thẳng mới thiếu đối-ứng". **Cả hai nay SAI** — giữ 1 dòng làm vết.*
🔴 **Bài học lấy từ chính chỗ này (H24 `lead-view-auditor` bắt @S146):** MÁY ở ngay trên (`:89` `:94` `:95`) **đã đọc `closed.md` thật** từ lúc block này ra đời, mà **PROSE vẫn khai "chưa wire"** ⇒ đây là **lật-ngược của ghost-wire**: **wired-but-declared-unwired** — và nó nằm đúng dưới nhãn *"chống ghost-wire H18"*. ⇒ **Ghost-wire soi được 2 CHIỀU**: khai-có-mà-không-chạy **và** chạy-thật-mà-khai-chưa. 🔴 **Bài học lấy từ chính chỗ này (H24 `lead-view-auditor` bắt @S146):** MÁY ở ngay trên (`:89` `:94` `:95`) **đã đọc `closed.md` thật** từ lúc block này ra đời, mà **PROSE vẫn khai "chưa wire"** ⇒ đây là **lật-ngược của ghost-wire**: **wired-but-declared-unwired** — và nó nằm đúng dưới nhãn *"chống ghost-wire H18"*. ⇒ **Ghost-wire soi được 2 CHIỀU**: khai-có-mà-không-chạy **và** chạy-thật-mà-khai-chưa.
- 🟢 **Hướng hỏng = AN-TOÀN:** quên ghi `resume-*` ⇒ sổ lệch ⇒ **KÊU ⇒ HỎI LẠI ANH**. Sai về phía *hỏi thừa*, không về phía *im lặng bỏ việc* — đúng thiên-hướng của Sàn-3. - 🟢 **Hướng hỏng = AN-TOÀN:** quên ghi `_tiep-<i>.md` ⇒ sổ lệch ⇒ **KÊU ⇒ HỎI LẠI ANH**. Sai về phía *hỏi thừa*, không về phía *im lặng bỏ việc* — đúng thiên-hướng của Sàn-3.
### 🟡 BẬC TRUNG-BÌNH — nhiều file bẩn **NGOÀI** 4 path hook persist ### 🟡 BẬC TRUNG-BÌNH — nhiều file bẩn **NGOÀI** 4 path hook persist
@ -143,10 +154,20 @@ Tín-hiệu **YẾU NHẤT**, dùng cuối. 🔴 **`porcelain` rỗng KHÔNG ph
- **Dòng-nợ (SAU tick — n-3):** gọi `powershell.exe -ExecutionPolicy Bypass -File scripts/nhip-no-probe.ps1` in dòng-nợ 4-vế (vế-1 nhịp-kiểm đọc post-tick). 🔴 **probe KHÔNG chặn điểm dừng**: lỗi → in `probe-loi (khong chan)` → đi tiếp. - **Dòng-nợ (SAU tick — n-3):** gọi `powershell.exe -ExecutionPolicy Bypass -File scripts/nhip-no-probe.ps1` in dòng-nợ 4-vế (vế-1 nhịp-kiểm đọc post-tick). 🔴 **probe KHÔNG chặn điểm dừng**: lỗi → in `probe-loi (khong chan)` → đi tiếp.
> 🔗 **Predicate C7 đầy-đủ (probe được phép chạy ở điểm dừng) = canonical [`harness-11-engine.md`](../../docs/governance/harness-11-engine.md) §N.8.** 🔴 **B1 — dòng này CHỈ TRỎ, CẤM chép predicate sang đây** (chép 2 nơi = 2 nguồn sự-thật, đúng thứ B1 sinh ra để chặn). > 🔗 **Predicate C7 đầy-đủ (probe được phép chạy ở điểm dừng) = canonical [`harness-11-engine.md`](../../docs/governance/harness-11-engine.md) §N.8.** 🔴 **B1 — dòng này CHỈ TRỎ, CẤM chép predicate sang đây** (chép 2 nơi = 2 nguồn sự-thật, đúng thứ B1 sinh ra để chặn).
## 2.6 Đọc context-map phiên-LOGIC + ghi biên-nhận `resume-*` (S146 — cặp với `/pause` bước 2.6) ## 2.5b Kéo thư cross-project (cửa VÀO 2/2 — owner chốt S148)
- **Đọc:** nếu có `.claude/sessions/session-<N>/` → đọc **TẤT CẢ** `pause-*.md` theo thứ-tự thời-gian = **bối-cảnh cộng-dồn** của phiên-logic (quyết-định đã chốt · delta còn sống · con-trỏ run/commit). 🔴 **Đây là lane CỘNG-DỒN, khác WAL** — WAL là sổ **mạch-sống ghi-đè ≤40 dòng** (liếc-một-cái-là-biết); context-map là **tích-luỹ, không ghi đè**. Đọc context-map **KHÔNG** thay việc chạy `verify:` — ground-truth vẫn thắng (§3). **DÒ 2 kênh** (`outbox/se` + `outbox/all` × 6 repo) — **NGAY SAU dòng-nợ** (vế-4 `pull-cach` vừa in xong ⇒ đọc số rồi dò là đúng thứ-tự nhân-quả). In `thu-moi: se=X all=Y`.
- **Ghi biên-nhận — 🔴 THỰC-HIỆN SAU §3** (đọc ở đây, ghi sau khi reconcile xong; **bắt buộc**): tạo `.claude/sessions/session-<N>/resume-S<nn>-<yyyyMMddTHHmm>.md`**1 khối ngắn**: nối từ `pause-*` nào · lệch gì so `chain:` (nếu có) · `next:` đang thực-thi. 🔴 **CHỈ list + so id. KHÔNG copy · KHÔNG hash · KHÔNG python** (đo @S148: dò ~5ms · kéo ~200500ms/thư). `X|Y > 0` ⇒ nêu **1 dòng** rồi đi tiếp §2.6; **kéo thật để bookend làm** (`se``/check-email` · `all``/adap-apply`).
> 🔗 **Luật nhịp đầy-đủ = canonical [`check-email.md`](check-email.md) §"Nhịp chạy — 4 CỬA PHIÊN".** 🔴 **B1 — CHỈ TRỎ + GỌI, CẤM chép luật.**
> 🔴 **KHÔNG chặn recovery:** lỗi ⇒ `check-email loi (khong chan)` → đi tiếp §2.6. **Cửa này quan-trọng NHẤT trong 4** — *"1 session kéo 2-3 phiên"* nghĩa là các phiên GIỮA đều vào bằng `/tiep`, nên bỏ cửa này là bỏ phần lớn lưu-lượng.
## 2.6 Đọc `_context-s-<N>.md` + ghi marker `_tiep-<i>.md` (S146 · form hub @S148 — cặp với `/pause §2.6`)
- **Đọc (form HUB, chuẩn @S148):** có `.claude/sessions/session-<N>/` → đọc **`_context-s-<N>.md`** — đó là **bối-cảnh cộng-dồn** (§(a) STOCK-map pointer + §(b) FLOW append-only: mọi entry `### PAUSE-<k>` theo thứ-tự, kèm dòng `> anh:` verbatim). Marker `_pause-*.md`/`_tiep-*.md`**5 trường máy**, đọc để biết `window-ordinal` + `head-sha`, KHÔNG chứa narrative.
🧊 **Legacy ≤S147:** thư-mục không có `_context-s-<N>.md` ⇒ đọc **TẤT CẢ** `pause-*.md` theo thứ-tự thời-gian (form cũ gộp marker+narrative). Nhận-để-đọc, **CẤM viết mới** kiểu đó. 🔴 **Đây là lane CỘNG-DỒN, khác WAL** — WAL là sổ **mạch-sống ghi-đè ≤40 dòng** (liếc-một-cái-là-biết); context-map là **tích-luỹ, không ghi đè**. Đọc context-map **KHÔNG** thay việc chạy `verify:` — ground-truth vẫn thắng (§3).
- **Ghi biên-nhận — 🔴 THỰC-HIỆN SAU §3** (đọc ở đây, ghi sau khi reconcile xong; **bắt buộc**): tạo **`_tiep-<i>.md`** (`<i>` = `max(_tiep-*)+1`) — đúng **5 trường ~200B**, y khuôn `_pause-<i>.md` (xem [`pause.md §2.6`](pause.md)): `ts` · `head-sha` · `window-ordinal` · `jsonl-hint` · `account-label`.
🔴 **Marker là MÁY, không phải bài luận** — kết-quả reconcile (lệch gì so `chain:`, `next:` đang chạy) thuộc về **`_context-s-<N>.md`**, không nhồi vào marker. *(Bản S148 đầu ghi `resume-S<nn>-<ts>.md` 2,5KB narrative — sai form, đã gỡ.)*
- 🔴 **Vì sao bắt buộc:** biên-nhận này là **đối-ứng cân-sổ** của tín-hiệu **ORPHAN-L ⑤** (§0). Thiếu nó ⇒ sổ lệch ⇒ lần sau **KÊU** ⇒ hỏi lại anh. Hỏng về phía *hỏi thừa* = an-toàn; nhưng ghi đủ thì sàn mới **im đúng**. - 🔴 **Vì sao bắt buộc:** biên-nhận này là **đối-ứng cân-sổ** của tín-hiệu **ORPHAN-L ⑤** (§0). Thiếu nó ⇒ sổ lệch ⇒ lần sau **KÊU** ⇒ hỏi lại anh. Hỏng về phía *hỏi thừa* = an-toàn; nhưng ghi đủ thì sàn mới **im đúng**.
- 🔸 `session-<N>` **KHÔNG mở mới ở `/tiep`**`/tiep` **nối** phiên-logic đang mở. Mở `<N>` mới = việc của `/session-start` (xem `session-start.md` BƯỚC 0.8). - 🔸 `session-<N>` **KHÔNG mở mới ở `/tiep`**`/tiep` **nối** phiên-logic đang mở. Mở `<N>` mới = việc của `/session-start` (xem `session-start.md` BƯỚC 0.8).

View File

@ -11,10 +11,10 @@
}, },
"_tick_invariant_note": "Tick invariant (hub dede7ec5 Delta-1, verbatim): moi LAN-CHOT +1; mot cap dung-noi tang DUNG +1, khong +2, khong +0. SE form = tick-at-entry-gate idempotent-per-label (hub Delta-2 recovery-gate +1 = permitted form) - NET +1/session-label EQUIV hub +1/cap on CLOSED pairs; an OPEN pair is transiently +0 until its entry gate fires (hub blessed, 9a35405b block-1 phep dung-noi). Guard song-con = label-convention (session-start 2.1.8: new conversation = new S<nn> label, NEVER reuse). history[] is append-unbounded BY DESIGN => absence of a marker = never-happened (safe semantics); IF a FIFO cap is ever added, eviction MUST be handled explicitly (absence-vi-bi-day != absence-vi-chua-xay-ra - log-BOUNDED design-note dede7ec5).", "_tick_invariant_note": "Tick invariant (hub dede7ec5 Delta-1, verbatim): moi LAN-CHOT +1; mot cap dung-noi tang DUNG +1, khong +2, khong +0. SE form = tick-at-entry-gate idempotent-per-label (hub Delta-2 recovery-gate +1 = permitted form) - NET +1/session-label EQUIV hub +1/cap on CLOSED pairs; an OPEN pair is transiently +0 until its entry gate fires (hub blessed, 9a35405b block-1 phep dung-noi). Guard song-con = label-convention (session-start 2.1.8: new conversation = new S<nn> label, NEVER reuse). history[] is append-unbounded BY DESIGN => absence of a marker = never-happened (safe semantics); IF a FIFO cap is ever added, eviction MUST be handled explicitly (absence-vi-bi-day != absence-vi-chua-xay-ra - log-BOUNDED design-note dede7ec5).",
"_seed_honesty": "Seeded UNTICKED on purpose. counter=0 and last_ticked_* = null mean 'no tick has ever happened', which is the truth at S121 - the ritual that performs the tick lands in W3. Seeding a fake first tick here would make the very first cadence reading a lie, and H24 exists to catch exactly that kind of invented number.", "_seed_honesty": "Seeded UNTICKED on purpose. counter=0 and last_ticked_* = null mean 'no tick has ever happened', which is the truth at S121 - the ritual that performs the tick lands in W3. Seeding a fake first tick here would make the very first cadence reading a lie, and H24 exists to catch exactly that kind of invented number.",
"counter": 21, "counter": 22,
"last_ticked_session": "S147", "last_ticked_session": "S148",
"last_ticked_head": "5f6c3becfb4ce958c7edad7d9f1058cb7289491e", "last_ticked_head": "581544200b4773db810808c063f4245fe54e004c",
"last_ticked_at": "2026-07-23", "last_ticked_at": "2026-07-24",
"last_audit": { "last_audit": {
"light_at_counter": 17, "light_at_counter": 17,
"deep_at_counter": 17, "deep_at_counter": 17,
@ -78,6 +78,11 @@
"at": "2026-07-23", "at": "2026-07-23",
"session": "S147", "session": "S147",
"event": "squash-benign (product/UAT session, tick @/pause - tick DAU cua S147 vi vao bang /tiep dau phien KHONG tick, da defer). Tick S146->S147: counter 20->21, old last_ticked_head 474199b3 (S146) -> new 5f6c3be. Phan-loai TRUOC tick: (a) git cat-file -t 474199b3 = commit => object CON TON TAI; (b) git merge-base --is-ancestor 474199b3 HEAD = exit 1 => KHONG reachable; (c) counter doc 20 == luu 20 => KHONG lui. => SQUASH-BENIGN => ghi vet + DI TIEP, KHONG alarm owner. Nguyen-nhan: S147 code-push reset 5 wal:-commit + docs-closeout squash 1 wal: nen head luc tick S146 roi khoi lich su - drift KY-VONG, khong tamper; nhan-phien cu 'S146' con nguyen = corroborate 2 nguon. OVERDUE sau tick: light 21-17=4 < 6, deep 21-17=4 < 15 => ca hai CHUA toi han (moc ke: light ~23, deep ~32). JUMP: class_repeat cao nhat gap-owner-specifics=2 < 3 => khong jump. => KHONG co H24 audit phien nay." "event": "squash-benign (product/UAT session, tick @/pause - tick DAU cua S147 vi vao bang /tiep dau phien KHONG tick, da defer). Tick S146->S147: counter 20->21, old last_ticked_head 474199b3 (S146) -> new 5f6c3be. Phan-loai TRUOC tick: (a) git cat-file -t 474199b3 = commit => object CON TON TAI; (b) git merge-base --is-ancestor 474199b3 HEAD = exit 1 => KHONG reachable; (c) counter doc 20 == luu 20 => KHONG lui. => SQUASH-BENIGN => ghi vet + DI TIEP, KHONG alarm owner. Nguyen-nhan: S147 code-push reset 5 wal:-commit + docs-closeout squash 1 wal: nen head luc tick S146 roi khoi lich su - drift KY-VONG, khong tamper; nhan-phien cu 'S146' con nguyen = corroborate 2 nguon. OVERDUE sau tick: light 21-17=4 < 6, deep 21-17=4 < 15 => ca hai CHUA toi han (moc ke: light ~23, deep ~32). JUMP: class_repeat cao nhat gap-owner-specifics=2 < 3 => khong jump. => KHONG co H24 audit phien nay."
},
{
"at": "2026-07-24",
"session": "S148",
"event": "CLEAN tick (KHONG squash-benign) qua /tiep noi mach S147. Nhan-phien MOI S148 theo label-convention S135 (conversation moi = nhan moi ke ca vao bang /tiep; reuse S147 se keo OR-guard session-clause NO-OP => tiep KHONG tick => phep-7 +0, cam). Tick S147->S148: counter 21->22, head 5f6c3be -> 5815442 (wal: pause local, chua push). Phan-loai TRUOC tick tren last_ticked_head cu 5f6c3be: (a) counter doc 21 == luu 21 => KHONG lui; (b) git cat-file -t 5f6c3be = commit => object CON TON TAI; (c) git merge-base --is-ancestor 5f6c3be HEAD = exit 0 => CON REACHABLE => tick SACH binh-thuong, KHONG squash-benign, KHONG fail-loud. Vi sao khac S147: S147 tick @/pause roi closeout squash nhac dau-tick; lan nay chua co closeout nao chay sau tick nen 5f6c3be van nam thang tren duong toi HEAD (HEAD = 5f6c3be + 1 commit wal: pause). OVERDUE sau tick: light 22-17=5 < 6, deep 22-17=5 < 15 => ca hai CHUA toi han (moc ke: light ~23 = NGAY PHIEN SAU, deep ~32). JUMP: class_repeat cao nhat gap-owner-specifics=2 < 3 => khong jump. => KHONG co H24 audit phien nay."
} }
] ]
} }

View File

@ -0,0 +1,57 @@
# _context-s-{{N}} — SESSION-MODEL narrative durable
<!-- SCAFFOLD-META: sID={{N}} | ts-moc={{TS}} | nguon-ts={{TS_SOURCE}} | generator=scripts/session_scaffold.py -->
> **sID (LOGIC-session) = {{N}}.** 1 phiên-logic (bootstrap → work → sweep) trải nhiều window vật-lý nối bằng `/pause`+`/tiep`. Window = 0-ID governance; **sID = đơn-vị LOGIC**. Folder pin: `.claude/sessions/session-{{N}}/`.
> ts-mốc khởi-tạo = **{{TS}}** (nguồn: `{{TS_SOURCE}}`) — mốc git-committer, KHÔNG phải wall-clock tuỳ-tiện.
>
> 🔴 **CẤM rewrite / tự-tóm block FLOW** (chống self-summary lossy). FLOW = append-only @`/pause`-only; vượt cap `session_ctx_kb` → distill SECTION CŨ thành pointer/gist, TUYỆT ĐỐI KHÔNG sửa entry verbatim đã ghi.
---
## (a) STOCK-map — reference-not-copy
> Bảng pointer tới nguồn durable. **KHÔNG copy nội-dung** (chống drift). Điền @bootstrap; row session-specific append dưới các row stable.
| Tên | Path | 1-dòng |
|---|---|---|
| WAL | `.claude/WAL.md` | mạch-việc-dở máy-state (H-22) — nối bằng `/tiep` |
| AI-context | `CLAUDE.md` | AI agent context SOLUTION_ERP (scope + quick-rules) |
| Trạng-thái | `docs/STATUS.md` | canonical mọi con-số (mig · bảng · test · roster) |
| Bàn-giao | `docs/HANDOFF.md` | brief 5 phút + bảng số chờ-anh + carry |
| Sổ đếm H24 | `.claude/governance/.session-counter.json` | counter + nhịp lead-self-audit |
| Thư cross-project | `broadcasts/_index.md` | INBOUND directed (`outbox/se`) — fan-out `outbox/all` KHÔNG vào đây |
| _(session-specific)_ | _(path)_ | _(1-dòng — append @bootstrap phiên này)_ |
---
## (b) FLOW — append-only @pause-only
> 🔴 **CẤM rewrite FLOW.** Mỗi `/pause` APPEND đúng 1 entry theo SCHEMA dưới (đặt DƯỚI dòng `FLOW-START`). Entry cũ = immutable. FLOW = SOURCE durable của "đang-đến-đâu"; bảng-đầu-việc = VIEW derive từ đây (single-source).
<!-- ENTRY-SCHEMA (guidance, KHONG phai entry that — /pause command dien; parser real-entry match `^### PAUSE-\d+`, placeholder `<k>` khong false-match):
### PAUSE-<k> <ts ISO-8601>
> anh: <tin-nhan anh VERBATIM — copy nguyen-van, KHONG paraphrase>
**(1) quyet-dinh da CHOT** / **(2) delta con SONG** / **(3) con-tro**
```json
{ "machine_block": "python scripts/session_ctx.py machine-block --session <N> --json" }
```
-->
<!-- FLOW-START — entries append bên dưới dòng này, mới nhất ở CUỐI -->
_(chưa có PAUSE — entry đầu append @`/pause` đầu tiên của phiên)_
---
## (c) STOCK-touched — máy-derive
> Bảng file-đã-đụng trong phiên, **máy-derive** từ `git diff --name-only <anchor>..HEAD` @pause/snapshot. KHÔNG điền tay.
| File | Δ | Ghi-chú |
|---|---|---|
| _(máy-derive @pause/snapshot)_ | | |

File diff suppressed because one or more lines are too long

220
scripts/session_ctx.py Normal file
View File

@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""session_ctx.py -- session-model derive helpers (SE port, S148 2026-07-24).
SE PORT of AI_INFRA scripts/session_ctx.py -- 🔴 TRIMMED ON PURPOSE, KHAI RO:
hub's original (~26 KB) also carries jsonl transcript parsing, anh-message containment
verification, token-overhead accounting and a context-cap getter. SE ports only the TWO
capabilities its rituals actually call:
machine-block -> derive {anchor, changed_files, run_id} for /snapshot + /pause
secrets-sweep -> pre-commit Category-5 gate over a session dir
NOT ported (do NOT claim these exist here): verify-containment / overhead / cap-getter /
window-jsonl resolution. Porting them without a caller would be ghost-wire -- the exact
class SE keeps getting bitten by. Add one ONLY when a ritual actually needs it.
Fidelity pin: field semantics + CLI shape follow hub so a future hub change can be
re-pulled by diff. Behaviour differences from hub are marked `SE-DELTA`.
stdlib-only . Python 3.11+ . UTF-8 no-BOM . LF . ASCII-only source.
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_SESSIONS_ROOT = ROOT / ".claude" / "sessions"
# Commits authored by the Stop-hook (wal-flush.ps1) -- never a valid anchor.
_WAL_SUBJECT_RE = re.compile(r'^wal:')
# runId as written into WAL by the hmw run-trace convention.
_RUNID_RE = re.compile(r'\b(wf_[A-Za-z0-9_-]+)')
# Category-5 secret prefixes. ACCEPTED-GAP: pattern-bounded, token-anchored -- this is a
# conservative pre-commit BLOCK, NOT a proof of cleanliness (a DB password in an anh-msg
# verbatim would pass). Say so; do not sell it as complete.
SECRET_PATTERNS = ('voyage', 'sk-', 'gitea_pat', 'cfut_')
_SECRET_RES = [(p, re.compile(r'\b' + re.escape(p), re.I)) for p in SECRET_PATTERNS]
# ---------------------------------------------------------------------------
# git helpers
# ---------------------------------------------------------------------------
def _git(args, repo_root):
"""Run `git -C <repo_root> <args>`; return stdout. Fail-loud: a bad range is a real
error for a derive that must be reproducible."""
try:
r = subprocess.run(['git', '-C', str(repo_root)] + list(args),
capture_output=True, text=True, encoding='utf-8', errors='replace')
except (OSError, subprocess.SubprocessError) as e:
raise SystemExit(f"[session_ctx] git failed to launch: {e}")
if r.returncode != 0:
raise SystemExit(f"[session_ctx] git {' '.join(args)} -> rc={r.returncode}: {r.stderr.strip()}")
return r.stdout
def _parse_log_line(line):
parts = line.split('\x1f')
return {'sha': parts[0], 'subject': parts[1], 'ts': parts[2]} if len(parts) == 3 else None
def find_anchor(repo_root, max_scan=100):
"""Nearest NON-wal named commit walking git log from HEAD -> {sha, subject, ts} or None.
Why not HEAD: the Stop-hook auto-commits `wal:` at every turn boundary, and closeout
squash rewrites those shas. Anchoring on a named commit survives the rewrite; anchoring
on HEAD rots (sha AND subject/ts both move)."""
out = _git(['log', f'-n{max_scan}', '--format=%H%x1f%s%x1f%cI'], repo_root)
for line in out.splitlines():
rec = _parse_log_line(line)
if rec is None or _WAL_SUBJECT_RE.match(rec['subject']):
continue
return rec
return None
def _resolve_ref(ref, repo_root):
"""Resolve an explicit anchor ref (sha/tag/HEAD~n) to {sha, subject, ts}."""
out = _git(['log', '-n1', '--format=%H%x1f%s%x1f%cI', ref], repo_root).strip()
return _parse_log_line(out.splitlines()[0]) if out else None
def _iso_utc(epoch):
return datetime.datetime.fromtimestamp(epoch, datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%S')
def last_snapshot_mtime(session_dir):
"""mtime of the most recent _snapshot-* file (fallback --since anchor). None if absent."""
snaps = [s for s in Path(session_dir).glob('_snapshot-*') if s.is_file()]
return max(s.stat().st_mtime for s in snaps) if snaps else None
def live_run_id(repo_root=None, wal_path=None):
"""Live runId from .claude/WAL.md (LAST match = most-recently-appended). None if absent."""
wal = Path(wal_path) if wal_path else (Path(repo_root or ROOT) / ".claude" / "WAL.md")
if not wal.exists():
return None
matches = _RUNID_RE.findall(wal.read_text(encoding='utf-8', errors='replace'))
return matches[-1] if matches else None
# ---------------------------------------------------------------------------
# (a) machine-block
# ---------------------------------------------------------------------------
def machine_block(session_n, sessions_root=None, repo_root=None, anchor=None):
"""Derive {session, anchor{subject,ts}, changed_files, changed_count, run_id}.
`anchor` = explicit ref override (pin it for reproducibility); default = nearest non-wal.
Anchor is identified by {subject, ts}, NOT sha -- sha does not survive closeout squash."""
repo_root = Path(repo_root or ROOT)
sessions_root = Path(sessions_root) if sessions_root else DEFAULT_SESSIONS_ROOT
session_dir = sessions_root / f"session-{session_n}"
a = _resolve_ref(anchor, repo_root) if anchor is not None else find_anchor(repo_root)
used_fallback = False
if a is not None:
files_out = _git(['diff', '--name-only', f'{a["sha"]}..HEAD'], repo_root)
anchor_id = {'subject': a['subject'], 'ts': a['ts']}
else:
used_fallback = True
mt = last_snapshot_mtime(session_dir)
files_out = (_git(['log', f'--since={_iso_utc(mt)}', '--name-only', '--format='], repo_root)
if mt is not None else '')
anchor_id = {'subject': None, 'ts': None}
changed = sorted({f.strip() for f in files_out.splitlines() if f.strip()})
return {
'session': session_n,
'anchor': anchor_id,
'anchor_from_fallback_since': used_fallback,
'changed_files': changed,
'changed_count': len(changed),
'run_id': live_run_id(repo_root),
}
def render_machine_block(mb):
"""Deterministic text rendering for embedding into a _context FLOW entry."""
a = mb['anchor']
lines = ['<!-- machine-block: scripts/session_ctx.py machine-block (script-derive) -->']
lines.append(f'anchor: "{a["subject"]}" @ {a["ts"]}' if a['subject'] is not None
else 'anchor: (fallback --since mtime of last _snapshot-*)')
lines.append(f'runId: {mb["run_id"] or "(none)"}')
lines.append(f'changed-files ({mb["changed_count"]}, git diff --name-only anchor..HEAD):')
lines.extend(f' {f}' for f in mb['changed_files'])
return '\n'.join(lines)
# ---------------------------------------------------------------------------
# (b) secrets-sweep
# ---------------------------------------------------------------------------
def secrets_sweep(session_dir):
"""Scan the WHOLE write-set of a session dir for Category-5 patterns.
Returns list of hits {file, line, pattern, snippet}. See ACCEPTED-GAP at module top."""
session_dir = Path(session_dir)
hits = []
if not session_dir.exists():
return hits
for p in sorted(session_dir.rglob('*')):
if not p.is_file():
continue
try:
text = p.read_text(encoding='utf-8', errors='replace')
except OSError:
continue
for lineno, line in enumerate(text.splitlines(), 1):
for pat, rx in _SECRET_RES:
if rx.search(line):
hits.append({'file': str(p), 'line': lineno, 'pattern': pat,
'snippet': line.strip()[:120]})
return hits
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main(argv=None):
ap = argparse.ArgumentParser(prog='session_ctx.py',
description='session-model derive helpers (SE port, trimmed).')
sub = ap.add_subparsers(dest='cmd', required=True)
mb = sub.add_parser('machine-block', help='derive anchor + changed-files + runId')
mb.add_argument('--session', type=int, required=True, metavar='N')
mb.add_argument('--anchor', default=None, help='explicit anchor ref (default: nearest non-wal)')
mb.add_argument('--json', action='store_true', help='emit JSON (default: rendered text)')
ss = sub.add_parser('secrets-sweep', help='Category-5 pre-commit gate over a session dir')
ss.add_argument('--session', type=int, required=True, metavar='N')
args = ap.parse_args(argv)
if args.cmd == 'machine-block':
block = machine_block(args.session, anchor=args.anchor)
print(json.dumps(block, ensure_ascii=False, indent=2) if args.json
else render_machine_block(block))
return 0
if args.cmd == 'secrets-sweep':
session_dir = DEFAULT_SESSIONS_ROOT / f"session-{args.session}"
hits = secrets_sweep(session_dir)
if not hits:
print(f"[secrets-sweep] session-{args.session}: 0 hit "
f"({len(SECRET_PATTERNS)} pattern, pattern-bounded -- KHONG phai chung-minh sach)")
return 0
# SE-DELTA: hub prints hits then exits 1; same contract, message localised.
print(f"[secrets-sweep] session-{args.session}: {len(hits)} HIT -- CHAN commit", file=sys.stderr)
for h in hits:
print(f" {h['file']}:{h['line']} [{h['pattern']}] {h['snippet']}", file=sys.stderr)
return 1
return 2
if __name__ == '__main__':
raise SystemExit(main())

154
scripts/session_scaffold.py Normal file
View File

@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""session_scaffold.py -- scaffold 1 logic-session folder + _context skeleton.
SE PORT of AI_INFRA scripts/session_scaffold.py (adopted S148, 2026-07-24, owner
"doi ung dung chinh xac nhu hub"). Near-verbatim: the logic is repo-agnostic; only
the template CONTENT differs (SE STOCK-map rows). Kept stdlib-only + same CLI so a
future hub change can be re-pulled by diff instead of re-derived.
Tach LOGIC-session khoi VAT-LY window: tao `.claude/sessions/session-<N>/` +
`_context-s-<N>.md` tu template `.claude/templates/session-context-template.md`.
Design pins:
- path HARD-PIN: `.claude/sessions/session-<N>/` (KHONG configurable -- uniform cross-repo).
- ts-moc = `git log -1 --format=%cI` (committer ISO-8601 cua HEAD), KHONG datetime.now
tuy-tien; nguon-ts ghi thang vao file (audit). Fallback datetime.now CHI khi git down,
tag ro "FALLBACK".
- Idempotent: folder ton tai -> bao + KHONG de `_context` (chi tao neu thieu).
- Cap so <N> KHONG phai viec cua script nay -- caller truyen vao. Doc-quyen cap so =
/session-start (session-start.md BUOC 0.8); /pause va /tiep chi tao folder cho <N> dang mo.
stdlib-only . Python 3.11+ . UTF-8 no-BOM . LF.
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
SESSIONS_SUBPATH: tuple[str, ...] = (".claude", "sessions")
TEMPLATE_SUBPATH: tuple[str, ...] = (".claude", "templates", "session-context-template.md")
def repo_root() -> Path:
"""Repo root via `git rev-parse --show-toplevel`; fallback = parent-of-scripts/."""
try:
out = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True, check=True,
)
root = out.stdout.strip()
if root:
return Path(root)
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
pass
return Path(__file__).resolve().parent.parent
def head_ts() -> tuple[str, str]:
"""Return (ts, source). Prefer HEAD committer ISO-8601, tagged with short-sha for audit."""
try:
out = subprocess.run(
["git", "log", "-1", "--format=%cI%x09%h"],
capture_output=True, text=True, check=True,
).stdout.strip()
if out:
ts, _, sha = out.partition("\t")
ts, sha = ts.strip(), sha.strip()
if ts:
return ts, f"git log -1 --format=%cI @ HEAD {sha}"
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
pass
return (
datetime.now(timezone.utc).isoformat(timespec="seconds"),
"datetime.now(UTC) FALLBACK (git unavailable -- non-deterministic)",
)
def render(template_text: str, n: int, ts: str, ts_source: str) -> str:
"""Fill placeholders with str.replace (NOT str.format) so literal `{`/`}` in the
template's JSON schema example survive untouched."""
return (
template_text
.replace("{{N}}", str(n))
.replace("{{TS}}", ts)
.replace("{{TS_SOURCE}}", ts_source)
)
def _rel(p: Path, root: Path) -> str:
try:
return p.relative_to(root).as_posix()
except ValueError:
return str(p)
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(
prog="session_scaffold.py",
description="Scaffold 1 logic-session folder + _context skeleton "
"(.claude/sessions/session-<N>/). Idempotent; safe to re-run.",
)
ap.add_argument("--session", type=int, required=True, metavar="N",
help="logic-session ID N (folder = session-<N>, regex ^session-\\d+$)")
ap.add_argument("--dry-run", action="store_true", help="print planned actions, write NOTHING")
args = ap.parse_args(argv)
n: int = args.session
if n < 0:
print(f"ERROR: --session must be a non-negative integer (got {n})", file=sys.stderr)
return 2
root = repo_root()
session_dir = root.joinpath(*SESSIONS_SUBPATH) / f"session-{n}"
context_path = session_dir / f"_context-s-{n}.md"
template_path = root.joinpath(*TEMPLATE_SUBPATH)
if not template_path.is_file():
print(f"ERROR: template not found: {template_path}", file=sys.stderr)
return 2
ts, ts_source = head_ts()
folder_exists = session_dir.exists()
context_exists = context_path.exists()
if args.dry_run:
print(f"[dry-run] session N = {n}")
print(f"[dry-run] ts (moc) = {ts}")
print(f"[dry-run] ts-source = {ts_source}")
print(f"[dry-run] template = {_rel(template_path, root)}")
print(f"[dry-run] session folder = {_rel(session_dir, root)} "
f"({'EXISTS' if folder_exists else 'would mkdir'})")
print(f"[dry-run] context file = {_rel(context_path, root)} "
f"({'EXISTS -> would SKIP (idempotent)' if context_exists else 'would write'})")
print("[dry-run] NOTHING written.")
return 0
created_folder = False
if not folder_exists:
session_dir.mkdir(parents=True, exist_ok=True)
created_folder = True
if context_exists:
print(f"[idempotent] {_rel(context_path, root)} EXISTS -- NOT overwriting.")
if created_folder:
print(f"[note] folder {_rel(session_dir, root)} was missing -> created "
f"(unusual: context present without folder).")
return 0
template_text = template_path.read_text(encoding="utf-8")
rendered = render(template_text, n, ts, ts_source)
with open(context_path, "w", encoding="utf-8", newline="\n") as fh:
fh.write(rendered)
print(f"[created] folder = {_rel(session_dir, root)} ({'new' if created_folder else 'existed'})")
print(f"[created] context = {_rel(context_path, root)}")
print(f"[created] ts(moc) = {ts} (source: {ts_source})")
print("[note] NOT committed (commit theo nghi-thuc /pause | /snapshot | /session-end).")
return 0
if __name__ == "__main__":
raise SystemExit(main())