Skip to content

feat(log): add append-only activity journal (log.md)#85

Merged
ethanj merged 8 commits into
atomicstrata:mainfrom
alvins82:claude/cranky-cray-b997ad
Jun 7, 2026
Merged

feat(log): add append-only activity journal (log.md)#85
ethanj merged 8 commits into
atomicstrata:mainfrom
alvins82:claude/cranky-cray-b997ad

Conversation

@alvins82

@alvins82 alvins82 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

What

Adds the log.md journal from Karpathy's llm-wiki gist: an append-only record of what happened and when, written automatically by ingest, compile, query, and lint.

Each entry is a grep-able heading followed by a markdown bullet body:

## [2026-06-05T09:14:02Z] ingest | Attention Is All You Need
- Source: https://arxiv.org/abs/1706.03762
- Saved: sources/attention-is-all-you-need.md
- Chars: 38,214

## [2026-06-05T09:15:30Z] compile | 1 source(s) → 6 page(s)
- Sources: attention-is-all-you-need.md
- Created: [[self-attention]], [[multi-head-attention]], [[transformer]]
- Updated: [[positional-encoding]]

## [2026-06-05T09:16:11Z] query | What is multi-head attention?
- Pages: [[multi-head-attention]], [[self-attention]]

Why

The gist treats log.md as a companion to index.md: where the index organizes content for discovery, the log tracks temporal progression. The project had no such journal.

Design notes

  • Format: ## [YYYY-MM-DDThh:mm:ssZ] operation | description (ISO 8601 UTC) + a - detail bullet body. Only headings start with ## [, so the gist's recipe grep "^## \[" log.md | tail -5 still returns the most recent operations even with bodies.
  • Per-operation detail:
    • ingestSource (input), Saved (output file), Chars
    • compileSources consumed, Created vs Updated pages (split via a pre-generation on-disk snapshot), Deleted sources count
    • query — cited Pages
    • lint — error/warning/info counts + Flagged pages
  • Placement: logging lives in the core functions (ingestSource, runCompilePipeline, generateAnswer, lint), so both the CLI and the MCP server are covered. It writes to log.md at the project root (not under wiki/) because ingest runs before any wiki/ exists.
  • Resilient: a log write failure warns but never throws — recording an event must not break the operation it records.
  • log.md is gitignored alongside /wiki/ and /sources/.
  • Page-link lists are capped (20) with a (+N more) suffix.

Out of scope

Token/cost was intentionally deferred to a follow-up (pairs naturally with the Claude Agent SDK provider, which reports usage/cost natively).

Testing

  • New test/activity-log.test.ts covers heading format, ISO timestamp, bullet body, wikilink/list truncation, append-only accumulation, and the never-throws guarantee.
  • npx tsc --noEmit, npm run build, npm test (1216 passed), and fallow (no dead code/duplication, 0 above threshold, maintainability 91.5) all pass.
  • Verified end-to-end against a real provider: ingest → compile → query → lint all journal correctly, including the Created/Updated split across two compiles.

🤖 Generated with Claude Code

Implements the log.md journal from Karpathy's llm-wiki gist: an
append-only record of what happened and when, written by ingest,
compile, query, and lint.

Each entry is a grep-able heading — `## [YYYY-MM-DDThh:mm:ssZ] operation
| description` (ISO 8601 UTC) — followed by a markdown bullet body with
page wikilinks and counts. Only headings start with `## [`, so the
gist's recipe `grep "^## \[" log.md | tail -5` still returns the most
recent operations.

Per-operation detail:
- ingest:  Source (input), Saved (output file), Chars
- compile: Sources consumed, Created vs Updated pages, Deleted count
- query:   cited Pages
- lint:    error/warning/info counts, Flagged pages

Logging lives in the core functions (ingestSource, runCompilePipeline,
generateAnswer, lint) so both the CLI and the MCP server are covered.
Writes are resilient — a log failure warns but never breaks the
operation it records. log.md is gitignored alongside wiki/ and sources/.

Token/cost intentionally deferred to a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alvins82 alvins82 force-pushed the claude/cranky-cray-b997ad branch from 5ca4429 to f153836 Compare June 5, 2026 07:35

@ethanj ethanj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. The core idea is useful, and the implementation is cleanly documented. I especially like that the log format keeps the gist’s grep "^## \\[" log.md | tail -5 workflow intact while still allowing richer detail lines.

