From 71b21f45eac4cb3bfb5cd40b0b4072ee9b6743c0 Mon Sep 17 00:00:00 2001 From: "sungkuk.chae-at-471112812288" <76669236+cskwork@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:26:41 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20records=20a=20human=20can=20read=20?= =?UTF-8?q?=E2=80=94=20digest,=20overview,=20unmerged=20knowledge=20(v0.13?= =?UTF-8?q?.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - summary.md (templates/summary.md): Problem/Cause/Change/Result/Lesson + Tags, bound by no approval so it stays current while intent.md stays frozen - kb.sh show is a digest: goal, summary, delivery, unmerged harvest candidates, lesson titles, then store-relative paths - kb.sh index: overview table (state, date, tags) newest first, a "Knowledge not merged yet" section, UTF-8-safe truncation, embedded headings demoted - kb.sh harvest [--stale N]: open features whose harvest.md no close merged, with idle time; AGENTS.md rule 4 allows merging a stale harvest without closing - kb.sh index --obsidian / index_style: obsidian: frontmatter + inline #tags, no timestamp so an unchanged page yields no diff; marker read within 12 lines - skills 1-intent/5-ship/6-maintain, SKILL.md routes, templates, both READMEs - knowledge-test.sh section H: 38 assertions (145 total) Validation: knowledge-test 145/0, selftest PASS, e2e 152/0, autotest 195/0. --- AGENTS.md | 11 ++ README.ko.md | 7 +- README.md | 7 +- SKILL.md | 3 +- VERSION | 2 +- gates/knowledge-test.sh | 99 +++++++++++ log/changelog-v0.13.0.md | 108 ++++++++++++ skills/1-intent/SKILL.md | 21 ++- skills/5-ship/SKILL.md | 8 +- skills/6-maintain/SKILL.md | 6 +- templates/harvest.md | 6 +- templates/intent.md | 3 + templates/summary.md | 18 ++ tools/kb.sh | 352 +++++++++++++++++++++++++++++++++---- 14 files changed, 597 insertions(+), 54 deletions(-) create mode 100644 log/changelog-v0.13.0.md create mode 100644 templates/summary.md diff --git a/AGENTS.md b/AGENTS.md index 7d2a850..5c6248e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -193,6 +193,17 @@ when closing. `.sdlc/work//harvest.md` (templates/harvest.md), never to the shared files. At close, merge harvest into lessons/INDEX/DOMAIN and delete it; `close.sh` blocks while harvest.md exists. + **A loop that never closes must not hide its knowledge.** An unmerged + harvest is readable before close — `tools/kb.sh show ` and the + contents page print its candidates, `tools/kb.sh harvest` lists every + open feature that holds one with its idle time — and a feature idle 30 + days or more (`--stale ` changes the threshold) may have its + harvest merged WITHOUT closing: the same procedure, the same one merge + at a time in the owning checkout, harvest.md deleted afterwards, the + feature stays open. Reporting is not merging: nothing in `kb.sh` writes + `memory/`. The feature's `summary.md` (templates/summary.md) is the one + record meant to be rewritten as the work moves — it is bound by no + approval — and is what `kb.sh show` prints first. **Recency wins on merge, three guards.** A contradicting candidate replaces the old entry with a fresh `[verified: how — YYYY-MM-DD]`; date every fact. Guards: weaker evidence never supersedes stronger (a diff --git a/README.ko.md b/README.ko.md index ae64ecf..90d51ee 100644 --- a/README.ko.md +++ b/README.ko.md @@ -156,7 +156,8 @@ agent APPROVED: intent of claims-status (.sdlc/work/claims-status/intent.md) │ ├── deviations.md # 빌드 중 편차 기록 │ ├── progress.md # 하트비트: 살아있는 한 줄 (규칙 9) │ ├── baseline.txt # 브라운필드의 변경 전 동작 -│ ├── harvest.md # 루프 중 교훈·도메인 후보, close에서 병합 +│ ├── summary.md # 읽는 사람용 페이지: 문제·원인·변경·결과·교훈·태그, 승인에 묶이지 않아 계속 갱신 +│ ├── harvest.md # 루프 중 교훈·도메인 후보, close에서 병합(그 전에도 kb.sh show / harvest로 읽힘) │ └── scratch/ # 대용량 로그 · 캡처 · 트레이스 └── archive// # 닫힌 피처, close.sh가 여기로 옮김 ├── CLOSED # shipped · abandoned · dead-end · handed-off @@ -167,7 +168,7 @@ agent APPROVED: intent of claims-status (.sdlc/work/claims-status/intent.md) **기록을 어디에 둘지는 사용자가 정합니다.** 기본값은 프로젝트 작업 사본 안이고, `init.sh . --area ~/knowledge`를 쓰면 사용자가 고른 폴더 아래 `/<단위이름>-<체크아웃 식별자>/`에 저장하고 `.sdlc`를 그곳으로 연결합니다. 체크아웃마다 저장소가 하나씩이므로 워크트리 두 개가 승인 상태를 공유하는 일이 없습니다. 영역이 프로젝트 안에 있거나 프로젝트가 영역 안에 있을 때, 다른 체크아웃이 이미 그 저장소를 소유할 때, 실제 `.sdlc` 디렉터리가 이미 있을 때(자동으로 옮기지 않습니다), 링크를 만들 수 없을 때는 아무것도 쓰지 않고 분명히 실패합니다. 이 소유권은 init 시점뿐 아니라 실행 시점에도 다시 확인합니다. `/PROJECT`에 적힌 체크아웃이 지금 실행 중인 체크아웃과 다르면 `check-gate.sh`, `approve.sh`, `close.sh`, `status.sh`, `tools/auto.sh`, `tools/verify.sh`, `tools/handoff.sh`가 판정을 내리거나 상태를 쓰기 전에 거부하므로, 심볼릭 링크를 그대로 복사한 작업 사본(`cp -R`, rsync, 대부분의 백업 복원)이 다른 체크아웃의 게이트를 열거나 그 피처를 닫을 수 없습니다. 읽기는 이 제약을 받지 않아 `tools/kb.sh show|search|list`는 그대로 쓸 수 있고, 소유권을 자동으로 옮기거나 다시 묶는 일은 없습니다. 어느 쪽을 고르든 **저장소 백업은 사용자의 몫입니다.** git이 더 이상 대신해 주지 않습니다. -**기록을 다시 읽는 도구는 `tools/kb.sh`입니다.** `index`는 목차 페이지를 다시 만들고(`init.sh`와 `close.sh`가 자동으로 실행합니다), `show `는 피처 하나의 목표·문서·교훈을 보여주며, `search "<문자열>"`은 열린 피처와 닫힌 피처, 지속 메모리를 대상으로 출력량을 제한한 문자열 검색을 합니다. `--area <폴더>`를 붙이면 그 폴더 안의 모든 저장소를 대상으로 같은 일을 하며, 원래 체크아웃이 사라진 피처도 읽을 수 있습니다. 종료 코드는 `0` 찾음, `1` 없음, `2` 사용법 오류 또는 거부입니다. +**기록을 다시 읽는 도구는 `tools/kb.sh`입니다.** `index`는 목차 페이지를 다시 만듭니다(`init.sh`와 `close.sh`가 자동으로 실행합니다). 페이지는 상태·날짜·태그를 담은 개요 표(최신순), 아직 close가 병합하지 않은 harvest 목록, 피처별 절 순서입니다. `show `는 피처 하나를 요약본으로 보여줍니다. 목표, `summary.md`(문제·원인·변경·결과·교훈을 담는, 계속 갱신하도록 만든 유일한 기록), 배포 상태, 병합되지 않은 harvest 후보, 교훈 제목이 먼저 나오고 파일 경로는 마지막입니다. `search "<문자열>"`은 열린 피처와 닫힌 피처, 지속 메모리를 대상으로 출력량을 제한한 문자열 검색을 하고, `harvest [--stale <일수>]`는 harvest.md가 아직 memory/에 들어가지 않은 열린 피처를 유휴 기간과 함께 나열합니다(유휴 상태가 오래된 피처는 close 없이 병합할 수 있습니다 — AGENTS.md 규칙 4). `--area <폴더>`를 붙이면 그 폴더 안의 모든 저장소를 대상으로 같은 일을 하며, 원래 체크아웃이 사라진 피처도 읽을 수 있습니다. `index --obsidian`(또는 저장소 config.md의 `index_style: obsidian`)은 Obsidian 볼트용 frontmatter와 인라인 `#태그`를 덧붙입니다. 생성 시각은 절대 쓰지 않으므로 내용이 같으면 diff도 생기지 않습니다. 종료 코드는 `0` 찾음, `1` 없음, `2` 사용법 오류 또는 거부입니다. 공개 sdlc-kit 저장소는 프레임워크만 담습니다. 기록은 작성된 자리, 즉 프로젝트 작업 사본이나 사용자가 고른 영역에 남아 그대로 읽힙니다. @@ -257,7 +258,7 @@ tools/auto.sh intent-check # 이 intent.md를 무인으로 실행해 tools/auto.sh checkpoint … # 대기 중인 단계, 제한된 재시도, 완료된 외부 효과 tools/verify.sh run|check # 프로젝트의 검증 레시피 실행(python3 필요), 소스에 결합된 영수증 기록 tools/handoff.sh push|check # 리뷰용 브랜치가 원격에 실제로 있음을 증명 -tools/kb.sh index|show|search|list # 지난 피처와 교훈 찾기(--area로 영역 전체) +tools/kb.sh index|show|search|list|harvest # 지난 피처와 교훈 찾기(--area로 영역 전체; harvest = 아직 병합되지 않은 지식) ``` 호스트가 에이전트를 깨우면, 에이전트는 `next`를 읽고 단계 지시서에 따라 그 액션 하나를 diff --git a/README.md b/README.md index ef1e6b3..a246f1b 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,8 @@ Per feature, inside the **target project**: │ ├── deviations.md # build-time differences │ ├── progress.md # heartbeat: ONE live line (rule 9) │ ├── baseline.txt # brownfield behavior before the change -│ ├── harvest.md # mid-loop lesson/domain candidates; merged at close +│ ├── summary.md # the reader's page: Problem/Cause/Change/Result/Lesson, Tags; kept current, bound by no approval +│ ├── harvest.md # mid-loop lesson/domain candidates; merged at close (readable before: kb.sh show / harvest) │ └── scratch/ # bulk logs, captures, traces └── archive// # closed features; close.sh moves them here ├── CLOSED # shipped · abandoned · dead-end · handed-off @@ -167,7 +168,7 @@ Per feature, inside the **target project**: **Where the records live is your choice.** By default they sit in the project's working copy. `init.sh . --area ~/knowledge` puts them in a folder you choose instead — `/-/`, with `.sdlc` linked to it, one store per checkout so two worktrees never share approvals. The area is refused if it sits inside the project (or the project inside it), if another checkout already owns that store, if a real `.sdlc` directory is already there (nothing is ever relocated for you), or if the link cannot be made. That ownership is re-checked at RUNTIME, not only at init: `check-gate.sh`, `approve.sh`, `close.sh`, `status.sh`, `tools/auto.sh`, `tools/verify.sh` and `tools/handoff.sh` refuse before any verdict or write when `/PROJECT` names a different checkout, so a copied working copy (`cp -R`, rsync and most restores keep the symlink) can neither open another checkout's gate nor close its features. Reading is never bound that way: `tools/kb.sh show|search|list` still works, and nothing is ever re-bound or moved for you. Whichever you choose, **the store is yours to back up** — git no longer does it for you. -**Reading the records back** is `tools/kb.sh`: `index` regenerates the contents page (`init.sh` and `close.sh` do it for you), `show ` prints one feature's goal, documents and lessons, `search ""` does a bounded literal search over open and closed features plus durable memory, and `--area ` does either across every store in that folder — including features whose checkout no longer exists. Exit codes: `0` found, `1` nothing found, `2` usage error or refusal. +**Reading the records back** is `tools/kb.sh`: `index` regenerates the contents page (`init.sh` and `close.sh` do it for you) — an overview table by state, date and tags, newest first, the harvests no close has merged yet, then one section per feature; `show ` prints one feature as a digest — goal, its `summary.md` (Problem/Cause/Change/Result/Lesson, the one record meant to be kept current), delivery, unmerged harvest candidates, lesson titles, then the paths; `search ""` does a bounded literal search over open and closed features plus durable memory; `harvest [--stale ]` lists open features whose harvest.md is not in memory yet, with idle time (a stale one may be merged without closing — AGENTS.md rule 4); and `--area ` does any of these across every store in that folder — including features whose checkout no longer exists. `index --obsidian` (or `index_style: obsidian` in the store's config.md) adds frontmatter and inline `#tags` for a vault; no timestamp is ever written, so an unchanged page produces no diff. Exit codes: `0` found, `1` nothing found, `2` usage error or refusal. The public sdlc-kit repository stays framework-only. The records live and stay readable where they were written — in the project's working copy, or in the area you chose. @@ -256,7 +257,7 @@ tools/auto.sh intent-check # is this intent.md safe to run unattende tools/auto.sh checkpoint … # pending step, bounded attempts, completed effects tools/verify.sh run|check # run the project's verification recipe (needs python3); receipt bound to the source tools/handoff.sh push|check # the review branch, proven to be on the remote -tools/kb.sh index|show|search|list # find past features and lessons (--area for every store) +tools/kb.sh index|show|search|list|harvest # find past features and lessons (--area for every store; harvest = knowledge not merged yet) ``` The host wakes an agent; the agent reads `next`, performs that ONE stage action diff --git a/SKILL.md b/SKILL.md index 7da9426..848bd33 100644 --- a/SKILL.md +++ b/SKILL.md @@ -35,7 +35,8 @@ they do it under this contract (gates, artifacts, memory), not beside it. | ticket too big or foggy for one intent pass | `map.md` in the same slug dir first (skills/1-intent "Chart a map first"); one Unknown per session, six sessions max. | | "continue " / "what's next" | Run `gates/status.sh ` from the project root. Its `next →` line names the stage skill or gate command. | | "where are we" / "sdlc status" | `gates/status.sh` (open features; `--all` adds the newest 20 archived) + `gates/stats.sh` (open + recent closed). Full-archive sweeps: `ls`/`grep .sdlc/archive/`, never the whole listing into context. | -| "has this been done before" / "why is X like this" / debugging an old area | `tools/kb.sh search ""` then `tools/kb.sh show ` — open and closed features plus durable memory, bounded output (`--area ` covers every store, even one whose checkout is gone). Exit 0 found · 1 nothing · 2 usage/refusal. | +| "has this been done before" / "why is X like this" / debugging an old area | `tools/kb.sh search ""` then `tools/kb.sh show ` — a digest (goal, summary.md, delivery, unmerged harvest candidates, lesson titles, then the paths) over open and closed features plus durable memory, bounded output (`--area ` covers every store, even one whose checkout is gone). Exit 0 found · 1 nothing · 2 usage/refusal. | +| "what did we learn that is not in memory yet" / many open features, few closes | `tools/kb.sh harvest [--stale ]` — open features whose harvest.md no close has merged, with idle time; a STALE one may be merged without closing (AGENTS.md rule 4). The contents page (`tools/kb.sh index`; `--obsidian` or `index_style: obsidian` in config.md for a vault) shows the same candidates and an overview table by state, date and tags. | | a host/scheduler drives the loop, or you need machine state | `gates/status.sh --json` (= `tools/auto.sh status --json`) and `tools/auto.sh next ` (exit 0 ready · 10 needs-human · 20 blocked · 30 complete). Verification receipts: `tools/verify.sh`; review handoff: `tools/handoff.sh`. Contract: `docs/automation.md`. These report and record — they run no stage. | | gate request answered "approve" in chat | `gates/approve.sh --delegated` per AGENTS.md rule 3. | | incident / bug / alert on a shipped feature | Read `skills/6-maintain/SKILL.md`. | diff --git a/VERSION b/VERSION index 87a1cf5..6345c21 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.12.0 +v0.13.0 diff --git a/gates/knowledge-test.sh b/gates/knowledge-test.sh index ba8493f..e29f4b3 100755 --- a/gates/knowledge-test.sh +++ b/gates/knowledge-test.sh @@ -566,6 +566,105 @@ assert_ok_msg "G8 the store records the kit version that seeded it" "kit_version assert_ok_msg "G9 the readable name carries into retrieval output" "지식-프로젝트" \ bash "$KIT/tools/kb.sh" list --area "$FIX/area-u" +echo +echo "=============== H. a human can read it: digest, overview, unmerged knowledge" +# A store with two open features and one closed. Dates and tags come from the +# records only; nothing is inferred from mtime except the idle time of `harvest`. +HP="$FIX/hproj"; newproj "$HP" +assert_ok "H0 the fixture project initializes" bash "$KIT/init.sh" . +mkdir -p .sdlc/work/h-new .sdlc/work/h-old .sdlc/work/h-quiet .sdlc/archive/h-done .sdlc/memory/lessons +cat > .sdlc/work/h-new/intent.md <<'EOF' +# Intent: h-new +- Goal: let a teacher export the roster as CSV +- Date: 2026-09-18 +- Track: compact +EOF +cat > .sdlc/work/h-new/summary.md <<'EOF' +# Summary: h-new +- Tags: roster, export +- Problem: the roster page has no export, so teachers retype names into spreadsheets +- Cause: not known yet +- Change: add a CSV endpoint behind the existing roster query +- Result: not delivered +- Lesson: none +EOF +cat > .sdlc/work/h-new/harvest.md <<'EOF' +# Harvest: h-new +## Domain candidates + +- the roster query already filters by term — [verified: RosterMapper.xml:40 — 2026-09-18] +## Lesson candidates +- [export,encoding] Excel needs a BOM to open UTF-8 CSV as UTF-8 +EOF +cat > .sdlc/work/h-old/intent.md <<'EOF' +# Intent: h-old +- Goal: 학생 현황에서 학습 이력이 있는 학생이 '-'로 보이는 문제를 고쳐 교사가 실제 점수를 볼 수 있게 한다. 이 문장은 표에서 잘려야 할 만큼 길게 이어진다. 그리고 더 이어진다. +- Date: 2026-08-01 +EOF +printf '# Intent: h-quiet\n- Goal: nothing to harvest here\n- Date: 2026-08-15\n' > .sdlc/work/h-quiet/intent.md +printf '# Intent: h-done\n- Goal: an archived one\n- Date: 2026-07-01\n' > .sdlc/archive/h-done/intent.md +printf 'state: shipped\nreason: done\nclosed_at: 2026-07-02T10:00:00Z\n' > .sdlc/archive/h-done/CLOSED +printf '# Lesson: a BOM makes Excel read UTF-8\n- Feature: h-new\n' > .sdlc/memory/lessons/2026-09-18-h-new-bom.md +# show — the digest +assert_ok_msg "H1 show prints the reader's summary before the paths" "teachers retype names" kb show h-new +assert_ok_msg "H2 show prints the unmerged harvest candidates" "Excel needs a BOM" kb show h-new +assert_ok_msg "H3 show prints a lesson's title, not only its file" "a BOM makes Excel read UTF-8" kb show h-new +assert_ok_msg "H4 show lists documents relative to the store" "work/h-new/summary.md" kb show h-new +assert_ok_msg "H5 show falls back to the H1 when an intent has no Goal line" "h-quiet" \ + sh -c "sed -i.bak '/^- Goal/d' .sdlc/work/h-quiet/intent.md && rm -f .sdlc/work/h-quiet/intent.md.bak && bash '$KIT/tools/kb.sh' show h-quiet" +SHOW=$(kb show h-new) +case "$SHOW" in *"Summary:"*"Documents:"*) pass "H6 the digest comes before the document list";; *) fail "H6 the document list is not last" "$SHOW";; esac +# index — overview table, newest first, tags, unmerged harvests +assert_ok "H7 index regenerates the page" kb index +PAGE=.sdlc/README.md +assert_ok_msg "H8 the page opens with an overview table" "| Feature | State | Date | Tags | Goal |" cat "$PAGE" +assert_ok_msg "H9 the table carries the tags a reader browses by" "roster, export" cat "$PAGE" +NEW_AT=$(grep -n '^| \[h-new\]' "$PAGE" | head -1 | cut -d: -f1); OLD_AT=$(grep -n '^| \[h-old\]' "$PAGE" | head -1 | cut -d: -f1) +if [ -n "$NEW_AT" ] && [ -n "$OLD_AT" ] && [ "$NEW_AT" -lt "$OLD_AT" ]; then pass "H10 open features are listed newest first" +else fail "H10 the newer feature is not listed first (new@${NEW_AT:-?} old@${OLD_AT:-?})"; fi +assert_ok_msg "H11 the closed feature carries its close date" "closed 2026-07-02" cat "$PAGE" +assert_ok_msg "H12 unmerged harvests have their own section" "## Knowledge not merged yet" cat "$PAGE" +assert_ok_msg "H13 the section names the feature and shows the candidate" "Excel needs a BOM" cat "$PAGE" +assert_exit "H14 a feature without a harvest is not listed as unmerged" 1 \ + sh -c "awk '/^## Knowledge not merged yet/,/^## Open features/' '$PAGE' | grep -q h-quiet" +assert_ok_msg "H15 the page still lists the summary of a feature" "teachers retype names" cat "$PAGE" +# the long goal is cut at a character boundary — never a broken glyph +CELL=$(grep '^| \[h-old\]' "$PAGE" | head -1) +case "$CELL" in *"…"*) pass "H16 a long goal is truncated in the table";; *) fail "H16 the long goal was not truncated" "$CELL";; esac +if command -v python3 >/dev/null 2>&1; then + if printf '%s' "$CELL" | python3 -c 'import sys; sys.stdin.buffer.read().decode("utf-8")' 2>/dev/null; then + pass "H17 the truncated cell is still valid UTF-8" + else fail "H17 the truncation split a multibyte character" "$CELL"; fi +else echo "NOT VERIFIED H17 (no python3 to check UTF-8 validity)"; fi +# regenerating an unchanged store changes nothing (no timestamp in the page) +cp "$PAGE" "$FIX/page-before"; kb index >/dev/null +if cmp -s "$PAGE" "$FIX/page-before"; then pass "H18 regenerating an unchanged store yields identical bytes" +else fail "H18 the page changed without the records changing" "$(diff "$FIX/page-before" "$PAGE" | head -5)"; fi +# harvest — the trigger +assert_ok_msg "H19 harvest lists the open feature holding candidates" "h-new" kb harvest +assert_exit "H20 harvest exits 0 when something is unmerged" 0 bash "$KIT/tools/kb.sh" harvest +assert_ok_msg "H21 --stale 0 marks it as mergeable without closing" "STALE" kb harvest --stale 0 +assert_ok_msg "H22 a fresh harvest is reported active under the default window" "active" kb harvest +assert_exit "H23 harvest exits 1 for a store with nothing unmerged" 1 \ + sh -c "mkdir -p '$FIX/hempty/work/x' && echo '# Intent: x' > '$FIX/hempty/work/x/intent.md' && bash '$KIT/tools/kb.sh' harvest --store '$FIX/hempty'" +assert_fail_msg "H24 harvest takes no positional argument" "takes no argument" bash "$KIT/tools/kb.sh" harvest h-new +assert_fail_msg "H25 --stale wants a number" "number of days" bash "$KIT/tools/kb.sh" harvest --stale soon +# obsidian — frontmatter and inline tags on the generated page only +assert_ok "H26 index --obsidian writes the page" kb index --obsidian +assert_ok_msg "H27 the page opens with frontmatter" "---" head -n 1 "$PAGE" +assert_ok_msg "H28 the frontmatter tags the page" "tags: [sdlc-kit, knowledge]" head -n 5 "$PAGE" +assert_ok_msg "H29 feature tags become inline #tags" "#roster #export" cat "$PAGE" +assert_ok_msg "H30 links stay relative markdown" "](work/h-new/intent.md)" cat "$PAGE" +assert_ok "H31 a frontmatter page is still recognized as generated (plain regenerate)" kb index +assert_exit "H32 a plain regenerate drops the frontmatter again" 1 sh -c "head -n 1 '$PAGE' | grep -q '^---'" +printf 'index_style: obsidian # tools/kb.sh index\n' >> .sdlc/config.md +assert_ok "H33 index_style in config.md selects the style without a flag" kb index +assert_ok_msg "H34 the configured style produced frontmatter" "sdlc_store:" head -n 5 "$PAGE" +assert_fail_msg "H35 --obsidian is refused for anything but index" "applies to index only" bash "$KIT/tools/kb.sh" show h-new --obsidian +assert_ok_msg "H36 records under work/ were not written by any of this" "not delivered" cat .sdlc/work/h-new/summary.md +assert_nofile ".sdlc/work/h-new/README.md" "H37 no page was written inside a feature directory" +cd "$FIX" || exit 2 + echo echo "================================================================" printf 'PASSED: %s FAILED: %s\n' "$PASSED" "$FAILED" diff --git a/log/changelog-v0.13.0.md b/log/changelog-v0.13.0.md new file mode 100644 index 0000000..59a38d3 --- /dev/null +++ b/log/changelog-v0.13.0.md @@ -0,0 +1,108 @@ +# v0.13.0 — records a human can read: digest, overview, unmerged knowledge + +v0.12.0 moved the records out of git and made them retrievable. Retrieval +returned paths: `kb.sh show` was a table of contents, the generated page was a +flat slug-ordered list, and a feature's knowledge became visible only when +`close.sh` merged its harvest — which, in a loop with many open features, is +rarely. This release changes what a reader gets, not what the loop records. +Gate authority, tamper detection, source binding, the `--json` schema and +lazymode are untouched; nothing under `work/` or `archive/` is written by any +of it. + +## Changes + +- **`summary.md` — the reader's page** (`templates/summary.md`). Five one-line + bullets (Problem, Cause, Change, Result, Lesson) and a `Tags` line, in plain + language, ten lines at most. It is not a stage artifact and no approval binds + it — deliberately, so it can stay TRUE while `intent.md` stays frozen by its + approval: written with the intent (skills/1-intent), Result and Lesson set at + ship from what `delivery.md` proves (skills/5-ship). `kb.sh show` and the + contents page print it before anything else. An older store that wrote a + `## Summary` section into `intent.md` is still read. +- **`kb.sh show` is a digest.** Goal (falling back to the intent's H1 when an + older intent has no `- Goal:` line), Track/Type/Date/Tags/Requested by/Refs on + one line, the summary, `Delivered:` with its confirmation and handoff, + `Closed:` with its date, the feature's **unmerged `harvest.md` candidates**, + lesson **titles** with their files — and the document paths last, relative to + the store. Every line comes from a record; the tool selects and bounds, it + never summarizes. Long blocks are cut with a `… N more line(s): ` line. +- **The contents page opens with an overview table** — feature, state, date, + tags, goal — open features first, **newest first** (the intent's `Date`, else + a `YYMMDD-` slug prefix; closed features by `closed_at`), then a + **"Knowledge not merged yet"** section listing every open feature whose + `harvest.md` holds candidates, with the first lines of each, then one section + per feature (goal, summary, track, close reason, documents, lesson titles). + Goal cells are bounded in bytes under `LC_ALL=C` and cut at a UTF-8 character + boundary, so a Korean goal is never rendered with a broken glyph. Record + headings embedded in the page are demoted to labels so a harvest's `###` + cannot hijack the page outline. +- **`kb.sh harvest [--stale ]`** — the trigger the close-only merge + lacked. Lists open features whose `harvest.md` no close has merged, with the + candidate line count and the idle time (newest mtime among the feature's own + records, never `scratch/`); `STALE` at or past the threshold (default 30 + days). Exit `0` something is unmerged, `1` nothing, `2` usage. It reports and + writes nothing. +- **AGENTS.md rule 4: a loop that never closes must not hide its knowledge.** + An unmerged harvest is readable before close (`show`, the page, `harvest`), + and a feature idle 30 days or more may have its harvest merged **without + closing** — the same procedure, one merge at a time in the owning checkout, + `harvest.md` deleted afterwards, the feature stays open. `close.sh` still + blocks on an unmerged harvest exactly as before. +- **`kb.sh index --obsidian`**, or `index_style: obsidian` in the store's + `config.md` so `init.sh` and `close.sh` keep the style: YAML frontmatter + (`title`, `sdlc_store`, `tags`) and the feature tags rendered as inline + `#tags` for a vault's tag pane. Links stay standard relative markdown, which + Obsidian resolves and graphs; only the generated page is touched. **No + timestamp is written into the page** in either style — a regenerated page + whose records did not change is byte-identical, so a vault under git gets no + diff and no sync conflict from it. The generated-page marker is now + recognized within the first 12 lines (after frontmatter); a page you wrote + yourself is still never overwritten. +- **Stage guidance and docs.** `skills/1-intent` (write `summary.md`, read the + digest before opening files), `skills/5-ship` (bring Result/Lesson up to + date), `skills/6-maintain` (`kb.sh harvest` before opening a fix-slug), + `templates/intent.md` (a pointer: the digest lives in `summary.md`), + `templates/harvest.md`, `SKILL.md` routes, `AGENTS.md` rule 4, both READMEs. + +## Usage + +```sh +~/sdlc-kit/tools/kb.sh show 260920-login-fix # digest first, paths last +~/sdlc-kit/tools/kb.sh harvest # what is learned but not in memory/ yet +~/sdlc-kit/tools/kb.sh harvest --stale 14 --area ~/knowledge +~/sdlc-kit/tools/kb.sh index --obsidian # once; or add to /config.md: +# index_style: obsidian # tools/kb.sh index +``` + +## Upgrade notes + +- **Nothing to migrate.** Existing records are read as they are; a feature + without `summary.md` shows its goal and harvest, and older intents without a + `- Goal:` line show their title. Add `summary.md` to a feature when you next + touch it. +- **The generated page changes shape** the first time `init.sh`, `close.sh` or + `kb.sh index` runs under this kit. A `README.md` you wrote yourself is still + refused, not overwritten. +- **`kb.sh show` output is longer** (digest before paths). Scripts that parsed + the old `documents:` block should read the `Documents:` block, whose paths are + now relative to the store. +- Merging a stale harvest without closing is permitted, not automated: the + agent does the merge under rule 4's guards, then deletes `harvest.md`. + +## Validation + +Local, on macOS (darwin 25.6.0, arm64, GNU bash 3.2.57, git 2.54.0, APFS +case-insensitive, UTF-8): + +- `bash gates/knowledge-test.sh` → `KNOWLEDGE-TEST PASS`, 145 assertions + (38 new in section H: the digest, the overview order, tags, the unmerged + section, UTF-8-safe truncation, byte-identical regeneration, `harvest` exit + codes and `--stale`, `--obsidian` and `index_style`, and that no record under + `work/` was written) +- `bash gates/selftest.sh` → `SELFTEST PASS` +- `bash gates/e2e.sh` → `E2E PASS`, 152 assertions, 0 failures +- `bash gates/autotest.sh` → `AUTOTEST PASS`, 195 assertions, 0 failures +- `bash -n` over every changed shell script: clean. `git diff --check`: clean. +- Smoke on a real store (84 open features, Korean records): `show` renders a + digest with the harvest facts; `harvest` lists 9 features / 157 lines; the + page's overview table cuts long goals at a character boundary. diff --git a/skills/1-intent/SKILL.md b/skills/1-intent/SKILL.md index 1d3cd97..c80e670 100644 --- a/skills/1-intent/SKILL.md +++ b/skills/1-intent/SKILL.md @@ -20,9 +20,11 @@ change, overwrite `.sdlc/work//progress.md` with one line — Then **retrieve what past features already decided about this area**: `tools/kb.sh search ""` over the request's main nouns (the module, the endpoint, the error text), and - `tools/kb.sh show ` for any feature the hits name. Closed features - are included, so a decision made a year ago surfaces with its evidence - and delivery. Two or three targeted searches, not a scan of the archive: + `tools/kb.sh show ` for any feature the hits name — a digest + (goal, summary.md, delivery, unmerged harvest candidates, lesson titles) + that answers most questions before a single record file is opened. + Closed features are included, so a decision made a year ago surfaces + with its evidence and delivery. Two or three targeted searches, not a scan of the archive: what you find goes into the Evidence section with its source path. 2. Pick a kebab-case feature slug; create `.sdlc/work//`. Prefix with the tracker key when one exists (`a20-1234-fix-login`), else the date @@ -125,11 +127,20 @@ If an Unknown turns out to be an independent shippable change, open a new feature slug for it and record the reference under Decided. When resolving an Unknown surfaces a durable fact about the system, add it to the feature's `.sdlc/work//harvest.md` as a domain candidate (merged into DOMAIN.md -at close — AGENTS.md rule 4) — Decided records the decision, the harvest -carries the fact to every later feature. +at close, or without closing once the feature is stale — AGENTS.md rule 4; +readable before either through `tools/kb.sh show`) — Decided records the +decision, the harvest carries the fact to every later feature. ## Write the artifact +Write `summary.md` beside it (templates/summary.md) — the page a human reads +instead of the stage files, printed first by `tools/kb.sh show` and the +contents page. Fill `Tags` (the domain areas a reader browses by), `Problem`, +and `Cause`/`Change` as far as they are known; leave `Result` at "not +delivered". No approval binds summary.md, so later stages keep it true +(skills/5-ship sets Result and Lesson). Ten lines, plain language, each +claim pointing at the record that proves it. + When the request has an origin — a ticket, a 기획서, an incident — snapshot it FIRST as `.sdlc/work//origin.md` (templates/origin.md): the intent approval binds it, so it is written before the gate, and an edit afterwards diff --git a/skills/5-ship/SKILL.md b/skills/5-ship/SKILL.md index 05ae30e..a101e24 100644 --- a/skills/5-ship/SKILL.md +++ b/skills/5-ship/SKILL.md @@ -71,7 +71,13 @@ no lesson. Write what there is into the feature's `.sdlc/work//harvest.md` (lesson candidates in the skill 6 format; durable terms, verified facts, and constraints as domain candidates). INDEX.md, DOMAIN.md, and lessons/ are written only at close, by the closer -(AGENTS.md rule 4). Domain facts describe the system; lessons describe mistakes. If a +(AGENTS.md rule 4) — or, for a feature that stays open and idle, by the +stale-harvest merge the same rule allows. Then bring the feature's +`summary.md` up to date: `Result` says what delivery.md confirms (a pushed +review branch is not a deployment), `Lesson` is the one line worth +remembering, `Cause`/`Change` match what was actually found and built. +summary.md is bound by no approval; `tools/kb.sh show` prints it first, so +a stale one misleads every later reader. Domain facts describe the system; lessons describe mistakes. If a stage skill should have prevented a mistake, add `promote: skills/` to the lesson candidate. A tag that appears three or more times in INDEX.md must be promoted: propose the stage-skill change to the human (`close.sh` diff --git a/skills/6-maintain/SKILL.md b/skills/6-maintain/SKILL.md index 9c27ac8..0aa9c5c 100644 --- a/skills/6-maintain/SKILL.md +++ b/skills/6-maintain/SKILL.md @@ -167,8 +167,10 @@ spec.md and run the full route instead; the intent gate still comes first. **Recurrence cap: three fix loops for one symptom.** Before opening a fix-slug, grep the symptom's tags in INDEX.md AND open features' harvests -(`grep -l .sdlc/work/*/harvest.md`) — in-flight lessons are not -merged yet. On the third match, stop fixing and start investigating: the repetition is +(`tools/kb.sh harvest` names every open feature still holding one; +`tools/kb.sh search ""` reads through them) — in-flight lessons are +not merged yet, and a harvest idle 30 days or more may be merged now +without closing its feature (AGENTS.md rule 4). On the third match, stop fixing and start investigating: the repetition is evidence that the cause found so far is not the cause. Widen the investigation — what the three incidents share, which invariant keeps breaking, what the earlier fixes actually changed — and take the finding to diff --git a/templates/harvest.md b/templates/harvest.md index 385c5cc..37f68ae 100644 --- a/templates/harvest.md +++ b/templates/harvest.md @@ -4,7 +4,11 @@ researchers append candidates HERE, never to INDEX.md or DOMAIN.md — parallel loops would race or merge-conflict on the shared files. At close: merge into memory/ (lesson files + INDEX.md lines + DOMAIN.md - facts), then DELETE this file. close.sh blocks while it exists. --> + facts), then DELETE this file. close.sh blocks while it exists. + Readable BEFORE close: `tools/kb.sh show ` and the contents page + print these candidates, and `tools/kb.sh harvest` lists every open + feature that still holds one. A feature idle 30 days or more may be + merged without closing (same procedure; it stays open — rule 4). --> ## Domain candidates - Date: YYYY-MM-DD - Type: greenfield | brownfield - Track: full (default) | compact — diff --git a/templates/summary.md b/templates/summary.md new file mode 100644 index 0000000..8c953dc --- /dev/null +++ b/templates/summary.md @@ -0,0 +1,18 @@ +# Summary: + + + +- Tags: +- Problem: +- Cause: +- Change: +- Result: +- Lesson: diff --git a/tools/kb.sh b/tools/kb.sh index 7aeb1f5..83fda74 100755 --- a/tools/kb.sh +++ b/tools/kb.sh @@ -1,11 +1,14 @@ #!/usr/bin/env bash # kb.sh — read the records back. The loop WRITES knowledge (intent, spec, plan, -# evidence, delivery, CLOSED, lessons); this is how a later feature FINDS it. +# evidence, delivery, CLOSED, lessons, harvest); this is how a later feature — +# or a human — FINDS it and reads it without opening six files. # -# kb.sh index [--store ] regenerate the contents page -# kb.sh list [--store | --area ] -# kb.sh show [--store | --area ] -# kb.sh search [--store | --area ] [--limit N] +# kb.sh index [--store ] [--obsidian] regenerate the contents page +# kb.sh list [--store | --area ] +# kb.sh show [--store | --area ] one feature as a digest +# kb.sh search [--store | --area ] [--limit N] +# kb.sh harvest [--store | --area ] [--stale ] +# open features whose harvest.md is not in memory/ yet # # Store resolution: --store is a store (or a project's .sdlc); with # neither flag the store is ./.sdlc. --area reads EVERY recognized @@ -18,19 +21,36 @@ # Never read: scratch/ (bulk evidence), approvals/ (machine records), # progress.md (heartbeat), baseline.txt, checkpoint.md, verify-receipt.md. # +# What a reader gets (v0.13): `show` is a digest — the goal, the feature's +# summary.md (templates/summary.md: Problem/Cause/Change/Result/Lesson, bound +# by no approval so it can stay current), delivery state, the unmerged +# harvest.md candidates, lesson titles — with the document paths last. `index` opens with +# an overview table (state, date, tags), newest first, and lists the harvests +# that no close has merged yet, so in-flight knowledge is visible from the +# entry page instead of only after `close.sh`. `--obsidian` (or +# `index_style: obsidian` in the store's config.md) adds YAML frontmatter and +# inline #tags to the generated page; links stay standard relative markdown, +# which Obsidian resolves and graphs. No timestamp is written into the page: +# a regenerated page that did not change must not produce a diff. +# # Nothing under work/ or archive/ is ever written — an approval-bound artifact # cannot change because the page was regenerated. `index` writes exactly one # file, /README.md, and refuses to overwrite one it did not generate. # -# Exit codes: 0 done / matches found, 1 nothing found (no match, unknown slug), -# 2 usage error or refusal (missing store, unowned path, unwritable page). +# Exit codes: 0 done / matches found, 1 nothing found (no match, unknown slug, +# no unmerged harvest), 2 usage error or refusal (missing store, unowned path, +# unwritable page). set -u MARKER='' LINE_MAX=200 +SUMMARY_MAX=12 # lines of summary.md shown by `show` / `index` +HARVEST_MAX=10 # harvest.md lines shown by `show` +HARVEST_INDEX_MAX=6 # harvest.md lines shown per feature on the contents page +TABLE_GOAL_MAX=120 # bytes of the goal kept in the overview table (≈40 CJK / 120 ASCII characters) usage() { - sed -n '3,9p' "$0" | sed 's/^# \{0,1\}//' >&2 + sed -n '3,11p' "$0" | sed 's/^# \{0,1\}//' >&2 exit 2 } @@ -64,12 +84,15 @@ kb_stores() { # } # --- feature reading --------------------------------------------------------- -KB_DOCS="origin.md intent.md spec.md plan.md map.md evidence.md delivery.md CLOSED" +KB_DOCS="summary.md origin.md intent.md spec.md plan.md map.md evidence.md delivery.md CLOSED" kb_field() { #