Skip to content

Latest commit

 

History

History
1639 lines (1159 loc) · 71.3 KB

File metadata and controls

1639 lines (1159 loc) · 71.3 KB

Learnings

Date Learning
2026-04-14 Constitution forbids context window as a deferral excuse
2026-04-14 docs/cli/system.md and embed/cmd/system.go diverged on bootstrap promotion intent
2026-04-14 Raft-lite trade-off is the load-bearing choice in internal/hub
2026-04-14 AST stutter test only checks FuncDecl, not GenDecl
2026-04-14 Brand-name handling in title-case engines must cover possessives
2026-04-13 GPG signing from non-TTY contexts requires pinentry-mac (or equivalent)
2026-04-13 Load average measures a queue, not CPU utilization
2026-04-13 rc.ContextDir() is the single source of truth — fix the resolver, not callers
2026-04-09 Pad index shifting is a real UX bug in batch operations
2026-04-08 fmt.Fprintf to strings.Builder silently discards errors
2026-04-08 AST audit tests must cover unexported functions too
2026-04-06 Agents ignore system-reminder content without explicit relay instructions
2026-04-04 Format-verb strings are localizable text, not exempt from magic string checks
2026-04-04 Agents add allowlist entries to make tests pass — guard every exemption
2026-04-03 Subagent scope creep and cleanup (consolidated)
2026-04-03 Bulk rename and replace_all hazards (consolidated)
2026-04-03 Import cycles and package splits (consolidated)
2026-04-03 Lint suppression and gosec patterns (consolidated)
2026-04-03 Skill lifecycle and promotion (consolidated)
2026-04-03 Cross-cutting change ripple (consolidated)
2026-04-03 Dead code detection (consolidated)
2026-04-03 desc.Text() is the single highest-connectivity symbol in the codebase
2026-04-01 Raw I/O migration unlocks downstream checks for free
2026-04-01 go/packages respects build tags — darwin-only violations invisible on Linux
2026-04-01 Copilot CLI skills need a sync mechanism to prevent drift from ctx skills
2026-04-01 Contributor PRs based on older code reintroduce removed features
2026-03-31 Magic string cleanup compounds: each pass reveals the next layer
2026-03-31 Force-loaded behavioral prose gets ignored — action-gating hooks don't
2026-03-31 Legacy key directory cleanup was specified but not automated
2026-03-31 Convention audits must check cmd/ purity, not just types and docstrings
2026-03-31 JSON Schema default fields cause linter errors with some validators
2026-03-30 Architecture diagrams drift silently during feature additions
2026-03-30 Python-generated doc.go files need gofmt — formatter strips bare // padding lines
2026-03-30 lint-docstrings.sh greedy sed hid all return-type violations
2026-03-25 Machine-generated CLAUDE.md content consumes per-turn budget without proportional value
2026-03-25 Template improvements don't propagate to existing projects
2026-03-24 lint-drift false positives from conflating constant namespaces
2026-03-24 git describe --tags follows ancestry, not global tag list
2026-03-23 Typography detection script needs exclusion lists for intentional uses
2026-03-23 Splitting core/ into subpackages reveals hidden structure
2026-03-23 Higher-order callbacks in param structs are a code smell
2026-03-20 Commit messages containing script paths trigger PreToolUse hooks
2026-03-18 Lazy sync.Once per-accessor is a code smell for static embedded data
2026-03-17 Write package output census: 69 trivial/simple, 38 consolidation candidates, 18 complex
2026-03-16 Docstring tasks require reading CONVENTIONS.md Documentation section first
2026-03-16 Convention enforcement needs mechanical verification, not behavioral repetition
2026-03-16 One-liner method wrappers hide dependencies without adding value
2026-03-16 Agents reliably introduce gofmt issues during bulk renames
2026-03-15 Contributor PRs need post-merge follow-up commits for convention alignment
2026-03-15 Grep for callers must cover entire working tree before deleting functions
2026-03-14 Stderr error messages are user-facing text that belongs in assets
2026-03-14 Hardcoded _alt suffixes create implicit language favoritism
2026-03-13 sync-why mechanism existed but was not wired to build
2026-03-12 Project-root files vs context files are distinct categories
2026-03-12 Constants belong in their domain package not in god objects
2026-03-07 Always search for existing constants before adding new ones
2026-03-07 SafeReadFile requires split base+filename paths
2026-03-06 Stale directory inodes cause invisible files over SSH
2026-03-06 Stats sort uses string comparison on RFC3339 timestamps with mixed timezones
2026-03-06 Claude Code supports PreCompact and SessionStart hooks that ctx does not use
2026-03-06 Package-local err.go files invite broken windows from future agents
2026-03-05 State directory accumulates silently without auto-prune
2026-03-05 Global tombstones suppress hooks across all sessions
2026-03-05 Claude Code has two separate memory systems behind feature flags
2026-03-05 Blog post editorial feedback is higher-leverage than drafting
2026-03-04 CONSTITUTION hook compliance is non-negotiable — don't work around it
2026-03-02 Hook message registry test enforces exhaustive coverage of embedded templates
2026-03-02 Existing Projects is ambiguous framing for migration notes
2026-03-02 Claude Code JSONL model ID does not distinguish 200k from 1M context
2026-03-01 Gosec G306 flags test file WriteFile with 0644 permissions
2026-03-01 Converting PersistentPreRun to PersistentPreRunE changes exit behavior
2026-03-01 Test HOME isolation is required for user-level path functions
2026-03-01 Task descriptions can be stale in reverse — implementation done but task not marked complete
2026-03-01 Model-to-window mapping requires ordered prefix matching
2026-03-01 TASKS.md template checkbox syntax inside HTML comments is parsed by RegExTaskMultiline
2026-03-01 Hook logs had no rotation; event log already did
2026-02-28 ctx pad import, ctx pad export, and ctx system resources make three hack scripts redundant
2026-02-28 Getting-started docs assumed Claude Code as the only agent
2026-02-28 Plugin reload script must rebuild cache, not just delete it
2026-02-27 site/ directory must be committed with docs changes
2026-02-27 Doctor token_budget vs context_window confusion
2026-02-27 Drift detector false positives on illustrative code examples
2026-02-27 Context injection and compliance strategy (consolidated)
2026-02-26 Webhook silence after ctxrc profile swap is the most common notify debugging red herring
2026-02-26 Documentation drift and auditing (consolidated)
2026-02-26 Agent context loading and task routing (consolidated)
2026-02-26 Go testing patterns (consolidated)
2026-02-26 PATH and binary handling (consolidated)
2026-02-26 Task management and exit criteria (consolidated)
2026-02-26 Agent behavioral patterns (consolidated)
2026-02-26 Hook compliance and output routing (consolidated)
2026-02-26 ctx add and decision recording (consolidated)
2026-02-24 CLI tools don't benefit from in-memory caching of context files
2026-02-22 Hook behavior and patterns (consolidated)
2026-02-22 UserPromptSubmit hook output channels (consolidated)
2026-02-22 Linting and static analysis (consolidated)
2026-02-22 Permission and settings drift (consolidated)
2026-02-22 Gitignore and filesystem hygiene (consolidated)
2026-01-28 IDE is already the UI

[2026-04-14-010134] Constitution forbids context window as a deferral excuse

Context: Mid-session, agent proposed pacing through doc.go rewrites with the reasoning that context budget was tight.

Lesson: The CONSTITUTION explicitly lists 'We are running out of context window' as a forbidden deferral phrase under No Excuse Generation. The rule is real and applies to agent self-pacing, not just user-facing answers.

Application: When tempted to scope down because context is tight, re-read the constitution. The right move is to do the work end-to-end, not to ask the user which slice to skip.


[2026-04-14-010134] docs/cli/system.md and embed/cmd/system.go diverged on bootstrap promotion intent