I’d like to request a few changes before merging:

  1. lint_wiki now mutates state, which conflicts with the MCP server contract. src/mcp/server.ts currently tells agents that read_page, lint_wiki, wiki_status, and fast unrecorded eval “do not mutate state,” but lint() now appends to log.md on every run, including clean 0/0/0 passes. Could we either avoid journaling lint from that read-only path, or deliberately update the contract and add a regression test showing lint_wiki is expected to write log.md?

  2. The compile created/updated split is re-derived from a bare-slug directory scan. listExistingPageSlugs() merges wiki/concepts/*.md and wiki/queries/*.md into one Set<string>, so an existing query page and a newly-created concept with the same slug can be misclassified as “Updated.” It would be better to thread the created/updated result from the exact page write path, or at least track namespaced IDs like concepts/foo and queries/foo.

  3. The actual journaling call sites need integration coverage. test/activity-log.test.ts covers the formatter and append helper, but not the real compile/lint/query/ingest paths. A small CLI-level test would make sure the feature keeps working through the user-facing surface and would catch issues like lint mutating unexpectedly or compile misreporting created vs updated pages.

A few smaller non-blocking notes:

  • generateAnswer() logs the query before the answer is generated, so a later LLM failure still records a query entry. That may be fine if the journal records attempts, but it is worth making that intentional because compile logs after successful finalization.
  • ingestSource() writes the log using process.cwd(), while compile/query/lint pass an explicit root. It works for the current CLI/MCP path, but passing root explicitly would make the invariant less fragile.
  • produced = [...pages, ...seedSlugs] is not deduped, so a collision could inflate the compile page count.

Good feature overall. Once the lint contract and compile classification are tightened, I think this will fit the repo well.

…fication, add integration tests

Responds to review feedback on the log.md journal:

1. lint stays read-only. Move lint journaling out of the core lint()
   function (which the MCP `lint_wiki` tool calls and documents as
   non-mutating) into the CLI command. MCP lint no longer writes log.md;
   the CLI still does. Regression test added for both paths.

2. Fix compile created/updated misclassification. listExistingPageIds
   now namespaces snapshot IDs by directory (wiki/concepts/foo vs
   wiki/queries/foo), so a query page and a same-slug new concept are no
   longer conflated as "Updated". Compile only writes concept-namespace
   pages, so existence is checked against that namespace.

3. Add integration coverage for the real journaling call sites:
   CLI lint, read-only core lint(), CLI ingest, and the compile
   created-vs-updated split across two compiles.

Also from the non-blocking notes:
- query now journals after the answer is produced (matching compile,
  which logs after finalization), so a mid-flight LLM failure records
  nothing.
- dedupe produced page slugs before counting/splitting, so a concept/seed
  slug collision can't inflate the compile page count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alvins82

alvins82 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all three blocking items addressed in f382606, plus two of the non-blocking notes. (Also rebased onto the latest branch head so #87's dep upgrade is included.)

1. lint_wiki read-only contract
Moved the lint journaling out of the core lint() function and into the CLI command (src/commands/lint.ts). The MCP lint_wiki tool calls lint() directly, so it no longer writes log.md; only llmwiki lint (CLI) journals. The contract in src/mcp/server.ts stands as-is. Added a regression test asserting core lint() does not write log.md alongside one asserting the CLI does.

2. Created/Updated misclassification
listExistingPageIds() now namespaces the snapshot by directory (wiki/concepts/foo vs wiki/queries/foo) instead of merging bare slugs into one set. Since compile only ever writes concept-namespace pages (concept + seed pages both land under wiki/concepts/), existence is checked against concepts/<slug> — so an existing query page and a same-slug new concept are no longer conflated as "Updated."

3. Integration coverage for the call sites
New test/activity-log-integration.test.ts exercises the real surfaces:

  • CLI llmwiki lint writes a lint entry (via the runCLI subprocess fixture)
  • core lint() stays read-only (no log.md)
  • CLI llmwiki ingest <file> writes an ingest entry with Saved/Chars
  • compile logs Created on first compile and Updated on recompile (stubbed provider, two passes)

Non-blocking notes:

  • query logs before generation ✅ — moved the query journal to after the answer is produced, matching compile (which logs after finalization), so a mid-flight LLM failure records nothing.
  • produced not deduped ✅ — deduped the produced slugs before counting/splitting, so a concept/seed slug collision can't inflate the page count.
  • ingestSource() uses process.cwd() — left as-is intentionally: the whole ingest path is cwd-relative (saveSource writes sources/ relative to cwd too), so process.cwd() for the journal is consistent. Threading an explicit root only into the log call would be inconsistent without also refactoring saveSource; happy to do that as a follow-up if you'd prefer.

tsc, build, npm test (1245 passed), and fallow (clean) all green.

Drop lint from the activity journal entirely. log.md now records only
ingest, compile, and query. lint is a read-only check (and the MCP
lint_wiki tool is documented as non-mutating), so it should leave no
trace in the journal.

Removes the lint journaling from the CLI command, updates docs, and
flips the integration test to assert `llmwiki lint` writes no log.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alvins82

alvins82 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up (9f7e524): decided to drop lint from the journal entirely rather than journal it from the CLI. log.md now records only ingest, compile, and query. lint is a read-only check (and lint_wiki is documented non-mutating), so it leaves no trace anywhere — which most cleanly resolves the read-only-contract concern from point 1. The integration test now asserts llmwiki lint writes no log.md. Docs updated. All checks green.

@alvins82 alvins82 requested a review from ethanj June 6, 2026 10:17

@ethanj ethanj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough follow-up. I re-reviewed the latest changes and this is in good shape now.

The main concerns from the earlier pass are addressed: lint_wiki preserves the no-mutation contract again, the created/updated split is now namespaced so concept/query slug collisions do not contaminate the journal, and the real call sites are covered through integration tests instead of only testing the formatter/helper layer.

The end-to-end coverage is the important part here. Pinning lint as read-only, ingest details, and first-compile-created / second-compile-updated behavior gives this feature the right protection going forward.

Approving. Nice work on the journal.

@ethanj ethanj merged commit a0127ff into atomicstrata:main Jun 7, 2026
2 checks passed
ethanj added a commit that referenced this pull request Jun 7, 2026
Reconcile the SDK's status/page extraction with #88's MCP freshness work:
- Unify collectStatus on #88's richer freshness-derived implementation, relocated into the shared src/status/collect.ts (consumed by both MCP and SDK); drop the now-redundant src/mcp/status.ts and repoint #88's tests at the shared module.
- Resolve src/mcp/tools.ts to use the extracted page-read/retrieval modules (the extracted readPageRecord already carries #88's safe-title handling).
- Journal ingests under the project root with a root-relative saved path, fixing the interaction between #85's activity journal and the root-explicit ingest change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants