One work log per bundle: the rule, the gate that enforces it, and the second log it came out of #57

Merged
dank merged 3 commits from docs/single-work-log-rule into main 2026-10-20 22:23:22 -07:00
10 changed files with 187 additions and 44 deletions

No files matched your search

+7
View File
@@ -69,6 +69,13 @@ Documentation exists for ONE reason: **point of use**. A page earns its bytes on
4. Always update the documentation when you've completed a task — and when updating means deleting a page or a paragraph, delete it. 4. Always update the documentation when you've completed a task — and when updating means deleting a page or a paragraph, delete it.
5. Compact the work log when necessary; a work log holds laws and incidents, never bookkeeping. 5. Compact the work log when necessary; a work log holds laws and incidents, never bookkeeping.
#### THE WORK LOG IS ONE FILE.
The work log is a single page in the knowledge base: `omp-docs/log.md` in this repo, or whichever change-log page the repo's docs index names. Write the entry when the change lands — in the same commit as the change — and read that page to recover where things stand before starting. It follows its own entry law: a dated entry is what changed plus the durable lesson, a few lines.
- NEVER create a second log: not per directory, not per project, feature, branch or run, not `NOTES.md` / `PROGRESS.md` / `STATUS.md` / `worklog/`, not a dated diary. Logging in fifty places is not logging — the entry is never where the next reader looks, and each copy re-dates the same event on its own authority. In `dank/command-center` the KB linter reports any `log.md` that is not the bundle root's.
- A log holds laws and incidents, never bookkeeping: no per-file inventories, no run-by-run numbers, no transcripts. A number in a log still has to pass the purpose test above.
- If a knowledge base has no log page, add one at its root and list it in the index. That is the fix — never a log somewhere else.
#### USE PROGRESSIVE DISCLOSURE. #### USE PROGRESSIVE DISCLOSURE.
Always ensure that all produced documentation is laid out in files following WIKI conventions: start with a top level README.md index file that provides a brief, concise introduction to the item being documented, and then SPLIT OFF individual aspects, features, or arguments into their own interlinked pages so that future agents can read the documentation in a piecewise fashion, only taking in the details they need. Where relevant, crosslink any created documentation with other relevant documentation. This structure may also be nested, if necessary. Always ensure that all produced documentation is laid out in files following WIKI conventions: start with a top level README.md index file that provides a brief, concise introduction to the item being documented, and then SPLIT OFF individual aspects, features, or arguments into their own interlinked pages so that future agents can read the documentation in a piecewise fashion, only taking in the details they need. Where relevant, crosslink any created documentation with other relevant documentation. This structure may also be nested, if necessary.
+1 -3
View File
@@ -16,6 +16,4 @@ These files define the structural conventions that govern the agent system itsel
* [OMP Status Line Customization](statusline.md) — How presets, composer shapes, and segment arrays actually interact; the fleet's tuned bar. (moved from /pi/, 2026-10-19) * [OMP Status Line Customization](statusline.md) — How presets, composer shapes, and segment arrays actually interact; the fleet's tuned bar. (moved from /pi/, 2026-10-19)
* [OMP Subagent Limits & Fan-out Policy](subagent-limits.md) — Live caps: concurrency 2, no nested spawns (the `maxRecursionDepth: 0` trap), per-endpoint in-flight ceilings, isolation policy. (moved from /pi/, 2026-10-19) * [OMP Subagent Limits & Fan-out Policy](subagent-limits.md) — Live caps: concurrency 2, no nested spawns (the `maxRecursionDepth: 0` trap), per-endpoint in-flight ceilings, isolation policy. (moved from /pi/, 2026-10-19)
## Change Tracking **Change log:** the bundle keeps exactly one, one level up — [Bundle Change Log](../log.md). A directory here never has its own.
* [Bundle Change Log](log.md) — Chronological update history.
-21
View File
@@ -1,21 +0,0 @@
---
type: changelog
title: Core Bundle Change Log
description: Change log for the core documentation directory — tracks migrations and structural changes.
tags:
- core
- changelog
- okf
timestamp: 2026-07-18
---
# Core Bundle Change Log
## 2026-07-18
- **Migration**: Converted `index.md` (root) to `core/index-doc.md` — moved root bundle index into core domain
- **Migration**: Converted `agents-md-format.md` (root) to `core/agents-md-format.md` — agents-md format belongs in core
- **Creation**: Established `core/` directory with `index.md` as the domain entry point
- **Creation**: This change log for tracking core directory modifications
## 2026-07-15
- **Initialization**: OKF v0.1 conversion of the entire docs bundle — frontmatter applied, structure established, first `log.md` entries created
+21
View File
@@ -16,6 +16,27 @@ lesson, a few lines. Full incident narratives are recoverable from git
(`git log --follow omp-docs/log.md`); knowledge that outlives an entry lives (`git log --follow omp-docs/log.md`); knowledge that outlives an entry lives
on its concept page, linked here. Never re-expand an entry in place. on its concept page, linked here. Never re-expand an entry in place.
This page is the bundle's **only** work log: one file, at the root. Never start a
second one in a subdirectory or under another name — `scripts/kb-lint.sh` reports any
`log.md` that is not this one.
## 2026-10-20 — one work log per bundle
The bundle had two: this page and `core/log.md`, created "for tracking core
directory modifications" — two entries of pure migration bookkeeping (file moved,
directory created, log created), and a date for the OKF conversion that neither this
page nor git agrees with: 2026-07-15 there, 2026-01-15 here, while `git log` dates the
OKF rebuild 2026-08-28. `core/index.md` was still pointing readers at it, which is how
a stale page stays alive. Trashed to `omp-docs/core/.AGENTTRASH/log.md` — its content
is recoverable from git, which is the entire argument against a second log.
Laws, now written down in [references/okf-spec.md](references/okf-spec.md) and
[references/okf-maintenance.md](references/okf-maintenance.md), and gated: a bundle has
ONE work log, `log.md`, at its root, and `scripts/kb-lint.sh` reports any `log.md` that
is not the root's — so the sprawl that made logging useless once cannot restart
quietly. The agent-side duty is in `agent/APPEND_SYSTEM.md` § 1: log here, read here,
never start another.
## 2026-10-20 — two instruments rescued from `$HOME` script sediment ## 2026-10-20 — two instruments rescued from `$HOME` script sediment
`$HOME` had accumulated 123 loose scripts at its root (archived to `$HOME` had accumulated 123 loose scripts at its root (archived to
+37 -15
View File
@@ -21,32 +21,46 @@ Knowledge bundles rot. It is the nature of knowledge to attract cobwebs, much li
## Change Tracking with `log.md` ## Change Tracking with `log.md`
Every OKF bundle has a `log.md` file. It is not optional. It is the bundle's memory. Every OKF bundle has a `log.md`, and it is the bundle's **only** work log: one file,
at the bundle root. A subdirectory never gets its own — not `core/log.md`, not
`notes.md`, not a per-project, per-branch or per-run diary. Sprawl is not caution:
with fifty logs the entry is never where the next agent looks, and each copy re-dates
the same event on its own authority. `scripts/kb-lint.sh` reports any `log.md` that is
not the root's.
**Format:** Date-grouped entries, newest first. One line per change with timestamp, file path, and what changed. **Format:** one dated section per day, newest first; a few lines per change — what
happened, and the durable lesson. It is not a file manifest.
```markdown ```markdown
# Bundle Change Log # Bundle Change Log
## 2026-01-15 ## 2026-01-15 — the gate was reading the wrong files
- `/docs/ghostty.md` — Updated keybind table after v1.2 release
- `/docs/pi-permission-system/` — Added `policy.md` and `audit.md` A gate that gets an empty file list reports success. Law: a check MUST fail closed;
- `/docs/index.md` — Synced with directory contents a silently-passing gate is worse than none, because it buys the belief of safety.
Details: [secret gate](../tools/index.md).
``` ```
**Rules:** **Rules:**
- **Newest entries first.** Putting old entries at the top inverts the timeline and makes it impossible to see what just happened. - **Newest entries first.** Old entries at the top invert the timeline.
- **One line per change.** A single line is sufficient. The git history is there if someone wants the gory details. - **What changed + the durable lesson, a few lines.** Full narratives are recoverable
- **Group by date.** Batch all changes from the same day together. from git (`git log --follow log.md`); never re-expand one in place.
- **Always date-stamp the section.** Use `## YYYY-MM-DD`. - **Laws and incidents, never bookkeeping.** No per-file inventories, no run-by-run
- **Mention `index.md` whenever you change a bundle.** If you added or removed a file, update the index and note it in the log. numbers, no transcripts. Measured numbers still have to pass the purpose test.
- **Link, do not duplicate.** Knowledge that outlives the entry belongs on its concept
page; the entry points there.
- **One dated section per day** (`## YYYY-MM-DD`), and mention `index.md` whenever a
file is added or removed.
### Common Mistakes ### Common Mistakes
- ❌ Writing `"Updated ghostty.md"` — Updated it with what? Be specific. - ❌ `Updated ghostty.md` — updated it with what, and what did it teach?
- ❌ Forgetting to write to `log.md` — This is the equivalent of doing surgery without keeping a record. - ❌ Forgetting the entry — the change lands undocumented and the next agent rediscovers
- ❌ Using relative paths — Use absolute paths (`/docs/ghostty.md`). the failure the hard way.
- ❌ Leaving stale entries from manual edits — The log is the single source of truth. - ❌ Starting a second log in a subdirectory — the sprawl that makes logging useless.
- ❌ Invented absolute paths in links — links resolve relative to the page, and the
linter checks them.
- ❌ Re-expanding an old entry in place — the entry is a pointer; git is the transcript.
--- ---
@@ -84,6 +98,14 @@ OKF is built on the principle that a file is one concept.
- ❌ Don't create a file called `misc.md` — `misc` is a graveyard, not a category. - ❌ Don't create a file called `misc.md` — `misc` is a graveyard, not a category.
- ❌ Don't use `# Other` or `# Additional Notes` sections — these are taping things to the wall. - ❌ Don't use `# Other` or `# Additional Notes` sections — these are taping things to the wall.
## Quarantine, Not Deletion
A page is never deleted outright: it moves to `.AGENTTRASH/` beside where it lived and
waits for a pruning session with Frank present. The quarantine is not part of the
bundle — `scripts/kb-lint.sh` and `scripts/kb-coverage.py` skip any path containing
`.AGENTTRASH`. That exemption is deliberate: a gate that goes green only once the
quarantined copy is gone is a gate that deletes history.
--- ---
## Auto-Generation Strategies ## Auto-Generation Strategies
+8 -2
View File
@@ -42,7 +42,7 @@ An OKF knowledge bundle is a directory tree. Every concept is one file. The file
``` ```
my-bundle/ my-bundle/
├── index.md # Optional directory listing ├── index.md # Optional directory listing
├── log.md # Optional update history ├── log.md # The bundle's ONE update history — root only
├── concept-one.md # A concept at the root level ├── concept-one.md # A concept at the root level
└── subdirectory/ └── subdirectory/
├── index.md # Optional subdirectory listing ├── index.md # Optional subdirectory listing
@@ -55,7 +55,13 @@ my-bundle/
| Filename | Purpose | | Filename | Purpose |
|----------|---------| |----------|---------|
| `index.md` | Directory listing for progressive disclosure | | `index.md` | Directory listing for progressive disclosure |
| `log.md` | Chronological update history | | `log.md` | Chronological update history — **exactly one, at the bundle root** |
**One work log per bundle.** `log.md` is reserved, and it is reserved for a single
file: the bundle root's. A subdirectory never keeps its own change log — a bundle
that logs in fifty places has no log, because the entry is never where the next
reader looks and every copy re-dates the same event. `scripts/kb-lint.sh` reports
any `log.md` that is not the root's.
--- ---
+13 -1
View File
@@ -4,7 +4,15 @@ import re, sys
from pathlib import Path from pathlib import Path
kb = Path(sys.argv[1]) kb = Path(sys.argv[1])
issues = [] issues = []
dirs = sorted({p.parent for p in kb.rglob("*.md")})
def kept(p):
# `.AGENTTRASH/` is quarantine, not bundle content: a gate that flags it pushes an
# agent to delete a page that is only waiting for a pruning session with the operator.
return ".AGENTTRASH" not in p.parts
dirs = sorted({p.parent for p in kb.rglob("*.md") if kept(p)})
for d in dirs: for d in dirs:
idx = d / "index.md" idx = d / "index.md"
if not idx.is_file() and d != kb: if not idx.is_file() and d != kb:
@@ -21,12 +29,16 @@ for d in dirs:
# wikilinks too # wikilinks too
wl_names = {m.group(1).strip().rsplit("/", 1)[-1] for m in re.finditer(r"\[\[([^\]|#]+)", text)} wl_names = {m.group(1).strip().rsplit("/", 1)[-1] for m in re.finditer(r"\[\[([^\]|#]+)", text)}
for sib in sorted(d.iterdir()): for sib in sorted(d.iterdir()):
if sib.name == ".AGENTTRASH":
continue
if sib.suffix != ".md" or sib.name == "index.md": if sib.suffix != ".md" or sib.name == "index.md":
continue continue
if sib.resolve() not in linked and sib.stem not in wl_names: if sib.resolve() not in linked and sib.stem not in wl_names:
issues.append(f"{d.relative_to(kb)}/index.md: does not link {sib.name}") issues.append(f"{d.relative_to(kb)}/index.md: does not link {sib.name}")
# subdirs of d linked as their index? # subdirs of d linked as their index?
for sub in sorted(d.iterdir()): for sub in sorted(d.iterdir()):
if sub.name == ".AGENTTRASH":
continue
if sub.is_dir() and (sub / "index.md").is_file(): if sub.is_dir() and (sub / "index.md").is_file():
subidx = (sub / "index.md").resolve() subidx = (sub / "index.md").resolve()
dirref = sub.resolve() dirref = sub.resolve()
+22 -2
View File
@@ -12,7 +12,15 @@ from pathlib import Path
kb = Path(sys.argv[1]) kb = Path(sys.argv[1])
violations = [] violations = []
for p in sorted(kb.rglob("*.md")): def pages():
"""Markdown pages that belong to the bundle. `.AGENTTRASH/` is a quarantine, not
content: linting a deleted-but-not-yet-pruned page would demand its destruction to
make the gate go green, which is the wrong incentive."""
for p in kb.rglob("*.md"):
if ".AGENTTRASH" not in p.parts:
yield p
for p in pages():
rel = p.relative_to(kb) rel = p.relative_to(kb)
text = p.read_text(errors="replace") text = p.read_text(errors="replace")
lines = text.splitlines() lines = text.splitlines()
@@ -38,9 +46,21 @@ for p in sorted(kb.rglob("*.md")):
if not isinstance(fm, dict) or not str(fm.get("type", "")).strip(): if not isinstance(fm, dict) or not str(fm.get("type", "")).strip():
violations.append(f"{rel}: frontmatter lacks required 'type:'") violations.append(f"{rel}: frontmatter lacks required 'type:'")
# One work log per bundle, at the bundle root. A `log.md` is a reserved name, but
# reserved does not mean "allowed in every directory": per-directory logs are how a
# bundle ends up with fifty of them, the entry never where the next agent looks, and
# each copy dating the same event differently. A bundle with no log at all is left to
# the maintenance page to nag about; this gate's job is sprawl.
logs = sorted(str(p.relative_to(kb)) for p in pages() if p.name == "log.md")
if len(logs) > 1 or (logs and logs[0] != "log.md"):
violations.append(
"a bundle has one work log, log.md, at the bundle root; found: " + ", ".join(logs)
)
# link integrity: fenced code blocks hold example paths, not live links — strip first. # link integrity: fenced code blocks hold example paths, not live links — strip first.
# Vendored spec is exempt: its example paths are illustrations, not live links. # Vendored spec is exempt: its example paths are illustrations, not live links.
for p in sorted(kb.rglob("*.md")): for p in pages():
if str(p.relative_to(kb)) == "references/okf-spec.md": if str(p.relative_to(kb)) == "references/okf-spec.md":
continue continue
text = re.sub(r"^```[^\n]*$^```[^\n]*$", "", p.read_text(errors="replace"), flags=re.M) text = re.sub(r"^```[^\n]*$^```[^\n]*$", "", p.read_text(errors="replace"), flags=re.M)
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bats
# tests/kb-coverage.bats — black-box tests for scripts/kb-coverage.py
#
# Contract: exit 0 when every directory of a knowledge bundle has an index.md and each
# index links its siblings. Observable surface: stdout lines and exit status.
setup() {
CHECK="$BATS_TEST_DIRNAME/../scripts/kb-coverage.py"
KB="$(mktemp -d)/kb"
mkdir -p "$KB/sub"
printf '# Index\n\n* [Good](good.md)\n* [Sub](sub/)\n' > "$KB/index.md"
printf '# Sub\n\n* [Deep](deep.md)\n' > "$KB/sub/index.md"
printf 'body\n' > "$KB/good.md"
printf 'body\n' > "$KB/sub/deep.md"
}
teardown() { rm -rf "$(dirname "$KB")"; }
@test "a bundle whose directories index their contents passes" {
run python3 "$CHECK" "$KB"
[ "$status" -eq 0 ]
[[ "$output" == *"0 issue(s)"* ]]
}
@test "a directory nobody indexes is reported" {
mkdir "$KB/stray"
printf 'body\n' > "$KB/stray/thing.md"
run python3 "$CHECK" "$KB"
[ "$status" -ne 0 ]
[[ "$output" == *"stray: no index.md"* ]]
}
@test "an index that forgets a sibling page is reported" {
printf 'body\n' > "$KB/orphan.md"
run python3 "$CHECK" "$KB"
[ "$status" -ne 0 ]
[[ "$output" == *"does not link orphan.md"* ]]
}
@test "the quarantine trash directory is not part of the bundle" {
# Pages deleted from the KB wait in .AGENTTRASH/ for a pruning session with Frank.
# Requiring an index there, or listing its contents, would push an agent to delete
# the quarantined copy just to make the gate green.
mkdir -p "$KB/.AGENTTRASH"
printf 'retired page\n' > "$KB/.AGENTTRASH/old-page.md"
run python3 "$CHECK" "$KB"
[ "$status" -eq 0 ]
[[ "$output" == *"0 issue(s)"* ]]
}
+29
View File
@@ -91,6 +91,35 @@ EOF
[ "$status" -eq 2 ] [ "$status" -eq 2 ]
} }
@test "a second work log in a subdirectory is reported" {
# One bundle, one work log. Per-directory logs are how a bundle ends up with fifty
# of them: the entry is never where the next agent looks, and each copy re-dates the
# same event on its own.
printf -- '---\ntype: changelog\n---\n## 2026-01-01\n- created a log\n' > "$KB/log.md"
printf -- '---\ntype: changelog\n---\n## 2026-01-02\n- also a log\n' > "$KB/sub/log.md"
run bash "$LINT" "$KB"
[ "$status" -ne 0 ]
[[ "$output" == *"one work log"*"at the bundle root"* ]]
[[ "$output" == *"sub/log.md"* ]]
}
@test "a work log that is not at the bundle root is reported" {
printf -- '---\ntype: changelog\n---\n## 2026-01-02\n- misplaced log\n' > "$KB/sub/log.md"
run bash "$LINT" "$KB"
[ "$status" -ne 0 ]
[[ "$output" == *"one work log"*"at the bundle root"* ]]
}
@test "the quarantine trash directory is not part of the bundle" {
# Deleted pages wait in .AGENTTRASH/ for a pruning session with Frank. Holding them
# to frontmatter and one-log rules would push an agent to delete history to go green.
mkdir -p "$KB/.AGENTTRASH"
printf 'no frontmatter, and a second log besides\n' > "$KB/.AGENTTRASH/log.md"
run bash "$LINT" "$KB"
[ "$status" -eq 0 ]
[[ "$output" == *"0 violation(s)"* ]]
}
@test "the shipped knowledge base itself is conformant" { @test "the shipped knowledge base itself is conformant" {
# The tests above exercise the linter against fixtures; nothing else in the # The tests above exercise the linter against fixtures; nothing else in the
# suite ever points it at the real bundle, so a page added without frontmatter # suite ever points it at the real bundle, so a page added without frontmatter