Context: Header comment in internal/config/embed/cmd/system.go claimed bootstrap was promoted to top-level; the bootstrap.go registration never actually promoted it. Two contradictory sources of truth coexisted silently.

Lesson: Header-comment claims about command-tree structure are unaudited; they can drift from registrations without any test failing. Trust the code, not the comment.

Application: When evaluating any package_name namespace cleanup type claim about command structure, verify against the actual cobra registration in internal/bootstrap/group.go before acting.


[2026-04-14-010134] Raft-lite trade-off is the load-bearing choice in internal/hub

Context: Discovered while writing thorough doc.go for internal/hub. The package embeds HashiCorp Raft for leader election only; data replication is sequence-based gRPC sync over the append-only JSONL store.

Lesson: A leader crash window between accept and replicate can lose the most recent write. Append-only storage plus idempotent clients make this acceptable; full Raft log replication would not be needed and would not be simpler.

Application: Any future make hub stronger proposal must engage with this trade-off explicitly. Do not abandon Raft-lite accidentally by introducing log-replicated state; that would invalidate the simplicity argument.


[2026-04-14-010134] AST stutter test only checks FuncDecl, not GenDecl

Context: tpl.TplEntryMarkdown stuttered for a long time because TestNoStutteryFunctions in internal/audit walks *ast.FuncDecl only; the constant slipped through.

Lesson: The audit suite has a real coverage gap for *ast.GenDecl (consts, vars, types). Stuttery type/const names will not be caught until the audit is extended to walk those node kinds.

Application: When a stuttery identifier is reported by a human, check both the offending file and whether the audit can catch it; if not, file an audit-extension task.


[2026-04-14-010105] Brand-name handling in title-case engines must cover possessives

Context: First pass of hack/title-case-headings.py produced 'Ctx's' from 'ctx's' because the brand check matched the bare token only.

Lesson: A brand allowlist needs to recognize , 's, s, and short apostrophe-suffixed variants. Single-word matching misses contractions and possessives.

Application: When adding a new always-lowercase brand to hack/title-case-headings.py, extend the suffix-aware loop in title_case_word, not just the BRAND_LOWER set.


[2026-04-13-153618] GPG signing from non-TTY contexts requires pinentry-mac (or equivalent)

Context: git commit failed from Claude Code's shell with 'gpg: signing failed: No such file or directory' — the default pinentry-curses cannot open a TTY in agent-invoked shells. Manual commits from a real terminal worked fine.

Lesson: GPG's default curses pinentry requires an interactive TTY. In non-TTY contexts (Claude Code, CI, scripts, cron), signing fails silently-ish. The fix is to configure a GUI pinentry that uses the OS keychain: brew install pinentry-mac; echo 'pinentry-program $(brew --prefix)/bin/pinentry-mac' >> ~/.gnupg/gpg-agent.conf; gpgconf --kill gpg-agent. Once the passphrase is saved in Keychain, signing works from any context.

Application: If agents or CI need to sign commits, configure pinentry-mac (macOS) or pinentry-gtk/pinentry-qt (Linux) with the OS keychain, not pinentry-curses. This is a one-time setup per machine.


[2026-04-13-153618] Load average measures a queue, not CPU utilization

Context: The 'Load Xx CPU count' resource alert fired at 1.74x while htop showed per-core utilization well under 50% and idle cores. Load average counts runnable + uninterruptible-sleep processes, smoothed over 1/5/15 minutes.

Lesson: Load average and CPU% measure different things. High load with low CPU% typically means many short-lived processes or I/O-bound work (e.g., go test spawning hundreds of parallel test binaries). The 1-minute average is too reactive for dev machines that periodically run test suites — 5-minute smooths transient spikes without hiding sustained pressure.

Application: For alerting thresholds based on system load, prefer 5-minute over 1-minute averages. 1-minute is useful for interactive debugging; 5-minute is better for automated alerts that should not fire on normal build/test activity.


[2026-04-13-153618] rc.ContextDir() is the single source of truth — fix the resolver, not callers

Context: When ctx init failed with a boundary error, my first instinct was to have init bypass rc.ContextDir() and use filepath.Join(cwd, dir.Context) directly. Volkan shut that down: rc.ContextDir() encodes invariants (team shares, symlinks, network mounts, .ctxrc overrides) that individual commands cannot reason about.

Lesson: Resolution chains with multiple fallbacks are contracts. If one command bypasses the chain, it silently diverges from every other command's notion of 'the context directory.' When a resolver produces a wrong answer for a specific case, fix the resolver — don't let callers opt out.

Application: Any time you see rc.ContextDir(), rc.RC(), or similar central resolvers producing a bad result, the fix belongs in the resolver itself (or in its input data like .ctxrc). Caller-side bypasses create drift.


[2026-04-09-001323] Pad index shifting is a real UX bug in batch operations

Context: ctx pad rm 10; rm 11; rm 12 deleted wrong entries because indices shifted after each deletion

Lesson: Any ID-based system where users chain operations needs stable IDs. Look-then-act is safe for single ops; look-then-batch-act breaks with shifting indices

Application: Both pad and remind now use stable IDs with batch delete and range support. Apply same pattern to any future numbered-list subsystem


[2026-04-08-074612] fmt.Fprintf to strings.Builder silently discards errors

Context: golangci-lint errcheck allows fmt.Fprintf to strings.Builder because Write never fails, but project convention says zero silent discard

Lesson: Linter coverage gaps exist where language guarantees mask conventions. AST tests fill the gap

Application: Created TestNoUncheckedFmtWrite to enforce fmt.Fprintf error handling. Use if _, err := fmt.Fprintf(...) with log.Warn on the error path


[2026-04-08-074604] AST audit tests must cover unexported functions too

Context: TestDocCommentStructure only checked exported functions, so agent-written helpers in format.go had no godoc enforcement

Lesson: Convention enforcement tests must default to scanning all documented functions. Use explicit opt-outs (test files) not opt-ins (exported only)

Application: When adding AST audit tests, scan all functions. We fixed TestDocCommentStructure to drop the IsExported gate and fixed 84 violations


[2026-04-06-204226] Agents ignore system-reminder content without explicit relay instructions

Context: Provenance line (Session: abc | Branch: main @ hash) was emitted by hook but agents in other projects silently ignored it. The line appeared in the system-reminder but the agent treated it as internal metadata.

Lesson: Claude Code surfaces hook stdout as system-reminder tags. Agents only relay content that has explicit display instructions. IMPORTANT: means pay attention internally. Display this line verbatim means show to user. Without the instruction, even correct output is invisible to the user.

Application: Any hook output intended for the user must include an explicit relay instruction like Display this line verbatim at the start of your response. Do not rely on IMPORTANT: alone — it signals internal priority, not user-facing output.


[2026-04-04-025813] Format-verb strings are localizable text, not exempt from magic string checks

Context: Strings like '%d entries checked' were passing TestNoMagicStrings because the format-verb exemption was too broad

Lesson: Any string containing English words alongside format directives is user-facing text that belongs in YAML assets

Application: Removed format-verb, URL-scheme, HTML-entity, and err/ exemptions from TestNoMagicStrings


[2026-04-04-025805] Agents add allowlist entries to make tests pass — guard every exemption

Context: Found that every exemption map/allowlist in audit tests is a tempting shortcut for agents

Lesson: Added DO NOT widen guard comments to all 10 exemption data structures across 7 test files

Application: Every new audit test with an exemption must include the guard comment. Review PRs for drive-by allowlist additions.


[2026-04-03-180000] Subagent scope creep and cleanup (consolidated)

Consolidated from: 4 entries (2026-03-06 to 2026-03-23)

  • Subagents reliably rename functions, restructure files, change import aliases, and modify function signatures beyond their stated scope — even narrowly scoped tasks like fixing em-dashes in comments
  • Subagents create new files during refactors but consistently fail to delete the originals — always audit for stale files, duplicate definitions, and orphaned imports afterward
  • After any agent-driven refactor: run git diff --stat and git diff --name-only HEAD, revert anything outside the intended scope, and check for stale package declarations before building

[2026-04-03-180000] Bulk rename and replace_all hazards (consolidated)

Consolidated from: 3 entries (2026-03-15 to 2026-03-20)

  • replace_all on short tokens (e.g. core., function names) matches inside longer identifiers and function definitions — remindcore. becomes remindtidy., func HumanAgo becomes func format.DurationAgo (invalid Go)
  • sed insert-before-first-match does not understand Go import aliases — the alias attaches to whatever line sed inserts, not the original target
  • For function renames: delete the old definition separately rather than using replace_all. For bulk import additions: check for aliased imports first and handle them separately, or use goimports

[2026-04-03-180000] Import cycles and package splits (consolidated)

Consolidated from: 5 entries (2026-03-06 to 2026-03-22)

  • Types in god-object files (e.g. hook/types.go with 15+ types from 8 domains) create circular dependencies — move types to their owning domain package
  • Tests in parent package X cannot import X/sub packages that import X back — move tests to the sub-package they exercise
  • Variable shadowing causes cascading failures after splits: dir, file, entry are common Go variable names that collide with new sub-package names — run go test ./... before committing splits
  • When moving constants between packages, change imports and all references in a single atomic write so the linter never sees an inconsistent state
  • Import cycle rule: the package providing implementation logic must own the shared types; the facade package aliases them (e.g. entry.Params aliases add/core.EntryParams)

[2026-04-03-180000] Lint suppression and gosec patterns (consolidated)

Consolidated from: 4 entries (2026-03-04 to 2026-03-19)

  • Rename constants to avoid gosec G101 false positives (Tokens->Usage, Passed->OK) instead of adding nolint/nosec/path exclusions — exclusions break on file reorganization
  • nolint:goconst for trivial values normalizes magic strings — use config constants instead of suppressing the linter
  • nolint:errcheck in tests teaches agents to spread the pattern to production code — use t.Fatal(err) for setup, defer func() { _ = f.Close() }() for cleanup
  • golangci-lint v2 ignores inline nolint directives for some linters — use config-level exclusions.rules for gosec patterns, fix the code instead of suppressing errcheck

[2026-04-03-180000] Skill lifecycle and promotion (consolidated)

Consolidated from: 4 entries (2026-03-01 to 2026-03-14)

  • Internal skill renames and promotions require synchronized updates across 6+ layers: SKILL.md frontmatter, internal cross-references, external docs, embed_test.go expected list, recipe/reference docs, and plugin cache rebuild + session restart
  • Skill behavior changes ripple through hook messages, fallback strings in Go code, doc descriptions, and Makefile hints — grep for the skill name across the entire repo
  • Skills without a trigger mechanism (no user invocation, no hook loading) are dead code — audit skills for reachability
  • After promoting skills: grep -r for the old name across the whole tree, run plugin-reload.sh, restart session to verify autocomplete, and clean stale Skill() entries from settings.local.json

[2026-04-03-180000] Cross-cutting change ripple (consolidated)

Consolidated from: 4 entries (2026-02-19 to 2026-03-01)

  • Path changes (e.g. key file location) ripple across 15+ doc files and 2 skills — grep broadly (not just code) and budget for 15+ file touches
  • Removing embedded asset directories requires synchronized cleanup across 5+ layers: embed directive, accessor functions, callers, tests, config constants, build targets, documentation — work outward from the embed
  • Absorbing shell scripts into Go commands creates a discoverability gap — update contributing.md, common-workflows.md, and CLI index as part of the absorption checklist
  • A feature without docs is invisible to users: always check feature page, cli-reference.md, relevant recipes, and zensical.toml nav after implementing a new CLI subcommand

[2026-04-03-180000] Dead code detection (consolidated)

Consolidated from: 3 entries (2026-03-15 to 2026-03-30)

  • Dead packages can build and test green while being completely unreachable — detection requires checking bootstrap registration, not just build success (e.g. internal/cli/recall/ existed with tests but was never wired into the command tree)
  • Files created by ctx init that no agent, hook, or skill ever reads are dead on arrival — verify there is at least one consumer before adding to init scaffolding
  • When touching legacy compat code, first ask whether the legacy path has real users — if not, delete it entirely rather than improving it (MigrateKeyFile had 5 callers and test coverage but zero users)

[2026-04-03-133244] desc.Text() is the single highest-connectivity symbol in the codebase

Context: GitNexus enrichment during architecture analysis revealed desc.Text() (internal/assets/read/desc/desc.go:75) has 30+ direct callers spanning every architectural layer (MCP handler, format, index, tidy, trace, memory, sysinfo, io) and participates in 53 execution flows.

Lesson: TestDescKeyYAMLLinkage is the most critical guard in the codebase — it protects the symbol with the widest blast radius. If YAML text loading breaks, the entire CLI and MCP server output blank strings silently (no crash, no warning).

Application: Treat desc.Text() as a frozen API — add new functions rather than modifying the existing signature. Any change to config/embed/text or assets/read/desc should be followed by running the linkage audit. Monitor this symbol during major refactors.


[2026-04-01-233250] Raw I/O migration unlocks downstream checks for free

Context: TestNoRawPermissions had zero violations because the raw I/O migration moved all octal literals into internal/io/ which already used config/fs constants

Lesson: Chokepoint migrations have cascading benefits — centralizing one concern (file I/O) automatically resolves other drift (raw permissions)

Application: Prioritize chokepoint migrations (io, exec, write, err) before smaller checks that depend on them


[2026-04-01-233248] go/packages respects build tags — darwin-only violations invisible on Linux

Context: TestNoExecOutsideExecPkg could not detect violations in _darwin.go files when running on Linux

Lesson: AST checks using go/packages only see files matching the current GOOS. Cross-platform violations need either multi-GOOS CI or a go/parser fallback

Application: When writing audit checks for code with build tags, fix the violations regardless (code correctness) but note that test coverage is platform-dependent


[2026-04-01-074419] Copilot CLI skills need a sync mechanism to prevent drift from ctx skills

Context: 5 Copilot CLI skills were condensed versions of ctx skills, independently maintained with no drift detection

Lesson: Any time the same content exists in two locations without a sync mechanism, it will drift silently

Application: make sync-copilot-skills added to build deps, make check-copilot-skills added to audit target


[2026-04-01-074418] Contributor PRs based on older code reintroduce removed features

Context: PR #45 brought back prompt templates, PROMPT.md, and IMPLEMENTATION_PLAN.md that were explicitly removed in March

Lesson: When resolving contributor merge conflicts, check decisions history for intentional removals — do not assume the PR content is additive

Application: Cross-reference DECISIONS.md before accepting PR content that adds files or features


[2026-03-31-224247] Magic string cleanup compounds: each pass reveals the next layer

Context: What started as fix 4 fmt.Fprintf(os.Stderr) calls expanded to over-tokenized format strings, magic hex perms, unstandardized TOML parsing tokens, missing docstrings on new constants — each fix exposed adjacent violations

Lesson: Mechanical cleanup is fractal. The first sweep finds the obvious violations, but fixing them puts adjacent code under scrutiny. Budget for 2-3x the initial estimate

Application: When scoping cleanup tasks, do not commit to done in one pass. Commit after each layer and let the user decide when to stop


[2026-03-31-182054] Force-loaded behavioral prose gets ignored — action-gating hooks don't

Context: AGENT_PLAYBOOK was force-injected at ~14k tokens every session. Agent routinely skipped its Context Readback directive when the user's first message was a concrete task. Meanwhile, hooks that gate actions (qa-reminder, specs-nudge, block-dangerous-commands) were consistently followed because they fire at the moment of violation.

