diff --git a/README.ko.md b/README.ko.md index 90d51ee..c5b5310 100644 --- a/README.ko.md +++ b/README.ko.md @@ -168,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`가 자동으로 실행합니다). 페이지는 상태·날짜·태그를 담은 개요 표(최신순), 아직 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` 사용법 오류 또는 거부입니다. +**기록을 다시 읽는 도구는 `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 <폴더>`를 붙이면 그 폴더 안의 모든 저장소를 대상으로 같은 일을 하며, 원래 체크아웃이 사라진 피처도 읽을 수 있습니다. 저장소 config.md에 `index_style: obsidian`을 적으면 Obsidian 볼트용 frontmatter와 인라인 `#태그`를 덧붙입니다. 생성 시각은 절대 쓰지 않으므로 내용이 같으면 diff도 생기지 않습니다. 종료 코드는 `0` 찾음, `1` 없음, `2` 사용법 오류 또는 거부입니다. 공개 sdlc-kit 저장소는 프레임워크만 담습니다. 기록은 작성된 자리, 즉 프로젝트 작업 사본이나 사용자가 고른 영역에 남아 그대로 읽힙니다. diff --git a/README.md b/README.md index a246f1b..87d5cab 100644 --- a/README.md +++ b/README.md @@ -168,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) — 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. +**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_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. diff --git a/SKILL.md b/SKILL.md index 848bd33..70f20b0 100644 --- a/SKILL.md +++ b/SKILL.md @@ -36,7 +36,7 @@ they do it under this contract (gates, artifacts, memory), not beside it. | "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 ` — 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. | +| "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`; `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 6345c21..b561134 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.13.0 +v0.13.1 diff --git a/gates/knowledge-test.sh b/gates/knowledge-test.sh index e29f4b3..eea8f9a 100755 --- a/gates/knowledge-test.sh +++ b/gates/knowledge-test.sh @@ -649,18 +649,19 @@ 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 +# obsidian — a store setting (config.md), so init.sh and close.sh keep the style; +# it changes the generated page only +printf 'index_style: obsidian # tools/kb.sh index\n' >> .sdlc/config.md +assert_ok_msg "H26 index_style: obsidian in config.md selects the style" "(obsidian)" kb index 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 "H31 a frontmatter page is still recognized as generated" kb index +sed -i.bak '/^index_style:/d' .sdlc/config.md && rm -f .sdlc/config.md.bak +assert_ok_msg "H32 removing the setting regenerates a plain page" "(plain)" kb index +assert_exit "H33 the plain page has no frontmatter" 1 sh -c "head -n 1 '$PAGE' | grep -q '^---'" +assert_fail_msg "H34 there is no style flag — one setting, one place" "unknown option" bash "$KIT/tools/kb.sh" index --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 diff --git a/log/changelog-v0.13.1.md b/log/changelog-v0.13.1.md new file mode 100644 index 0000000..2831022 --- /dev/null +++ b/log/changelog-v0.13.1.md @@ -0,0 +1,43 @@ +# v0.13.1 — kb.sh without the duplication v0.13.0 shipped with + +A refactor of `tools/kb.sh` with one intentional behaviour change. Output of +`show`, `index`, `list`, `search` and `harvest` is unchanged byte for byte +except where noted; the gates are untouched. + +## Changes + +- **One definition of "unmerged harvest".** `index` and `harvest` each had their + own loop deciding which open features still hold candidates; both now read + `kb_unmerged`. `index`'s two passes over the features (overview table, then + one section each) share `kb_meta` for state, date, tags and title instead of + extracting them twice. +- **`kb_get`** replaces the nine `kb_real "$(kb_field …)"` idioms; `kb_closed` + replaces the three awk one-liners over `CLOSED`. +- **Dropped: the `## Summary`-inside-intent.md fallback.** It supported a format + no store ever wrote — designed and abandoned inside the v0.13.0 work — and its + helper `kb_summary_where` with it. `summary.md` is the digest. +- **Dropped: `index --obsidian`.** The page style is a property of the store, and + `init.sh` and `close.sh` regenerate the page without flags, so + `index_style: obsidian` in the store's `config.md` was already the setting + that mattered. One setting, one place; the flag is now an unknown option. +- **Fixed on the way:** `show` printed `Tags:` only when the intent carried them, + although the tags live in `summary.md` — the meta line now uses the same + `kb_tags` the page uses. A feature that carries a `CLOSED` record but was never + archived (an interrupted close) is now treated as closed everywhere on the + page — sorted by `closed_at` and labelled `closed ` — where before only + `archive/` entries were. +- Internal record lines use `\037` as separator, not a tab: a tab is IFS + whitespace, so `read` collapses adjacent tabs and an empty field (no tags, no + date) shifted the fields after it. +- Header comment shortened; `tools/kb.sh` 605 → 538 lines. + +Checked against a real store (84 open, 2 archived, Korean records): the page +and the `show` digests are byte-identical to v0.13.0 except for the two lines +the fixes above describe; `harvest` output is identical. + +## Validation + +- `bash gates/knowledge-test.sh` → `KNOWLEDGE-TEST PASS`, 144 assertions + (section H rewritten for the config-only style; the flag refusal is asserted) +- `bash gates/selftest.sh` → `SELFTEST PASS` +- `bash gates/e2e.sh` → `E2E PASS`, 152 · `bash gates/autotest.sh` → `AUTOTEST PASS`, 195 diff --git a/tools/kb.sh b/tools/kb.sh index c2d349f..7fdef6b 100755 --- a/tools/kb.sh +++ b/tools/kb.sh @@ -3,7 +3,7 @@ # 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 ] [--obsidian] regenerate the contents page +# kb.sh index [--store ] 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] @@ -21,17 +21,12 @@ # 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. +# `show` is a digest — goal, summary.md (templates/summary.md), delivery, +# unmerged harvest.md candidates, lesson titles, then the paths. `index` opens +# with an overview table (state, date, tags) newest first and lists the harvests +# no close has merged yet. `index_style: obsidian` in the store's config.md adds +# YAML frontmatter and inline #tags to the page; links stay relative markdown. +# No timestamp is written into the page: an unchanged store yields no diff. # # Nothing under work/ or archive/ is ever written — an approval-bound artifact # cannot change because the page was regenerated. `index` writes exactly one @@ -44,10 +39,14 @@ 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` +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) +TABLE_GOAL_MAX=120 # bytes of the goal kept in the overview table (≈40 CJK / 120 ASCII characters) +# Field separator for this tool's own record lines. Not a tab: a tab is IFS +# whitespace, so `read` collapses adjacent tabs and an EMPTY field (no tags, no +# date) would shift the fields after it. \037 (ASCII unit separator) is not. +SEP=$(printf '\037') usage() { sed -n '3,11p' "$0" | sed 's/^# \{0,1\}//' >&2 @@ -83,67 +82,41 @@ kb_stores() { # printf '%s\n' "$store" } -# --- feature reading --------------------------------------------------------- +# --- record fields ----------------------------------------------------------- KB_DOCS="summary.md origin.md intent.md spec.md plan.md map.md evidence.md delivery.md CLOSED" -kb_field() { #