Lesson: Prose instructions compete with the user's immediate request and lose. Hooks that intercept actions at execution time are enforceable. More injected content means less attention per token — slim injection to only what must be internalized before any action.

Application: When adding agent directives, prefer action-gating hooks over injected prose. If it must be injected, keep it small and directive-only. Reserve force-injection for hard rules (CONSTITUTION) and distilled actionable checklists (gate file).


[2026-03-31-112534] Legacy key directory cleanup was specified but not automated

Context: ~/.local/ctx/keys/ accumulated 584 orphan keys from test runs before the v0.8.0 migration to ~/.ctx/.ctx.key

Lesson: Migration specs that call for manual cleanup of old paths should include an automated step — either in the migration code itself or as a post-release cleanup task. Tests that write to global paths must isolate HOME.

Application: When writing migration specs, always include automated cleanup of the old path. When writing tests that touch user-level directories, verify HOME is isolated via t.Setenv.


[2026-03-31-005112] Convention audits must check cmd/ purity, not just types and docstrings

Context: Placed needsSpec helper in cmd/root/run.go instead of core/entry/predicate.go. Missed it because the audit checklist only covered types and docstrings

Lesson: cmd/ directories must contain only Cmd() and Run*() — all helper functions, unexported logic, and types belong in core/. Added TestCmdDirPurity compliance test to enforce this mechanically

Application: The compliance test now catches this automatically. 28 pre-existing violations grandfathered in the allowlist


[2026-03-31-005110] JSON Schema default fields cause linter errors with some validators

Context: ctxrc.schema.json had default: values on 16 fields that triggered incompatible type errors in the user's linter

Lesson: Move default values into the description string instead of using the default keyword — Go rc.*() accessors handle the actual defaults

Application: When adding new .ctxrc fields, document defaults in the description, never use default: in the schema


[2026-03-30-075941] Architecture diagrams drift silently during feature additions

Context: During the journal-recall merge, architecture-dia-build.md listed 23 CLI packages but 31 existed. 8 packages added over months without updating the diagram.

Lesson: Exhaustive lists and counts in architecture docs go stale every time a package is added. The drift is invisible because nobody re-counts.

Application: After adding a new CLI package, grep architecture diagrams for package counts and directory listings. Consider adding a drift-check comment that validates the count programmatically.


[2026-03-30-003734] Python-generated doc.go files need gofmt — formatter strips bare // padding lines

Context: Batch-generated doc.go files used blank // lines for padding, which gofmt removes as unnecessary whitespace

Lesson: Programmatic Go file generation must produce substantive content lines, not blank comment padding — gofmt enforces this

Application: Always run gofmt after any scripted Go file generation


[2026-03-30-003707] lint-docstrings.sh greedy sed hid all return-type violations

Context: sed 's/.*) //' consumed return type parens, leaving { — functions with return types were invisible to the script for months

Lesson: Greedy regex in shell scripts can silently suppress entire categories of lint violations — test with edge cases, not just happy paths

Application: When writing sed-based lint checks, test with multi-paren signatures (func Foo() (string, error))


[2026-03-25-234039] Machine-generated CLAUDE.md content consumes per-turn budget without proportional value

Context: GitNexus injected 121 lines (61% of CLAUDE.md) with auto-generated skill pointers like 'Work in the Watch area (39 symbols)' — generic index data loaded on every conversation turn

Lesson: CLAUDE.md is prime real estate — every token competes with project-specific instructions. Auto-generated content belongs in on-demand skills, not in always-loaded files

Application: Audit CLAUDE.md periodically for content that could be delivered via skills instead. Prefer a one-line pointer over inline content for companion tools


[2026-03-25-173338] Template improvements don't propagate to existing projects

Context: 5 of 8 context files in the ctx project itself had stale/missing comment headers — templates evolved but non-destructive init never re-synced them

Lesson: Any template change is invisible to existing users until they run ctx init --force

Application: Added drift detection (checkTemplateHeaders) to ctx drift. Consider surfacing this during ctx status too.


[2026-03-24-001001] lint-drift false positives from conflating constant namespaces

Context: lint-drift.sh checked all string constants in embed/cmd/.go against commands.yaml, but Use constants are cobra syntax strings, not YAML lookup keys

Lesson: Shell grep on constant values cannot distinguish constant types; only DescKey* constants are YAML keys. AST-based analysis is needed for type-aware checks

Application: Already captured in specs/ast-audit-tests.md; the lint-drift fix is shipped in v0.8.0


[2026-03-24-000959] git describe --tags follows ancestry, not global tag list

Context: Release notes skill diffed against v0.3.0 instead of v0.6.0 because the release branch diverged before v0.6.0 was tagged

Lesson: git describe --tags --abbrev=0 follows reachability from HEAD; use git tag --sort=-v:refname | head -1 for the latest tag globally

Application: Any script or skill that needs the latest release should use sorted tag list, not describe


[2026-03-23-165611] Typography detection script needs exclusion lists for intentional uses

Context: detect-ai-typography.sh flagged config/token/delim.go (intentional delimiter constants) and test files (test data containing em-dashes)

Lesson: Detection scripts for convention enforcement need exclusion patterns for files where the flagged patterns are intentional data, not prose

Application: Add exclusion patterns proactively when creating detection scripts; *_test.go and constant-definition files are common false positive sources


[2026-03-23-003544] Splitting core/ into subpackages reveals hidden structure

Context: init core/ was a flat bag of domain objects — splitting into backup/, claude/, entry/, merge/, plan/, plugin/, project/, prompt/, tpl/, validate/ exposed duplicated logic, misplaced types, and function-pointer smuggling that were invisible in the flat layout

Lesson: Flat core/ packages hide coupling — circular dependency resolution during splits naturally groups related items, increases cohesion, and surfaces objects that don't belong

Application: When a core/ package grows, split it into subpackages even if it creates temporary circular deps — resolving those deps is the design work that reveals the right structure


[2026-03-23-003353] Higher-order callbacks in param structs are a code smell

Context: MergeParams.UpdateFn and DeployParams.ListErr/ReadErr were function pointers where all callers passed thin wrappers varying only by a text key

Lesson: If all callers pass thin wrappers around the same pattern (fmt.Errorf with different keys), the callback is just data in disguise

Application: When a struct field is a function pointer, check if all callers vary only by a string key — if so, replace the callback with the key and let the consumer do the dispatch


[2026-03-20-160112] Commit messages containing script paths trigger PreToolUse hooks

Context: Git commit message body contained a path to a shell script under the hack directory which matched a hook pattern that blocks direct script invocation

Lesson: Hooks scan all Bash tool input including heredoc content used for commit messages, not just the command itself

Application: Rephrase commit messages and ctx add content to avoid paths that match hook deny patterns, use generic references instead of literal file paths


[2026-03-18-133457] Lazy sync.Once per-accessor is a code smell for static embedded data

Context: assets package had 4 sync.Once guards, 4 exported maps, 4 Load*() functions, and a wrapper desc package — all to lazily load YAML from embed.FS that never mutates. Every accessor call went through sync.Once + global map + wrapper indirection.

Lesson: When data is static and loaded from embedded bytes, scatter-loading with per-accessor sync.Once is over-engineering. A single Init() called eagerly at startup is simpler, and one sync.Once on Init() itself provides the test safety net. Exported maps that exist only for wrapper packages to reach are a sign the abstraction boundary is wrong.

Application: Prefer eager Init() in main.go for static embedded data. Keep maps unexported. Accessors do plain map lookups. If a wrapper package exists solely to break a cycle caused by exported state, delete the wrapper and unexport the state.


[2026-03-17-105637] Write package output census: 69 trivial/simple, 38 consolidation candidates, 18 complex

Context: Full audit of internal/write/ (26 files, 160 functions, 337 Println calls) to evaluate whether block template consolidation is worth a systematic refactor.

Lesson: Only 30% of write functions benefit from output consolidation. The sweet spot is multi-line (16) and conditional (22) functions.

Application: Check function category before consolidating. Trivial/simple stay as-is. Conditional functions need pre-computation before block templates. Loop-based complex functions stay imperative. Don't bulk-refactor.


[2026-03-16-114227] Docstring tasks require reading CONVENTIONS.md Documentation section first

Context: Agent was asked to review docstrings in server.go but skipped convention loading, missed incomplete Parameter/Returns sections, and needed three hints to recall the known issue

Lesson: Any task involving docstrings, comments, or documentation formatting is a convention-sensitive task — read CONVENTIONS.md (Documentation section) and LEARNINGS.md (for known gaps) before reviewing or writing

Application: On any docstring/comment task: (1) load CONVENTIONS.md Documentation section, (2) check LEARNINGS.md for related entries, (3) audit all functions in scope against the convention template, not just the ones in the diff


[2026-03-16-104146] Convention enforcement needs mechanical verification, not behavioral repetition

Context: Godoc Parameters/Returns sections were missed repeatedly across sessions despite memory entries and feedback

Lesson: System-level brevity instructions outcompete context-injected conventions. Memory shifts probability (~40% to ~70%) but doesn't create invariants. The competing pressures are architectural, not a recall problem.

Application: Invest in linter rules or PreToolUse gates for mechanically-checkable conventions. Reserve behavioral nudges for judgment calls that can't be linted. See ideas/spec-convention-enforcement.md for the three-tier strategy.


[2026-03-16-022650] One-liner method wrappers hide dependencies without adding value

Context: checkBoundary() and loadContext() were methods on Handler that just called validation.ValidateBoundary and context.Load with h.ContextDir

Lesson: If a method only passes a struct field to a stdlib function, inline it — the wrapper obscures the real dependency

Application: Before extracting a helper method, check if it just forwards a field to another function. If so, call the function directly.


[2026-03-16-022642] Agents reliably introduce gofmt issues during bulk renames

Context: Subagents renamed consequences->consequence across 75+ files but left formatting errors in 12 Go files

Lesson: Always run gofmt -l after agent-driven refactors before trusting the build

Application: Add gofmt -w pass as a standard step after any agent-driven bulk edit


[2026-03-15-101342] Contributor PRs need post-merge follow-up commits for convention alignment

Context: PR #42 (MCP v0.2) addressed bulk of review feedback but left ~12 inline strings, no embed_test coverage, and substring matching in containsOverlap

Lesson: Merging with known gaps is fine when the gaps are mechanical, but the follow-up must be immediate — track in ideas/done/ with a review status doc

Application: For future contributor PRs: create ideas/pr{N}-review-status.md during review, merge when architecture is sound, fix convention gaps in a same-day follow-up commit


[2026-03-15-040642] Grep for callers must cover entire working tree before deleting functions

Context: Deleted 7 err/prompt functions as dead code, but callers existed in unstaged refactoring files — caused build failures

Lesson: When the working tree has unstaged changes from a prior session, grep hits only committed+staged code; must grep the full tree or build-test before declaring functions dead

Application: Always run make build after deleting functions, even if grep shows zero callers


[2026-03-14-180903] Stderr error messages are user-facing text that belongs in assets

Context: Added fmt.Fprintf(os.Stderr) error reporting to event log, initially with inline strings

Lesson: Any string that reaches the user, including stderr warnings, routes through assets.TextDesc() for i18n readiness

Application: When adding stderr output, create text.yaml entries and asset keys first


[2026-03-14-131202] Hardcoded _alt suffixes create implicit language favoritism

Context: Session parser had session_prefix_alt hardcoding Turkish as a special case alongside English default

Lesson: Naming a constant _alt and hardcoding one non-English language as a built-in default discriminates by giving that language special status. The pattern doesn't scale (alt_2? alt_3?) and signals that adding languages requires code changes.

Application: When a feature needs multi-value support, use configurable lists from the start — not hardcoded pairs with _alt suffixes. Default to a single canonical value; all extensions are user-configured equally.


[2026-03-13-151952] sync-why mechanism existed but was not wired to build

Context: assets/why/ had drifted from docs/ — the sync targets existed in the Makefile but build did not depend on sync-why

Lesson: Freshness checks that are not in the critical path will be forgotten. Wire them as build prerequisites, not optional audit steps

Application: Any derived or copied asset should be a prerequisite of build, not just audit


[2026-03-12-133008] Project-root files vs context files are distinct categories

Context: Tried moving ImplementationPlan constant to config/ctx assuming it was a context file. (Note: IMPLEMENTATION_PLAN.md was removed in 2026-03-25 as a dead file — no agent consumer.)

Lesson: Files created by ctx init in the project root (Makefile) are scaffolding, not context files loaded via ReadOrder. They belong in config/file, not config/ctx

Application: Before moving a file constant, check whether it is in ReadOrder (context) or created by init (project-root)


[2026-03-12-133007] Constants belong in their domain package not in god objects

Context: file.go held agent scoring constants, budget percentages, cooldown durations — none related to file config

Lesson: When a constant is only used by one domain (e.g. agent scoring), it should live in that domain's config package

Application: Check callers before placing constants; if all callers are in one domain, the constant belongs there


[2026-03-07-221151] Always search for existing constants before adding new ones

Context: Added ExtJsonl constant to config/file.go but ExtJSONL already existed with the same value, causing a duplicate

Lesson: Grep for the value (e.g. '.jsonl') across config/ before creating a new constant — naming variations (camelCase vs ALLCAPS) make duplicates easy to miss

Application: Before adding any new constant to internal/config, search by value not just by name


[2026-03-07-221148] SafeReadFile requires split base+filename paths

Context: During system/core cleanup, persistence.go passed a full path to validation.SafeReadFile which expects (baseDir, filename) separately

Lesson: Use filepath.Dir(path) and filepath.Base(path) to split full paths when adapting os.ReadFile calls to SafeReadFile

Application: When converting os.ReadFile to SafeReadFile, always check whether the existing code has a full path or separate components


[2026-03-06-141506] Stale directory inodes cause invisible files over SSH

Context: Files created by Claude Code hooks were visible inside the VM but not from the SSH terminal

Lesson: If a directory is recreated (e.g. by auto-prune), an SSH shell holding the old directory inode will not see new files — ls returns no such file even though cat with the full path works from other shells

Application: After ctx system prune or any state directory recreation, SSH sessions need cd-dot or re-login to pick up the new inode


[2026-03-06-141504] Stats sort uses string comparison on RFC3339 timestamps with mixed timezones

Context: ctx system stats showed only old sessions, hiding the current one

Lesson: RFC3339 string comparison breaks when entries mix UTC (Z) and offset (-08:00) formats — 13:00-08:00 sorts before 18:00Z lexicographically despite being later in absolute time

Application: Always parse to time.Time before comparing RFC3339 timestamps; never rely on lexicographic sort


[2026-03-06-184820] Claude Code supports PreCompact and SessionStart hooks that ctx does not use

Context: context-mode proves both hooks work in production across 5 platforms

Lesson: ctx's hook architecture only uses UserPromptSubmit, PreToolUse, and PostToolUse — two lifecycle events are untapped

Application: PreCompact snapshot plus SessionStart re-injection would eliminate post-compaction disorientation without any new persistence layer since ctx agent already generates the content


[2026-03-06-050125] Package-local err.go files invite broken windows from future agents

Context: Found err.go files in 5 CLI packages with heavily duplicated error constructors (errFileWrite, errMkdir, errZensicalNotFound repeated across packages)

Lesson: Centralizing errors in internal/err eliminates duplication and prevents agents from continuing the pattern of adding local err.go files when they see one exists

Application: New error constructors go to internal/err/errors.go. No err.go files in CLI packages.


[2026-03-05-205422] State directory accumulates silently without auto-prune

Context: Found 234 files in .context/state/ from weeks of sessions with no cleanup mechanism

Lesson: Session tombstones are write-only. Without auto-prune, the state directory grows unbounded. Added autoPrune(7) to context-load-gate so cleanup happens once per session at startup.

Application: Auto-prune is now wired into session start via context-load-gate. Manual prune still available via ctx system prune for aggressive cleanup.


[2026-03-05-205419] Global tombstones suppress hooks across all sessions

Context: Memory drift nudge used memory-drift-nudged with no session ID in filename

Lesson: Any tombstone file intended to be session-scoped must include the session ID in its filename, otherwise it suppresses across all concurrent and future sessions. Use the UUID pattern so prune can clean them up.

Application: Audit all tombstone files for session-scoping; fixed memory-drift, but backup-reminded, ceremony-reminded, check-knowledge, journal-reminded, version-checked, ctx-wrapped-up still have this bug


[2026-03-05-042157] Claude Code has two separate memory systems behind feature flags

Context: Filesystem and behavioral analysis of Claude Code v2.1.69

Lesson: Claude Code has two separate memory systems behind feature flags. Auto memory writes MEMORY.md to disk (user-visible, toggleable via settings). Session memory is a separate background extraction pipeline with compaction and team sync (push/pull model). The two systems serve different purposes and are independently feature-flagged.

Application: ctx memory bridge targets auto memory (MEMORY.md on disk). Session memory is API-side and not directly accessible. Full findings in ideas/claude-code-project-directory-structure.md.


[2026-03-05-023941] Blog post editorial feedback is higher-leverage than drafting

Context: Draft of Agent Memory Is Infrastructure was publication-quality on first pass; user editorial feedback (structural emphasis, rhetorical sharpening, amnesia/archaeology bridge) elevated it significantly more than initial generation

Lesson: For narrative content, the first draft captures the argument; the editorial pass captures the voice. Both are necessary but the editorial pass has disproportionate impact on quality.

Application: For future blog posts, invest more in the editorial cycle (structural feedback then targeted refinements) rather than trying to nail voice on first generation.


[2026-03-04-105239] CONSTITUTION hook compliance is non-negotiable — don't work around it

Context: After make build, ran ./ctx deps --help which was blocked by block-non-path-ctx. Instead of asking user to install, tried cp ctx ~/bin/ — escalating workarounds.

Lesson: When a hook blocks an action, the correct response is to follow the hook's instruction (ask the user to sudo make install), not to find creative bypasses.

Application: Always ask the user to install when testing a freshly built binary. Never attempt alternative install paths to circumvent a hook.


[2026-03-02-165039] Hook message registry test enforces exhaustive coverage of embedded templates

Context: Adding billing.txt to embedded assets without a registry entry caused TestRegistryCoversAllEmbeddedFiles to fail immediately

Lesson: Every new .txt file under internal/assets/hooks/messages/ must have a corresponding entry in registry.go — the test acts as an exhaustive bidirectional check

Application: When adding new hook message variants, update the registry entry before running tests


[2026-03-02-123613] Existing Projects is ambiguous framing for migration notes

Context: A doc admonition said Existing Projects: if you have an older key at X, it auto-migrates. Every project is existing once installed — the framing does not tell you how far behind you need to be.

Lesson: Version-anchored framing (Key Folder Change v0.7.0+) is clearer than relative framing (Existing Projects, Legacy). State the version boundary and the concrete action.

Application: When writing migration notes, anchor to a version number and give copy-pasteable commands, not vague auto-handled assurances.


[2026-03-02-005217] Claude Code JSONL model ID does not distinguish 200k from 1M context

Context: Heartbeat hook was reporting 16% usage at 162k tokens because it assumed claude-opus-4-6 always has 1M context window

Lesson: The JSONL model field is identical for both variants (both report claude-opus-4-6). The 1M context requires a beta header, not a different model ID. The user's model selection is stored in ~/.claude/settings.json with a [1m] suffix when 1M is active.

Application: Auto-detect context window from ~/.claude/settings.json model field containing [1m]. Default to 200k for all Claude models. The .ctxrc context_window setting is a no-op for Claude Code users.


[2026-03-01-222739] Gosec G306 flags test file WriteFile with 0644 permissions

Context: New tests used os.WriteFile(..., 0o644) for temp context files; lint flagged all three occurrences

Lesson: Gosec enforces 0600 max on WriteFile even in test code. Use 0o600 for test temp files

Application: Default to 0o600 for os.WriteFile in tests; only use wider permissions when testing permission behavior specifically


[2026-03-01-222738] Converting PersistentPreRun to PersistentPreRunE changes exit behavior

Context: Boundary violation test used subprocess pattern because original code called os.Exit(1)

Lesson: With PersistentPreRunE, errors propagate through Cobra Execute() return — no os.Exit call. Subprocess-based tests that expected exit codes need converting to direct error assertions

Application: When converting PreRun to PreRunE in Cobra commands, audit all tests that relied on os.Exit behavior


[2026-03-01-161459] Test HOME isolation is required for user-level path functions

Context: After adding ~/.ctx/.ctx.key as global key location, test suites wrote real files to the developer home directory

Lesson: Any code that uses os.UserHomeDir() needs t.Setenv(HOME, tmpDir) in tests — especially test helpers called by many tests (like setupEncrypted and helper)

Application: When adding features that write to user-level paths (~/.ctx/, ~/.config/), always add HOME isolation to test setup functions first


[2026-03-01-133014] Task descriptions can be stale in reverse — implementation done but task not marked complete

Context: ctx recall sync task said 'command is not registered in Cobra' but the code was fully wired and all tests passed. The task description was stale.

Lesson: Tasks can become stale in the opposite direction from docs: implementation gets completed but the task is not updated. Always verify with ctx --help before assuming work remains.

Application: Before starting implementation on a 'code exists but not wired' task, run the command first to check if it already works.


[2026-03-01-124921] Model-to-window mapping requires ordered prefix matching

Context: Implementing modelContextWindow() for the three-tier context window fallback. Claude model IDs use nested prefixes (claude-sonnet-4-5 vs claude-sonnet-4-20250514).

Lesson: A switch with ordered HasPrefix cases (most specific first) is cleaner and safer than iterating separate prefix lists. The catch-all 'claude-*' returns 200k for unrecognized Claude models.

Application: When adding new model families to modelContextWindow() in session_tokens.go, add the most specific prefix first to avoid shadowing shorter prefixes.


[2026-03-01-095709] TASKS.md template checkbox syntax inside HTML comments is parsed by RegExTaskMultiline

Context: Template had example checkboxes (- [x], - [ ]) in HTML comments that the line-based regex matched as real tasks, causing TestArchiveCommand_NoCompletedTasks to fail

Lesson: RegExTaskMultiline is line-based and has no awareness of HTML comment blocks — checkbox-like patterns inside comments get counted as real tasks

Application: Use backtick-quoted or indented references instead of actual checkbox syntax in template comments. When adding examples to TASKS.md templates, avoid patterns that match regExTaskPattern


[2026-03-01-092611] Hook logs had no rotation; event log already did

Context: Investigated .context/logs/ and .context/state/ file management

Lesson: eventlog already rotates at 1MB with one previous generation. logMessage() in state.go was pure append-only with no size check.

Application: When adding new log sinks, follow the established rotation pattern (size-based, single previous generation)


[2026-02-28-184758] ctx pad import, ctx pad export, and ctx system resources make three hack scripts redundant

Context: Audited hack/ scripts against ctx CLI surface

Lesson: As ctx CLI grew, several hack scripts became wrappers around built-in commands (pad-import.sh -> ctx pad import, pad-export-blobs.sh -> ctx pad export, resource-watch.sh -> watch -n5 ctx system resources)

Application: Periodically audit hack/ for scripts that ctx has absorbed


[2026-02-28-184647] Getting-started docs assumed Claude Code as the only agent

Context: The installation section opened with 'A full ctx installation has two parts' — binary + Claude Code plugin — leaving non-Claude-Code users without a clear path

Lesson: Installation docs should lead with the universal requirement (the binary) and present agent-specific integration as conditional

Application: When writing docs for multi-tool projects, frame the common denominator first, then branch by tool


[2026-02-28-150701] Plugin reload script must rebuild cache, not just delete it

Context: hack/plugin-reload.sh was deleting ~/.claude/plugins/cache/activememory-ctx/ without repopulating it. Claude Code's installed_plugins.json still referenced the cache path, so the plugin appeared enabled but hooks.json was missing — all plugin hooks silently stopped firing.

Lesson: Claude Code snapshots plugin hooks from the cache directory at session startup. If the cache is deleted, plugin hooks vanish silently with no error. The reload script must rebuild the cache from source assets (internal/assets/claude/) after clearing it, and warn that a session restart is required.

Application: Always rebuild the plugin cache in hack/plugin-reload.sh. When debugging hooks that don't fire, check ~/.claude/plugins/cache/ first — a missing hooks.json is the most likely cause.


[2026-02-27-231228] site/ directory must be committed with docs changes

Context: The site/ directory contains generated HTML served directly from the repo (no CI build step). Multiple sessions have committed docs/ changes without the corresponding site/ output, or ignored site/ as 'generated noise'.

Lesson: site/ is intentionally tracked in git — there is no GitHub Pages workflow or CI step to build it. When docs change, the regenerated site/ HTML must be staged and committed alongside the source.

Application: Always git add site/ when committing changes under docs/. Never gitignore site/.


[2026-02-27-230741] Doctor token_budget vs context_window confusion

Context: ctx doctor reported context size against token_budget (8k) instead of context_window (200k), making 22k tokens look alarming.

Lesson: token_budget (ctx agent output trim target) and context_window (model capacity) serve different purposes. Health checks about context fitting should use context_window, with warning threshold proportional (e.g., 20% of window).

Application: Doctor now uses rc.ContextWindow() with 20% threshold and shows per-file token breakdown for actionable insight into which files are heavy.


[2026-02-27-230738] Drift detector false positives on illustrative code examples

Context: ctx drift flagged 23 warnings for backtick-quoted paths in CONVENTIONS.md and ARCHITECTURE.md that were prose examples (loader.go, session/run.go, sync.Once), not real file references.

Lesson: Path reference detection should verify the top-level directory exists on disk before flagging. Bare filenames and paths under non-existent directories are almost always examples in documentation.

Application: The fix checks os.Stat(topDir) on the first path component. Future drift checks on documentation-heavy files should use the same heuristic.


[2026-02-27-002830] Context injection and compliance strategy (consolidated)

Consolidated from: 3 entries (2026-02-26)

  • Verbal summaries with linked diagram files cut ARCHITECTURE.md from ~12K to ~3.8K tokens. Extract diagrams to linked files outside FileReadOrder; keep prose summaries inline. The 4-chars-per-token estimator is accurate — optimize content, not the estimator.
  • Soft instructions have a ~75-85% compliance ceiling because "don't apply judgment" is itself evaluated by judgment. When 100% compliance is required, don't instruct — inject via additionalContext. Reserve soft instructions for ~80% acceptable compliance.
  • Once ~7K tokens are auto-injected (fait accompli), the agent's rationalization inverts from "skip to save effort" to "marginal cost is trivial." Front-load highest-value content as injection, then use sunk cost to motivate on-demand reads for the remainder.

[2026-02-26-003854] Webhook silence after ctxrc profile swap is the most common notify debugging red herring

Context: Spent time investigating why webhooks weren't firing — checked binary version, hook configs, notify.Send internals. Actual cause was .ctxrc swapped to prod profile (notify commented out) earlier in session.

Lesson: When webhooks stop, check .ctxrc profile first (ctx config status). Also: not all tool uses trigger webhook-sending hooks — Read only triggers context-load-gate (one-shot) and ctx agent (no webhook). qa-reminder requires Edit matcher.

Application: Before debugging notify internals, run ctx config status and verify the event would actually match a hook with notify.Send.


[2026-02-26-100000] Documentation drift and auditing (consolidated)

Consolidated from: 6 entries (2026-01-29 to 2026-02-24)

  • CLI reference docs can outpace implementation: ctx remind had no CLI, ctx recall sync had no Cobra wiring, key file naming diverged between docs and code. Always verify with ctx <cmd> --help before releasing docs.
  • Structural doc sections (project layouts, command tables, skill counts) drift silently. Add <!-- drift-check: <shell command> --> markers above any section that mirrors codebase structure.
  • Agent sweeps for style violations are unreliable (8 found vs 48+ actual). Always follow agent results with targeted grep and manual classification.
  • ARCHITECTURE.md missed 4 core packages and 4 CLI commands. The /ctx-drift skill catches stale paths but not missing entries — run /ctx-architecture after adding new packages or commands.
  • Documentation audits must compare against known-good examples and pattern-match for the COMPLETE standard, not just presence of any comment.
  • Dead link checking belongs in /consolidate's check list (check 12), not as a standalone concern. When a new audit concern emerges, check if it fits an existing audit skill first.

[2026-02-26-100002] Agent context loading and task routing (consolidated)

Consolidated from: 5 entries (2026-01-20 to 2026-01-25)

  • ctx agent is optimized for task execution (filters pending tasks, surfaces constitution, token-budget aware). Manual file reading is better for exploratory/memory questions (session history, timestamps, completed tasks).
  • On "Do you remember?" questions, immediately read .context/ files and run ctx journal source --limit 5. Never ask "would you like me to check?" — that is the obvious intent.
  • .context/ is NOT a Claude Code primitive. Only CLAUDE.md and .claude/settings.json are auto-loaded. The .context/ directory requires a hook or explicit CLAUDE.md instruction to be discovered.
  • Orchestrator (IMPLEMENTATION_PLAN.md) and agent (.context/TASKS.md) task lists must be separate. (Superseded 2026-03-25: IMPLEMENTATION_PLAN.md removed. TASKS.md is the single task source.)
  • Only CLAUDE.md is auto-loaded by Claude Code. Projects using ctx should rely on the CLAUDE.md -> AGENT_PLAYBOOK.md chain, not AGENTS.md.

[2026-02-26-100005] Go testing patterns (consolidated)

Consolidated from: 7 entries (2026-01-19 to 2026-02-26)

  • Compiler-driven refactoring misses test files: go build ./... catches production callsite breaks but not test files. Always run go test ./... after signature changes.
  • All runCmd() returns must be consumed in tests: even setup calls need _, _ = runCmd(...) to satisfy errcheck.
  • Set color.NoColor = true in a package-level init function to disable ANSI codes for CLI test string assertions.
  • Recall CLI tests isolate via HOME env var: t.Setenv("HOME", tmpDir) with .claude/projects/ structure gives full isolation from real session data.
  • formatDuration accepts an interface with a Minutes method, not time.Duration directly. Use a stubDuration struct for testing.
  • CI tests need CTX_SKIP_PATH_CHECK=1 env var because init checks if ctx is in PATH.
  • CGO must be disabled for ARM64 Linux (CGO_ENABLED=0) — CGO causes cross-compilation issues with -m64 flag.

[2026-02-26-100006] PATH and binary handling (consolidated)

Consolidated from: 3 entries (2026-01-21 to 2026-02-17)

  • Always use ctx from PATH, never ./dist/ctx-linux-arm64 or go run ./cmd/ctx. Check which ctx if unsure.
  • Hooks must use PATH, not hardcoded paths. ctx init checks if ctx is in PATH before proceeding. Tests can skip with CTX_SKIP_PATH_CHECK=1.
  • Agent must never place binaries in any bin directory (not via cp, mv, or go install). Build with make build, then ask the user to run the privileged install step. Hooks in block-dangerous-commands.sh enforce this.

[2026-02-26-100007] Task management and exit criteria (consolidated)

Consolidated from: 4 entries (2026-01-21 to 2026-02-17)

  • Specs get lost without cross-references from TASKS.md. Three-layer defense: (1) playbook instruction, (2) spec reference in Phase header, (3) bold breadcrumb in first task.
  • Subtask completion is implementation progress, not delivery. Parent tasks should have explicit deliverables; don't close until deliverable is verified.
  • Exit criteria must include verification: integration tests (binary executes correctly), coverage targets, and smoke tests. "All tasks checked off" does not equal "implementation works."
  • Reports graduate to ideas/done/ only after all items are tracked or resolved. Cross-reference every item against TASKS.md and the codebase before moving.

[2026-02-26-100008] Agent behavioral patterns (consolidated)

Consolidated from: 5 entries (2026-01-25 to 2026-02-22)

  • Interaction pattern capture risks softening agent rigor. Do not build implicit user-modeling from session history. Rely on explicit, human-reviewed context (learnings, conventions, hooks) for behavioral shaping.
  • Chain-of-thought prompting improves agent reasoning accuracy (17.7% to 78.7%). Added "Reason Before Acting" to AGENT_PLAYBOOK.md and reasoning nudges to 7 skills.
  • Say "project conventions" not "idiomatic X" to ensure Claude looks at project files first rather than triggering training priors (stdlib conventions).
  • Autonomous "YOLO mode" is effective for feature velocity but accumulates technical debt (magic strings, monolithic tests, hardcoded paths). Schedule periodic consolidation sessions.
  • Trust the binary output over source code analysis. A single ambiguous CLI output is not proof of absence — re-run the exact command before claiming something is missing.

[2026-02-26-100009] Hook compliance and output routing (consolidated)

Consolidated from: 3 entries (2026-02-22 to 2026-02-25)

  • Plain-text hook output is silently ignored by the agent. Claude Code parses hook stdout starting with { as JSON directives; plain text is disposable. All hooks should return JSON via printHookContext().
  • Hook compliance degrades on narrow mid-session tasks (~15-25% partial skip rate). Root cause: CLAUDE.md's "may or may not be relevant" system reminder competes with hook authority. Fix: CLAUDE.md explicitly elevates hook authority. The mandatory checkpoint relay block is the compliance canary.
  • No reliable agent-side before-session-end event exists. SessionEnd fires after the agent is gone. Mid-session nudges and explicit /ctx-wrap-up are the only reliable persistence mechanisms.

[2026-02-26-100010] ctx add and decision recording (consolidated)

Consolidated from: 4 entries (2026-01-27 to 2026-02-14)

  • ctx add learning requires --context, --lesson, --application flags. ctx add decision requires --context, --rationale, --consequence. A bare string only sets the title and the command will fail without required flags.
  • Structured entries with Context/Lesson/Application are more useful than one-liners. Agents are guided via AGENT_PLAYBOOK.md.
  • Always complete decision record sections — placeholder text like "[Add context here]" is a code smell. Decisions without rationale lose their value over time.
  • Slash commands using ! bash syntax require matching permissions in settings.local.json. When adding new /ctx-* commands, ensure ctx init pre-seeds the required Bash(ctx <subcommand>:*) permissions.

[2026-02-24-032945] CLI tools don't benefit from in-memory caching of context files

Context: Discussed whether ctx should read and cache LEARNINGS.md, DECISIONS.md etc. in memory

Lesson: ctx is a short-lived CLI process, not a daemon. Context files are tiny (few KB), sub-millisecond to read. Cache invalidation complexity exceeds the read cost. Caching only makes sense if ctx becomes a long-lived process (MCP server, watch daemon).

Application: Don't add caching layers to ctx's file reads. If an MCP server mode is ever added, revisit then.


[2026-02-22-120000] Hook behavior and patterns (consolidated)

Consolidated from: 8 entries (2026-01-25 to 2026-02-17)

  • Hook scripts receive JSON via stdin (not env vars); parse with HOOK_INPUT=$(cat) then jq
  • Hook key names are case-sensitive: PreToolUse and SessionEnd (not PreToolUseHooks)
  • Use $CLAUDE_PROJECT_DIR in hook paths, never hardcode absolute paths
  • Hook regex can overfit: ctx as binary vs directory name differ; anchor patterns to command-start positions with (^|;|&&|\|\|)\s*
  • grep patterns match inside quoted arguments — test with ctx add learning "...blocked words..." to verify no false positives
  • Hook scripts can silently lose execute permission; verify with ls -la .claude/hooks/*.sh after edits
  • Two-tier output is sufficient: unprefixed (agent context, may or may not relay) and IMPORTANT: Relay VERBATIM (guaranteed relay); don't add new severity prefixes
  • Repeated injection causes agent repetition fatigue; use --session $PPID --cooldown 10m and pair with a readback instruction

[2026-02-22-120001] UserPromptSubmit hook output channels (consolidated)

Consolidated from: 2 entries (2026-02-12)

  • UserPromptSubmit hook stdout is prepended as AI context (not shown to user); stderr with exit 0 is swallowed entirely
  • User-visible output requires {"systemMessage": "..."} JSON on stdout (warning banner) or exit 2 (blocks prompt)
  • There is no non-blocking user-visible output channel for this hook type
  • Design hooks for their actual audience: AI-facing = plain stdout, user-facing = systemMessage JSON

[2026-02-22-120002] Linting and static analysis (consolidated)

Consolidated from: 7 entries (2026-01-25 to 2026-02-20)

  • Full pre-commit gate: (1) CGO_ENABLED=0 go build ./cmd/ctx, (2) golangci-lint run, (3) CGO_ENABLED=0 go test — all three, every time
  • Own the codebase: fix pre-existing lint issues even if you didn't introduce them
  • gosec G301/G306: use 0o750 for dirs, 0o600 for files everywhere including tests
  • gosec G304 (file inclusion): safe to suppress with //nolint:gosec in test files using t.TempDir() paths
  • golangci-lint errcheck: use cmd.Printf/cmd.Println in Cobra commands instead of fmt.Fprintf
  • defer os.Chdir(x) fails errcheck; use defer func() { _ = os.Chdir(x) }()
  • golangci-lint Go version mismatch in CI: use install-mode: goinstall to build linter from source

[2026-02-22-120006] Permission and settings drift (consolidated)

Consolidated from: 4 entries (2026-02-15)

  • Permission drift is distinct from code drift — settings.local.json is gitignored, no review catches stale entries
  • Skill() permissions don't support name prefix globs — list each skill individually
  • Wildcard trusted binaries (Bash(ctx:*), Bash(make:*)), but keep git commands granular (never Bash(git:*))
  • settings.local.json accumulates session debris; run periodic hygiene via /sanitize-permissions and /ctx-drift

[2026-02-22-120008] Gitignore and filesystem hygiene (consolidated)

Consolidated from: 3 entries (2026-02-11 to 2026-02-15)

  • Gitignored directories are invisible to git status; stale artifacts persist indefinitely — periodically ls gitignored working directories
  • Add editor artifacts (*.swp, *.swo, *~) to .gitignore alongside IDE directories from day one
  • Gitignore entries for sensitive paths are security controls, not documentation — never remove during cleanup sweeps

[2026-01-28-051426] IDE is already the UI

Context: Considering whether to build custom UI for .context/ files

Lesson: Discovery, search, and editing of .context/ markdown files works better in VS Code/IDE than any custom UI we'd build. Full-text search, git integration, extensions - all free.

Application: Don't reinvent the editor. Let users use their preferred IDE.


Module-specific, niche, and historical learnings: learnings-reference.md