From fc7db228181c0800b9e925ce345f3a02b617e23b Mon Sep 17 00:00:00 2001 From: Jose Alekhinne Date: Fri, 22 May 2026 13:27:26 -0700 Subject: [PATCH 1/5] feat(cwd-anchored): drop CTX_DIR + ctx activate/deactivate; jumbo Implements specs/cwd-anchored-context.md end-to-end across two sessions. `ctx` is now anchored to its working directory: every resolver reads $PWD/.context/, and there is no env-var or walk-up. Mental model matches zensical (zensical.toml), terraform (.tf), and Claude Code (CLAUDE_PROJECT_DIR): the tool anchors to a config location, not a parallel declaration channel. Net: 283 files changed, +6004 / -11546 (~5500 net deletion). == Steps 1+2 (prior session, included here): == * rc.ContextDir collapses to filepath.Join(cwd, dir.Context) after a single os.Stat; basename guard, env channel, ScanCandidates, ErrDirNotDeclared/ErrRelativeNotAllowed/ ErrNonCanonicalBasename all removed. * gitmeta.RequireGitTree collapses to os.Stat($PWD/.git); upward walk gone. * internal/cli/initialize/core/envmatch/ deleted (the env-vs-CWD mismatch guard becomes vestigial). * ctx init drops the env-vs-CWD branch; always targets $PWD/.context. == Step 3 - Hook cd migration: == * internal/assets/claude/hooks/hooks.json migrated from `CTX_DIR="${CLAUDE_PROJECT_DIR:?...}/.context" ctx system ` to `cd "${CLAUDE_PROJECT_DIR:?...}" && ctx system ` across all 22 hook commands. * check-anchor-drift hook entry removed (the drift it detected cannot occur under cwd-anchored). * Trace hooks (post-commit.sh, prepare-commit-msg.sh) and copilot-cli scripts already cwd-correct; no migration needed. == Step 4 - activate/deactivate/anchor-drift deletion: == Packages deleted: * internal/cli/activate/ (whole subtree) * internal/cli/deactivate/ (whole subtree) * internal/write/activate/ * internal/err/activate/ * internal/cli/system/cmd/checkanchordrift/ * internal/cli/system/core/anchor/ * internal/config/shell/ Files deleted: * internal/config/embed/text/err_activate.go * internal/config/embed/text/check_anchor.go * internal/config/embed/flag/activate.go * internal/rc/candidates.go Constants removed: * env.CtxDir, env.CtxDirInherited, env.Shell * hook.CheckAnchorDrift * flag.Shell * cmd.UseActivate / UseDeactivate / DescKeyActivate / DescKeyDeactivate * cmd.UseSystemCheckAnchorDrift / DescKeySystemCheckAnchorDrift * text.DescKeyErrActivateNoLocalContext, text.DescKeyCheckAnchorDrift*, text.DescKeyWriteInitActivateHint Registrations trimmed: * bootstrap/group.go: activate/deactivate imports + getting-started entries removed. * cli/system/system.go: checkanchordrift import + Cmd() entry removed. YAML pruned: * commands.yaml: activate, deactivate, system.checkanchordrift * examples.yaml: activate, deactivate, system.checkanchordrift * flags.yaml: activate.shell * errors.yaml: err.activate.no-local-context * hooks.yaml: check-anchor-drift.* (4 keys) * write.yaml: write.init-activate-hint + reflow of write.init-anatomy-preamble == Step 5 - Docs sweep: == Recipes deleted (no longer applicable under cwd-anchored): * docs/recipes/activating-context.md * docs/recipes/external-context.md Recipes updated (warning-block removal + eval-line removal, some prose rewrites): 22 recipes across hub, session, knowledge, permission, journal, scratchpad families. Bulk pattern was the `!!! warning "Activate the Project First"` boilerplate plus inline `eval "$(ctx activate)"` snippets. Home docs updated: getting-started.md (steps renumbered), first-session.md, joining-a-project.md, is-ctx-right.md, configuration.md (External `.context/` example removed, CTX_DIR env-var table row removed), opencode.md, vscode.md. CLI docs updated: cli/index.md (activate/deactivate command rows removed, env-var table row removed, declaration-model preamble rewritten), cli/init-status.md (activate/deactivate sections deleted), cli/bootstrap.md, cli/mcp.md. Operations docs updated: integrations.md, autonomous-loop.md, migration.md, upgrading.md, and 5 runbooks (architecture-exploration.md, breaking-migration.md, hub-deployment.md, new-contributor.md, release-checklist.md). AI-facing templates updated: * assets/claude/CLAUDE.md, assets/integrations/copilot-cli/INSTRUCTIONS.md, CLAUDE.md: ctx.ist/recipes/activating-context/ URL repointed to ctx.ist/home/getting-started/. * assets/integrations/opencode/plugin/index.ts: shell.env hook now sets output.cwd instead of injecting CTX_DIR; ctx.$ uses .cwd(ctx.directory). * 4 journal-enrich SKILL.md files: local var renamed CTX_DIR -> CTX_PATH (cosmetic; the var was already local-only). Spec status updates: * specs/activate-strict-cwd.md: marked superseded by this spec. * specs/single-source-context-anchor.md: marked superseded (large sections; basename-guard rationale and no-walk-up principle survive in stronger form). == Tests: == Cleaned dead t.Setenv("CTX_DIR", ...) calls from: init_test.go (16 calls), pad_test.go (3), remind_test.go, checkreminder/run_test.go, notify_test.go, cli_test.go, change/core/cmd_test.go (replaced with t.Chdir + mkdir .context), drift/check_ext_test.go (mkdir .context), mcp/server/server_test.go. mcp/cmd/root/cmd_test.go rewritten: TestMcpServe_FailsClosedOnUnsetCTXDIR -> TestMcpServe_FailsClosedWhenNoDotContext: now uses t.Chdir(t.TempDir()) and asserts the fail-closed behaviour under cwd-anchored. User-facing warning updated: config/warn/warn.go's RCNoContextDir message changed from "rc.RC: no CTX_DIR declared" to "rc.RC: no .context/ at $PWD". == Also in this jumbo (prior + this session): == * AI backend spec scaffolding (block A contract + block B+C sketched-not-contracted): - .context/briefs/20260522T001011Z-ctx-ai-backend.md - specs/ctx-ai-backend.md (with paste-ready task breakdown appended) - specs/ctx-ai-extraction-and-recall.md * KB editorial pipeline scaffolding (Phase KB landed in prior session): - .context/kb/ (topic pages, index, source-coverage, evidence-index) - .context/ingest/ (KB-RULES.md, OPERATOR.md, PROMPT.md, SESSION_LOG.md, schemas) - .context/archive/closeouts/ (folded closeouts) - first kb topic: .context/kb/topics/vllm/index.md (contrastive study of vLLM's example landing page vs ctx's recipe surface) * Substrate-vs-artifact placement convention codified: CONVENTIONS.md entry under File Organization plus a DECISIONS.md entry capturing the three coupling tests (consumed via ctx-mediated paths; tightly coupled to ctx pipeline machinery; authored under ctx skill discipline) and the three rejected alternatives. * Handover artifact migration (prior sessions): - .context/HANDOVER-2026-04-22.md removed (legacy flat shape) - .context/handovers/ now uses timestamped -.md (20260521T045353Z, 20260521T211326Z) * CI/lint helpers: - hack/lint-shellcheck.sh, hack/lint-powershell.sh - .github/workflows/ci.yml gains shellcheck + powershell jobs * Typography guide: - .context/typography.md (contributor/agent surface for Title Case, monotype ctx, banner shape, recipe Problem->TL;DR arc) * Many small assets edits (test surface fallout, skill polish, YAML key renames captured in prior commits, leftover here). * docs/ site/ regenerated to match prose edits. == Verification: == * golangci-lint run: 0 issues. * go test ./...: all packages green, no failures. == Provenance: == Two-session work on feat/cwd-anchored-context; the user's jumbo strategy explicitly accumulates across sessions until a single comprehensive commit lands. Not pushed. Spec: specs/cwd-anchored-context.md Spec: specs/ctx-ai-backend.md Spec: specs/ctx-ai-extraction-and-recall.md Spec: specs/activate-strict-cwd.md (superseded) Spec: specs/single-source-context-anchor.md (superseded) Signed-off-by: Jose Alekhinne --- .context/CONVENTIONS.md | 9 + .context/DECISIONS.md | 77 +- .context/HANDOVER-2026-04-22.md | 213 -- .context/LEARNINGS.md | 44 + .context/TASKS.md | 290 +- .../20260521T202635Z-ingest-closeout.md | 121 + .../briefs/20260522T001011Z-ctx-ai-backend.md | 173 ++ .context/handovers/.gitkeep | 0 ...718-anchor-undercover-to-spec-and-tasks.md | 123 - ...7-skill-surface-polish-validate-cleanup.md | 190 -- ...8Z-phase-kb-sentinel-strings-punch-list.md | 129 - .context/ingest/00-GROUND.md | 92 + .context/ingest/30-INGEST.md | 260 ++ .context/ingest/40-ASK.md | 104 + .context/ingest/50-SITE_REVIEW.md | 123 + .context/ingest/KB-RULES.md | 354 +++ .context/ingest/OPERATOR.md | 153 + .context/ingest/PROMPT.md | 95 + .context/ingest/SESSION_LOG.md | 12 + .context/ingest/closeouts/.gitkeep | 0 .context/ingest/schemas/contradictions.md | 45 + .context/ingest/schemas/domain-decisions.md | 58 + .context/ingest/schemas/evidence-index.md | 45 + .context/ingest/schemas/glossary.md | 54 + .../ingest/schemas/outstanding-questions.md | 51 + .context/ingest/schemas/relationship-map.md | 41 + .context/ingest/schemas/session-log.md | 52 + .context/ingest/schemas/source-coverage.md | 65 + .context/ingest/schemas/source-map.md | 45 + .context/ingest/schemas/timeline.md | 49 + .context/kb/evidence-index.md | 14 + .context/kb/index.md | 63 + .context/kb/source-coverage.md | 13 + .context/kb/source-map.md | 12 + .context/kb/topics/.gitkeep | 0 .context/kb/topics/vllm/index.md | 120 + .context/typography.md | 240 ++ .github/workflows/ci.yml | 28 + .gitignore | 6 + CLAUDE.md | 2 +- Makefile | 23 +- README.md | 22 +- docs/cli/bootstrap.md | 7 +- docs/cli/index.md | 37 +- docs/cli/init-status.md | 74 +- docs/cli/mcp.md | 13 +- docs/home/configuration.md | 61 +- docs/home/first-session.md | 17 +- docs/home/getting-started.md | 21 +- docs/home/is-ctx-right.md | 10 +- docs/home/joining-a-project.md | 15 - docs/home/opencode.md | 9 +- docs/home/vscode.md | 2 +- docs/operations/autonomous-loop.md | 12 +- docs/operations/integrations.md | 24 +- docs/operations/migration.md | 17 +- .../runbooks/architecture-exploration.md | 44 +- .../operations/runbooks/breaking-migration.md | 6 +- docs/operations/runbooks/hub-deployment.md | 13 +- docs/operations/runbooks/new-contributor.md | 8 +- docs/operations/runbooks/release-checklist.md | 8 +- docs/operations/upgrading.md | 4 +- docs/recipes/activating-context.md | 212 -- docs/recipes/autonomous-loops.md | 23 +- docs/recipes/context-health.md | 6 - docs/recipes/customizing-hook-messages.md | 8 - docs/recipes/external-context.md | 334 -- docs/recipes/hub-cluster.md | 1 - docs/recipes/hub-getting-started.md | 2 - docs/recipes/hub-multi-machine.md | 2 - docs/recipes/hub-personal.md | 10 - docs/recipes/hub-team.md | 13 +- docs/recipes/index.md | 10 - docs/recipes/knowledge-capture.md | 7 - docs/recipes/memory-bridge.md | 6 - docs/recipes/multi-tool-setup.md | 53 +- docs/recipes/multilingual-sessions.md | 6 - docs/recipes/permission-snapshots.md | 6 - docs/recipes/publishing.md | 6 - docs/recipes/scratchpad-sync.md | 13 +- docs/recipes/scratchpad-with-claude.md | 6 - docs/recipes/session-archaeology.md | 6 - docs/recipes/session-changes.md | 6 - docs/recipes/session-lifecycle.md | 15 - docs/recipes/session-reminders.md | 6 - docs/recipes/state-maintenance.md | 6 - docs/recipes/task-management.md | 7 - docs/recipes/troubleshooting.md | 62 +- hack/lint-powershell.sh | 94 + hack/lint-shellcheck.sh | 56 + internal/assets/claude/CLAUDE.md | 2 +- internal/assets/claude/hooks/hooks.json | 43 +- .../skills/ctx-journal-enrich-all/SKILL.md | 12 +- .../claude/skills/ctx-journal-enrich/SKILL.md | 4 +- .../assets/claude/skills/ctx-plan/SKILL.md | 25 +- .../assets/claude/skills/ctx-spec/SKILL.md | 23 + internal/assets/commands/commands.yaml | 50 - internal/assets/commands/examples.yaml | 11 - internal/assets/commands/flags.yaml | 2 - internal/assets/commands/text/errors.yaml | 42 +- internal/assets/commands/text/hooks.yaml | 17 - internal/assets/commands/text/write.yaml | 20 +- .../assets/context/AGENT_PLAYBOOK_GATE.md | 17 +- .../integrations/copilot-cli/INSTRUCTIONS.md | 2 +- .../skills/ctx-journal-enrich-all/SKILL.md | 12 +- .../skills/ctx-journal-enrich/SKILL.md | 4 +- .../copilot-cli/skills/ctx-spec/SKILL.md | 23 + .../integrations/opencode/plugin/index.ts | 12 +- internal/bootstrap/bootstrap_test.go | 17 +- internal/bootstrap/cmd.go | 36 +- internal/bootstrap/group.go | 4 - internal/cli/activate/activate.go | 22 - internal/cli/activate/activate_test.go | 275 -- internal/cli/activate/cmd/root/cmd.go | 77 - internal/cli/activate/cmd/root/doc.go | 33 - internal/cli/activate/cmd/root/run.go | 76 - internal/cli/activate/core/emit/doc.go | 28 - internal/cli/activate/core/emit/emit.go | 90 - internal/cli/activate/core/emit/posix.go | 57 - internal/cli/activate/core/emit/types.go | 12 - internal/cli/activate/core/resolve/doc.go | 26 - .../cli/activate/core/resolve/internal.go | 49 - internal/cli/activate/core/resolve/resolve.go | 32 - internal/cli/activate/doc.go | 36 - internal/cli/activate/testmain_test.go | 22 - internal/cli/change/core/cmd_test.go | 20 +- internal/cli/change/core/detect/detect.go | 4 +- internal/cli/cli_test.go | 9 +- internal/cli/config/cmd/status/run_test.go | 10 + internal/cli/config/cmd/switchcmd/run_test.go | 8 + .../cli/config/core/profile/profile_test.go | 10 + internal/cli/deactivate/cmd/root/cmd.go | 58 - internal/cli/deactivate/cmd/root/doc.go | 19 - internal/cli/deactivate/cmd/root/run.go | 34 - internal/cli/deactivate/deactivate.go | 22 - internal/cli/deactivate/deactivate_test.go | 73 - internal/cli/deactivate/doc.go | 27 - internal/cli/deactivate/testmain_test.go | 21 - internal/cli/doctor/cmd/root/run.go | 6 +- internal/cli/doctor/core/check/check.go | 20 +- internal/cli/doctor/doctor_test.go | 10 +- internal/cli/drift/drift_test.go | 9 +- internal/cli/handover/core/path/path_test.go | 7 +- .../cli/hub/cmd/status/integration_test.go | 72 +- internal/cli/initialize/cmd/root/run.go | 50 +- .../core/project/getting_started.go | 26 +- internal/cli/initialize/init_test.go | 23 +- internal/cli/kb/core/path/path_test.go | 7 +- internal/cli/mcp/cmd/root/cmd_test.go | 17 +- internal/cli/notify/notify_test.go | 2 - internal/cli/pad/pad_test.go | 4 - internal/cli/pause/pause_test.go | 5 +- internal/cli/remind/remind_test.go | 2 - internal/cli/resume/resume_test.go | 5 +- internal/cli/serve/serve_test.go | 20 +- internal/cli/setup/core/opencode/mcp.go | 47 +- internal/cli/setup/core/opencode/mcp_test.go | 71 +- internal/cli/sync/sync_test.go | 10 +- internal/cli/system/cmd/bootstrap/run.go | 24 +- .../cli/system/cmd/checkanchordrift/cmd.go | 35 - .../cli/system/cmd/checkanchordrift/doc.go | 64 - .../cli/system/cmd/checkanchordrift/run.go | 73 - .../system/cmd/checkanchordrift/run_test.go | 153 - .../cmd/checkanchordrift/testmain_test.go | 22 - .../cli/system/cmd/checkreminder/run_test.go | 3 +- internal/cli/system/core/anchor/doc.go | 26 - internal/cli/system/core/anchor/equal.go | 44 - .../cli/system/core/check/full_preamble.go | 4 +- internal/cli/system/core/health/prune.go | 4 +- internal/cli/system/core/message/message.go | 2 +- .../cli/system/core/message/message_cmd.go | 2 +- internal/cli/system/core/state/state.go | 8 +- internal/cli/system/core/state/state_test.go | 37 +- internal/cli/system/doc.go | 1 - internal/cli/system/system.go | 2 - internal/cli/watch/watch_test.go | 12 +- internal/config/embed/cmd/base.go | 8 - internal/config/embed/cmd/system.go | 6 - internal/config/embed/flag/activate.go | 14 - internal/config/embed/text/check_anchor.go | 25 - internal/config/embed/text/doctor.go | 2 +- internal/config/embed/text/err_activate.go | 15 - internal/config/embed/text/err_fs.go | 47 +- internal/config/embed/text/initialize.go | 6 - internal/config/env/env.go | 15 - internal/config/file/ignore.go | 9 + internal/config/flag/flag.go | 8 - internal/config/hook/hook.go | 3 - internal/config/rc/rc.go | 5 - internal/config/shell/doc.go | 17 - internal/config/shell/shell.go | 81 - internal/config/warn/warn.go | 4 +- internal/context/resolve/resolve.go | 8 +- internal/drift/check_ext_test.go | 14 +- internal/err/activate/activate.go | 23 - internal/err/activate/doc.go | 21 - internal/err/context/context.go | 198 +- internal/gitmeta/require.go | 31 +- internal/gitmeta/require_test.go | 15 +- internal/journal/parser/markdown_test.go | 7 + internal/log/event/event_test.go | 9 + internal/mcp/server/server_test.go | 7 +- internal/notify/notify.go | 2 +- internal/rc/candidates.go | 50 - internal/rc/doc.go | 42 +- internal/rc/load.go | 59 +- internal/rc/rc.go | 128 +- internal/rc/rc_test.go | 314 +- internal/rc/require.go | 95 +- internal/rc/require_test.go | 103 +- internal/testutil/testctx/testctx.go | 47 +- internal/write/activate/activate.go | 88 - internal/write/activate/doc.go | 27 - internal/write/initialize/info.go | 23 - site/cli/bootstrap/index.html | 7 +- site/cli/index.html | 45 +- site/cli/init-status/index.html | 183 +- site/cli/mcp/index.html | 21 +- site/home/configuration/index.html | 203 +- site/home/first-session/index.html | 236 +- site/home/getting-started/index.html | 82 +- site/home/is-ctx-right/index.html | 26 +- site/home/joining-a-project/index.html | 64 +- site/home/opencode/index.html | 11 +- site/home/vscode/index.html | 2 +- site/operations/autonomous-loop/index.html | 12 +- site/operations/integrations/index.html | 405 ++- site/operations/migration/index.html | 47 +- .../architecture-exploration/index.html | 44 +- .../runbooks/breaking-migration/index.html | 11 +- .../runbooks/hub-deployment/index.html | 15 +- .../runbooks/new-contributor/index.html | 8 +- .../runbooks/release-checklist/index.html | 14 +- site/operations/upgrading/index.html | 8 +- site/recipes/activating-context/index.html | 2413 --------------- site/recipes/autonomous-loops/index.html | 29 +- site/recipes/building-skills/index.html | 2 +- .../claude-code-permissions/index.html | 2 +- .../recipes/configuration-profiles/index.html | 2 +- site/recipes/context-health/index.html | 9 +- .../customizing-hook-messages/index.html | 11 +- site/recipes/design-before-coding/index.html | 2 +- site/recipes/external-context/index.html | 2710 ----------------- site/recipes/guide-your-agent/index.html | 20 +- site/recipes/hook-output-patterns/index.html | 2 +- site/recipes/hub-cluster/index.html | 9 +- site/recipes/hub-getting-started/index.html | 12 +- site/recipes/hub-multi-machine/index.html | 12 +- site/recipes/hub-overview/index.html | 2 +- site/recipes/hub-personal/index.html | 13 +- site/recipes/hub-team/index.html | 15 +- site/recipes/import-plans/index.html | 2 +- site/recipes/index.html | 25 +- site/recipes/knowledge-capture/index.html | 10 +- site/recipes/memory-bridge/index.html | 9 +- site/recipes/multi-tool-setup/index.html | 115 +- site/recipes/multilingual-sessions/index.html | 27 +- site/recipes/parallel-worktrees/index.html | 2 +- site/recipes/permission-snapshots/index.html | 9 +- site/recipes/publishing/index.html | 9 +- site/recipes/scratchpad-sync/index.html | 20 +- .../recipes/scratchpad-with-claude/index.html | 9 +- site/recipes/session-archaeology/index.html | 9 +- site/recipes/session-ceremonies/index.html | 2 +- site/recipes/session-changes/index.html | 7 - site/recipes/session-lifecycle/index.html | 143 +- site/recipes/session-pause/index.html | 2 +- site/recipes/session-reminders/index.html | 9 +- site/recipes/state-maintenance/index.html | 7 - site/recipes/steering/index.html | 2 +- site/recipes/system-hooks-audit/index.html | 2 +- site/recipes/task-management/index.html | 10 +- site/recipes/triggers/index.html | 2 +- site/recipes/troubleshooting/index.html | 153 +- site/recipes/webhook-notifications/index.html | 2 +- .../when-to-use-agent-teams/index.html | 2 +- site/search.json | 2 +- site/sitemap.xml | 6 - specs/activate-strict-cwd.md | 165 + specs/ctx-ai-backend.md | 402 +++ specs/ctx-ai-extraction-and-recall.md | 329 ++ specs/cwd-anchored-context.md | 314 ++ specs/single-source-context-anchor.md | 7 +- 283 files changed, 6004 insertions(+), 11546 deletions(-) delete mode 100644 .context/HANDOVER-2026-04-22.md create mode 100644 .context/archive/closeouts/20260521T202635Z-ingest-closeout.md create mode 100644 .context/briefs/20260522T001011Z-ctx-ai-backend.md create mode 100644 .context/handovers/.gitkeep delete mode 100644 .context/handovers/2026-05-10-071718-anchor-undercover-to-spec-and-tasks.md delete mode 100644 .context/handovers/2026-05-11-000847-skill-surface-polish-validate-cleanup.md delete mode 100644 .context/handovers/20260518T004118Z-phase-kb-sentinel-strings-punch-list.md create mode 100644 .context/ingest/00-GROUND.md create mode 100644 .context/ingest/30-INGEST.md create mode 100644 .context/ingest/40-ASK.md create mode 100644 .context/ingest/50-SITE_REVIEW.md create mode 100644 .context/ingest/KB-RULES.md create mode 100644 .context/ingest/OPERATOR.md create mode 100644 .context/ingest/PROMPT.md create mode 100644 .context/ingest/SESSION_LOG.md create mode 100644 .context/ingest/closeouts/.gitkeep create mode 100644 .context/ingest/schemas/contradictions.md create mode 100644 .context/ingest/schemas/domain-decisions.md create mode 100644 .context/ingest/schemas/evidence-index.md create mode 100644 .context/ingest/schemas/glossary.md create mode 100644 .context/ingest/schemas/outstanding-questions.md create mode 100644 .context/ingest/schemas/relationship-map.md create mode 100644 .context/ingest/schemas/session-log.md create mode 100644 .context/ingest/schemas/source-coverage.md create mode 100644 .context/ingest/schemas/source-map.md create mode 100644 .context/ingest/schemas/timeline.md create mode 100644 .context/kb/evidence-index.md create mode 100644 .context/kb/index.md create mode 100644 .context/kb/source-coverage.md create mode 100644 .context/kb/source-map.md create mode 100644 .context/kb/topics/.gitkeep create mode 100644 .context/kb/topics/vllm/index.md create mode 100644 .context/typography.md delete mode 100644 docs/recipes/activating-context.md delete mode 100644 docs/recipes/external-context.md create mode 100755 hack/lint-powershell.sh create mode 100755 hack/lint-shellcheck.sh delete mode 100644 internal/cli/activate/activate.go delete mode 100644 internal/cli/activate/activate_test.go delete mode 100644 internal/cli/activate/cmd/root/cmd.go delete mode 100644 internal/cli/activate/cmd/root/doc.go delete mode 100644 internal/cli/activate/cmd/root/run.go delete mode 100644 internal/cli/activate/core/emit/doc.go delete mode 100644 internal/cli/activate/core/emit/emit.go delete mode 100644 internal/cli/activate/core/emit/posix.go delete mode 100644 internal/cli/activate/core/emit/types.go delete mode 100644 internal/cli/activate/core/resolve/doc.go delete mode 100644 internal/cli/activate/core/resolve/internal.go delete mode 100644 internal/cli/activate/core/resolve/resolve.go delete mode 100644 internal/cli/activate/doc.go delete mode 100644 internal/cli/activate/testmain_test.go delete mode 100644 internal/cli/deactivate/cmd/root/cmd.go delete mode 100644 internal/cli/deactivate/cmd/root/doc.go delete mode 100644 internal/cli/deactivate/cmd/root/run.go delete mode 100644 internal/cli/deactivate/deactivate.go delete mode 100644 internal/cli/deactivate/deactivate_test.go delete mode 100644 internal/cli/deactivate/doc.go delete mode 100644 internal/cli/deactivate/testmain_test.go delete mode 100644 internal/cli/system/cmd/checkanchordrift/cmd.go delete mode 100644 internal/cli/system/cmd/checkanchordrift/doc.go delete mode 100644 internal/cli/system/cmd/checkanchordrift/run.go delete mode 100644 internal/cli/system/cmd/checkanchordrift/run_test.go delete mode 100644 internal/cli/system/cmd/checkanchordrift/testmain_test.go delete mode 100644 internal/cli/system/core/anchor/doc.go delete mode 100644 internal/cli/system/core/anchor/equal.go delete mode 100644 internal/config/embed/flag/activate.go delete mode 100644 internal/config/embed/text/check_anchor.go delete mode 100644 internal/config/embed/text/err_activate.go delete mode 100644 internal/config/shell/doc.go delete mode 100644 internal/config/shell/shell.go delete mode 100644 internal/err/activate/activate.go delete mode 100644 internal/err/activate/doc.go delete mode 100644 internal/rc/candidates.go delete mode 100644 internal/write/activate/activate.go delete mode 100644 internal/write/activate/doc.go delete mode 100644 site/recipes/activating-context/index.html delete mode 100644 site/recipes/external-context/index.html create mode 100644 specs/activate-strict-cwd.md create mode 100644 specs/ctx-ai-backend.md create mode 100644 specs/ctx-ai-extraction-and-recall.md create mode 100644 specs/cwd-anchored-context.md diff --git a/.context/CONVENTIONS.md b/.context/CONVENTIONS.md index a5f383b0f..6e774db75 100644 --- a/.context/CONVENTIONS.md +++ b/.context/CONVENTIONS.md @@ -13,6 +13,13 @@ DO NOT UPDATE FOR: - Personal preferences without team consensus --> +## Typography and Document Shape + +See [`typography.md`](typography.md) for the full guide: Title Case +headings, monotype `` `ctx` ``, no em-dashes / smart quotes / quad +backticks, doc frontmatter / banner conventions, recipe arc, admonition +variants. Linters in `hack/` enforce the hard rules. + ## Naming - **Constants use semantic prefixes**: Group related constants with prefixes @@ -295,3 +302,5 @@ DO NOT UPDATE FOR: following a literal . dot, but explicit backticks remain the clearest signal. - New editor integrations include an MCP-merge test covering: create / empty file / preserve existing keys / skip when registered / reject malformed JSON + +- Substrate vs. artifact placement: cognitive substrate (consumed and mutated via ctx-mediated paths — `ctx agent`, `ctx decision add`, `/ctx-kb-ingest`, `/ctx-handover`, ceremonies) lives under `.context/`; project artifacts (read and edited directly by humans — `specs/`, `CLAUDE.md`, `GETTING_STARTED.md`, `docs/`) live at the project root; tool config and tool homes (`.ctxrc`, `.claude/`) live at root by dotfile/tool convention. The kb is substrate, not artifact: direct file edits remain possible per Invariant 1, but the skill-mediated path is the discipline. Rationale recorded in DECISIONS.md. diff --git a/.context/DECISIONS.md b/.context/DECISIONS.md index 2331914f8..53ede85ca 100644 --- a/.context/DECISIONS.md +++ b/.context/DECISIONS.md @@ -3,7 +3,12 @@ | Date | Decision | |----|--------| -| 2026-05-17 | entity.Sentinel lives in internal/entity/ because the cross-package-types audit treats entity/ as the canonical home for shared types | +| 2026-05-21 | Substrate vs. artifact placement: .context/ vs. project root | +| 2026-05-21 | Spec steps 1+2 merged into a single commit (cwd-anchored-context) | +| 2026-05-20 | Anchor ctx to CWD; drop activate, drop env-var resolver, drop all walks (proposed) | +| 2026-05-20 | ctx activate is strict-CWD; drop upward walk | +| 2026-05-20 | Gitignore .context/handovers/; track only .gitkeep | +| 2026-05-17 | `entity.Sentinel` lives in `internal/entity/` because the cross-package-types audit treats `entity/` as the canonical home for shared types | | 2026-05-16 | Phase KB lifts the current upstream editorial-pipeline shape, superseding the 4-phase predecessor in the brief | | 2026-05-11 | Embedded and separately-published harnesses use distinct CI and release pipelines | | 2026-05-11 | Embedded foreign-language assets under internal/assets/ are intentional, not a smell | @@ -142,6 +147,76 @@ For significant decisions: --> +## [2026-05-21-203052] Substrate vs. artifact placement: .context/ vs. project root + +**Status**: Accepted + +**Context**: Question surfaced while scaffolding specs/ctx-ai-backend.md and specs/ctx-ai-extraction-and-recall.md. User observed that specs/ is the only folder (aside from GETTING_STARTED.md) ctx-managed but outside .context/, and asked whether the placement was philosophically correct. Initial 'state vs. artifact' framing was challenged with 'by that token, isn't kb a project artifact?' — exposing that the binary cut was too coarse. + +**Decision**: Substrate vs. artifact placement: .context/ vs. project root + +**Rationale**: Distinguish cognitive substrate (lives under .context/) from project artifact (lives at root) by the *consumption/mutation path*, not by who manages the files. Substrate is read AND written through ctx-mediated paths (ctx agent, ctx decision add, /ctx-kb-ingest, /ctx-handover, ceremonies); artifacts are read AND edited directly by humans (specs/, CLAUDE.md, GETTING_STARTED.md, docs/). Three coupling tests sharpen the line: (a) queried via ctx-mediated paths, (b) tightly coupled to ctx pipeline machinery, (c) authored under ctx skill discipline. The kb passes all three (kb closeouts fold into handovers, /ctx-kb-ingest enforces pass-mode and citations, /ctx-kb-ask is the primary read path) so it stays under .context/. Specs pass none (referenced by commits, never loaded by ctx agent, no pipeline coupling) so they live at root. Rejected alternatives: (1) move specs/ under .context/specs/ for boundary cleanliness — fails because specs are project artifacts written for humans/reviewers/community devs and hiding them under a dotfile breaks navigability; (2) move kb/ to project root because it has artifact-like properties — fails because kb machinery (closeouts, source-coverage ledger, evidence-index schema) cannot be lifted out of .context/ without splitting things that live together; (3) keep the original 'state vs. artifact' framing — too binary, kb pushback proved a third axis was needed. + +**Consequence**: Codified as a CONVENTIONS.md entry under 'File Organization'. Placement test for new ctx-related files or folders: is this consumed/mutated through ctx-mediated paths (substrate, .context/) or read/edited directly by humans (artifact, root)? Visibility complaint about .context/ being a dotfile is acknowledged but acceptable — humans navigate substrate via ctx commands and generated views (ctx site kb build, ctx serve), not via file browsers. Trade-off: the rule's correctness depends on the ctx-mediated paths actually existing for substrate files; if substrate is added but no skill/command consumes it, the placement test misclassifies. See also: CONVENTIONS.md 'File Organization' section. + +--- + +## [2026-05-21-140236] Spec steps 1+2 merged into a single commit (cwd-anchored-context) + +**Status**: Accepted + +**Context**: Yesterday's spec (specs/cwd-anchored-context.md) decomposed the cwd-anchored refactor into 5 sequential steps, each intended to land as a separate commit. Step 1 (resolver swap, rc.ContextDir → cwd-anchored os.Stat) cannot compile without Step 2 (init guard removal, deletion of internal/cli/initialize/core/envmatch/) because envmatch references the soon-to-be-deleted ErrDirNotDeclared sentinel. + +**Decision**: Spec steps 1+2 merged into a single commit (cwd-anchored-context) + +**Rationale**: Cleanest commit boundaries beat strict spec adherence when the spec's boundaries are mechanically infeasible. Steps 1 and 2 were merged into one atomic commit; remaining steps 3 (hook cd migration), 4 (activate/deactivate deletion), 5 (docs sweep) stay as discrete commits per the spec. + +**Consequence**: Spec stays authoritative for what; commit-slicing diverges for practical reasons. Future cwd-anchored work follows a 4-commit (merged) decomposition, not the spec's 5. Spec text remains as-written; the divergence is documented here, not in the spec. + +--- + +## [2026-05-20-214812] Anchor ctx to CWD; drop activate, drop env-var resolver, drop all walks (proposed) + +**Status**: Accepted + +**Context**: Even after strict-CWD activate landed, eval $(ctx activate) remains an opaque per-shell ceremony. Two-channel resolution (env CTX_DIR + cwd) is the residual complexity; activate/deactivate exist only because of the env channel; the env channel exists to avoid the walk. With .context/ mandated as .git/'s sibling (CONSTITUTION require-git), if cwd must contain .context/ then both .context/ AND .git/ are in cwd — and every resolver across rc, gitmeta, and the activate commands collapses to os.Stat. + +**Decision**: Anchor ctx to CWD; drop activate, drop env-var resolver, drop all walks (proposed) + +**Rationale**: User counter to the agent's walk-to-.git/ proposal: the walk infrastructure (rc.ScanCandidates, gitmeta upward walk) is precisely what we want to delete; keeping ANY walk forces us to maintain two implementations. Mental model anchor matches zensical (zensical.toml), helm (Chart.yaml), terraform (.tf), Claude Code ($CLAUDE_PROJECT_DIR). Subdir convenience tax is a fixed per-shell cost (cd $(git rev-parse --show-toplevel)) for the user who knows their project root; agents pay no tax (cd is mechanical for them). + +**Consequence**: Spec written at specs/cwd-anchored-context.md (314L); supersedes specs/activate-strict-cwd.md entirely and large sections of specs/single-source-context-anchor.md. Implementation queued as TASKS.md item at #priority:medium #added:2026-05-20 — multi-step (rc + gitmeta resolver simplification → init guard removal → hook cd migration → activate/deactivate deletion → docs sweep), estimated ~600-1000 LOC net deletion. Four open questions to resolve before code: CTX_DIR transition policy, deprecation shim, editor-integration grep, implementation order. + +--- + +## [2026-05-20-214801] ctx activate is strict-CWD; drop upward walk + +**Status**: Accepted + +**Context**: Bug TASKS:58 — fresh git init under a workspace with its own .context/ silently bound the parent context because activate walked up past the git boundary. Previous design (specs/single-source-context-anchor.md) preserved walk-up under 'interactive discovery' on the rationale that workspace-shared .context/ next to per-project ones was a legitimate layout. + +**Decision**: ctx activate is strict-CWD; drop upward walk + +**Rationale**: ctx activate is a state-setting command (exports CTX_DIR); state commands follow git's read-vs-state pattern (read walks freely, state refuses to cross repo boundaries). The workspace-shared use case is preserved by user action (cd to workspace before activating), not by inferred walk. The 'also visible upward' stderr advisory was invisible to eval-bindable invocations anyway. + +**Consequence**: scan() in internal/cli/activate/core/resolve/internal.go collapsed from 49 LOC walking via rc.ScanCandidates to a single os.Stat; resolve.Selected() signature went (string, []string, error) → (string, error); writeActivate.AlsoVisible and FormatAlsoVisibleAdvisory deleted; errActivate.NoCandidates renamed to NoLocalContext(cwd) and now names PWD verbatim. Spec: specs/activate-strict-cwd.md. + +--- + +## [2026-05-20-214753] Gitignore .context/handovers/; track only .gitkeep + +**Status**: Accepted + +**Context**: Per-session, operator-specific artifacts that grow without bound and can leak host/internal identifiers (ari, asgard, broadcom-class) into public mirrors when the project's .context/ is committed. + +**Decision**: Gitignore .context/handovers/; track only .gitkeep + +**Rationale**: Aligns with the existing per-personal-state gitignore family (journal, memory, state, logs, reminders.json, scratchpad.enc); the directory's .gitkeep keeps the read-side missing-dir gate passing on fresh clones; the rest of the closeout-fold pipeline already lives in .context/archive/closeouts/ which IS tracked. + +**Consequence**: ctx init template (internal/config/file/ignore.go) added .context/handovers/* and !.context/handovers/.gitkeep; existing tracked handovers untracked via git rm --cached but kept on disk; the 'handover is the sole authoritative recall artifact' phrasing in KB-RULES.md still holds — it's local-machine authoritative. + +--- + ## [2026-05-17-181500] `entity.Sentinel` lives in `internal/entity/` because the cross-package-types audit treats `entity/` as the canonical home for shared types **Status**: Accepted diff --git a/.context/HANDOVER-2026-04-22.md b/.context/HANDOVER-2026-04-22.md deleted file mode 100644 index 84b18c949..000000000 --- a/.context/HANDOVER-2026-04-22.md +++ /dev/null @@ -1,213 +0,0 @@ -# Session Handover — 2026-04-22 - -This file captures state from session `d6889b7c` (branch -`feat/explicit-context-dir`) so the next session can pick up -without re-investigating. Delete this file after it's been read. - -## Branch State - -- Branch: `feat/explicit-context-dir` (0 commits ahead of main — - everything is in the working tree). -- **328 files changed**, 4402 insertions, 4652 deletions. -- `go build ./...` clean, `make lint` 0 issues, `go test ./...` - exit 0 as of end of session. -- No commits were made. **First order of business next session: - decide the commit strategy.** Logical splits that make sense: - - 1. Agent-docs rewrite (AGENT_PLAYBOOK, CLAUDE.md, gate, internal - assets — watermelon-rind removal + triage error policy). - 2. Explicit-context-dir hardening in `internal/**` (resolver - plumbing, ContextChild removal, KeyPath signature change, - ctxrcPath rename, DirLine/AppendDir propagation). - 3. FullPreamble 5-value return + 16 hook/callsite migration. - 4. RequireContextDir command-entry sweep (25 RunE gates). - 5. `ctx backup` full deprecation (per `specs/deprecate-ctx-backup.md`). - 6. Pre-existing build-fix: missing `DescKeyWriteSnapshotUpdated` / - `Saved` constants added (un-breaks `internal/write/restore`). - - Splitting (2) from (3) may be impractical — the callsite diff - is tangled. One "explicit-context-dir hardening" commit + one - "ctx backup removal" commit is the minimum defensible split. - One giant commit also works and may be honest given the churn. - -## What Changed, By Theme - -### 1. Agent docs rewrite - -Rewrote `ctx`'s agent-facing prose to drop prohibitions about -behaviors agents wouldn't otherwise invent (watermelon-rind -pattern) and to split the old blanket "relay ctx errors verbatim -and stop" into an **invocation-error vs everything-else triage**. - -Touched: `CLAUDE.md`, `.context/AGENT_PLAYBOOK.md`, -`.context/AGENT_PLAYBOOK_GATE.md`, `internal/assets/claude/CLAUDE.md`, -`internal/assets/context/AGENT_PLAYBOOK.md`, same gate copy. - -### 2. Explicit-context-dir hardening - -Sharpened the "explicit declaration or nothing" model: - -- **Deleted `resolve.ContextChild`**. Inlined all 10 callers with - explicit `rc.ContextDir()` + `errors.Is(err, ErrDirNotDeclared)` + - `filepath.Join` (no string concat). -- **`rc.KeyPath()`** now returns `(string, error)`. Propagates - resolver failures instead of silently handing `""` to - `crypto.ResolveKeyPath` (which used to either filepath-join a - CWD-relative path or fall through to a global key — both bugs - this branch was built to kill). -- **`rc.ctxRcPath`** → `(string, error)` (plus renamed from - `ctxRcPath` to `ctxrcPath` to clear the stutter audit). -- **`resolve.DirLine` / `resolve.AppendDir`** now return - `(string, error)`. Noisy-TUI warn log stays (intentional, - documented) — return channel added so non-rendering callers - can propagate. -- **`notify.LoadWebhook`**, **`message.OverridePath`**, - **`message.HasOverride`**, **`hub.LoadBodies`**, - **`merge.LoadKey`**, **`store.KeyPath`**, - **`context/validate.Exists`**, **`sync.CheckNewDirectories`** - all propagate `ErrDirNotDeclared` instead of swallowing into - `(zero, nil)` pairs. -- **`knowledge.CheckHealth(sessionID, ctxDir)`**, - **`health.ReadMapTracking(ctxDir)`**, - **`hubsync.Connected(ctxDir)`**, - **`oversizeContent(ctxDir)`** now take `ctxDir` as a parameter - from the calling hook (dead internal resolver call eliminated). -- **`coreArchive.BackupProject` CWD-based resolution**: would - have been a latent bug but the whole archive package went away - with `ctx backup`. - -### 3. FullPreamble extended - -`internal/cli/system/core/check/full_preamble.go` now returns -`(input, sessionID, ctxDir, stateDir, ok)`. All 16 callers -updated. 4 hooks (`checkpersistence`, `checkmemorydrift`, -`checkceremony`, `checkjournal`) had their redundant -`errors.Is(ctxErr, ErrDirNotDeclared)` blocks deleted and use the -preamble's `ctxDir` directly. The 3 non-FullPreamble hooks -(`contextloadgate`, `checkcontextsize`, `heartbeat`) had -their `ErrDirNotDeclared` branches replaced with a defensive -"unreachable but log loudly" fallback after the `state.Initialized` -gate. - -**Pending follow-up** (already in TASKS.md): refactor -`FullPreamble` to return a `Preamble` struct instead of a -5-value tuple. Ugly positional return is tolerated for now. - -### 4. RequireContextDir command-entry sweep - -Added `rc.RequireContextDir()` gates at the top of 25 command -RunE entry points so users get the rich multi-line error instead -of a terse sentinel propagated through library layers. -Commands touched: -`add`, `agent`, `backup` (conditional on scope — then the whole -command died; see theme 5), `change`, `compact`, `drift`, -`journal schema check`, `load`, `memory unpublish` (already -gated via callee — only `SilenceUsage` added), `message {edit, -list, reset, show}`, `notify {setup, test}`, `pad {add, edit, -export, merge, mv, normalize, rm, root, show, tag}`, `sync`, -`watch`. - -Decision heuristic for which commands got gated: anything that -writes/reads files under `.context/` or calls a library that -does. Exempt: `init`, `activate`, `deactivate`, `bootstrap`, -`version`, `help` — they handle not-declared themselves. - -### 5. `ctx backup` full deprecation - -Executed `specs/deprecate-ctx-backup.md` end-to-end: - -- **Deleted**: `internal/cli/backup/`, - `internal/cli/system/cmd/check_backup_age/`, - `internal/cli/system/core/archive/`, `internal/write/backup/`, - `internal/exec/gio/`, - `internal/assets/hooks/messages/check-backup-age/`, - `.claude/skills/_ctx-backup/`, `docs/cli/backup.md`. -- **Trimmed**: `internal/config/archive/` to task-archive - constants only; `internal/err/backup/` to four generic - constructors (`Create`, `CreateArchiveDir`, `WriteArchive`, - `ContextDirNotFound`) that `init`, task archival, and - bootstrap still use — package name kept as a historical - label rather than churning the non-backup callers. -- **Surgical edits**: `bootstrap/group.go`, `cli/system/system.go`, - `config/env/env.go`, `config/hook/hook.go`, `entity/system.go`, - `cli/system/doc.go`, and every `internal/assets/commands/**.yaml`. -- **Tests updated**: `registry_test.go` count `32 → 31`; - `watch_test.go` expects `"no context directory"` instead of - the old `"ctx init"` suggestion. -- **All shipped docs updated**: `docs/cli/index.md`, - `docs/cli/system.md`, `docs/home/common-workflows.md`, - `docs/home/contributing.md`, `docs/recipes/customizing-hook-messages.md`, - `docs/recipes/hook-sequence-diagrams.md`, - `docs/recipes/hook-output-patterns.md`, - `internal/cli/system/README.md`, `zensical.toml`, - `CONTRIBUTING-SKILLS.md`. -- **New**: `docs/operations/runbooks/backup-strategy.md` — the - migration path (rsync, cron, Time Machine, Borg/restic). - -Historical artifacts intentionally left alone (per explicit -call): `.context/journal-site/**`, `.context/DECISIONS.md`, -`.context/decisions-reference.md`, `specs/released/**`, -`specs/deprecate-ctx-backup.md` itself, other spec drafts in -`specs/**`, `ideas/**`. Editing those would rewrite history. - -Reminder [12] — "had to stop sync-to-asgard because broadcom -mirror repo is huge; solve it to resume backups" — now has a -different resolution path: there is no `ctx backup` anymore. -The runbook suggests external tools (rsync, Borg, etc.) and -hub for knowledge. Worth reviewing whether the reminder should -be dismissed or reframed. - -### 6. Pre-existing build break (fixed while owning the branch) - -`internal/write/restore/permission.go` referenced -`DescKeyWriteSnapshotUpdated` and `DescKeyWriteSnapshotSaved` -that never existed in any `internal/config/embed/text/*.go` -file. Caused a hard build failure mid-session. Added both to -`internal/config/embed/text/restore.go`. Corresponding YAML -entries are **not** added (write.yaml has no `write.snapshot-*` -keys yet) — the restore flow may print empty strings until -those entries are authored. **Worth verifying** next session: -either add the YAML entries or confirm this feature was -stubbed-but-incomplete before my session. - -## Persisted Memories (global, not project-local) - -Three feedback memories saved in -`~/.claude/projects/-Users-volkan-Desktop-WORKSPACE-ctx/memory/`: - -- `feedback_no_watermelon_rinds.md` — don't warn agents off - behaviors they wouldn't invent. -- `feedback_no_preserve_old_behavior.md` — "preserves existing - behavior" is not a justification for leaving silent-skip code. -- `feedback_no_phase_deferral.md` — phasing sweeps into "Round 1 - now, Round 2 later" is deferral; do the sweep in one pass. - -## What Was NOT Touched - -- **Reminders [4] through [11]** are standing todos from earlier - sessions, out of scope for this one. [12] is partially - resolved by the backup deprecation (see theme 5). -- **Ideas and spec drafts** that reference `ctx backup` — left - intentionally as historical artifacts. -- **Journal import / enrichment backlog** (129 unimported, - 435 unenriched at session start) — not addressed. -- **Knowledge-file growth warnings** (DECISIONS.md 79 entries, - LEARNINGS.md 103, CONVENTIONS.md 272 lines) — consolidation - not attempted this session. - -## Quick Gotchas For Next Session - -- `go build ./...` still works, but the diff is enormous. - `go test -count=1 ./...` to rule out cache issues before - claiming green. -- The `state/` directory at the repo root was deleted manually - earlier in this session and did NOT reappear under normal - use — the earlier "state leaks outside `.context/state/`" - concern looks resolved. If `state/` shows up in `git status` - during next session, investigate before deleting. -- When committing, **do not** let a pre-commit hook drop the - new memories or this handover. -- `/ctx-wrap-up` was not run this session despite the - heuristic. If you run it, it will suggest capturing much of - this file's content into LEARNINGS.md / DECISIONS.md — - de-dupe carefully. diff --git a/.context/LEARNINGS.md b/.context/LEARNINGS.md index a75c4b0ed..6bb343702 100644 --- a/.context/LEARNINGS.md +++ b/.context/LEARNINGS.md @@ -17,6 +17,10 @@ DO NOT UPDATE FOR: | Date | Learning | |----|--------| +| 2026-05-21 | Sentinel-removal refactors cascade through test surface | +| 2026-05-20 | macOS /var symlink trips path-equality; use EvalSymlinks with parent-resolution fallback | +| 2026-05-20 | Handover filenames are archaeology; parse by generated-at, not filename | +| 2026-05-20 | /ctx-plan is named after its input, not its output | | 2026-05-17 | Creator confusion is the strongest doc-quality signal — louder than any user signal | | 2026-05-17 | Sentinel errors use typed zero-data structs with lazy `desc.Text()` — never Go string consts | | 2026-05-17 | `_helpers.go` / `_utils.go` filenames are project anti-pattern; use domain nouns | @@ -148,6 +152,46 @@ DO NOT UPDATE FOR: --- +## [2026-05-21-140230] Sentinel-removal refactors cascade through test surface + +**Context**: Spec specs/cwd-anchored-context.md decomposed the work into 5 discrete steps; in practice steps 1 and 2 had to merge. Removing ErrDirNotDeclared from rc.ContextDir cascaded through ~10 errors.Is consumers and ~30 test fixtures that used t.Setenv(env.CtxDir, ...). + +**Lesson**: Spec-level decomposition that treats 'swap resolver' and 'remove init guard' as separable does not survive contact when the second step references the soon-to-be-deleted sentinel from the first. Both have to compile against the new sentinel set in the same commit. + +**Application**: When a future spec proposes step boundaries that hinge on a sentinel rename or removal, plan the merged commit up front rather than discover the cascade mid-implementation. The compile-surface analysis belongs at spec time, not implementation time. + +--- + +## [2026-05-20-214839] macOS /var symlink trips path-equality; use EvalSymlinks with parent-resolution fallback + +**Context**: TestRunInit_EnvCwdMatch_Succeeds in internal/cli/initialize/init_test.go failed on first run despite a deliberate setup where the env path and cwd candidate matched. Diagnosis: t.TempDir() returns paths like /var/folders/..., os.Getwd() after t.Chdir() returns the canonical /private/var/folders/... (because macOS's /var is a symlink to /private/var). filepath.Clean preserves the symlink form; equality fails. + +**Lesson**: filepath.Clean alone is insufficient for path equality on macOS (and other systems with symlinked top-level dirs). filepath.EvalSymlinks resolves the symlinks but fails when the target path does not yet exist — common case for /Users/volkan/Desktop/WORKSPACE/ctx/.context BEFORE ctx init runs. The right pattern is a layered fallback: try EvalSymlinks(full), then EvalSymlinks(parent) + rejoin basename, then filepath.Clean as last resort. + +**Application**: Encapsulated as internal/cli/initialize/core/envmatch/{envmatch.go,internal.go}. The Same(a, b) public function calls resolve() on each side; resolve() tries EvalSymlinks on the full path, falls back to EvalSymlinks on the parent (rejoining the basename), and falls through to filepath.Clean if both fail. Reusable for any future env-vs-cwd-style equality check. The package is per-feature (core/envmatch/) per the cmd/core/ purity rule enforced by internal/compliance/TestCmdDirPurity. + +--- + +## [2026-05-20-214830] Handover filenames are archaeology; parse by generated-at, not filename + +**Context**: User observed three coexisting handover filename shapes: .context/HANDOVER-2026-04-22.md (pre-skill root file), .context/handovers/YYYY-MM-DD-HHMMSS-slug.md (skill-era pre-CLI), .context/handovers/-slug.md (current CLI). User asked whether this was a regression or a skill-interpretation problem. + +**Lesson**: Neither. The .context/HANDOVER-* root file predates the handovers/ directory contract entirely (the body even said 'delete this file after reading'). The YYYY-MM-DD-HHMMSS shape was an earlier skill iteration writing free-form before ctx handover write existed (commit 60543e46, 2026-05-17, introduced the CLI as sole writer per the anti-pattern note in /ctx-handover SKILL.md). The current parser at internal/write/handover/parse.go:75-107 keys on the 'generated-at' YAML frontmatter, not the filename — so legacy shapes still sort correctly via LatestHandoverCursor. Only files without frontmatter (the root April file) are invisible. + +**Application**: When unifying filename shapes across history, use git mv to preserve rename detection. Derive the canonical timestamp from the file's own generated-at frontmatter rather than from the filename — that's the source of truth the parser uses anyway. If a handover predates frontmatter entirely (rare, pre-skill era), it's safe to delete because the parser never read it. + +--- + +## [2026-05-20-214821] /ctx-plan is named after its input, not its output + +**Context**: Agent (and apparently other agents in prior sessions per user observation) repeatedly inverted the canonical chain, treating /ctx-spec as the entry point and /ctx-plan as a post-spec step. The skill description starts 'stress-test a plan' (implying user brings a plan IN) while line 44 of the body says 'the deliverable is a debated brief, not a task list' (the OUTPUT is a brief, not a plan). + +**Lesson**: Skill names that reference their INPUT bias the agent toward the wrong canonical position. The /ctx-plan skill takes a plan and produces a brief; the natural mental model when scanning the name is 'plan = output', which makes the agent place it AFTER spec instead of before. Also: /ctx-spec's 'When to Use' section listed /ctx-brainstorm as a predecessor but never /ctx-plan, so an agent skimming the top of the skill never learned the full chain. + +**Application**: Made the canonical chain explicit at the top of both /ctx-plan and /ctx-spec skills (Canonical Chain block with the brainstorm → plan → spec → implement diagram) and in AGENT_PLAYBOOK_GATE Planning Work section. /ctx-spec When-to-Use now lists /ctx-plan as a predecessor; When-NOT-to-Use says 'when the bet is contested but not yet stress-tested, use /ctx-plan first'. /ctx-plan description now ends with '; produces a debated brief at .context/briefs/-.md that /ctx-spec --brief consumes'. + +--- + ## [2026-05-17-200000] Creator confusion is the strongest doc-quality signal — louder than any user signal **Context**: In this session the project author asked *"why diff --git a/.context/TASKS.md b/.context/TASKS.md index f25f8422d..df06fd9bd 100644 --- a/.context/TASKS.md +++ b/.context/TASKS.md @@ -43,21 +43,90 @@ TASK STATUS LABELS: inconsistencies, and move useful functions that are utility and/or reusable to relevant convenience packages. -- [ ] Create a typography.md somewhere so that we don't have to remind tha +- [x] Create a typography.md somewhere so that we don't have to remind tha Agent things like this: "❯"## What the editorial pipeline is NOT" our headings are Title Case, it always has been; it always will be. Do a full sweep. -- in addition (not checked, just to make sure); `ctx` is always in backticks whenever possible; it's part of the branding." - - -- [ ] Bug: Fresh folder: git init; eval $(ctx activate); ctx init + Landed at `.context/typography.md` (contributor/agent surface, not + public docs); CONVENTIONS.md points at it. Codifies Title Case, monotype + `ctx`, no em-dashes/smart-quotes/quad-backticks, banner/icon header + shape, recipe Problem→TL;DR arc, admonition variants. Sweep across + existing docs deferred (linter already enforces the hard rules on every + edit). + +- [x] Bug: Fresh folder: git init; eval $(ctx activate); ctx init will catch parent .context folder and raise a warning expectation: ctx activate should bail if there is no .context folder in the same level and ask user to run `ctx init` first. discuss this with the Agent too. -- [ ] Bug: if context is active (eval ctx activate); `ctx init` + Fixed by `specs/activate-strict-cwd.md`: dropped the upward walk + in `internal/cli/activate/core/resolve/`; `ctx activate` now + succeeds iff `$PWD/.context/` exists, otherwise returns + `errActivate.NoLocalContext` naming `$PWD` and pointing at + `ctx init`. Removed `writeActivate.AlsoVisible`, + `FormatAlsoVisibleAdvisory`, multi-candidate test. New test + `TestActivate_DeepSubdir_WithParentContext_Bails` guards the + regression. +- [x] Bug: if context is active (eval ctx activate); `ctx init` on a brand new project can (and probably will) fail. Probably need to nudge user to ctx deactivate first. + Paired with the activate strict-CWD fix above. Strict activate + reduces how often this fires (no more silent parent-binds), but + the deliberate path (activated A, cd to B, ran `ctx init`) still + needs an init-side guard: refuse when `$CTX_DIR` is set and + `realpath($CTX_DIR) != realpath($PWD/.context)`; suggest + `ctx deactivate` or a `cd` back to A. + Fixed: env-vs-cwd mismatch guard added to + `internal/cli/initialize/cmd/root/run.go` via new + `internal/cli/initialize/core/envmatch/` package + (symlink-aware via `filepath.EvalSymlinks`, with parent-resolution + fallback for paths that do not yet exist). Skipped when + `--caller` is set (editors / scripted callers pre-set CTX_DIR by + contract). New typed error `errInit.ErrEnvCwdMismatch` with + multi-line message naming both paths and pointing at + `ctx deactivate`. Three tests guard the regression + (`EnvCwdMismatch_Refuses`, `EnvCwdMatch_Succeeds`, + `EnvCwdMismatch_CallerSkipsGuard`). + +- [ ] Bug: `ctx handover write` emits literal `branch: --show-current` + in handover frontmatter instead of the resolved branch name. Spotted + during the 2026-05-20 wrap-up; the handover at + `.context/handovers/20260521T045353Z-...md` shows the issue. Looks + like `gitmeta`'s branch resolver is leaking a `git symbolic-ref` + flag literal. Read side parses it but downstream tooling that + expects `main` (or whatever) will get tripped. #priority:medium + #added:2026-05-20 + +- [x] Implement `specs/cwd-anchored-context.md` — drop the `CTX_DIR` + env channel and `ctx activate`/`ctx deactivate` entirely; resolver + becomes a single `os.Stat($PWD/.context)`; `gitmeta` stops walking + too. Multi-step (rc + gitmeta simplification → init guard removal + → hook `cd` migration → activate/deactivate deletion → docs sweep). + Supersedes `specs/activate-strict-cwd.md` (marked superseded) and + large sections of `specs/single-source-context-anchor.md` (marked + superseded). + #priority:medium #added:2026-05-20 + Done end-to-end on `feat/cwd-anchored-context` (uncommitted, jumbo + strategy). Steps 1+2 (rc + gitmeta + init guard) were landed in a + prior session; steps 3–5 (hook `cd` migration, activate/deactivate + deletion, docs sweep) landed this session. Net diff: ~1410 + insertions, ~4560 deletions, 196 files. Deletions include four + package directories (`internal/cli/activate/`, `internal/cli/deactivate/`, + `internal/write/activate/`, `internal/err/activate/`), the + check-anchor-drift system subcommand and its core/anchor package, + the `internal/config/shell/` package, the `err_activate.go` and + `activate.go` flag/text files, two recipes (`activating-context.md`, + `external-context.md`), and YAML entries across + `commands.yaml`/`examples.yaml`/`flags.yaml`/`errors.yaml`/`hooks.yaml`/`write.yaml`. + Hooks migrated: `internal/assets/claude/hooks/hooks.json` now uses + `cd "${CLAUDE_PROJECT_DIR:?...}" && ctx system ` instead of + the `CTX_DIR=` prefix; check-anchor-drift hook line removed. Tests + cleaned of dead `t.Setenv("CTX_DIR", ...)` calls across init, + pad, remind, checkreminder, notify, change/core, mcp/server, and + drift suites; `mcp/cmd/root/cmd_test.go` rewritten to test the + cwd-anchored fail-closed behaviour. Lint clean (`golangci-lint run`), + full `go test ./...` green. - [-] Add TypeScript `tsc --noEmit` gate for the embedded OpenCode plugin (`internal/assets/integrations/opencode/plugin/index.ts`). @@ -73,17 +142,29 @@ TASK STATUS LABELS: convention) rather than Bun; `tsc` is the same compiler either way and `@types/bun` provides Bun globals to the type-checker. -- [ ] Add `shellcheck` gate for embedded shell scripts +- [x] Add `shellcheck` gate for embedded shell scripts (`internal/assets/integrations/copilot-cli/scripts/*.sh` and `internal/assets/hooks/trace/*.sh`). Run in CI; fail on findings at severity `warning` and above. #priority:low #added:2026-05-11 #grounding-gap + Done: `hack/lint-shellcheck.sh` (severity=warning, scoped to + embedded scripts), `make lint-shellcheck` target, `audit` target + invokes it when shellcheck is present, dedicated `shellcheck` + CI job (`.github/workflows/ci.yml`). 10 embedded scripts scan + clean at warning+. -- [ ] Add `PSScriptAnalyzer` gate for embedded PowerShell scripts +- [x] Add `PSScriptAnalyzer` gate for embedded PowerShell scripts (`internal/assets/integrations/copilot-cli/scripts/*.ps1`). Run in CI on a Windows or pwsh-enabled runner; fail on findings at severity `Warning` and above. #priority:low #added:2026-05-11 #grounding-gap + Done: `hack/lint-powershell.sh` (severity=Warning, scoped to + embedded `*.ps1`), `make lint-powershell` target, `audit` + target invokes it when pwsh is present, dedicated + `powershell` CI job that `Install-Module`s PSScriptAnalyzer + on the ubuntu-latest runner (pwsh ships pre-installed on + GitHub Actions ubuntu images). Local verification deferred to + CI (no pwsh on dev box). - [ ] Add skill frontmatter validity test covering every embedded `SKILL.md` (Claude skills, OpenCode skills, Copilot CLI skills): @@ -2053,6 +2134,201 @@ Phase KB-3 (documentation): reference: `internal/err/context/NotFoundError` (commit `e524dd98`). Captured as a learning to prevent recurrence. +- [ ] Bug / gap: Phase KB scaffold has no retrofit path for projects + that pre-date the kb subsystem. `coreKB.Scaffold(contextDir)` is + only called from `internal/cli/initialize/cmd/root/run.go`'s init + flow; `ctx init` itself refuses on populated projects without + `--reset` (destructive). On the ctx project this branch is in, + `.context/kb/` and `.context/ingest/` were missing entirely until + hand-rolled on 2026-05-21 by copying + `internal/assets/kb/templates/{ingest,kb}/*` into place. Add a + dedicated `ctx kb init` subcommand (or `ctx init --kb-only`) that + calls `coreKB.Scaffold` and nothing else; existing per-file + preservation in `Scaffold` already makes it idempotent. Wire the + command annotation so it bypasses the require-context-dir + PreRunE gate (the gate already passes when `.context/` exists, + but a freshly-init'd project in the same shell session must work + too). Update `docs/recipes/build-a-knowledge-base.md` to point at + the new subcommand for retrofit. #priority:medium #added:2026-05-21 + +- [ ] Bug / gap: `ctx init` refuses on a populated project without + `--reset`, but `--reset` is destructive (it backs up populated + files then overwrites them). There is no path between "project + already exists, do nothing" and "blow it all away." Add an + `--upgrade` mode that runs the scaffolding stages that are + per-file-existence-preserving (kb, steering foundation, entry + templates, scratchpad bootstrap if absent, gitignore amends, + Makefile.ctx, settings.local.json permission merge) but skips + reset-required stages (CLAUDE.md merge, populated-file refuse). + Pairs with `ctx kb init` above; same shape, broader surface. + #priority:medium #added:2026-05-21 + +#### Adjacent-tool kb ingests `#added:2026-05-21` + +The kb's declared scope (`.context/kb/index.md`) covers design +lessons and operational patterns from adjacent / inspirational +AI infrastructure projects. Each entry below is a separate +`/ctx-kb-ingest` pass. Topic slugs follow the +lowercase-kebab-case convention used by `ctx kb topic new`. +Suggested invocation per row is a starting point; the operator +can refine the source URL during the pass. Mark +`[x]` only when the topic page clears the cold-reader rubric and +the source-coverage ledger has the row at `comprehensive` (or +honestly at `topic-page-drafted` if the page is good but the +ledger admits residue). + +- [x] `vllm` — landing page ingested 2026-05-21 in this branch's + scaffold pass. Page deferred (build-validation gap); follow-up + per-category deep dive tracked via the source-coverage ledger. + See `.context/kb/topics/vllm/index.md`. + +- [ ] `claude-code` — Anthropic's official CLI for Claude. Surface + to study: hooks, slash commands, skills (`~/.claude/skills/`), + settings.json, plugin system. Suggested seed: + `/ctx-kb-ingest https://docs.claude.com/en/docs/claude-code claude-code`. + Question: how does Claude Code's hook + skill surface compare + to ctx's, and what entry points (e.g. settings.json structure) + is ctx echoing vs diverging from? #priority:medium + +- [ ] `opencode` — sst/opencode terminal AI agent. Surface to + study: plugin model (TypeScript `index.ts`), MCP-server + registration, skill discovery, command palette shape. + Suggested seed: `/ctx-kb-ingest https://opencode.ai/docs opencode`. + Question: ctx already integrates with OpenCode via + `internal/assets/integrations/opencode/` — what's the kb + reading of that integration as a pattern, and where could it + generalise to other host CLIs? #priority:medium + +- [ ] `cursor` — Cursor editor. Surface to study: workspace + hooks (`.cursorrules`, `.cursor/`), MCP integration, the + cross-IDE settings-leak that motivated ctx's state.Initialized + gate (spec: `specs/state-dir-no-mkdir-when-uninitialized.md`). + Suggested seed: + `/ctx-kb-ingest https://cursor.com/docs cursor`. Question: how + does Cursor's workspace-level hook discipline shape what ctx + has to defend against (cross-workspace state leaks), and what + would ctx-on-Cursor parity look like beyond the current + defensive gate? #priority:medium + +- [ ] `gitnexus` — code-intelligence MCP toolchain that ships as + a companion to ctx (see `.claude/skills/gitnexus/`, + `GITNEXUS.md`). Surface to study: MCP tool catalogue (cypher, + impact, route_map, tool_map, group_*), graph-backed code + navigation, the impedance match with Go projects of ctx's + size. Suggested seed: + `/ctx-kb-ingest GITNEXUS.md gitnexus` plus discovery enabled + to pull official docs. Question: which GitNexus capabilities + is ctx *not* using that would meaningfully change how ctx + develops itself (e.g. blast-radius checks pre-refactor)? + #priority:medium + +- [ ] `mempalace` — memory-palace / spatial-recall AI project + (operator: confirm the canonical URL; tentative + `https://github.com/mempalace` or similar). Surface to study: + whatever the project's memory-persistence model is, and how it + differs from ctx's file-anchored memory model. Question: is + there a spatial / graph / vector substrate worth lifting into + ctx's memory layer, or is the contrast purely contrastive + (ctx commits to file-anchored; mempalace commits to + something else)? #priority:low + +- [ ] `deepwiki` — Devin's auto-generated wiki for any GitHub + repo. Surface to study: how it derives a wiki structure from + code+commits, and whether that output is usable as a ctx + substrate (per reminder [6]: *"use deepwiki to enhance docs + of ctx and use it as a substrate for further analysis of + other stuff"*). Suggested seed: + `/ctx-kb-ingest https://deepwiki.com/ActiveMemory/ctx deepwiki`. + Question: is deepwiki's auto-derived structure complementary + to ctx's hand-authored docs (use both, treat them as + different views) or competitive (one supersedes the other)? + Connects to reminders [6, 7]. #priority:medium + +- [ ] `zensical` — static-site generator that anchors to + `zensical.toml` (referenced as the canonical + config-file-anchored precedent in + `specs/cwd-anchored-context.md`). Surface to study: the + anchor-to-config-file pattern, recipe library shape, how + zensical handles cwd vs config-dir resolution. Question: ctx + cited zensical as precedent for the cwd-anchored decision; + what other zensical patterns are worth borrowing or + rejecting? #priority:low + +- [ ] Discuss: rename `ctx kb site build` (referenced in the + `/ctx-kb-ingest` skill's circuit-breaker item #3 but absent + from the installed binary) into a top-level family — + `ctx site kb build`, `ctx site journal build`, etc. The + motivation: ctx now ships multiple site-shaped surfaces + (kb topic pages, journal entries, possibly more); the + current `kb site-review` placement under `kb/` no longer + generalises. A top-level `site` subcommand would let each + domain register its own `build` and `review` verbs without + cross-domain namespace bleed. Open questions: where do the + per-domain build implementations live (`internal/cli/site/cmd/kb/build/`? + `internal/cli/kb/cmd/site/build/`?), how does this interact + with the existing `kb site-review` / `ctx kb reindex`, and + what becomes of the `/ctx-kb-ingest` skill's circuit-breaker + reference? Treat this as a naming + topology discussion before + any code lands; the vllm topic page is `topic-page: deferred` + partly because of the missing build subcommand, so resolving + this unblocks the circuit breaker too. #priority:medium #added:2026-05-21 + +Each row is a single `/ctx-kb-ingest` pass when started; further +follow-ups for that tool (per-category deep dives, sub-page +splits) get tracked on the source-coverage ledger, not as +TASKS.md children. Open a new TASKS row only when a *different* +adjacent tool joins the list. + +- [ ] Feature: skill usage tally + ceremony-time nudge. + Motivation: ctx ships 60+ skills; discoverability is a real + problem. A usage tally would (a) surface usage patterns, and + (b) let ceremonies remind the operator about under-used skills + that might help current work. Two phases: + + **Phase 1 — instrument.** Extend the journal-enrich pipeline + (`/ctx-journal-enrich-all` or sibling) to scan + `~/.claude/projects/*/*.jsonl` for `Skill` tool uses and write + two artifacts: + - **Time-series** at `~/.ctx/state/skill-usage.jsonl`: + append-only, one row per invocation, fields + `{ts, project, session_id, skill_name, source: "claude-code"|"opencode"|...}`. + - **Aggregate** at `~/.ctx/state/skill-usage.json`: derived + rollup, `{skill_name → {count, first_used, last_used, projects[]}}`. + Stays in `~/.ctx/state/` (user-global), not per-project, so + patterns survive across projects. + + **Phase 2 — wire ceremony nudges (NOT auto-prompts).** Surface + the tally inside two existing ceremonies, never as session-start + noise: + - `/ctx-remember`: at the end of the recall readback, add a + *"unused-but-might-help"* line that names 1-3 skills with + `last_used > 30d ago` (or `never`) whose descriptions match + keywords from current TASKS.md focus / branch name / recent + commits. + - `/ctx-wrap-up`: in the candidate-proposal phase, include a + *"this session's skill mix"* line summarising which skills + fired this session, and surface 1-2 skills that would have + fit the work but weren't invoked. + - Explicitly NOT in `/ctx-handover` — that ceremony is for the + next agent, not introspection. + + Hard anti-patterns: stale-skill-name pollution (when skills + rename, the tally must reconcile by reading the current skill + catalogue and dropping unknowns to a `*.deprecated.jsonl` + archive); skill-nudge inside a tool-use loop (only at ceremony + invocation, never via PreToolUse hook); LLM-judged matching at + Phase 1 (start with naive string-match of skill descriptions + against TASKS.md / branch / recent commits; revisit if the + signal is too weak). + + Open questions: where exactly the journal-enrich pipeline + writes the artifacts (does it touch `~/.ctx/` or keep + per-project state and aggregate at read time?); whether the + nudge text is rendered by ctx or by the skill itself reading + the JSON; whether the "match current work" heuristic lives in + Go or in the skill prompt. Tackle these at spec time, not + implementation time. #priority:medium #added:2026-05-21 + ### Phase KB-followup: Adversarial design review of parallel skill trees `#priority:medium #added:2026-05-17` `ctx` ships skills to three host trees: diff --git a/.context/archive/closeouts/20260521T202635Z-ingest-closeout.md b/.context/archive/closeouts/20260521T202635Z-ingest-closeout.md new file mode 100644 index 000000000..5249b8452 --- /dev/null +++ b/.context/archive/closeouts/20260521T202635Z-ingest-closeout.md @@ -0,0 +1,121 @@ +--- +sha: 8c02b754 +branch: feat/cwd-anchored-context +mode: ingest +pass-mode: topic-page +life-stage: bootstrap +generated-at: 2026-05-21T20:26:39Z +--- + +# Ingest closeout — vllm + +## Inputs + +- `https://docs.vllm.ai/en/latest/examples/` — vLLM examples + landing page (the only source supplied). +- Discovery was not invited by the operator; this pass stayed + at the supplied source. The thirteen GitHub category + directories the page links to were NOT followed. + +## Pass-mode + +- **Pass-mode:** `topic-page` +- **Reason:** Default; single source with a clear topic intent + (vllm). +- **Definition of done:** topic page at + `.context/kb/topics/vllm/index.md` extended past template + with cited prose, at least one EV-### row in + `evidence-index.md`, cold-reader rubric passes, ledger + updated honestly. + +## Topic(s) touched + +- `vllm` — scaffolded via `ctx kb topic new "vllm"`; lede, + *What It Is*, *Why This KB Cares*, *Sources and Further + Reading*, *Related Concepts* sections written; five EV + citations. + +## What changed + +- New: `.context/kb/topics/vllm/index.md` (scaffolded by CLI, + prose synthesised in this pass). +- New: `.context/kb/evidence-index.md` (5 rows, `EV-001..EV-005`). +- New: `.context/kb/source-map.md` (1 row, `VLLM-EXAMPLES`). +- New: `.context/kb/source-coverage.md` (1 row, vllm at + `topic-page-drafted`). +- Updated: `.context/kb/index.md` — scope paragraph replaced + the TODO placeholder; `CTX:KB:TOPICS` managed block now + lists the `vllm` topic (refreshed via `ctx kb reindex`). + +Cold-reader orientation: + +- Concept clear? yes: lede defines vLLM and frames the contrastive-study purpose in 4 sentences. +- Why this kb cares clear? yes: *Why This KB Cares* enumerates three concrete design choices ctx has made that vLLM made differently. +- Canonical evidence reachable? yes: source named in `Sources and Further Reading`; one click to `source-map.md`, one more to the original URL. +- Boundaries clear? yes: explicit "ingested only the landing page, not the per-category GitHub directories" note; kb scope paragraph names what's in and out. + +Result: pass + +## New questions + +None opened in this pass. The driving open question +(*"what from vLLM is worth lifting into ctx?"*) is the topic +page's framing question and remains open by design — it +resolves only through cross-topic comparison once more +adjacent-tool ingests land. + +## New contradictions + +None. + +## Confidence drift + +n/a — this is the first ingest pass against this source. Page +Confidence is set to `medium`, the floor of cited bands +(EV-003 is the only `medium` row; the other four are `high`). + +## Source-coverage updates + +- `VLLM-EXAMPLES` advanced: absent → discovered → admitted → + `topic-page-drafted`. Did **not** advance to `comprehensive`; + the per-category GitHub directories were not fetched, and + `ctx kb site build` was not run (see *Next pass hint*). + +## Overflow + +None. Single source, no discovery, no overflow into +`candidate-sources.md`. + +## Adjacency pre-flight + +`no incomplete adjacent topics surfaced` — `source-coverage.md` +did not exist before this pass; this is the first row written. +Acknowledged explicitly per the silence-is-not-clean rule. + +## Next pass hint + +Two distinct follow-ups, listed in increasing scope: + +1. **Build-validation gap.** The installed `ctx` binary + (`/usr/local/bin/ctx`) exposes `kb site-review` but no + `kb site build` subcommand; the skill's circuit-breaker + item #3 (*"`ctx kb site build` ran clean"*) could not be + exercised in this pass. The pass reports + `topic-page: deferred` for that reason. Either implement + `ctx kb site build` and re-run on this topic, or amend the + skill to relax item #3 when the build command is not yet + wired in the host CLI. + +2. **Per-category deep dive.** The thirteen vLLM example + categories (basic, generate, pooling, speech_to_text, + features, reasoning, tool_calling, applications, rl, + deployment, ray_serving, disaggregated, observability) are + currently surface-only. A follow-up pass with discovery + enabled could fetch each GitHub directory listing, mint + per-category EV rows, and split each category onto a + sibling sub-page under + `.context/kb/topics/vllm/.md` when the + cold-reader rubric's "boundaries clear?" check starts + failing on the index. Suggested invocation: + `/ctx-kb-ingest https://github.com/vllm-project/vllm/tree/main/examples/deployment vllm` + (start with deployment as the most ctx-adjacent category). diff --git a/.context/briefs/20260522T001011Z-ctx-ai-backend.md b/.context/briefs/20260522T001011Z-ctx-ai-backend.md new file mode 100644 index 000000000..a69f9e6a9 --- /dev/null +++ b/.context/briefs/20260522T001011Z-ctx-ai-backend.md @@ -0,0 +1,173 @@ +--- +generated-at: 2026-05-22T00:10:11Z +sha: 8c02b754 +branch: feat/cwd-anchored-context +title: ctx ai backend (vLLM-canonical, OpenAI-compatible) +slug: ctx-ai-backend +consumer: /ctx-spec --brief +deliverables: + - specs/ctx-ai-backend.md (full spec for block A — first deliverable contract) + - specs/ctx-ai-extraction-and-recall.md (supplementary spec for blocks B + C — sketched, re-debatable after A ships) +--- + +# ctx ai backend — debated brief + +## The bet + +ctx grows an **optional, local-first AI backend layer** that talks to +any OpenAI-compatible HTTP endpoint. vLLM is the canonical local +backend (because it restores air-gap capability, supports +schema-constrained structured outputs, and ships prefix caching that +rewards stable-prefix prompt structure). The same contract works +against OpenAI, Anthropic, Ollama, LM Studio, and any other +OpenAI-compatible server. + +The layer powers three capability blocks, scoped into two specs: + +- **A. Backend abstraction (first deliverable; full spec).** + `ctx ai `-style commands, a backend registry, endpoint + contract = OpenAI-compatible HTTP, `ctx setup --backend vllm` + wiring, auth/env-var handling, reachability check, graceful + failure when unreachable. A is what the community developer + ships end-to-end. + +- **B. Structured extraction (supplementary spec, re-debatable + after A ships).** `ctx ingest`, `ctx evidence extract`, + `ctx ai extract-decisions`, `ctx compact --emit + decisions,learnings,tasks,open-questions`. JSON-schema / + grammar-constrained outputs become *proposed patches* against + the five canonical files plus the kb evidence-index; + human/agent ratifies via the existing diff/PR mechanism. + +- **C. Embedding-backed recall (supplementary spec, re-debatable + after A ships).** `ctx index`, `ctx search`, `ctx related`, + `ctx doctor --semantic`, `ctx drift` semantic mode, + `ctx assemble --rank-with vllm`. Default storage: SQLite — "a + filesystem wearing a DB hat." Other vector stores + (LanceDB / Qdrant / pgvector / FAISS / HNSW) are opt-in escape + hatches the user wires themselves. + +The user's framing was: "*The user's context stays local or inside +an internal cluster, while the CLI talks to vLLM through an +OpenAI-compatible endpoint. That makes vLLM a drop-in backend +alongside OpenAI, Anthropic, Ollama, LM Studio, etc.*" + +## What was rejected + +- **Pure-recipe-only integration.** Rejected because *users may not + know what they do not know* (inference is opaque to non-ML users) + and *agents may not know whether to use the tool*. A recipe alone + doesn't lower the cognitive ladder. + +- **Per-skill HTTP gating proxy.** Considered and killed under the + user's own manifesto. Three structural problems: + - Cost / secrets / observability / multi-backend routing are all + service-shaped concerns that "monumentally increase scope and + dilute the product's surface area" and conflict with Invariant + 2 ("zero runtime dependencies for core functionality"). + - The proxy gates HTTP/MCP traffic only — it cannot see local + tools (ripgrep, grep), so it structurally cannot fix the + "agent shortcuts to ripgrep instead of using GitNexus" failure + mode that motivated it. + - Per-skill enforcement requires ctx to own MCP registration and + maintain a side-channel telling the proxy which skill is active + — three runtime deps for one feature. + +- **Recipe-structure restructuring (vLLM-as-contrast lessons mapped + onto `docs/recipes/`).** This was what the prior handover's + next-line implied. The user's exact words: "*That's definitely + NOT what I want. This is NOT a knowledge architecture work.*" + +- **Subordinating embeddings storage to a service-shape DB.** SQLite + default is non-negotiable; vector services are opt-in only. + +## Top three failure modes + +1. **AI commands silently become required (Invariant 2 breach).** + If `ctx status` or `ctx agent` ever depends on a reachable + backend, the core stops being zero-dependency. Mitigation: + AI surface is strictly additive; existing commands keep working + with no backend; AI commands fail closed with a clear "no + backend reachable" error rather than degrading silently. + +2. **Deterministic and semantic assembly paths drift into each + other (Invariant 3 breach).** If `ctx agent` ever gains a "use + vLLM if available, fall back to deterministic otherwise" + behavior, reproducibility dies. Mitigation: deterministic + `ctx agent` is untouched; semantic assembly is a sibling + command (`ctx assemble --rank-with vllm`) with different + determinism guarantees written on its tin. + +3. **Auto-extraction replaces human curation (Pattern 6 breach).** + If `ctx compact --emit` lands extracted decisions directly into + `.context/DECISIONS.md`, humans stop running `/ctx-wrap-up` + and the curation discipline that gives ctx its compounding + returns (§7.4) erodes. Mitigation: extracted artifacts are + *proposed patches into a review queue* (likely under + `.context/proposals/` or kb-closeout-style), never applied + directly; the existing ceremony skills remain the ratification + path. + +## Cheapest way to validate the bet + +Ship **A + one concrete B-consumer** — specifically +`ctx compact ~/.cursor/sessions/latest.jsonl --emit +decisions,learnings,tasks,open-questions`. Measure whether the +extraction-to-ratified-patch loop produces *better-than-hand-curation* +records on three real wrap-ups. If proposed patches consistently need +heavy rewriting, B is worse than the status quo and we learn that +cheaply, before C (with its larger surface and storage layer) is +built on the same backend. + +`ctx compact` is the right validation slice because it lands directly +in the existing wrap-up ceremony shape, the JSON-schema-constrained +output story is well-defined for it, and "did this beat hand-curation?" +is a measurable question on real artifacts. + +## What becomes expensive to unwind + +- **The `ctx ai` namespace shape.** Once shipped, users will write + scripts against it. Renaming or restructuring is expensive. The + A spec must decide carefully: is it `ctx ai ` (new + top-level), or are AI capabilities woven into existing commands + via flags (`ctx ingest --use-ai`, `ctx compact --emit ...`)? + Pick once. + +- **The backend contract = OpenAI-compatible HTTP.** Cheap to + extend (new optional fields, additional endpoints), expensive + to break (changing the contract to require Anthropic Messages + shape, for instance, gives integrators whiplash). The A spec + should commit to OpenAI-compatible as the floor and treat + Anthropic Messages as a *strict superset* that some backends + also support, not as a competing contract. + +- **SQLite embeddings schema (C-spec land).** When C ships, the + table shape becomes a de-facto contract; users will build + workflows on top. Cheap to defer because C ships later; the A + spec just needs to *not* make forward-looking assumptions about + C's schema. + +## Slicing intent (for /ctx-spec --brief) + +- **First spec file:** `specs/ctx-ai-backend.md` — full spec for + block A. Contract the community developer ships. Manifesto-fit + invariants are enforced here (additive/opt-in, fail-closed, + deterministic core untouched). + +- **Second spec file:** `specs/ctx-ai-extraction-and-recall.md` — + supplementary, sketched-not-contracted spec for blocks B + C. + Captures the design intent so A's interface choices don't + accidentally foreclose B/C, but the B/C contracts themselves + are explicitly marked *re-debatable after A ships*. + +Both files reference this brief. + +## Open questions left for /ctx-spec + +- **`ctx ai ` vs flags-on-existing-commands.** The naming + decision belongs to the A spec; the brief does not pre-commit. +- **Proposal queue location.** `.context/proposals/`? + Kb-closeout-style under `.context/ingest/`? Belongs in B-spec + design. +- **Default extraction model.** A-spec should leave model choice + to the user; recommended models per task type can be a recipe. diff --git a/.context/handovers/.gitkeep b/.context/handovers/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.context/handovers/2026-05-10-071718-anchor-undercover-to-spec-and-tasks.md b/.context/handovers/2026-05-10-071718-anchor-undercover-to-spec-and-tasks.md deleted file mode 100644 index 9fceb9f0e..000000000 --- a/.context/handovers/2026-05-10-071718-anchor-undercover-to-spec-and-tasks.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -generated-at: 2026-05-10T07:17:18Z ---- -# Handover [2026-05-10-071718] anchor undercover analysis → spec and task breakdown for kb-editorial-pipeline + require-git - -**Provenance:** commit `f9582163` on branch `main` - -## Summary - -Did an undercover analysis of a sibling clean-room editorial-memory -tool under `~/Desktop/WORKSPACE/anchor/`, deliberately scrubbed of -company / project naming so its design ideas could travel into ctx -without inadvertent specifics. Produced three idea documents: -`ideas/001-sibling-project-undercover-analysis.md` (feature inventory -and ranked borrowable mechanisms), `ideas/002-editorial-pipeline-and- -skill-rigor.md` (deeper editorial-pipeline lift plan plus side-by-side -skill ceremony comparison highlighting the wishy-washy plan→spec -handoff in our skills vs the sibling's path-required spec contract), -`ideas/003-editorial-pipeline-debated-brief.md` (debated brief output -of a `/ctx-plan` adversarial-interview pass; carries the two -organizing principles). - -Wrote two specs: `specs/kb-editorial-pipeline.md` (4 modes, -9 KB artifacts, closeout/fold mechanism, handover, render via existing -zensical) and `specs/require-git.md` (constitutional precondition; -breaking change). Appended three task phases to `.context/TASKS.md`: -**Phase SK** (skill surface polish — `MarkFlagRequired` on capture -flags, `--brief ` on `/ctx-spec`, authority boundary sections, -"light compression" wording standardization), **Phase RG** (require git -as architectural precondition; refuse-on-no-git with no auto-init), -**Phase KB** (editorial pipeline + handover; depends on Phase SK + -Phase RG; sub-phases KB-2 `your-project` port for validation and KB-3 docs). - -Saved one feedback memory at `~/.claude/projects/-Users-volkan-Desktop- -WORKSPACE-ctx/memory/feedback_no_defer_unfamiliar_scope.md` capturing -the meta-correction: when lifting from a battle-tested external design, -default to lifting the whole shape; do not yeet on uncertainty. Two -organizing principles surfaced in the planning round and earned their -place at the top of the brief: **P1** — the LLM is the migration tool -(makes committing to specific schemas in v1 cheap rather than reckless); -**P2** — a KB of KBs is a KB (recursive composability collapses -federation, multi-team consolidation, and taxonomy-was-wrong recovery -into one mechanism). Captured 5 decisions and 5 learnings via the -ceremony skills before this handover. Working tree carries the new -specs/handover plus the unrelated pre-session -`.context/DECISIONS.md` and `.context/LEARNINGS.md` modifications; -nothing committed deliberately. - -## Next session - -Pick **Phase SK** (skill surface polish — smaller, unblocks -`/ctx-spec --brief` for cheaper future spec sessions) OR **Phase RG** -(require-git constitutional change — deserves its own PR with -prominent breaking-change notes in `dist/RELEASE_NOTES.md`) as the -first concrete implementation work. Both are independent and both -are hard prerequisites for Phase KB. - -Specific first action: `cd ~/Desktop/WORKSPACE/ctx && ls -.context/TASKS.md` to find the chosen phase header, mark its first -task `#in-progress`, then read the corresponding source — for SK -that is `ideas/002-editorial-pipeline-and-skill-rigor.md` §3 -("Reframing the wishy-washy skills"); for RG that is -`specs/require-git.md`. Slight argument for SK first because once -`--brief` ships, future spec sessions (including iterating on -`specs/require-git.md` itself) become cheaper and more disciplined. - -Before writing any code: review the unrelated modifications already -sitting in `.context/DECISIONS.md` and `.context/LEARNINGS.md` from -before this session started — decide whether to commit them first, -stash, or fold into the SK or RG PR. The HARD GATE hook will block -any commit until lint + tests pass and the working tree is clean -of uncommitted incidentals. - -## Highlights - -- New (ideas/): `001-sibling-project-undercover-analysis.md`, - `002-editorial-pipeline-and-skill-rigor.md`, - `003-editorial-pipeline-debated-brief.md`. -- New (specs/): `kb-editorial-pipeline.md`, `require-git.md`. -- New (auto-memory): - `feedback_no_defer_unfamiliar_scope.md` plus a row added to - the auto-memory `MEMORY.md` index. -- Modified: `.context/TASKS.md` (Phase SK / Phase RG / Phase KB - appended at end of file; structure mirrors sibling's named- - phase grammar; references back to the two specs and the brief). -- Captured this session: 5 decisions (lift-pipeline, mandate-git, - KB-ontology, pair-handover-and-editorial, KB-RULES-naming) + - 5 learnings (P1 LLM-migration-tool, P2 KB-of-KBs-is-a-KB, - KB epistemology, lift-renames-with-features, workaround-tax-as- - validation). -- Side-task crystallization: Phase 0 Grounding's existing item - about "good 'phasing' mechanism for tasks" now has empirical - material (this session used named phase codes SK/RG/KB, spec - references inline, prerequisites called out) — useful when - whoever picks that exploratory task up models what - `ctx task add --phase` should look like. -- Dogfooded the future handover shape from - `specs/kb-editorial-pipeline.md`: this file lives at - `.context/handovers/-.md` with `generated-at` YAML - frontmatter, Provenance line, and Summary / Next session / - Highlights / Open questions H2 sections — the exact shape the - Phase KB CLI will produce. `/ctx-remember` won't auto-pick-it-up - until Phase KB ships, but a future cold-start can manually - point to it as a forward-compatible artifact. - -## Open questions - -- **Phase ordering:** SK before RG, or RG before SK? Both are - independent and Phase KB needs both. Slight argument for SK first - (the `--brief` flag makes iterating on `specs/require-git.md` - cheaper), but RG first is also defensible because it's a - constitutional change that wants a clean focused PR. -- **Phase KB subcommand grammar:** `ctx kb ingest|ask|...` - (kb-prefixed) vs `ctx ingest|ask|...` (top-level, sibling-style). - Spec leans prefixed; pin during Phase KB implementation kickoff. -- **Phase RG opt-out list completeness:** beyond `--help` / - `--version` / `ctx system bootstrap`, audit the command tree - during Phase RG implementation; default new entries to - git-required. -- **Pre-session unrelated `.context/DECISIONS.md` and - `.context/LEARNINGS.md` modifications** carried into next session; - the new captures from this session sit on top of them. Decide - intent before committing. diff --git a/.context/handovers/2026-05-11-000847-skill-surface-polish-validate-cleanup.md b/.context/handovers/2026-05-11-000847-skill-surface-polish-validate-cleanup.md deleted file mode 100644 index e668e50d0..000000000 --- a/.context/handovers/2026-05-11-000847-skill-surface-polish-validate-cleanup.md +++ /dev/null @@ -1,190 +0,0 @@ ---- -generated-at: 2026-05-11T00:08:47Z ---- -# Handover [2026-05-11-000847] Phase SK shipped; 4 polish items on `internal/validate.BodyFlags` for next session - -**Provenance:** commit `971bf767` on branch `feat/skill-surface-polish` - -## Summary - -Phase SK (Skill Surface Polish) — all 7 tasks landed across -6 commits on branch `feat/skill-surface-polish` (off `main` at -`a44edfe3`): - -``` -971bf767 refactor(validate): consolidate body-flag helpers into internal/validate -ba2faa54 refactor(validate): pure BodyFlags, no PreRunE decoration -1156e44a docs(blog): align thought-piece bylines (← unrelated, stacked intentionally) -92507039 refactor(validate): single PreRunE enforcement, no panic, no swallowed errors -55acbd81 feat(skills): /ctx-spec --brief, authority boundaries, plan brief offer -f32c8fd9 feat(validate): require body flags on decision/learning add -``` - -All signed off, `make lint` 0 issues, `go test ./...` 0 failures, -working tree clean. Branch is local-only (not pushed). Spec is at -`specs/skill-surface-polish.md`; design ref at -`ideas/002-editorial-pipeline-and-skill-rigor.md` §3. - -The 5-commit churn on the `validate` package (4 commits between -`f32c8fd9` and `971bf767`) reflects iterative correction during -the session — each commit removed a code-smell the previous one -introduced. Functionally correct now, but the API surface still -has 4 specific polish items the user surfaced at session end. -They are non-blocking (the code works, the tests pass) but -should be addressed before PR review. - -## Outstanding polish items (next session) - -All in `internal/validate/` and its two call sites -(`internal/cli/decision/cmd/add/cmd.go`, -`internal/cli/learning/cmd/add/cmd.go`): - -### 1. `RejectPlaceholder` should be unexported - -Currently exported as `validate.RejectPlaceholder`. Only call -site is `BodyFlags` in the same package. Rename to -`rejectPlaceholder`. Update test references. - -### 2. Per-file convention: private helper lives alone - -Convention check confirmed by `internal/sanitize/truncate.go` -(single unexported `truncate` in its own file). After renaming, -move the helper to `internal/validate/rejectplaceholder.go` and -its tests to `internal/validate/rejectplaceholder_test.go`. The -remaining `bodyflags.go` should hold only `BodyFlags`. - -### 3. `BodyFlags` takes too much - -```go -// Current -func BodyFlags(c *cobra.Command, flags ...string) error -``` - -Only uses `c.Flags()`. Change to: - -```go -func BodyFlags(flags *pflag.FlagSet, names ...string) error -``` - -Call site: -```go -c.PreRunE = func(cobraCmd *cobra.Command, _ []string) error { - return validate.BodyFlags(cobraCmd.Flags(), cFlag.Context, ...) -} -``` - -Update tests — the fixture no longer needs a full -`cobra.Command`; constructing a `pflag.FlagSet` with two flags -and calling `Parse(args)` is simpler. - -### 4. `Placeholders` map shape is confusing - -`internal/config/validate/placeholder.go` currently defines: - -```go -const ( - PlaceholderTBD = "tbd" - PlaceholderNA = "n/a" - ... -) - -var Placeholders = map[string]struct{}{ - PlaceholderTBD: {}, // reads as if the key were the identifier - PlaceholderNA: {}, - ... -} -``` - -In a map literal, `PlaceholderTBD:` evaluates to its const value -`"tbd"` — the map key stored is the string, not the identifier. -The user surfaced this as a real code smell: the surface reads -"enum-keyed map" but it's actually a string-keyed set. - -**Resolution**: switch to a slice + linear scan (N=9; cost is -negligible): - -```go -var placeholders = []string{ - PlaceholderTBD, PlaceholderNA, PlaceholderNAShort, - PlaceholderNone, PlaceholderSeeChat, PlaceholderSeeAbove, - PlaceholderSeeBelow, PlaceholderPending, PlaceholderToBeDone, -} -``` - -```go -// in rejectPlaceholder -key := strings.ToLower(strings.TrimSpace(value)) -for _, p := range cfgValidate.Placeholders { - if key == p { - return errCli.FlagPlaceholder(flag, value) - } -} -``` - -The slice's contents are the same constants, and the slice name -documents the set's purpose without map-key sleight-of-hand. The -audit's magic-strings check passes because every literal lives -in `const PlaceholderXxx`. - -After the rename, `Placeholders` (capital P) should be -`placeholders` (private) since only `internal/validate` reads it. - -## Gating - -- Each item, when fixed, requires the normal gate: `make lint` - clean, `go test ./...` clean, working tree clean before any - commit. -- Sign every commit (`git commit -s`). -- Do not add a `Co-Authored-By:` line to commits. -- Stay on `feat/skill-surface-polish`; the branch is the - active feature branch and stacking polish on top is intended. -- The 4 items are independent; one focused commit per item is - fine, or fold into a single `refactor(validate): polish surface` - commit if the diff stays small. - -## Session meta — read before resuming - -This session accumulated 5 saved feedback memories in -`~/.claude/projects/-Users-volkan-Desktop-WORKSPACE-ctx/memory/` -(2026-05-10), all from the same root cause: I acted on first -impulse instead of grepping the codebase before scaffolding or -editing. Specifically saved: - -- `feedback_no_coauthored_by.md` — strip Claude line only, never - the whole signoff block -- `feedback_branch_before_commit.md` — branch off main first; - honour "stacking is intentional" -- `feedback_always_signoff.md` — DCO requires `git commit -s` -- `feedback_no_silent_errors_no_panic.md` — propagate errors, no - `_ =` or `panic` outside `Must`-prefixed functions -- `feedback_no_silent_decoration.md` — helpers do not wrap - caller's cobra hooks -- `feedback_grep_before_creating_packages.md` — extend existing - packages by default - -The user observed the cumulative drift mid-session and offered -the handover. Resuming agent: **read those memory files first.** -The 4 polish items above are technically small but every fix -this session has introduced a new issue. Slow down: read the -target file fully, grep for the convention, then edit. - -## Next session - -1. Pull this branch (`feat/skill-surface-polish @ 971bf767`). -2. Read `internal/validate/bodyflags.go` and - `internal/config/validate/placeholder.go` end-to-end. -3. Read `internal/sanitize/truncate.go` for the - single-private-helper-per-file pattern. -4. Apply items 1–4 above. One or four commits, either is fine. -5. After lint+test green and tree clean, hand back for push. - -Optional follow-up after the 4 fixes land: open the PR. PR body -draft already prepared mid-session; see commit `55acbd81` body -for the user-facing scope summary. - -## Open questions - -None. The 4 polish items have unambiguous resolutions above. -The user-visible behaviour (decision/learning reject empty and -placeholder body flags) is correct and tested. Polish is purely -about code shape and API surface. diff --git a/.context/handovers/20260518T004118Z-phase-kb-sentinel-strings-punch-list.md b/.context/handovers/20260518T004118Z-phase-kb-sentinel-strings-punch-list.md deleted file mode 100644 index 102d3bdf7..000000000 --- a/.context/handovers/20260518T004118Z-phase-kb-sentinel-strings-punch-list.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -sha: 60543e46 -branch: feat/phase-kb -mode: handover -pass-mode: n/a -life-stage: maintenance -generated-at: 2026-05-18T00:41:18Z -title: phase-kb sentinel-strings + post-amend punch list ---- - -## Summary - -Phase KB branch (`feat/phase-kb` @ `60543e46`) is landed and -linted clean across ~313 files folded into one signed commit. -This session's final iterations did, in order: package -relocation (`initkb` → `initialize/kb`, kb-prefix flat dirs -→ `kb/` nested, plural→singular kb subdir names, -`topicnew/` flat → `topic/cmd/newcmd/` nested, -`internal/cli/kb/cmd.go` → `kb.go`); function and file -renames (`RunNew` → `Scaffold`, `LatestCursor` → `Latest`, -`CopyKBLanding` → `CopyLanding` for the stutter audit, -`nextid.go` → `next_id.go`, `ctxio` alias → `ctxIo`, -`topics.go` → `topic.go`, `Slugify` removed in favor of -`internal/slug.Path`); shared-package extractions -(`internal/slug/` from `internal/cli/journal/core/slug/`, -`internal/write/kb/row/` from triplicated -contradiction/decision/question append flows, -`internal/cli/setup/core/copilot_cli/github_asset.go` from -the `deployAgent`+`deployInstructions` duplication); -`cmd.go`/`run.go` split across 8 kb+handover subcommands; -Phase KB skill parity ported to copilot-cli and opencode -trees; the -`.context/{TASKS,DECISIONS,LEARNINGS}.md` Phase KB additions -em-dash-swept; markdown→Markdown, en-US, Title-Case, -NDA-name (your-project / your-domain placeholders) and -rot-prone repo-spec-link sweeps run across docs/specs/skills; -`commands.yaml` gained Examples blocks; localizable strings -moved from cfg `messages.go` files into -`commands/text/{errors,write}.yaml` + DescKey constants in -`internal/config/embed/text/`; YAML hierarchy aligned to -`err.kb..` with dot separators; finally, all 13 -remaining `messages.go` files were renamed to package-singular -names — no `messages.go` exists anywhere in the repo now. - -## Next Session - -Fix the `ErrMsg`-as-string-sentinel anti-pattern. Currently -in `internal/config/{handover,closeout,git_meta,kb/cli, -kb/evidence,kb/sourcecoverage,rc,initialize}/.go` we -still have things like - -```go -ErrMsgMissingGitTree = "git working tree required" -``` - -These strings back package-level -`var ErrX = errors.New(cfgPkg.ErrMsgX)`. The `errors.Is` -contract uses identity, not text — but the embedded English -string still leaks into `.Error()` output and breaks -localization. The correct shape: (a) the sentinel value -carries identity, not text — use `errors.New("")` or a tiny -typed sentinel with `Is(target error) bool`; (b) the -user-facing text moves into `commands/text/errors.yaml` as -`err..` and is rendered at error-display time by -the `err//.go` wrapping constructor (which already -uses `desc.Text` for the format wrapping). - -Sweep every `ErrMsg*` in `internal/config/**/*.go`. Verify -with: - -```bash -grep -rn 'ErrMsg.*= "' internal/config/ -``` - -After the sweep, also kill any "sentinel mirrors YAML key" -doc comments I left in the cfg files explaining the -duplication — that justification was wrong. - -## Highlights - -- 8 amends this session culminating at `60543e46`. -- Phase KB skill parity across 3 host trees - (`claude` / `copilot-cli` / `opencode`). -- Phase RG (`git`-required) and Phase KB (editorial - pipeline) shipped paired. -- `internal/cli/handover/core/path/` extracted from - `kb/core/path/` (handover is session-glue, not a KB - feature; `.context/handovers/-.md` files are - timestamped so concurrent agent runs never overwrite). -- `docs/cli/kb-handover.md` split into `kb.md` + `handover.md` - (the combined page was a category error). -- `CLAUDE.md` restructured: `## Session Handovers` is now an - h2 sibling of `## KB Editorial Workflow`, not a sub-step. -- `Phase KB-followup` task filed in `.context/TASKS.md` for - the adversarial design review of the 3-tree skill drift - problem (parallel claude/copilot-cli/opencode trees). -- New `internal/slug.Path` for slash-preserving slugs; - duplicate `Slugify` in `kb/core/topic` removed. -- New `internal/write/kb/row.Append` + `entity.KBRowHooks` - collapsed three triplicated append flows. -- Doc structure audit clean; cross-package types audit - clean; mixed-visibility audit clean; gocritic clean; - stutter audit clean. - -## Open Questions - -- Should we keep `errors.Is`-style sentinels at all for - these wrappers, or switch to typed-error structs with - `Is(target error) bool` methods? Either works; pick one - and apply uniformly. -- My "`ErrMsg` consts must stay Go-const for package-init - timing" framing in several recent commit messages and - doc comments is partially wrong — only the - `var ErrX = errors.New()` values need a non-empty - backing string for the `.Error()` fallback, but if every - caller uses the wrapping constructor (which goes through - `desc.Text`), the backing string is dead text. Decide - whether to delete the `ErrMsg` consts entirely (typed - sentinel with empty `.Error()`), or keep them as a - degraded-mode fallback when the embedded YAML lookup - table hasn't been populated yet. -- The `Phase KB-followup` adversarial-review task assumes a - body-extract builder is the right shape. Confirm before - building anything. - -## Folded Closeouts - -none (this is a manual session-end handover; no editorial -closeouts pending). diff --git a/.context/ingest/00-GROUND.md b/.context/ingest/00-GROUND.md new file mode 100644 index 000000000..0bad0a807 --- /dev/null +++ b/.context/ingest/00-GROUND.md @@ -0,0 +1,92 @@ +# Ground Mode + +External-grounding pass. This mode reaches **outside** the +project to pull authoritative facts that the kb depends on. +Internal grounding (reading the project's own code, specs, +transcripts) belongs in `ctx kb ingest`, not here. + +Read `KB-RULES.md` first; it carries the editorial contract, +the demotion policy, evidence discipline, and the closeout +shape. This file describes the per-mode procedure only. + +--- + +## Inputs + +The skill reads `grounding-sources.md`. Each non-comment, +non-blank line is one of: + +- `mcp::` — an MCP-reachable resource. +- `https://...` — a URL the skill should fetch (prefer a + grounded-search MCP when available; web fetch is fallback). +- `./relative/path` or `/abs/path` — a local folder or file + **outside** `.context/`. +- `symlink:` — a stable symlink the project uses for + cross-repo grounding. +- `NONE` — explicit no-op for this pass; re-prompts on next + invocation. **Not** a project-wide opt-out. + +Empty file (no non-comment lines) ⇒ the skill prompts once +for sources before doing any work and exits. It does **not** +fabricate sources, and it does **not** scan the project for +"likely" external references. + +--- + +## Pre-Write Gates + +Before the first fetch, confirm: + +1. `.context/` and `.context/kb/` exist. +2. `.context/kb/index.md` has a non-placeholder `## Scope` + section (no `` marker present). +3. `grounding-sources.md` has at least one resolvable line. + +If any gate fails, refuse cleanly with the recovery hint +named in `KB-RULES.md` and `OPERATOR.md`. + +--- + +## Constraints + +- Ground does **not** rewrite kb prose by itself. It produces + `evidence-index.md` rows; the human (or a follow-up + `ctx kb ingest` pass) weaves new claims into glossary, + domain-decisions, contradictions, timeline. +- A `low`-confidence claim grounded externally promotes one + band **only** when the external source is independent of + the original. Two transcripts of the same meeting do not + count as independent. +- Out-of-repo citations omit the `sha:` field on their + evidence rows; in-repo grounding is not this mode's job. +- If a source returns nothing (fetch failed, MCP unreachable, + page 404), name the failure in the closeout's + `Next pass hint` and continue with the remaining sources. + +--- + +## Procedure + +1. **Resolve sources.** Read `grounding-sources.md`; expand + each non-comment line to a concrete fetchable target. +2. **Fetch.** Call the appropriate retrieval mechanism per + source kind. Prefer a grounded-search MCP for URLs. +3. **Extract.** Pull the smallest set of atomic claims that + bear on the kb's declared scope. Cite each by source + short name (defined in `source-map.md`) plus a locator. +4. **Advance the ledger.** Update `source-coverage.md` for + each source touched in this pass per the state-machine + rules in `KB-RULES.md`. Honest state only — refusing to + record incomplete coverage is the anti-pattern. +5. **Reconcile.** Compare new claims against `glossary.md`, + `domain-decisions.md`, `contradictions.md`. Where external + evidence diverges from internal claims, add a + `contradictions.md` row and demote per the demotion + policy. +6. **Append evidence.** Add new rows to `evidence-index.md`. + Never renumber existing rows. +7. **Write the closeout** under + `.context/ingest/closeouts/-ground-closeout.md` with + the frontmatter shape from `KB-RULES.md` (`mode: ground`). +8. **Append a `SESSION_LOG.md` line** at the closeout phase + boundary in the exact shape described in `KB-RULES.md`. diff --git a/.context/ingest/30-INGEST.md b/.context/ingest/30-INGEST.md new file mode 100644 index 000000000..319c7ca98 --- /dev/null +++ b/.context/ingest/30-INGEST.md @@ -0,0 +1,260 @@ +# Ingest Mode + +The primary editorial pass. Turns semi-structured inputs +(notes, transcripts, source code, diagrams, upstream docs) +into evidence-backed Markdown kb content with explicit +confidence bands. + +Read `KB-RULES.md` first; it carries the inviolable rules, +the pass-mode contract, the topic-page circuit breaker, the +source-coverage state machine, the topic-adjacency pre-flight, +the cold-reader rubric, evidence discipline, confidence +bands, the demotion policy, and the closeout shape. This +file describes **how a single ingest pass executes**. It does +not duplicate `KB-RULES.md`; it cites it. + +The `/ctx-kb-ingest` skill reads this file before doing +anything else. + +--- + +## Inputs + +The skill takes either a folder (recurse), a list of paths, +a URL, an MCP resource, or inline natural-language context +naming the materials. **No input ⇒ refuse cleanly:** + +> no sources provided; pass a folder, a URL, an MCP resource, +> or describe the materials inline. + +Refuse-on-empty is the contract; do not prompt the user for +sources mid-skill. Exit non-zero so the operator re-invokes +with arguments. + +--- + +## Pre-Write Gates (All Must Pass Before Any Extraction) + +1. `.context/` and `.context/kb/` exist. +2. `.context/kb/index.md` has a non-placeholder `## Scope` + section (no `` marker present). +3. `.context/ingest/KB-RULES.md` exists and was read this + pass. +4. Source paths supplied on the command line all resolve. A + missing path is a refuse condition, not a partial-extract + condition. + +Any gate failure ⇒ refuse with the recovery hint named in +`KB-RULES.md`'s error table. Do not partially execute. + +--- + +## Up-Front Pass-Mode Declaration (MANDATORY) + +After pre-write gates pass and **before** topic resolution, +emit a visible three-line block: + +> **Pass-mode:** `` +> **Reason:** `` +> **Definition of done:** `` + +The declaration is a contract, not a label. The closeout's +body block restates these three lines verbatim, and the +closeout's frontmatter records `pass-mode:` to match. Doctor +advisory flags mismatches between the body and frontmatter. + +**Mode selection (see `KB-RULES.md` "Pass-mode contract"):** + +- Default is `topic-page`. +- `triage` fires when the user supplied multiple disparate + sources with no clear single topic, OR explicitly invoked + triage language ("triage these"; "just classify"). +- `evidence-only` fires **only** on explicit user request + matching valid-trigger criteria ("just mint EV rows"; + "backfill evidence"). Size, ambiguity, time pressure, and + operator convenience are NOT valid triggers. Inferring + `evidence-only` to dodge topic-page validation is a hard + anti-pattern. + +**Mid-pass mode-switching is forbidden.** If the work in +flight no longer fits, abort with a partial closeout citing +what was done so far and recommend re-invocation under the +correct mode. Never silent-switch. + +--- + +## Procedure + +### 1. Resolve Topic and Run Adjacency Pre-Flight + +Identify the slug. In `topic-page` mode, before scaffolding +anything: + +1. Read `.context/kb/source-coverage.md`. +2. Scan for rows whose state is **not** in + `{comprehensive, skipped, superseded}` and whose `Topic` + is plausibly adjacent (shared first slug segment, shared + vendor / surface, explicit cross-reference). +3. Record the result in the closeout's `Adjacency pre-flight` + block. Use count + location (*"seventeen rows in + `evidence-index.md`"*); do **not** name individual + `EV-###` IDs (naming a lower-confidence sibling's row + demotes the floor of cited bands on this page). +4. If zero matches, record explicit + *"no incomplete adjacent topics surfaced"*. Silence is + not allowed. + +Each surfaced incomplete adjacent topic MUST be acknowledged +in `## Related concepts in this kb` on the page being +authored. + +### 2. Life-Stage Check + +Count `.context/kb/topics/*/index.md` before synthesizing: + +- `< 5` topic pages → **bootstrap**. Skip reconciliation + ceremony; synthesize aggressively. Exception: surface a + contradiction even in bootstrap if the new material + plainly contradicts existing kb claims. +- `>= 5` topic pages → **maintenance**. Apply full + reconciliation discipline (laddering, demotion, + contradictions). + +Record the life-stage call in the closeout frontmatter +(`life-stage:`) and `What changed` section. + +### 3. Scaffold (Topic-Page Mode Only) + +If `.context/kb/topics//index.md` does not exist, +shell out to `ctx kb topic new ""`. **Topic-page file +creation is performed only by `ctx kb topic new`.** Skills +do not synthesize the scaffold by hand. + +If the slug already exists, append/extend the existing +`index.md` rather than creating a new one. + +### 4. Extract and Synthesise + +For each input, extract atomic claims. One claim = one row +in `evidence-index.md`. Each row carries: claim text (single +sentence, declarative, present tense), source short name + +locator, `sha:` for in-repo citations only, confidence band +per `KB-RULES.md`. + +Mode-specific output: + +- `topic-page`: mint prose AND `EV-###` rows AND topic + scaffold AND cross-links AND ledger updates. +- `triage`: classify sources into the inbox, advance ledger + rows to `discovered` or `admitted`. **No EV rows minted.** + No topic page touched. No prose written. +- `evidence-only`: mint `EV-###` rows tagged with the source. + No topic page. No prose. No glossary / domain-decisions + edits. + +### 5. Reconcile (Maintenance Life-Stage Only) + +For each new claim, decide: + +- **Net new** ⇒ append to the appropriate kb file + (`glossary.md` for terms; `domain-decisions.md` for + editorial calls; `timeline.md` for events; the relevant + topic-page prose for narrative context). +- **Reinforces existing** ⇒ promote the existing claim's + confidence band per `KB-RULES.md`; cross-link the new + evidence row. +- **Contradicts existing** ⇒ add a `contradictions.md` row; + demote per the demotion policy; open an + `outstanding-questions.md` entry. + +In bootstrap, the reconciliation ceremony is skipped except +for the contradiction exception. + +### 6. Advance the Source-Coverage Ledger + +Update `.context/kb/source-coverage.md` for every source +touched in this pass, per the state-machine transitions in +`KB-RULES.md`. Illegal transitions are refused at write time +by `AdvanceLedger`. **Lying to the ledger is a hard +anti-pattern**: record `topic-page-drafted` honestly when +the page is incomplete; do not claim `comprehensive` to make +the closeout look clean. + +### 7. Topic-Page Circuit Breaker (Topic-Page Mode Only) + +Before claiming `topic-page: produced` or +`topic-page: extended`, verify ALL FOUR invariants from +`KB-RULES.md` "Topic-page circuit breaker": + +1. The page exists and was created/extended this pass. +2. The page cites at least one `EV-###` row that resolves to + `evidence-index.md`. +3. `ctx kb site build` ran clean (or its failure is named in + `Next pass hint` AND the pass reports `topic-page: + deferred`). +4. The cold-reader orientation rubric records + `Result: pass`. + +Any failure ⇒ `topic-page: deferred`; ledger advances to +`topic-page-drafted` (not `comprehensive`). + +### 8. Cold-Reader Orientation Rubric (Topic-Page Mode Only) + +Record in the closeout's `What changed` section: + +``` +Cold-reader orientation: +- Concept clear? yes|no: +- Why this kb cares clear? yes|no: +- Canonical evidence reachable? yes|no: +- Boundaries clear? yes|no: +Result: pass | fail +``` + +All four `yes` ⇒ `Result: pass`. Any `no` ⇒ `Result: fail` +⇒ circuit-breaker fails ⇒ `topic-page: deferred`. Name +which items returned `no` in the closeout body. + +If `Boundaries clear?` is `no` because `index.md` has +outgrown a single page, **propose** a sub-page split (e.g. +`.context/kb/topics//security.md`) and wait for +operator confirmation. Never auto-split. + +### 9. Write the Closeout + +Path: +`.context/ingest/closeouts/-ingest-closeout.md`. + +Required frontmatter (see `KB-RULES.md` "Closeout shape"): +`sha`, `branch`, `mode: ingest`, `pass-mode`, `life-stage`, +`generated-at`. + +Body sections (mode-aware): Inputs, Pass-mode block, Topic(s) +touched, What changed (including the Cold-reader rubric in +topic-page mode), New questions, New contradictions, +Confidence drift, Source-coverage updates, Overflow, +Adjacency pre-flight, Next pass hint. + +Closeouts are append-never-rewrite. Archived closeouts are +immutable. + +### 10. Append the SESSION_LOG Line + +At the closeout phase boundary, append a line to +`.context/ingest/SESSION_LOG.md` in the exact shape defined +by `KB-RULES.md` "SESSION_LOG line shape". + +--- + +## What This Mode Does NOT Do + +- Does not web-fetch new external sources; that is + `ctx kb ground`'s job. Inputs are project-internal or + user-supplied URLs. +- Does not mechanically audit existing kb structure; that + is `ctx kb site-review`. +- Does not answer questions from the kb; that is + `ctx kb ask`. +- Does not write to canonical files (`TASKS.md`, + `DECISIONS.md`, `LEARNINGS.md`, `CONVENTIONS.md`). The + authority boundary in `KB-RULES.md` is strict. diff --git a/.context/ingest/40-ASK.md b/.context/ingest/40-ASK.md new file mode 100644 index 000000000..04482b5f1 --- /dev/null +++ b/.context/ingest/40-ASK.md @@ -0,0 +1,104 @@ +# Ask Mode + +Q&A pass grounded in the kb. The operator has a question; +the skill answers from `.context/kb/` content with explicit +confidence bands and citation chains. **Read-only on kb +prose.** The only kb-side write allowed is a new row in +`outstanding-questions.md` when the answer surfaces a gap. + +Read `KB-RULES.md` first; it carries the authority boundary, +evidence discipline, confidence bands, and the closeout +shape. This file describes the per-mode procedure only. + +--- + +## Inputs + +The skill takes a question as its single argument, or inline +natural-language context with the question embedded. **No +question ⇒ refuse cleanly:** + +> no question provided; pass a question or describe it inline. + +Refuse-on-empty is the contract; the skill does not prompt +mid-flight. + +--- + +## Pre-Write Gates + +1. `.context/` and `.context/kb/` exist. +2. `.context/kb/index.md` has a non-placeholder `## Scope` + section. + +If either gate fails, refuse with the recovery hint from +`KB-RULES.md`. + +--- + +## Procedure + +1. **Restate** the question in one sentence. If the operator + gave a multi-part question, split it into sub-questions and + answer each in turn. Record the decomposition in the + closeout's `Inputs` section. +2. **Search** `.context/kb/` and `evidence-index.md` for + relevant claims. Use the source short names from + `source-map.md` to track citation chains. Walk topic + `index.md` files first; descend into sub-pages only when + the lead claim is in one. +3. **Compose** the answer in this shape: + + - **Direct answer** in 1-3 sentences. + - **Confidence** band (`high` / `medium` / `low` / + `speculative`), determined by the floor of the cited + `EV-###` bands per `KB-RULES.md` "Confidence bands". + - **Sources**: bullet list of evidence rows that ground + the answer (cite by `EV-###` plus the source short + name). + - **Caveats**: any contradictions or open questions that + bear on the answer. + +4. **Detect gaps.** If the answer is `speculative` or relies + on `low`-confidence claims, OR if no evidence row backs + the answer at all: + + - Append a `Q-###` row to `outstanding-questions.md` with + the question text and a one-line summary of why current + evidence is insufficient. + - Suggest a concrete next pass in the closeout's + `Next pass hint` (usually `ctx kb ground` for external + evidence or `ctx kb ingest ` for internal). + +5. **Write the closeout** under + `.context/ingest/closeouts/-ask-closeout.md` with + `mode: ask` in the frontmatter and the body shape from + `KB-RULES.md`. + +6. **Append a SESSION_LOG line** at the closeout phase + boundary. + +--- + +## Constraints + +- **Read-only on kb prose.** This mode does not append + paragraphs to `glossary.md`, `domain-decisions.md`, + `timeline.md`, or any topic-page prose. The single + kb-side write allowed is a new row in + `outstanding-questions.md`. +- **No web-jumping.** If the answer is not in the kb, + surface that fact and recommend `ctx kb ground` as the + next pass. Do not fetch external sources to fabricate an + answer. +- **Never invent citations.** If a claim is in working + memory but not in `evidence-index.md`, mark the answer + `speculative` and open a `Q-###` row rather than inflating + the confidence band with a fictional row. +- Multi-part questions get a single closeout, not one per + sub-question. The `Inputs` section captures the + decomposition. +- The `pass-mode:` frontmatter field is required by the + closeout shape, but ask mode does not carry a pass-mode + contract; set `pass-mode: n/a`. Doctor advisory tolerates + `n/a` for non-ingest modes. diff --git a/.context/ingest/50-SITE_REVIEW.md b/.context/ingest/50-SITE_REVIEW.md new file mode 100644 index 000000000..7056a7000 --- /dev/null +++ b/.context/ingest/50-SITE_REVIEW.md @@ -0,0 +1,123 @@ +# Site-Review Mode + +Mechanical structural audit. Walks `.context/kb/` and (if it +exists) the rendered site under `.context/site/kb/` looking +for structural issues that can be fixed **without making +editorial judgment calls**. Defers anything requiring evidence +weighing to a follow-up `ctx kb ingest` or `ctx kb ground` +pass. + +Read `KB-RULES.md` first; it carries the authority boundary, +the closeout shape, the validation rules for confidence +bands, and the demotion policy this mode refuses to apply. +This file describes the per-mode procedure only. + +--- + +## Inputs + +No arguments required. The skill walks `.context/kb/` +automatically. The operator may pass a scope hint inline +(e.g. *"focus on glossary.md"*) which the skill records in +the closeout's `Inputs` section. + +--- + +## Pre-Write Gates + +1. `.context/` and `.context/kb/` exist. +2. `.context/kb/index.md` has a non-placeholder `## Scope` + section. + +--- + +## What Site-Review Fixes (No Truth-Content Needed) + +Mechanical issues. The skill resolves them in place and +records the fix in the closeout's `What changed`: + +- **Broken internal links** — relative paths under + `.context/kb/` that no longer resolve. Retarget to the + renamed file when the target is unambiguous; otherwise + remove the link and open an `outstanding-questions.md` + row. +- **Orphaned source short names** — referenced in + `evidence-index.md` but missing from `source-map.md`. + Add the source-map entry when the source identifier is + obvious from context; flag otherwise. +- **Confidence bands missing or malformed** — must be one of + `high|medium|low|speculative`. Coerce malformed + capitalization in place (`High` → `high`, `MEDIUM` → + `medium`). Flag any other malformation (typos, unknown + bands) without changing the band. +- **Missing closeout frontmatter fields** in + `.context/ingest/closeouts/`. Required fields per + `KB-RULES.md` "Closeout shape": `sha`, `branch`, `mode`, + `pass-mode`, `life-stage`, `generated-at`. Generate the + missing field where derivable from filename + git context; + flag where not derivable. +- **Dangling `outstanding-questions.md` entries** — + questions whose linked contradictions or claims no longer + exist after a rename. Update the link or close the row + with a `(superseded)` annotation citing the rename. +- **Source-coverage ledger inconsistencies** that are purely + mechanical (a row's `Updated` predates the file's mtime, a + row references a source not in `source-map.md`). Refresh + the `Updated` cell; flag the source-map mismatch. + +--- + +## What Site-Review Defers (Requires Evidence Judgment) + +These are recorded in `outstanding-questions.md` and as +`Next pass hint`s in the closeout, but **not** resolved in +this pass: + +- Confidence-band changes that would require new evidence. +- Promotion or demotion of any claim based on + stale-vs-fresh judgment. +- Contradictions that surface during the walk but lack a + clear resolution path. +- Glossary entries with wording the reviewer "doesn't like" + but that aren't factually wrong. +- Illegal source-coverage ledger state transitions (e.g. + `comprehensive → highlights-extracted` without an explicit + `superseded` step). Flag for `ctx kb ingest`; do not + rewrite the state. +- Sub-page splits suggested by oversized `index.md` files. + Site-review surfaces the candidate but never auto-splits. + +--- + +## Procedure + +1. **Plan** the walk: list the files the skill will touch in + the closeout's `Inputs` section. +2. **Walk** each file in order. Per file: detect, fix what's + mechanical, record what's deferred. +3. **Aggregate** into the closeout's `What changed` + (mechanical fixes) and `New questions` (deferred items). +4. **Write the closeout** under + `.context/ingest/closeouts/-site-review-closeout.md` + with `mode: site-review` in the frontmatter. Set + `pass-mode: n/a` — site-review does not carry a pass-mode + contract. +5. **Append a SESSION_LOG line** at the closeout phase + boundary. + +--- + +## Constraints + +- Mechanical fixes are batchable; do **not** write one + closeout per file. One pass = one closeout. +- Do not touch the rendered site under `.context/site/kb/`. + The renderer (`zensical`) owns that directory; site-review + only reads it for cross-referencing. +- If a "fix" turns out to need judgment mid-walk (e.g. a + broken link could be retargeted to one of two files), + defer it rather than guessing. The bias in this mode is + toward refusing to act, not toward acting hastily. +- Site-review never demotes or promotes claims. It never + edits `evidence-index.md` rows except to fix malformed + confidence-band capitalization in place. diff --git a/.context/ingest/KB-RULES.md b/.context/ingest/KB-RULES.md new file mode 100644 index 000000000..7513b15b4 --- /dev/null +++ b/.context/ingest/KB-RULES.md @@ -0,0 +1,354 @@ +# KB Rules + +Stable workflow contract for the editorial knowledge-ingestion +pipeline shipped by `ctx`. These rules govern the +**`.context/ingest/`** working directory and the mode skills +(`/ctx-kb-ingest`, `/ctx-kb-ask`, `/ctx-kb-site-review`, +`/ctx-kb-ground`, `/ctx-kb-note`). + +This file is project memory. Hand-edit it the same way you +hand-edit the five canonical `.context/` files. Round-trips +through git like any Markdown file. + +The filename is `KB-RULES.md`, not `CONSTITUTION.md`, to avoid +collision with `.context/CONSTITUTION.md`. Per DECISIONS.md +"Editorial constitution at `.context/ingest/KB-RULES.md`, not +`CONSTITUTION.md`" (2026-05-10). + +--- + +## Four Inviolable Rules + +These four rules are load-bearing for the entire workflow. +Breaking any one of them silently corrupts the knowledge base. + +1. **Skills are the sole writers of `INBOX.md`.** The inbox is + an audit trail of the last skill run, never a hand-edit + form. Args or inline natural-language context drive what the + skill writes; cold invocation with no input causes the skill + to refuse cleanly. The pipeline takes no hand-edited config + file; sources are supplied at invocation time. To configure + external grounding inputs, edit `grounding-sources.md`. + +2. **The handover is the sole authoritative recall artifact.** + `/ctx-wrap-up` writes a handover at the end of every session + (always delegating to `/ctx-handover` as its final step, + regardless of whether `.context/kb/` exists). Handovers live + at `.context/handovers/-.md` (timestamped so + concurrent agent runs never overwrite). `Do you remember?` + reads the latest handover unconditionally; when + `.context/kb/` exists, it additionally folds any closeouts + whose `generated-at` postdates the handover. + `SESSION_LOG.md` is mid-flight working memory; it is **not** + read on session start. + +3. **Provenance lives on operational artifacts, not knowledge + artifacts.** `SESSION_LOG.md` entries, closeouts, and + handovers carry `sha=` and `branch=`. KB prose, + glossary, domain-decisions, contradictions, + outstanding-questions, timeline, schemas, and the rendered + site stay SHA-free. Single exception: `evidence-index.md` + entries pointing at in-repo files pin to a SHA at extraction + time so cited bytes are recoverable via `git show`. + +4. **Schemas ship with shape, not content.** Each + `schemas/*.md` lists fields and one worked example; no + domain entries. The drift-prevention property the schemas + folder exists for is preserved without prescribing values + per project. + +--- + +## Pass-Mode Contract + +Every `ctx kb ingest` invocation declares its mode **before any +source extraction begins**: + +| Mode | Mints prose? | Mints EV-### ? | Touches topic page? | Default? | +|------------------|--------------|----------------|---------------------|----------| +| `topic-page` | yes | yes | yes (create/extend) | yes | +| `triage` | no | **no** | no | no | +| `evidence-only` | no | yes (tagged) | no | no | + +The declaration is a contract, not a label. The skill emits a +three-line block in the response stream: + +> **Pass-mode:** `` +> **Reason:** `` +> **Definition of done:** `` + +**Mid-pass mode-switching is forbidden.** If the work in flight +no longer fits, abort with a partial closeout citing what was +done, and recommend re-invocation under the correct mode. + +**Inferring `evidence-only` to avoid topic-page validation is a +hard anti-pattern.** Mode is explicit-request-only; size, +ambiguity, time pressure, and operator convenience are NOT +valid triggers. + +--- + +## Topic-Page Circuit Breaker + +A pass in `topic-page` mode MAY NOT report +`topic-page: produced` or `topic-page: extended` unless ALL of +the following are true at completion: + +1. `.context/kb/topics//index.md` (or a sibling sub-page) + exists and was created or extended in this pass. +2. The page cites at least one `EV-###` row that resolves to + `evidence-index.md`. +3. `ctx kb site build` ran clean (or its failure is named in + the closeout's `Next pass hint` AND the pass reports + `topic-page: deferred`). +4. The cold-reader orientation rubric records `Result: pass` + in the closeout (all four items at `yes`). + +Any failure → `topic-page: deferred` and the source-coverage +ledger advances to `topic-page-drafted` (not `comprehensive`). + +--- + +## Source-Coverage Ledger (State Machine) + +`.context/kb/source-coverage.md` is a state machine over every +source the kb has touched. Allowed transitions: + +| state | next states | +|-------------------------|-------------| +| `discovered` | `admitted`, `skipped` | +| `admitted` | `highlights-extracted`, `partially-ingested`, `topic-page-drafted`, `comprehensive` | +| `highlights-extracted` | `partially-ingested`, `topic-page-drafted`, `comprehensive` | +| `partially-ingested` | `topic-page-drafted`, `comprehensive` | +| `topic-page-drafted` | `comprehensive` | +| `comprehensive` | terminal until source updates | +| `superseded` | terminal | +| `skipped` | terminal until scope changes | + +Every pass updates the ledger before writing the closeout. +**Lying to the ledger is a hard anti-pattern.** Set the state +honestly even when it means recording incomplete work. + +--- + +## Topic-Adjacency Pre-Flight + +Before resolving the topic in `topic-page` mode, scan the +ledger for rows whose state is **not** in +`{comprehensive, skipped, superseded}` AND whose `Topic` is +plausibly adjacent (shared first segment of a slug, +shared vendor / surface, explicit cross-references). + +For each adjacent incomplete topic surfaced, the pass MUST: + +1. Acknowledge it in `## Related concepts in this kb` on the + topic page being authored. +2. Surface it in the closeout's `Adjacency pre-flight` block. +3. Surface it in the response contract's + `Adjacent topics noted` field. + +**Do NOT enumerate `EV-###` IDs by name in the adjacency +block.** Use count + location (*"seventeen rows in +`evidence-index.md`"*) instead; naming an EV row from a +lower-confidence sibling demotes the floor of cited bands. + +Silence is not the same as a clean pre-flight; when zero +matches, explicitly record *"no incomplete adjacent topics +surfaced"*. + +--- + +## Cold-Reader Orientation Rubric + +Four yes/no items recorded in the closeout's `What changed` +section, in `topic-page` mode: + +``` +Cold-reader orientation: +- Concept clear? yes|no: +- Why this kb cares clear? yes|no: +- Canonical evidence reachable? yes|no: +- Boundaries clear? yes|no: +Result: pass | fail +``` + +`Result: pass` requires all four `yes`. Any `no` → +`Result: fail` → circuit-breaker fails → `topic-page: +deferred`. + +--- + +## Life-Stage Discipline + +Count `.context/kb/topics/*/index.md` before the pass begins +synthesizing: + +- `< 5` topic pages → **bootstrap** mode. Skip reconciliation + ceremony; synthesize topic pages aggressively. Exception: + surface a contradiction even in bootstrap if the new + material plainly contradicts existing kb claims. +- `>= 5` topic pages → **maintenance** mode. Apply full + reconciliation: claim laddering, demotion, contradiction + detection. + +Document the life-stage call in the closeout's frontmatter +(`life-stage:`). + +--- + +## Evidence Discipline + +- Every claim in `.context/kb/` carries at least one citation + in `evidence-index.md`. Claims without citations stay in + `outstanding-questions.md` until grounded. +- Citations name the source by **short name** (defined in + `source-map.md`) plus a locator: line range for files, + timestamp for transcripts, anchor for URLs, symbol for code. +- In-repo citations include a `sha:` field set to the short + SHA at extraction time. Out-of-repo citations omit `sha:`. +- A claim with **only** verbal-source backing (transcript, + meeting note) is `confidence: low` until reinforced by a + written source. + +--- + +## Confidence Bands + +Every claim and definition in `.context/kb/` carries an +explicit confidence band: + +- **`high`**: corroborated by ≥ 2 independent sources, or by + one source plus a working code-level check (compiles, runs, + matches). +- **`medium`**: single authoritative source, no independent + corroboration; or two sources that agree on the conclusion + but not the mechanism. +- **`low`** — single source only; or sources disagree but the + claim is the best current synthesis. +- **`speculative`** — no source backing; an inference the + human or agent flagged for follow-up. + +`speculative` content does **not** ship in the rendered site. +`low`-confidence content ships only when paired with the +matching `outstanding-questions.md` entry. + +**Floor-of-cited-bands rule:** a topic page's Confidence is +the *lowest* band cited on the page. Refuse to set Confidence +above the floor. Refuse to set above `speculative` while any +`TBD-cite` remains. + +--- + +## Demotion Policy + +When new evidence contradicts an existing claim: + +1. Add a row to `contradictions.md` naming both EV-### IDs and + one-line summary of the disagreement. +2. Demote the older claim one band + (`high → medium → low → speculative`), citing the new EV + row as the cause. +3. Open an `outstanding-questions.md` entry naming both sides + and what evidence would resolve. + +**Never renumber or delete `EV-###` rows.** Demote in place. + +--- + +## Authority Boundary + +- `/ctx-kb-ingest` writes prose AND evidence rows AND topic + scaffold AND cross-links AND ledger updates in the same + pass. That combination is unique to ingest. +- `/ctx-kb-ask` is Q&A grounded in the kb; read-only on prose; + refuses to web-jump; flags gaps the kb cannot answer. +- `/ctx-kb-site-review` is a structural audit; mechanical + fixes only. Defers anything that requires evidence judgment. +- `/ctx-kb-ground` re-grounds against external sources listed + in `grounding-sources.md`. +- `/ctx-kb-note` is a lightweight capture into + `ingest/findings.md`; never writes to a topic page or to + evidence-index. + +**Topic-page file creation is performed only by `ctx kb topic +new`.** Skills invoke that CLI; they do not synthesize a +scaffold by hand. + +--- + +## Closeout Shape + +Every pass that clears pre-write gates writes a closeout under +`.context/ingest/closeouts/--closeout.md` +with required frontmatter: + +```yaml +--- +sha: +branch: +mode: +pass-mode: +life-stage: +generated-at: +--- +``` + +Body sections (mode-aware): Inputs, Pass-mode block, Topic(s) +touched, What changed (including the Cold-reader rubric in +topic-page mode), New questions, New contradictions, +Confidence drift, Source-coverage updates, Overflow, Adjacency +pre-flight, Next pass hint. + +Closeouts are append-never-rewrite. Archived closeouts are +immutable. + +--- + +## SESSION_LOG Line Shape + +Each phase-boundary line: + +``` +[YYYY-MM-DD HH:MM:SS sha= branch=] phase= status= note=<≤120 chars> +``` + +`` is the 7-char git short SHA. `` is the current +symbolic ref or `detached` (HEAD is not on a branch). +`` is one of `resolve`, `synthesise`, `reconcile`, +`closeout`. + +--- + +## Hard Anti-Patterns + +- Treating closeout existence as topic-page validation. +- Skipping the topic-page circuit breaker in topic-page mode. +- Inferring `evidence-only` mode from source size, ambiguity, + time pressure, or operator convenience. +- Mid-pass mode-switching (abort and re-invoke instead). +- Hiding incomplete coverage under a comprehensive-looking + closeout (lying to the ledger). +- Skipping the topic-adjacency pre-flight or running it but + failing to acknowledge surfaced incomplete adjacent topics. +- Inventing claims beyond what the source backs. +- Inventing `EV-###` citations to make a page look complete. +- Promoting claims above `speculative` without an + `evidence-index.md` row. +- Promoting a topic page above its weakest cited band. +- Setting `Confidence` above `speculative` while `TBD-cite` + remains. +- Renumbering or deleting `EV-###` rows when reconciling. +- Skipping the closeout once the pass clears pre-write gates. +- Bypassing `ctx kb topic new` when scaffolding a page. +- Running maintenance discipline against a bootstrap-stage kb. +- Hand-editing `INBOX.md`. + +--- + +## See Also + +- `specs/kb-editorial-pipeline.md` — full spec, including + failure analysis and open questions. +- `OPERATOR.md` — human-facing framing. +- `PROMPT.md` — fallback auto-router when no skill is + installed. diff --git a/.context/ingest/OPERATOR.md b/.context/ingest/OPERATOR.md new file mode 100644 index 000000000..3891c2157 --- /dev/null +++ b/.context/ingest/OPERATOR.md @@ -0,0 +1,153 @@ +# Operator Manual + +How a human drives the kb editorial pipeline. If you are the +operator, read this once before running any of the mode +skills. + +--- + +## The Shape of the Work + +The pipeline lifts knowledge-shaped work (research projects, +domain modeling, post-incident reviews, vendor-spec analysis) +into a discipline that's separate from but adjacent to `ctx`'s +five canonical files. A kb has its own truth basis (cited +evidence, confidence bands) which doesn't fit cleanly into +`DECISIONS.md` or `LEARNINGS.md`. + +You do not *decide* in a kb; you *learn*, and your confidence +in claims increases. A claim with confidence >0.9 is a +fact-by-contract. There is no decide-skill on the kb side; +ad-hoc capture flows through `ctx kb note` (lightweight) or +hand-edit (escape hatch). + +--- + +## Day-Zero Workflow + +1. Run `git init && ctx init` in the project root. The init + lays down the five canonical files **plus** the kb + pipeline scaffolding under `.context/ingest/` and + `.context/kb/`. +2. Open `.context/kb/index.md` and replace the + `` placeholder in `## Scope` with a + one-paragraph statement of what this kb covers and what it + deliberately doesn't. **Mode skills refuse to operate + until you do.** +3. Run `ctx setup` to deploy the kb skills (`/ctx-kb-ingest`, + `/ctx-kb-ask`, `/ctx-kb-site-review`, `/ctx-kb-ground`, + `/ctx-kb-note`, plus the session-wide `/ctx-handover`). +4. Drop your raw materials anywhere. The ingest skill takes + a path or inline reference (folders, files, URLs, MCP + resources). +5. Invoke `/ctx-kb-ingest `. +6. When the pass completes, read the closeout under + `.context/ingest/closeouts/-ingest-closeout.md`. + +--- + +## Steady-State Workflow + +| Trigger | Command | +|---------|---------| +| New material arrives | `/ctx-kb-ingest ` | +| Question the kb should answer | `/ctx-kb-ask ""` | +| Suspect kb has structural rot | `/ctx-kb-site-review` | +| Need fresh external grounding | `/ctx-kb-ground` | +| Quick capture for the next pass | `/ctx-kb-note "..."` | +| Stepping away from the session | `/ctx-wrap-up` (always delegates to `/ctx-handover` as its tail; KB presence affects what gets folded, not whether the handover is written) | + +`/ctx-wrap-up` **always** delegates to `/ctx-handover` as its +final step, in every ctx project, regardless of whether +`.context/kb/` exists. When `.context/kb/` does exist, the +wrap-up additionally surfaces pending closeouts plus the +outstanding-questions count before delegating, and the +handover step folds postdated closeouts and archives them. +The handover artifact lands at +`.context/handovers/-.md` (timestamped so concurrent +agent runs never overwrite). Treat `/ctx-handover` as +wrap-up's handover step, not as a user-facing trigger; the +legitimate direct-invocation cases are `--no-fold` mid-session +checkpoints and recovery after an aborted session. + +--- + +## Reading the KB + +- **`.context/kb/index.md`**: top-level entry and scope + declaration. The `` managed block + in this file is regenerated by `ctx kb reindex`; do not + hand-edit between the markers. +- **`.context/kb/topics//index.md`**: one folder per + topic; sub-pages (`security.md`, `multi-surface.md`) split + off lazily when an `index.md` outgrows the cold-reader + rubric's *"boundaries clear?"* check. +- **`.context/kb/glossary.md`**: terms with definitions and + confidence bands. +- **`.context/kb/domain-decisions.md`**: editorial calls + about how the domain is described. Separate from + `.context/DECISIONS.md` (different schema, different + authority). +- **`.context/kb/contradictions.md`**: unresolved or + historical contradictions, both sides cited. +- **`.context/kb/outstanding-questions.md`**: open gaps; + what to ground next. +- **`.context/kb/evidence-index.md`**: claim -> source map; + the spine of the workflow. +- **`.context/kb/source-coverage.md`**: state machine over + every source the kb has touched. +- **`.context/kb/timeline.md`**: events the domain cares + about. + +--- + +## The Inbox Is an Audit Trail, Not a Form + +`.context/ingest/INBOX.md` is rewritten by the mode skills +on every run. **Do not hand-edit it.** Edits are silently +discarded the next time a skill runs, with no archive. + +To configure external grounding inputs, edit +`.context/ingest/grounding-sources.md`. That is the only +hand-editable surface that drives mode-skill behavior. + +--- + +## Recovering an Aborted Session + +If a session ends before `/ctx-wrap-up` runs, closeouts +accumulate in `.context/ingest/closeouts/`. The next time +you ask *"do you remember?"*, the agent reads the latest +handover plus any closeout whose `generated-at` postdates +it. So you do not lose the residue; you just don't get it +folded into a clean handover until the next wrap-up's +handover step runs. + +To recover explicitly: invoke `/ctx-wrap-up`, which always +delegates to `/ctx-handover` as its final step. The handover +step folds the postdated closeouts into the new handover and +archives the source files to `.context/archive/closeouts/`. +You may also invoke `/ctx-handover` directly for recovery +(the one legitimate direct-invocation case outside mid-session +checkpoints). + +For mid-session checkpoint without consuming closeouts, use +`ctx handover write --no-fold ...`. + +--- + +## Things This Pipeline Does NOT Do + +- It does **not** invoke the static-site renderer itself. + `ctx kb site build` and `ctx kb site serve` shell out to + `zensical`; missing-binary case fails with a one-line + install hint and non-zero exit. +- It does **not** lint schemas. Schemas under + `.context/ingest/schemas/` are guidance; field drift is + caught by reviewers, not by tools. +- It does **not** programmatically enforce confidence bands. + Discipline is rule-driven via `KB-RULES.md`. +- It does **not** write to the five canonical files + (`TASKS.md`, `DECISIONS.md`, `LEARNINGS.md`, + `CONVENTIONS.md`, `CONSTITUTION.md`). Authority is + enforced by path constants in the CLI. diff --git a/.context/ingest/PROMPT.md b/.context/ingest/PROMPT.md new file mode 100644 index 000000000..a4d834a45 --- /dev/null +++ b/.context/ingest/PROMPT.md @@ -0,0 +1,95 @@ +# Auto-Router Prompt + +Paste this prompt into Claude Code (or any agent IDE) to +enter editorial mode by hand. The agent reads `KB-RULES.md` +first, then routes to the matching mode prompt based on what +you describe next. + +This prompt exists so the workflow is drivable **by hand** +even without the mode skills installed. If `/ctx-kb-ingest`, +`/ctx-kb-ask`, `/ctx-kb-site-review`, `/ctx-kb-ground` are +deployed (run `ctx setup` to deploy them); prefer those. +They are wired with `MarkFlagRequired`-equivalent argument +discipline and refuse-on-empty contracts. + +--- + +``` +You are operating the editorial knowledge-ingestion pipeline +for this project. Read these files before doing anything else: + + .context/ingest/KB-RULES.md (the contract; read in full) + .context/ingest/OPERATOR.md (operator framing) + +Then route to one of the four modes based on what I tell you +next: + + - "ingest " + → read .context/ingest/30-INGEST.md and run. + - "ask " + → read .context/ingest/40-ASK.md and run. + - "site review" + → read .context/ingest/50-SITE_REVIEW.md and run. + - "ground" + → read .context/ingest/00-GROUND.md and run + (sources from grounding-sources.md; + prompt me once if the file is empty). + +You are the sole writer of .context/ingest/INBOX.md. Rewrite +it at the start of every pass; never preserve hand-edits. + +If we are in ingest mode, emit the up-front three-line +pass-mode declaration BEFORE topic resolution, per KB-RULES.md +"Pass-mode contract": + + **Pass-mode:** + **Reason:** + **Definition of done:** + +Mid-pass mode-switching is forbidden. If the work no longer +fits the declared mode, abort with a partial closeout and +recommend re-invocation under the correct mode. + +Append one line to .context/ingest/SESSION_LOG.md at every +phase boundary, in the exact shape from KB-RULES.md +"SESSION_LOG line shape": + + [YYYY-MM-DD HH:MM:SS sha= branch=] phase= status= note=<≤120 chars> + +End every pass by writing a closeout under +.context/ingest/closeouts/--closeout.md (where + is one of ingest|ask|site-review|ground|note) with +the required frontmatter: + + --- + sha: + branch: + mode: + pass-mode: + life-stage: + generated-at: + --- + +In topic-page mode, the four-invariant circuit breaker and +the cold-reader rubric apply per KB-RULES.md. Do not claim +`topic-page: produced` or `topic-page: extended` unless ALL +four invariants hold. + +Do not invent claims. Do not promote confidence bands without +independent corroboration. Do not delete kb content; demote +per the demotion policy. Do not lie to the source-coverage +ledger. + +Now: which mode? +``` + +--- + +## When to Update This Prompt + +If `KB-RULES.md` changes substantively, update the routing +list above so the auto-router stays aligned with the +contract. Do **not** quote `KB-RULES.md` content here +verbatim. That is drift waiting to happen. Reference it by +path; let the agent read the latest authoritative copy at +run time. diff --git a/.context/ingest/SESSION_LOG.md b/.context/ingest/SESSION_LOG.md new file mode 100644 index 000000000..ea8260955 --- /dev/null +++ b/.context/ingest/SESSION_LOG.md @@ -0,0 +1,12 @@ +# Session Log + +Ingest skill activity, append-only. One line per +phase-transition (`resolve`, `synthesise`, `closeout`). The +file materialises on first skill run and grows with every +subsequent pass. + +``` +[2026-05-21 20:26:00 sha=8c02b754 branch=feat/cwd-anchored-context] phase=resolve status=done note=VLLM-EXAMPLES landing page; topic slug "vllm"; single source, no discovery +[2026-05-21 20:26:15 sha=8c02b754 branch=feat/cwd-anchored-context] phase=synthesise status=done note=vllm: lede + 4 sections + 5 EV rows (EV-001..EV-005); confidence floor = medium +[2026-05-21 20:26:45 sha=8c02b754 branch=feat/cwd-anchored-context] phase=closeout status=done note=vllm: topic-page deferred (ctx kb site build absent from installed binary); ledger at topic-page-drafted +``` diff --git a/.context/ingest/closeouts/.gitkeep b/.context/ingest/closeouts/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.context/ingest/schemas/contradictions.md b/.context/ingest/schemas/contradictions.md new file mode 100644 index 000000000..4d09a4315 --- /dev/null +++ b/.context/ingest/schemas/contradictions.md @@ -0,0 +1,45 @@ +# Contradictions Schema + +Shape for `.context/kb/contradictions.md`. Each entry records +two or more `EV-###` rows that disagree, the demotion applied +under the demotion policy, and a resolution status. Resolved +contradictions stay in the file as audit trail. + +## Fields + +| Field | Description | +|---------------------|-------------------------------------------------------------------| +| `id` | Stable `C-###` identifier; zero-padded; never renumbered. | +| `opened` | ISO date the contradiction was filed. | +| `evidence` | Two or more `EV-###` IDs that disagree. | +| `summary` | One-line statement of what the rows disagree about. | +| `demotion-applied` | Which claim was demoted to which band, with reason. | +| `resolution-status` | `open` or `resolved`; resolved entries name the winning claim. | + +## Example + +```markdown +## C-007: Widget Scope (Per-Folder vs Per-File) + +- **Opened:** 2026-05-16. +- **Evidence:** EV-042, EV-051. +- **Summary.** EV-042 says a widget is a folder; EV-051 says a + widget is a single `.md` file with frontmatter. +- **Demotion applied.** EV-051 demoted from `medium` to `low`; + EV-042 corroborated by EV-043 holds at `high`. +- **Resolution status:** resolved; folder-shaped (2026-05-16). +``` + +## Rules + +- Every contradiction applies the demotion policy at filing + time; recording the disagreement without demoting an older + claim is a hard anti-pattern. +- Cite at least two `EV-###` IDs; a contradiction is between + evidence rows, not between prose paragraphs. +- Never renumber or delete `EV-###` rows when reconciling; + demote in place and let the row stand as audit trail. +- Resolved entries are kept; deletion erases kb history. +- Open contradictions surface in `/ctx-kb-site-review` and + drive a paired `outstanding-questions.md` row naming both + sides and what evidence would resolve. diff --git a/.context/ingest/schemas/domain-decisions.md b/.context/ingest/schemas/domain-decisions.md new file mode 100644 index 000000000..833939a2f --- /dev/null +++ b/.context/ingest/schemas/domain-decisions.md @@ -0,0 +1,58 @@ +# Domain Decisions Schema + +Shape for `.context/kb/domain-decisions.md`. Kb-scoped +decisions about the subject matter under study, distinct +from project-level `.context/DECISIONS.md`, which records +decisions about how this project is built. + +The two files have different schemas, different write +authority, and different lifecycles. `domain-decisions.md` +is written by `/ctx-kb-ingest`; `DECISIONS.md` is written by +`/ctx-decision-add`. + +## Fields + +| Field | Description | +|-----------------|-------------------------------------------------------------------| +| `id` | Stable `DD-###` identifier; zero-padded; never renumbered. | +| `date` | ISO date the decision was recorded in the kb. | +| `context` | What in the domain prompted the decision; observable facts only. | +| `decision` | The position taken, in one sentence. | +| `rationale` | Why this position over the alternatives that were on the table. | +| `consequence` | What now changes for topic pages, glossary, or downstream claims. | +| `supporting-ev` | Comma-separated `EV-###` references that ground the decision. | + +## Example + +```markdown +## DD-004: Treat Widget Bundles as Opaque to the Consumer + +- **Date:** 2026-05-16. +- **Context.** Two upstream sources disagreed on whether a + widget consumer should inspect bundle internals (EV-042, + EV-051); the disagreement was resolved in `C-007`. +- **Decision.** The kb treats widget bundles as opaque from + the consumer's perspective; inspection is an implementation + concern of the producer. +- **Rationale.** Opacity preserves the producer's freedom to + evolve internals without breaking downstream call sites. +- **Consequence.** The `widget-composition` topic page is + rewritten to lead with opacity; the glossary's `widget` + entry gains an `opacity` cross-reference. +- **Supporting EV:** EV-042, EV-043. +``` + +## Rules + +- Do not confuse this with `.context/DECISIONS.md`; that file + records project-build decisions and is owned by a different + CLI surface. +- A domain decision is a kb-scoped position on the subject + matter; it cites `EV-###` rows, not commit SHAs. +- Every entry cites at least one supporting `EV-###`; a + domain decision without evidence belongs in + `outstanding-questions.md` instead. +- Decisions are append-only and never renumbered; supersede + in place by recording a new `DD-###` that cites the prior. +- Do not write to `.context/DECISIONS.md` from + `/ctx-kb-ingest`; the authority boundary is strict. diff --git a/.context/ingest/schemas/evidence-index.md b/.context/ingest/schemas/evidence-index.md new file mode 100644 index 000000000..276925ff8 --- /dev/null +++ b/.context/ingest/schemas/evidence-index.md @@ -0,0 +1,45 @@ +# Evidence Index Schema + +Shape for `.context/kb/evidence-index.md`. Each row is one +atomic claim, minted by `/ctx-kb-ingest` and never renumbered. +The evidence index is the spine of the kb: glossary entries, +contradictions, domain-decisions, timeline events, and topic +pages all cite back to specific `EV-###` IDs here. + +Rows are append-only. To retire a stale claim, demote its +`confidence` band per the demotion policy in `KB-RULES.md`. +Never delete the row. + +## Fields + +| Column | Description | +|--------------|------------------------------------------------------------| +| `id` | Stable `EV-###` identifier; zero-padded; never renumbered. | +| `claim` | One-sentence atomic claim; declarative, source-backed. | +| `source` | Short name from `source-map.md` (e.g. `CURSOR-HOOKS`). | +| `locator` | Line range, timestamp, anchor, or symbol within source. | +| `sha` | Optional short SHA; required only for in-repo citations. | +| `confidence` | One of `high`, `medium`, `low`, `speculative`. | +| `tags` | Comma-separated; `evidence-only` is additive (mode tag). | +| `occurred` | Optional ISO date the underlying event occurred. | +| `extracted` | ISO date the row was extracted into the kb. | + +## Example + +| id | claim | source | locator | sha | confidence | tags | occurred | extracted | +|--------|--------------------------------------------------------------------------------------|--------------|-----------|---------|------------|-----------------------|------------|------------| +| EV-042 | The widget contract pins its schema version in the first frontmatter line of `SKILL.md`. | WIDGET-SPEC | §Schema | | high | widget, contract | | 2026-05-16 | + +## Rules + +- Append-only. Renumbering or deleting an `EV-###` row is a + hard anti-pattern; demote in place per the demotion policy. +- Rows tagged `evidence-only` come from `evidence-only`-mode + passes and have no topic-page prose backing them yet; + citing one requires re-reading the underlying source. +- In-repo citations include `sha:` set to the short SHA at + extraction time. Out-of-repo citations omit `sha:`. +- A claim whose only backing is a transcript or meeting note + is `confidence: low` until a written source reinforces it. +- Never invent an `EV-###` to make a topic page look complete; + every cited ID must resolve to a row in this file. diff --git a/.context/ingest/schemas/glossary.md b/.context/ingest/schemas/glossary.md new file mode 100644 index 000000000..95e193f7d --- /dev/null +++ b/.context/ingest/schemas/glossary.md @@ -0,0 +1,54 @@ +# Glossary Schema + +Shape for `.context/kb/glossary.md`. Canonical terms scoped to +this kb's `## Scope` statement. Each entry is a declarative +definition with a confidence band and at least one +`evidence-index.md` row backing it. + +Glossary confidence reflects definition quality ("do we agree +on what this term means?"), independent of whether the cited +evidence rows themselves carry `high` or `low` bands. + +## Fields + +| Field | Description | +|-----------------|--------------------------------------------------------------| +| `term` | Canonical term as a level-2 heading; lowercase preferred. | +| `aliases` | Optional comma-separated list of synonyms or short forms. | +| `definition` | One paragraph; declarative; no hedging beyond the band. | +| `confidence` | One of `high`, `medium`, `low`, `speculative`. | +| `evidence` | Comma-separated `EV-###` references; at least one required. | +| `related-terms` | Optional links to other glossary entries in this file. | + +## Example + +```markdown +## widget + +**Aliases:** widget-the-primitive, the unit. + +**Definition.** A widget is the smallest packaging boundary in +the system: a folder containing `SKILL.md` plus optional +`scripts/`, `references/`, and `assets/` subdirectories. + +**Confidence:** high. + +**Evidence:** EV-042, EV-043. + +**See also:** [widget contract](#widget-contract). +``` + +## Rules + +- Do not define a term outside the kb's declared `## Scope` in + `.context/kb/index.md`; out-of-scope terms belong elsewhere. +- Every entry cites at least one `EV-###`; ungrounded terms + open an `outstanding-questions.md` row instead. +- Aliases are additive, not renamings; the canonical term is + the heading and never changes once published. +- Glossary confidence tracks definitional consensus, not the + bands of the cited evidence rows; do not mechanically + downgrade a definition just because its evidence was demoted. +- Cross-reference related terms within the file; do not link + to topic pages from the definition body (use the topic page's + `## Related concepts in this kb` block for that direction). diff --git a/.context/ingest/schemas/outstanding-questions.md b/.context/ingest/schemas/outstanding-questions.md new file mode 100644 index 000000000..c38ca4589 --- /dev/null +++ b/.context/ingest/schemas/outstanding-questions.md @@ -0,0 +1,51 @@ +# Outstanding Questions Schema + +Shape for `.context/kb/outstanding-questions.md`. Each entry +is an open gap the kb has not yet resolved. Stable `Q-###` +IDs; newest first; never renumber. + +Questions block the promotion of any claim that depends on +them: a topic page with an open `Q-###` cited in its +`## Open questions` section cannot ship at `confidence: high`. + +## Fields + +| Field | Description | +|----------------------------|------------------------------------------------------------| +| `id` | Stable `Q-###` identifier; zero-padded; never renumbered. | +| `opened` | ISO date the question was opened. | +| `question` | One-sentence open question, phrased as a question. | +| `why-it-matters` | Why answering this changes a topic page or a band. | +| `what-evidence-would-resolve` | The shape of evidence that would close this entry. | +| `related-ev` | Optional comma-separated `EV-###` references. | + +## Example + +```markdown +## Q-009: Does the Widget Contract Permit Nested Skills? + +- **Opened:** 2026-05-16. +- **Question.** Does the widget folder shape allow a nested + `SKILL.md` inside a subfolder, or is one skill per folder + the hard limit? +- **Why it matters.** The `widget-composition` topic page + cannot promote above `low` until this is answered; the page + currently hedges with both readings. +- **What evidence would resolve.** A statement in the upstream + spec, or a worked example from the vendor's own repo. +- **Related EV:** EV-042, EV-043. +``` + +## Rules + +- Opening a question without stating what evidence would + resolve it is a hard anti-pattern; the field is required. +- Phrase the question as an interrogative; declarative + statements belong in `evidence-index.md` with a band. +- `low`-confidence topic-page content ships only when paired + with a matching `outstanding-questions.md` entry; the entry + is the license to ship the hedge. +- Closed questions are not deleted; they record what was once + open and how it resolved. +- Do not reuse a retired `Q-###` ID for a new question; the + ID space is monotonic. diff --git a/.context/ingest/schemas/relationship-map.md b/.context/ingest/schemas/relationship-map.md new file mode 100644 index 000000000..475f823b5 --- /dev/null +++ b/.context/ingest/schemas/relationship-map.md @@ -0,0 +1,41 @@ +# Relationship Map Schema + +Shape for `.context/kb/relationship-map.md`. Cross-topic and +cross-source relationships recorded as rows keyed on topic +slug or `EV-###` ID. The relationship map is the kb's edge +list; nodes are topic pages and evidence rows. + +Relationships key on *slug + EV-### ID*, never on file path, +so folder reorganisations and lazy sub-page splits do not +invalidate the graph. + +## Fields + +| Column | Description | +|-----------|--------------------------------------------------------------------------| +| `from` | Originating topic slug or `EV-###` ID. | +| `to` | Destination topic slug or `EV-###` ID. | +| `kind` | Relationship kind (`depends-on`, `refines`, `contradicts`, `supersedes`).| +| `summary` | One-line description of the relationship. | + +## Example + +| from | to | kind | summary | +|-------------|-----------|-------------|----------------------------------------------------------| +| widgets | EV-042 | depends-on | Widget topic page leans on EV-042 for its scope claim. | + +## Rules + +- Key on slug and `EV-###`, never on file path; lazy + sub-page splits and folder moves must not break edges. +- `kind` is drawn from a controlled vocabulary; introducing + a new kind requires a `domain-decisions.md` entry naming + the rationale. +- `contradicts` edges pair with a row in + `contradictions.md`; the relationship map is the index, + the contradictions file is the body. +- `supersedes` edges pair with the superseded row's demotion + in `evidence-index.md`; never delete the superseded row. +- Self-edges (a slug pointing to itself, or an `EV-###` + pointing to itself) are not meaningful and are rejected + on write. diff --git a/.context/ingest/schemas/session-log.md b/.context/ingest/schemas/session-log.md new file mode 100644 index 000000000..cfd78b143 --- /dev/null +++ b/.context/ingest/schemas/session-log.md @@ -0,0 +1,52 @@ +# Session Log Schema + +Shape for `.context/ingest/SESSION_LOG.md`. One line per +phase boundary during a `/ctx-kb-ingest` pass. The session +log is mid-flight working memory: it records the agent's +movement through the pass so a partial closeout can name +exactly where the work stopped. + +The session log is not the recall artifact. The handover +(`.context/handovers/-.md`) is the sole +authoritative source for "what happened in the last +session"; the session log is operational telemetry for the +pass currently in flight. + +## Fields + +The single-line format encodes every field positionally: + +``` +[YYYY-MM-DD HH:MM:SS sha= branch=] phase= status= note=<≤120 chars> +``` + +| Field | Description | +|------------|-------------------------------------------------------------------| +| timestamp | `YYYY-MM-DD HH:MM:SS` local time at the phase boundary. | +| `sha` | 7-char git short SHA of HEAD at the boundary. | +| `branch` | Current symbolic ref, or `detached` if HEAD is not on a branch. | +| `phase` | One of `resolve`, `synthesise`, `reconcile`, `closeout`. | +| `status` | One of `done`, `partial`, `blocked`. | +| `note` | Free text, at most 120 characters; no newlines. | + +## Example + +``` +[2026-05-16 14:32:08 sha=88d5287 branch=main] phase=synthesise status=done note=widget topic page extended; cited EV-042..EV-046 +``` + +## Rules + +- Do not read `SESSION_LOG.md` on session start; it is + mid-flight working memory, not a recall artifact. The + handover is the recall surface. +- Append-only; never edit a past line. Corrections go on a + new line with a `note=` explaining the prior entry. +- The four phase tokens are the closed set; introducing a + new phase requires a `domain-decisions.md` entry and a + spec update. +- Keep notes under the 120-character budget; longer + observations belong in the closeout body, not here. +- Lines without `sha=` and `branch=` are malformed; doctor + advisory flags them. Provenance is non-optional on this + operational artifact. diff --git a/.context/ingest/schemas/source-coverage.md b/.context/ingest/schemas/source-coverage.md new file mode 100644 index 000000000..9ce5d8261 --- /dev/null +++ b/.context/ingest/schemas/source-coverage.md @@ -0,0 +1,65 @@ +# Source Coverage Schema + +Shape for `.context/kb/source-coverage.md`. Per-source +completeness ledger maintained by `/ctx-kb-ingest`. Each row +tracks **coverage state**, not just existence: a source can be +known to the kb at any of several stages between discovery and +comprehensive understanding. + +This file is the canonical answer to *"what is incomplete?"* +Distinct from `source-map.md` (which records WHAT a source +is); the ledger records HOW COMPLETE the work against it is. + +## Fields + +| Column | Description | +|---------------|------------------------------------------------------------------------| +| `Source` | Short name from `source-map.md`. | +| `Topic` | Kb topic slug, or `n/a` if no topic page applies (e.g. `triage` runs). | +| `State` | One of the state-machine values listed in the transitions table. | +| `EV coverage` | Range like `EV-018..EV-034`, comma list, or `none`. | +| `Residue` | One-line free text describing what was deliberately left out. | +| `Next action` | Exact resumption invocation (e.g. `/ctx-kb-ingest (resume ...)`). | +| `Updated` | ISO date the row was last written. | + +## Allowed Transitions + +| state | next states | +|-------------------------|----------------------------------------------------------------------------------------------| +| `discovered` | `admitted`, `skipped` | +| `admitted` | `highlights-extracted`, `partially-ingested`, `topic-page-drafted`, `comprehensive` | +| `highlights-extracted` | `partially-ingested`, `topic-page-drafted`, `comprehensive` | +| `partially-ingested` | `topic-page-drafted`, `comprehensive` | +| `topic-page-drafted` | `comprehensive` | +| `comprehensive` | terminal until source updates | +| `superseded` | terminal | +| `skipped` | terminal until scope changes | + +Backward steps (e.g. `comprehensive → highlights-extracted`) +require an explicit `superseded` step first; otherwise the +transition is illegal and `AdvanceLedger` refuses it. + +## Example + +| Source | Topic | State | EV coverage | Residue | Next action | Updated | +|--------------|---------|------------------------|-----------------|------------------------------------|------------------------------------------|------------| +| WIDGET-SPEC | widgets | highlights-extracted | EV-042..EV-051 | composition examples, error cases | /ctx-kb-ingest widgets (resume topic-page) | 2026-05-16 | + +## Rules + +- Lying to the ledger is a hard anti-pattern; record the + state honestly even when it means writing + `topic-page-drafted` instead of `comprehensive`. +- Illegal transitions are refused at write time; backing out + of `comprehensive` requires an explicit `superseded` step + first, naming the superseder. +- `comprehensive` requires cited bands ≥ `medium`, no + `TBD-cite` markers, and a passing cold-reader rubric; + topic-page-drafted is the right state when any of those + three conditions fail. +- `Next action` is the exact resumption invocation a future + agent should run; free prose like "finish later" defeats + the ledger's purpose. +- Every pass that touches a source updates its row *before* + writing the closeout; doctor advisory flags rows whose + `Updated` predates the file's last-modified time. diff --git a/.context/ingest/schemas/source-map.md b/.context/ingest/schemas/source-map.md new file mode 100644 index 000000000..41ce65cfe --- /dev/null +++ b/.context/ingest/schemas/source-map.md @@ -0,0 +1,45 @@ +# Source Map Schema + +Shape for `.context/kb/source-map.md`. One row per source +cited from `evidence-index.md`. Records what a source *is* +and whether it was admitted against the kb's scope. + +Distinct from `source-coverage.md`: this file records WHAT a +source is (identity, locator, admission); the coverage ledger +records HOW COMPLETE the kb's work against it is (state +machine over discovered → comprehensive). + +## Fields + +| Column | Description | +|------------------------|--------------------------------------------------------------| +| `short-name` | Stable identifier used in `evidence-index.md` cites. | +| `kind` | One of `transcript`, `code`, `doc`, `url`, `mcp`, `kb`. | +| `locator` | URL, repo path, MCP resource ID, or kb pointer. | +| `admission-status` | `admitted`, `rejected`, or `pending`. | +| `admission-rationale` | One-sentence reason the source was admitted or rejected. | +| `dated` | Optional ISO date the source itself is dated. | + +## Example + +| short-name | kind | locator | admission-status | admission-rationale | dated | +|--------------|------|----------------------------------------|------------------|--------------------------------------------------|------------| +| WIDGET-SPEC | url | `https://example.org/widget/v2/spec` | admitted | Canonical upstream spec; in scope per index.md. | 2026-05-16 | + +## Rules + +- Short names are stable; rename via alias-add, not by + rewriting in place. Existing `EV-###` rows pin to the + original short name. +- Admission is against the kb's declared `## Scope` in + `.context/kb/index.md`; rejected sources stay in the map + with their rationale as audit trail. +- `kind: kb` marks federation into another kb (per the + KB-of-KBs organizing principle); the locator points at + another `.context/kb/` directory. +- A source can appear here at `admission-status: pending` + before it is admitted; the coverage ledger picks up only + admitted sources. +- Do not conflate this file with `source-coverage.md`; the + identity-vs-progress split is load-bearing for the + state-machine ledger. diff --git a/.context/ingest/schemas/timeline.md b/.context/ingest/schemas/timeline.md new file mode 100644 index 000000000..bc46d7c82 --- /dev/null +++ b/.context/ingest/schemas/timeline.md @@ -0,0 +1,49 @@ +# Timeline Schema + +Shape for `.context/kb/timeline.md`. Dated events worth +recording in the kb, oldest first. Each entry pins a real +occurrence in the domain to `EV-###` rows so the timeline +can be cross-checked against its sources. + +The timeline is not a changelog of kb activity; that lives +in `SESSION_LOG.md` and the closeouts under +`.context/ingest/closeouts/`. The timeline records events +the kb is studying, not events the kb performed. + +## Fields + +| Field | Description | +|------------------|----------------------------------------------------------------| +| `date` | ISO date the event occurred (not the date it was ingested). | +| `event` | One-paragraph description of what happened. | +| `source-ev` | Comma-separated `EV-###` references that ground the event. | +| `related-topics` | Optional topic slugs touched by the event. | + +## Example + +```markdown +## 2026-05-16: Widget Contract v2 Published + +- **Event.** The upstream maintainer published v2 of the + widget contract, adding the `compatibility` frontmatter + field and tightening the `description` length budget. +- **Source EV:** EV-042, EV-051. +- **Related topics:** widgets, widget-composition. +``` + +## Rules + +- Pin the event to its `occurred:` date, not the date it was + ingested; the `extracted:` field on the evidence row + records the latter. +- Cite at least one `EV-###`; an entry without source + backing is speculation, not a timeline event. +- Speculation about future events belongs in + `outstanding-questions.md`, not here; filing a dated event + for something the kb merely expects to happen is a hard + anti-pattern. +- Newer events are appended below older ones; do not + reorder once published. +- Cross-reference related topic slugs so the timeline can be + scanned for blast radius when a single event touches + multiple pages. diff --git a/.context/kb/evidence-index.md b/.context/kb/evidence-index.md new file mode 100644 index 000000000..5ba610ec0 --- /dev/null +++ b/.context/kb/evidence-index.md @@ -0,0 +1,14 @@ +# Evidence Index + +Append-only ledger of atomic claims minted by `/ctx-kb-ingest`. +Shape lives in `../ingest/schemas/evidence-index.md`. Every +`EV-###` cited from a topic page, glossary entry, timeline event, +contradiction, or domain-decision MUST resolve to a row here. + +| id | claim | source | locator | sha | confidence | tags | occurred | extracted | +|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------|----------------------|-----|------------|----------------------------|----------|------------| +| EV-001 | vLLM organizes its public examples landing page into thirteen named categories: basic, generate, pooling, speech_to_text, features, reasoning, tool_calling, applications, rl, deployment, ray_serving, disaggregated, observability. | VLLM-EXAMPLES | category table | | high | vllm, taxonomy | | 2026-05-21 | +| EV-002 | The vLLM examples landing page contains no inline code; each of its thirteen categories links to a directory under github.com/vllm-project/vllm/tree/main/examples/, and the example files live next to source. | VLLM-EXAMPLES | category table links | | high | vllm, docs-structure | | 2026-05-21 | +| EV-003 | vLLM's example taxonomy operates on two dimensions simultaneously without labeling them: capability (generate, pooling, reasoning, tool_calling, speech_to_text) and deployment context (basic, deployment, ray_serving, disaggregated, observability), with crosscut entries (features, applications, rl). | VLLM-EXAMPLES | category table | | medium | vllm, taxonomy, analysis | | 2026-05-21 | +| EV-004 | The vLLM examples landing page contains no numbered workflows, no procedural recipes, and no "if you want X follow steps 1..N" framing; categories are presented as a flat bulleted index. | VLLM-EXAMPLES | full page | | high | vllm, docs-structure, contrast | | 2026-05-21 | +| EV-005 | The vLLM examples landing page introduces its taxonomy with the verbatim line "vLLM's examples are organized into the following categories", presenting taxonomy-as-taxonomy with no entry-point guidance. | VLLM-EXAMPLES | intro paragraph | | high | vllm, docs-structure | | 2026-05-21 | diff --git a/.context/kb/index.md b/.context/kb/index.md new file mode 100644 index 000000000..d435aa289 --- /dev/null +++ b/.context/kb/index.md @@ -0,0 +1,63 @@ +# Knowledge Base + +The kb is the project's evidence-tracked knowledge surface. +It lives at `.context/kb/` and is governed by the editorial +contract in `../ingest/KB-RULES.md`. Topic pages live under +`topics//index.md`. Cross-cutting artifacts +(`glossary.md`, `domain-decisions.md`, `contradictions.md`, +`outstanding-questions.md`, `evidence-index.md`, +`source-coverage.md`, `timeline.md`) live alongside this +file. + +--- + +## Scope + +This kb captures design lessons and operational patterns the +ctx project has learned from adjacent / inspirational AI +infrastructure projects, including vLLM, Claude Code, +zensical, GitNexus, and similar single-purpose tools that +manage local project state. In scope: serving architectures, +CLI ergonomics, doc/recipe patterns, hook and skill surfaces, +and editorial workflows that ctx can lift or contrast against. +Deliberately NOT in scope: production deployment guides for +any of those tools (we are studying them, not running them), +end-user tutorials, or marketing-shaped overviews. Audience: +Volkan (project owner) and the `/ctx-kb-ingest`, +`/ctx-kb-ask`, `/ctx-kb-ground`, `/ctx-kb-site-review`, +`/ctx-kb-note` skills. + +--- + +## Topics + + +- [`vllm`](topics/vllm/) + + +--- + +## Conventions + +This kb is governed by: + +- **`../ingest/KB-RULES.md`** is the editorial contract: + pass-mode discipline, topic-page circuit breaker, + source-coverage state machine, topic-adjacency pre-flight, + cold-reader rubric, life-stage check, evidence discipline, + confidence bands, demotion policy, closeout shape. +- **`../ingest/schemas/`** holds field-level shape for each + cross-cutting artifact (`evidence-index.md`, `glossary.md`, + `contradictions.md`, `outstanding-questions.md`, + `domain-decisions.md`, `timeline.md`, `source-map.md`, + `source-coverage.md`, `relationship-map.md`). Each schema + ships shape, not content. +- **`../../specs/kb-editorial-pipeline.md`** is the full spec, + including the failure-analysis section and the v1 + non-goals. Read this when you need to understand *why* a + rule exists, not just *what* it says. + +The mode skills (`/ctx-kb-ingest`, `/ctx-kb-ask`, +`/ctx-kb-site-review`, `/ctx-kb-ground`, `/ctx-kb-note`) are +the canonical writers. Hand-edits to kb files are an escape +hatch, not the primary path. diff --git a/.context/kb/source-coverage.md b/.context/kb/source-coverage.md new file mode 100644 index 000000000..9c70b00e9 --- /dev/null +++ b/.context/kb/source-coverage.md @@ -0,0 +1,13 @@ +# Source Coverage + +Per-source completeness ledger maintained by `/ctx-kb-ingest`. +Each row tracks coverage state, not just existence: a source can +be at any of several stages between discovery and comprehensive +understanding. Shape and state-machine transitions live in +`../ingest/schemas/source-coverage.md`. + +This file is the canonical answer to *"what is incomplete?"* + +| Source | Topic | State | EV coverage | Residue | Next action | Updated | +|----------------|-------|----------------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|------------| +| VLLM-EXAMPLES | vllm | topic-page-drafted | EV-001..EV-005 | Per-category GitHub directories (basic/, generate/, pooling/, speech_to_text/, features/, reasoning/, tool_calling/, applications/, rl/, deployment/, ray_serving/, disaggregated/, observability/) were not fetched. Page-level only. | `/ctx-kb-ingest https://github.com/vllm-project/vllm/tree/main/examples/deployment vllm` (or any single category to deepen the topic) | 2026-05-21 | diff --git a/.context/kb/source-map.md b/.context/kb/source-map.md new file mode 100644 index 000000000..e0133fbde --- /dev/null +++ b/.context/kb/source-map.md @@ -0,0 +1,12 @@ +# Source Map + +One row per source cited from `evidence-index.md`. Records what +each source IS (identity, locator, admission). Distinct from +`source-coverage.md`, which records HOW COMPLETE the kb's work +against each source is. + +Shape lives in `../ingest/schemas/source-map.md`. + +| short-name | kind | locator | admission-status | admission-rationale | dated | +|----------------|------|----------------------------------------------------|------------------|--------------------------------------------------------------------------------------------------------------------|------------| +| VLLM-EXAMPLES | url | `https://docs.vllm.ai/en/latest/examples/` | admitted | Public documentation page from an adjacent AI-infrastructure project; in scope per kb/index.md (`Scope` paragraph). | 2026-05-21 | diff --git a/.context/kb/topics/.gitkeep b/.context/kb/topics/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.context/kb/topics/vllm/index.md b/.context/kb/topics/vllm/index.md new file mode 100644 index 000000000..346b58d1f --- /dev/null +++ b/.context/kb/topics/vllm/index.md @@ -0,0 +1,120 @@ +--- +Subject: vLLM examples landing page as a substrate for ctx recipe-surface lessons +Last verified: 2026-05-21 +Author: agent-ingested +Confidence: medium +sha: 8c02b754 +branch: feat/cwd-anchored-context +--- + +# vLLM + +vLLM is a Python-based, high-throughput LLM serving and inference +engine that publishes its example surface as a thirteen-category +index of GitHub directory pointers rather than an in-docs recipe +library [EV-001][EV-002]. This kb tracks vLLM as a *contrastive +study*, not as a tool ctx will use: vLLM's example taxonomy is +useful precisely because it is the opposite of ctx's prose-rich +recipe surface — studying the contrast surfaces design choices +ctx has made implicitly. The open question driving this topic: +what, if anything, from vLLM's example surface is worth lifting +into ctx's recipe library or doc structure? + +--- + +## What It Is + +The vLLM examples landing page at `docs.vllm.ai/en/latest/examples/` +is a single-page index that groups example code into thirteen named +categories: `basic`, `generate`, `pooling`, `speech_to_text`, +`features`, `reasoning`, `tool_calling`, `applications`, `rl`, +`deployment`, `ray_serving`, `disaggregated`, `observability` [EV-001]. + +The page itself contains no inline code. Each category resolves to a +GitHub directory under `github.com/vllm-project/vllm/tree/main/examples/`, +and the example files live next to vLLM's source tree, not under +its docs [EV-002]. The page is a catalogue of pointers; the +examples are versioned with the code. + +The taxonomy operates on two dimensions simultaneously, presented +as a single flat list [EV-003]: + +- **Capability dimension** — what the engine can do: `generate` + (text generation, including multimodal), `pooling` (embedding, + classification, scoring, reward), `speech_to_text` (transcription, + translation, real-time audio), `reasoning`, `tool_calling`. +- **Deployment dimension** — how the engine is operated: + `basic` (minimal offline + serving), `deployment` (production), + `ray_serving` (scalable serving via Ray), `disaggregated` (KV + cache connectors, failure recovery), `observability` (metrics, + logging, tracing, dashboards). +- **Crosscut categories** — `features` (per-capability vLLM features + like prefix caching, speculative decoding, LoRA), `applications` + (chatbots, RAG), `rl` (reinforcement learning). + +The page contains no numbered workflows, no procedural recipes, +no "if you want X, follow steps 1..N" framing [EV-004]. Its +intro reads, verbatim, *"vLLM's examples are organized into the +following categories"* — taxonomy presented as taxonomy, no +narration about which entry-point a new user should pick [EV-005]. + +--- + +## Why This KB Cares + +ctx's recipe surface at `docs/recipes/` is the structural opposite +of vLLM's: a small set of named procedural workflows (`activating-context.md`, +`build-a-knowledge-base.md`, etc.), each containing prose, command +sequences, and rationale. The contrast surfaces three choices +ctx has made implicitly that are worth examining: + +1. **Prose vs index.** ctx assumes the reader needs guidance through + a workflow; vLLM assumes the reader needs only a pointer to + versioned code. Different audiences (ctx: humans + AI agents + reading recipes as instructions; vLLM: ML engineers reading + examples as templates to adapt). +2. **Docs-tree vs code-tree.** ctx's recipes live in `docs/recipes/`, + physically separated from `internal/`. vLLM's examples live + in `examples/` next to source, with the docs site just indexing + them. Both approaches have a trade-off: docs-tree lets prose + evolve independently of code; code-tree keeps examples + compile-fresh and version-pinned. +3. **Taxonomy depth.** vLLM ships thirteen categories despite the + page lacking any guided entry point; the breadth IS the value + when each entry is itself a directory of working code. ctx + currently has a flatter recipe surface; if it grows beyond ten + recipes, the question becomes whether to keep the flat list or + adopt a vLLM-style category split. + +The kb tracks this topic because future ctx design discussions +about recipe surface, doc/code separation, and example versioning +will benefit from having a worked contrastive reference. It also +seeds the broader pattern of *studying adjacent AI-infrastructure +tools' doc strategies* as a recurring kb activity. + +--- + +## Sources and Further Reading + +- `VLLM-EXAMPLES`: vLLM's examples landing page on the public docs + site; canonical index of category pointers. Source recorded in + `../../source-map.md`; rows extracted on 2026-05-21 in + `../../evidence-index.md`. + +This pass ingested only the landing page itself, not the per-category +GitHub directories. Per-category deep dives (e.g. what does +`deployment/` actually demonstrate beyond a directory name?) are +deferred. A future pass with discovery enabled could follow the +thirteen GitHub links and mint per-category EV rows; that work +would land as sub-pages under this topic, not as a single mega-page. + +--- + +## Related Concepts in This KB + +`none surfaced` — this is the first topic in this kb. The +topic-adjacency pre-flight ran against an empty +`source-coverage.md` and found nothing to surface. Future passes +that ingest comparable doc structures (Claude Code's recipe +surface, zensical's example tree, GitNexus's docs layout) should +be cross-linked here. diff --git a/.context/typography.md b/.context/typography.md new file mode 100644 index 000000000..d503839da --- /dev/null +++ b/.context/typography.md @@ -0,0 +1,240 @@ +# Typography and Document Shape + +Conventions for agents and contributors editing this repo. Loaded by the +agent alongside `CONVENTIONS.md`; the linter scripts under `hack/` enforce +the hard rules on every edit so the agent should not need to be told +twice. + +## Headings + +### Title Case for Every Heading + +All Markdown headings, at every depth, use Title Case. Minor words +(`a`, `an`, `the`, `and`, `or`, `but`, `of`, `for`, `in`, `on`, `to`, +`with`) stay lowercase except when they open the heading. + +Enforced by [`hack/title-case-headings.py`](https://github.com/ActiveMemory/ctx/blob/main/hack/title-case-headings.py). + +Yes: + +```markdown +## Build a Knowledge Base +### When to Use a Skill +#### Folding Closeouts Into the Handover +``` + +No: + +```markdown +## Build a knowledge base +### When to use a skill +#### Folding closeouts into the handover +``` + +### Code Spans in Headings Stay as Written + +Backticked identifiers inside a heading keep their literal casing. +Title Case applies to the prose, not the code. + +```markdown +## `ctx init` and the Bootstrap Path +### Why `internal/write/handover` Is the Sole Writer +``` + +## Inline Code + +### `ctx` Is Always Monotype + +The tool name `ctx` is wrapped in backticks every time it appears in +prose, unless it is part of a heading where it is the first word and a +backtick would distort the section anchor. The same rule applies to +sibling tool names (`gitnexus`, `gemini-search`) and to command names, +flags, paths, file names, package paths, and configuration keys. + +Yes: + +```markdown +Run `ctx status` to inspect the working tree. The `.context/` directory +lives at the project root. Configure `ctx.handover.subdir` in `.ctxrc`. +``` + +No: + +```markdown +Run ctx status to inspect the working tree. The .context/ directory +lives at the project root. +``` + +### Exception for Prose Where the Brand Is the Subject + +Marketing taglines that treat the project name as a brand may drop the +backticks. Reserve this for landing pages and the manifesto; every other +surface uses the monotype form. + +## Punctuation + +### No Em-Dashes, No En-Dashes + +Prose uses ASCII only. Hyphens (`-`) connect compound modifiers; colons, +parentheses, or sentence breaks carry the load that an em-dash would. + +Enforced by [`hack/detect-ai-typography.sh`](https://github.com/ActiveMemory/ctx/blob/main/hack/detect-ai-typography.sh). +The script flags U+2013 (`–`) and U+2014 (`—`) on sight; both are +heuristic signals of AI prose that was not human-reviewed. + +Yes: + +```markdown +The handover is the sole authoritative recall artifact: the next session +reads it before anything else. +``` + +No: + +```markdown +The handover is the sole authoritative recall artifact — the next session +reads it before anything else. +``` + +### No Smart Quotes + +Use straight ASCII quotes (`"` and `'`). Curly quotes (U+2018, U+2019, +U+201C, U+201D) are flagged by the detector and auto-fixed by +[`hack/fix-smart-quotes.sh`](https://github.com/ActiveMemory/ctx/blob/main/hack/fix-smart-quotes.sh). + +### Space-Padded Double Hyphens Are Not a Dash + +The detector flags `" -- "` as an AI-prose tell. CLI flag examples +(`--summary`, `git rebase --no-edit`) are unaffected because they have +no surrounding spaces. Table separators (`| -- |`) are also fine. + +### No Quad Backticks + +Triple backticks (` ``` `) are the project maximum for code fences. AI +often emits ` ```` ` (quad) which the Zensical renderer does not +support. The detector flags these. + +## Code Fences + +Every fence carries a language tag (`bash`, `go`, `markdown`, `yaml`, +`text`, `json`). Untagged fences render without syntax highlighting and +should not appear in the docs. + +Open with `` ```bash `` (tagged) rather than bare `` ``` `` (untagged): + +```bash +ctx status +``` + +Triple backticks are the project maximum. Quad backticks (`` ```` ``) +are an AI artifact and do not render in Zensical. When you need to show +a fenced block inside another, use a four-space indent for the outer +demonstration rather than nesting fences. + +## Document Header (Docs Site) + +### Frontmatter Shape + +Every `docs/` Markdown file opens with YAML frontmatter that carries the +SPDX banner as YAML comments, then the `title:` and `icon:` fields: + +```markdown +--- +# / ctx: https://ctx.ist +# ,'`./ do you remember? +# `.,'\ +# \ Copyright 2026-present Context contributors. +# SPDX-License-Identifier: Apache-2.0 + +title: Title in Title Case +icon: lucide/ +--- +``` + +The `title:` value uses Title Case. The `icon:` value selects from the +Lucide icon set (the Material for MkDocs default), referenced as +`lucide/`. Browse available icons at +[lucide.dev/icons](https://lucide.dev/icons). + +### Banner Image as the First Body Line + +The first non-frontmatter line is the project banner image, with a +relative path from the file's location: + +```markdown +![ctx](../images/ctx-banner.png) +``` + +Top-level pages (`docs/index.md`) use `images/ctx-banner.png`; pages one +directory deep use `../images/ctx-banner.png`. The banner anchors every +page visually and gives the rendered output a consistent opening. + +### Optional `# H1` Page Title + +Material for MkDocs renders the `title:` frontmatter as the page title. +A leading `# H1` is optional and used when the in-body title differs +from the navigation title (longer, more descriptive, or with backticks). + +## Document Structure (Recipes) + +Recipe pages under `docs/recipes/` follow a shared narrative arc that +makes them comparable side by side. Not every page hits every section, +but the order is fixed: + +```markdown +## The Problem +## TL;DR +## The Solution (or: ## Setup, ## Process) +## When to Use +## When NOT to Use +## Quality Checklist (optional) +``` + +The `## The Problem` section is the strongest pattern: 34 of 48 recipes +open this way. Reach for it before inventing a custom opening. + +## Admonitions + +Use Material for MkDocs admonition syntax. Five variants are in active +use across the corpus: + +```markdown +!!! note + Neutral context the reader should not miss. + +!!! tip + Optional improvement or shortcut. + +!!! warning + Behavior that may surprise; recoverable. + +!!! danger + Destructive or irreversible operation. + +!!! info + Background detail; safe to skip. +``` + +Admonition titles use Title Case when given: + +```markdown +!!! tip "Prefer Skills Over Raw Commands" + ... +``` + +## Enforcement Summary + +| Rule | Enforced By | +|------|-------------| +| Title Case headings | [`hack/title-case-headings.py`](https://github.com/ActiveMemory/ctx/blob/main/hack/title-case-headings.py) | +| No em-dashes / en-dashes / smart quotes / quad backticks / padded `--` | [`hack/detect-ai-typography.sh`](https://github.com/ActiveMemory/ctx/blob/main/hack/detect-ai-typography.sh) | +| Auto-fix curly quotes | [`hack/fix-smart-quotes.sh`](https://github.com/ActiveMemory/ctx/blob/main/hack/fix-smart-quotes.sh) | + +Run the detector before submitting docs changes: + +```bash +./hack/detect-ai-typography.sh docs +``` + +It exits non-zero on any hit and prints file + line number so an editor +jump-to-line takes you straight to the offender. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e121b1b10..b3db00e85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,3 +114,31 @@ jobs: - name: Type-check production code working-directory: editors/vscode run: npx tsc --noEmit -p tsconfig.ci.json + + shellcheck: + name: ShellCheck (embedded *.sh) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install shellcheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + + - name: Run shellcheck + run: ./hack/lint-shellcheck.sh + + powershell: + name: PSScriptAnalyzer (embedded *.ps1) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install PSScriptAnalyzer + shell: pwsh + run: | + Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser + + - name: Run PSScriptAnalyzer + run: ./hack/lint-powershell.sh diff --git a/.gitignore b/.gitignore index 178bd8c85..defc441bd 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,12 @@ dist/ .context/journal-site/ .context/journal-obsidian/ +# Session handovers (per-session, operator-specific, unbounded growth). +# .gitkeep stays tracked so the read-side missing-dir gate passes for +# fresh clones. +.context/handovers/* +!.context/handovers/.gitkeep + # KB rendered site output (Phase KB; analogous to journal-site) .context/site/ .context/site-config/ diff --git a/CLAUDE.md b/CLAUDE.md index 29a50b1d2..30588e81b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ This project uses Context (`ctx`) for context persistence across sessions. This tells you where the context directory is. If it returns any error, relay the error output to the user verbatim, point them at - https://ctx.ist/recipes/activating-context/ for setup, and STOP. + https://ctx.ist/home/getting-started/ for setup, and STOP. Do not try to activate, initialize, or otherwise recover: **those are the user's decisions**. Wait for their next instruction. 2. **Read AGENT_PLAYBOOK.md** from the context directory: it explains diff --git a/Makefile b/Makefile index 599038d0a..d7aeb3db7 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,8 @@ # # Common targets for Go developers -.PHONY: build test vet fmt fmt-context lint lint-style lint-drift clean all release build-all help \ +.PHONY: build test vet fmt fmt-context lint lint-style lint-drift lint-shellcheck lint-powershell \ +clean all release build-all help \ test-coverage smoke site site-feed site-serve site-serve-lan site-setup audit check plugin-reload \ journal journal-serve journal-serve-lan gpg-fix gpg-test register-mcp reinstall \ sync-version check-version-sync sync-why check-why sync-copilot-skills check-copilot-skills gemini-search \ @@ -119,6 +120,14 @@ lint-style: lint-drift: @./hack/lint-drift.sh +## lint-shellcheck: Run shellcheck on embedded *.sh scripts (warning+) +lint-shellcheck: + @./hack/lint-shellcheck.sh + +## lint-powershell: Run PSScriptAnalyzer on embedded *.ps1 scripts (Warning+) +lint-powershell: + @./hack/lint-powershell.sh + ## audit: Run all CI checks locally (fmt, vet, lint, drift, docs, test) audit: @echo "==> Checking formatting..." @@ -129,6 +138,18 @@ audit: @golangci-lint run --timeout=5m @echo "==> Running style checks..." @$(MAKE) --no-print-directory lint-style + @if command -v shellcheck >/dev/null 2>&1; then \ + echo "==> Running shellcheck..."; \ + $(MAKE) --no-print-directory lint-shellcheck; \ + else \ + echo "==> Skipping shellcheck (not installed locally; CI enforces this)"; \ + fi + @if command -v pwsh >/dev/null 2>&1; then \ + echo "==> Running PSScriptAnalyzer..."; \ + $(MAKE) --no-print-directory lint-powershell; \ + else \ + echo "==> Skipping PSScriptAnalyzer (pwsh not installed locally; CI enforces this)"; \ + fi @echo "==> Checking version sync..." @$(MAKE) --no-print-directory check-version-sync @echo "==> Checking why docs freshness..." diff --git a/README.md b/README.md index b4fb039cf..e9a364578 100644 --- a/README.md +++ b/README.md @@ -101,12 +101,8 @@ instructions. # Run `git init` first if the project does not have a repo yet.) ctx init -# Activate it for the current shell (binds CTX_DIR). Required -# before every other command: ctx no longer walks up the -# filesystem looking for .context/. -eval "$(ctx activate)" - -# Check context status +# Run subsequent commands from the project root. ctx always +# reads $PWD/.context/; there is no env-var or walk-up. ctx status # Get an AI-ready context packet @@ -149,14 +145,12 @@ for the full workflow, including the pass-mode contract, source-coverage state-machine ledger, and the closeout/fold mechanism. -`ctx activate` emits `export CTX_DIR=...` for your shell; one-shot -callers can prefix the binding inline as `CTX_DIR= ctx ...`. -The value must be an absolute path with `.context` as its basename; -relative paths and other names are rejected on first use. A small -allowlist (`init`, `activate`, `deactivate`, `version`, `help`, -`system bootstrap`, `doctor`, `guide`, `why`, `config switch/status`, -`hub *`) runs without CTX_DIR declared; every other command exits -with a next-step hint when it is unset. +`ctx` reads `$PWD/.context/` — run commands from the project root +(the directory that holds both `.git/` and `.context/`). A small +allowlist (`init`, `version`, `help`, `system bootstrap`, `doctor`, +`guide`, `why`, `config switch/status`, `hub *`) runs without +`.context/` present; every other command exits with a next-step +hint when it is missing. ## Documentation diff --git a/docs/cli/bootstrap.md b/docs/cli/bootstrap.md index 06ee3b819..25d1112ec 100644 --- a/docs/cli/bootstrap.md +++ b/docs/cli/bootstrap.md @@ -41,6 +41,7 @@ ctx system bootstrap -q # Just the context directory path ctx system bootstrap --json # Structured output for automation ``` -**Note**: `-q` prints just the resolved directory path. See -[Activating a Context Directory](../recipes/activating-context.md) -if you hit a "*no context directory specified*" error. +**Note**: `-q` prints just the resolved directory path. `ctx` +reads `$PWD/.context/`; if you hit a "*no context here*" error, +run `ctx init` from the project root or `cd` to one that already +has `.context/`. diff --git a/docs/cli/index.md b/docs/cli/index.md index 64ab46237..5882d69d5 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -25,30 +25,16 @@ All commands support these flags: | `--version` | Show version | | `--tool ` | Override active AI tool identifier (e.g. `kiro`, `cursor`) | -**Tell `ctx` which `.context/` to use.** `ctx` does not search the -filesystem for `.context/`: you have to declare it. Three ways: - -- `eval "$(ctx activate)"` (recommended): binds `CTX_DIR` for the - current shell. -- `export CTX_DIR=/abs/path/to/.context` directly, then run any - `ctx` command. -- `CTX_DIR=/abs/path/to/.context ctx ` inline, for a - one-shot or CI step. - -`CTX_DIR` must be an absolute path with `.context` as its basename. -Relative paths and other names are rejected on first use; the -basename guard catches the common footgun -(`export CTX_DIR=$(pwd)`) before stray writes can leak to the -project root. - -If you forget, commands fail fast with a linkable -`Error: no context directory specified` pointing at -[Activating a Context Directory](../recipes/activating-context.md). -A handful of commands run without a declaration because they don't -need a project: `ctx init`, `ctx activate`, `ctx deactivate`, -`ctx version`, `ctx help`, `ctx system bootstrap`, `ctx doctor`, -`ctx guide`, `ctx why`, `ctx config switch/status`, and -`ctx hub *`. +**Tell `ctx` which `.context/` to use.** `ctx` reads +`$PWD/.context/` — run commands from the project root (the +directory that holds both `.git/` and `.context/`). There is no +env-var or walk-up resolution; `ctx` does not search the +filesystem. If `$PWD/.context/` is missing, commands fail fast +with a clear error pointing at `ctx init`. A handful of commands +run without that gate because they don't need a project: +`ctx init`, `ctx version`, `ctx help`, `ctx system bootstrap`, +`ctx doctor`, `ctx guide`, `ctx why`, `ctx config switch/status`, +and `ctx hub *`. **Initialization required.** Once declared, the target must already have been initialized by `ctx init` (otherwise commands return @@ -60,8 +46,6 @@ have been initialized by `ctx init` (otherwise commands return | Command | Description | |-----------------------------------------------------|----------------------------------------------------------| | [`ctx init`](init-status.md#ctx-init) | Initialize `.context/` directory with templates | -| [`ctx activate`](init-status.md#ctx-activate) | Emit `export CTX_DIR=...` to bind context for the shell | -| [`ctx deactivate`](init-status.md#ctx-deactivate) | Emit `unset CTX_DIR` to clear the binding | | [`ctx status`](init-status.md#ctx-status) | Show context summary (files, tokens, drift) | | [`ctx guide`](guide.md#ctx-guide) | Quick-reference cheat sheet | | [`ctx why`](why.md#ctx-why) | Read the philosophy behind `ctx` | @@ -154,7 +138,6 @@ have been initialized by `ctx init` (otherwise commands return | Variable | Description | |-------------------------|-----------------------------------------------------| -| `CTX_DIR` | Override default context directory path | | `CTX_TOKEN_BUDGET` | Override default token budget | | `CTX_SESSION_ID` | Active AI session ID (used by `ctx trace` for context linking) | diff --git a/docs/cli/init-status.md b/docs/cli/init-status.md index ffe9bd1ed..53fc2f8f9 100644 --- a/docs/cli/init-status.md +++ b/docs/cli/init-status.md @@ -74,76 +74,10 @@ ctx init --reset ctx init --merge ``` -After `ctx init` succeeds, the final output includes a hint showing -the exact `eval "$(ctx activate)"` line to bind the new directory -for your shell. Every other `ctx` command requires that binding -(or an equivalent direct `CTX_DIR=/abs/path/.context` export) before -it will run. - ---- - -### `ctx activate` - -Emit a shell-native `export CTX_DIR=...` line for the target -`.context/` directory. `ctx` does not search the filesystem during -day-to-day commands: each one needs `CTX_DIR` set before it runs. -`activate` is the convenience that figures out the path for you so -you can bind it with one line. - -```bash -# Walk up from CWD, emit if exactly one candidate visible. -eval "$(ctx activate)" -``` - -**Flags**: - -| Flag | Description | -|-----------|------------------------------------------------------------------------------------------| -| `--shell` | Shell dialect override. POSIX-family (`bash`, `zsh`, `sh`) all share one syntax today; the flag exists for future fish/nushell/powershell support. Auto-detected from `$SHELL`. | - -**Resolution**: - -| Candidate count from CWD | Behavior | -|--------------------------|--------------------------------------------------------------------------| -| Zero | Error. Use `ctx init` to create one, or `cd` closer to the project root. | -| One | Emit `export CTX_DIR=` for that candidate. | -| Two or more | Refuse. List every candidate. Re-run from a more specific cwd. | - -`activate` is args-free under the single-source-anchor model; the -explicit-path mode was removed because hub-client / hub-server -scenarios store at `~/.ctx/hub-data/` and never read `.context/`, -so they activate from the project root like everyone else. Direct -binding without a project-local scan is still available via -`export CTX_DIR=/abs/path/.context` or the inline form. - -If the parent shell already has `CTX_DIR` set to a different value, -the output gains a leading `# ctx: replacing stale CTX_DIR=...` -comment so the user sees the change in `eval` output before the -replacement takes effect. - -**See also**: [Activating a Context Directory](../recipes/activating-context.md) -for the full recipe including direnv setup and CI patterns. - ---- - -### `ctx deactivate` - -Emit a shell-native `unset CTX_DIR` line. Pairs with `activate`. - -```bash -eval "$(ctx deactivate)" -``` - -**Flags**: - -| Flag | Description | -|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--shell` | Shell dialect override. POSIX-family (`bash`, `zsh`, `sh`) all share one `unset` syntax today; the flag exists for future fish/nushell/powershell support. Auto-detected from `$SHELL`. | - -`deactivate` does not touch the filesystem, doesn't require a -declared context directory, and never fails under normal operation; -unsetting an already-unset variable is a no-op across supported -shells. +After `ctx init` succeeds, `.context/` and the canonical files +are created in `$PWD`. Run subsequent `ctx` commands from the +same directory (the project root); `ctx` always reads +`$PWD/.context/`. --- diff --git a/docs/cli/mcp.md b/docs/cli/mcp.md index c2529730b..701133c5e 100644 --- a/docs/cli/mcp.md +++ b/docs/cli/mcp.md @@ -35,19 +35,18 @@ by MCP clients (Claude Desktop, Cursor, VS Code Copilot), **not run directly from a shell**. See [Configuration](#configuration) below for how each host launches it. -**Flags:** None. The server uses the declared context directory -from `CTX_DIR`. As with every other `ctx` command, that variable -must be set: the server does not walk the filesystem. +**Flags:** None. The server resolves the context directory by +reading `$PWD/.context/`. The MCP host must launch the server +from the project root (or its launch wrapper must `cd` first). +There is no env-var or walk-up resolution. **Examples**: ```bash -# Normal invocation (by an MCP client via stdio transport) +# Normal invocation (by an MCP client via stdio transport, +# from the project root) ctx mcp serve -# Pin a context directory for a specific workspace -CTX_DIR=/path/to/project/.context ctx mcp serve - # Verify the binary starts without a client attached (Ctrl-C to exit) ctx mcp serve < /dev/null ``` diff --git a/docs/home/configuration.md b/docs/home/configuration.md index 8f7db1891..faf713c07 100644 --- a/docs/home/configuration.md +++ b/docs/home/configuration.md @@ -43,11 +43,11 @@ my-project/ └── src/ ``` -`ctx` reads `.ctxrc` from the **project root** (*i.e. the parent of -`CTX_DIR`, or `dirname(CTX_DIR)/.ctxrc`*). It does not walk up from CWD. -That means whichever project you've activated via `eval "$(ctx activate)"` -(or by exporting `CTX_DIR` directly), its paired `.ctxrc` is what governs the -invocation. There is no global or user-level config file: configuration is +`ctx` reads `.ctxrc` from the **current working directory** (the +project root, sibling of `.context/`). It does not walk up. `ctx` +commands must be run from the project root; subdirectories are +not supported by design (see [Getting Started](getting-started.md)). +There is no global or user-level config file: configuration is always per-project. !!! note "Contributors: Dev Configuration Profile" @@ -56,15 +56,6 @@ always per-project. via `ctx config switch dev` / `ctx config switch base`. See [Contributing: Configuration Profiles](contributing.md#configuration-profiles). -!!! tip "Using a Different `.context` Directory" - You point `ctx` at a `.context/` directory by setting the - `CTX_DIR` environment variable, not through `.ctxrc`. `ctx` - does not search the filesystem. Use `eval "$(ctx activate)"` - to bind `CTX_DIR` for your shell. `CTX_DIR` must be an - absolute path with `.context` as its basename. - - See [Environment Variables](#environment-variables) below for details. - ### Full Reference @@ -180,17 +171,13 @@ behind this ordering. Environment variables override `.ctxrc` values but are overridden by CLI flags. -| Variable | Description | Equivalent `.ctxrc` key | -|--------------------|-------------------------------------------------------------|-------------------------| -| `CTX_DIR` | Declare the context directory path (required, no fallback) | *(none)* | -| `CTX_TOKEN_BUDGET` | Override the default token budget | `token_budget` | +| Variable | Description | Equivalent `.ctxrc` key | +|--------------------|-----------------------------------|-------------------------| +| `CTX_TOKEN_BUDGET` | Override the default token budget | `token_budget` | ### Examples ```bash -# Use a shared context directory -CTX_DIR=/shared/team-context ctx status - # Increase token budget for a single run CTX_TOKEN_BUDGET=16000 ctx agent ``` @@ -209,13 +196,6 @@ CLI flags have the highest priority and override both environment variables and | `--version` | Show version and exit | | `--help` | Show command help and exit | -### Examples - -```bash -# Point to a different context directory inline: -CTX_DIR=/path/to/project/.context ctx status -``` - --- ## Priority Order @@ -229,9 +209,8 @@ CLI flags > Environment variables > .ctxrc > Built-in defaults ``` The context directory itself is resolved differently: it lives *outside* -this priority chain. `CTX_DIR` (env) must be declared; `.ctxrc` does not -carry a fallback for it, and there is no built-in default. See -[Activating a Context Directory](../recipes/activating-context.md). +this priority chain. `ctx` always reads `$PWD/.context/`; if that path +does not exist, the command refuses with a clear error. **Example resolution for `token_budget`:** @@ -245,26 +224,6 @@ carry a fallback for it, and there is no built-in default. See ## Examples -### External `.context` Directory - -Store a project's context outside the project tree (*useful when a -repo is read-only, or when you want to keep notes adjacent rather -than checked in*). Declare the path via `CTX_DIR`: - -```bash -export CTX_DIR=/home/you/ctx-stores/my-project/.context -``` - -!!! warning "One `.context/` per project" - The parent of the context directory is the project root by - contract: `ctx sync`, `ctx drift`, and the memory-drift hook - all read the codebase from `filepath.Dir(ContextDir())`. - Pointing two projects at the same `.context/` directory will - collide their journals, state, and secrets. To share knowledge - (CONSTITUTION / CONVENTIONS / ARCHITECTURE) across projects, - use [`ctx hub`](../recipes/hub-overview.md), not a shared - `.context/`. - ### Custom Token Budget Increase the token budget for projects with large context: diff --git a/docs/home/first-session.md b/docs/home/first-session.md index 0cd518e95..5424451eb 100644 --- a/docs/home/first-session.md +++ b/docs/home/first-session.md @@ -54,22 +54,7 @@ This created your `.context/` directory with template files. For Claude Code, [install the `ctx` plugin](getting-started.md#installation) to get automatic hooks and skills. -## Step 2: Activate the Project - -Tell `ctx` which `.context/` directory the rest of these commands -should use: - -```bash -eval "$(ctx activate)" -``` - -You only need to run this once per terminal. If you skip it, the -next steps fail with `Error: no context directory specified`. Direnv -users can wire it into `.envrc` and forget about it. For more -options (multiple `.context/` directories, scripts, CI), see -[Activating a Context Directory](../recipes/activating-context.md). - -## Step 3: Populate Your Context +## Step 2: Populate Your Context Add a **task** and a **decision**: These are the entries your AI will remember: diff --git a/docs/home/getting-started.md b/docs/home/getting-started.md index 7a73d85e3..28debe7e0 100644 --- a/docs/home/getting-started.md +++ b/docs/home/getting-started.md @@ -250,22 +250,7 @@ tool scoping are covered in [Writing Steering Files](../recipes/steering.md) and [`ctx steering`](../cli/steering.md). -### 3. Activate the Project - -Tell `ctx` which `.context/` directory the rest of these commands -should use: - -```bash -eval "$(ctx activate)" -``` - -You only need to run this once per terminal. If you skip it, the -next steps fail with `Error: no context directory specified`. Direnv -users can wire it into `.envrc` and forget about it. For more -options (multiple `.context/` directories, scripts, CI), see -[Activating a Context Directory](../recipes/activating-context.md). - -### 4. Check Status +### 3. Check Status ```bash ctx status @@ -273,7 +258,7 @@ ctx status Shows context summary: files present, token estimate, and recent activity. -### 5. Start Using with AI +### 4. Start Using with AI With Claude Code (*and the `ctx` plugin installed*), context loads automatically via hooks. @@ -290,7 +275,7 @@ For other tools, paste the output of: ctx agent --budget 8000 ``` -### 5B. Set Up for Your AI Tool +### 5. Set Up for Your AI Tool If you use an MCP-compatible tool, generate the integration config with `ctx setup`: diff --git a/docs/home/is-ctx-right.md b/docs/home/is-ctx-right.md index 8070f248c..28f73c00b 100644 --- a/docs/home/is-ctx-right.md +++ b/docs/home/is-ctx-right.md @@ -112,21 +112,17 @@ Zero commitment. Try it, and delete `.context/` if it's not for you. cd your-project ctx init -# 2. Activate the project (bind CTX_DIR for this shell). -# Required: ctx does not walk the filesystem to find .context/. -eval "$(ctx activate)" - -# 3. Add one real decision from your project +# 2. Add one real decision from your project ctx decision add "Your actual architectural choice" \ --context "What prompted this decision" \ --rationale "Why you chose this approach" \ --consequence "What changes as a result" \ --session-id abc12345 --branch main --commit 68fbc00a -# 4. Check what the AI will see +# 3. Check what the AI will see ctx status -# 5. Start an AI session and ask: "Do you remember?" +# 4. Start an AI session and ask: "Do you remember?" ``` If the AI cites your decision back to you, it's working. diff --git a/docs/home/joining-a-project.md b/docs/home/joining-a-project.md index 7ccca44f4..27cb46391 100644 --- a/docs/home/joining-a-project.md +++ b/docs/home/joining-a-project.md @@ -35,21 +35,6 @@ top-down: See [Context Files](context-files.md) for detailed documentation of each file's structure and purpose. -## Activate the Project - -Tell `ctx` which `.context/` directory to read from: - -```bash -eval "$(ctx activate)" -``` - -You only need to run this once per terminal. If you skip it, the -commands in the rest of this guide fail with -`Error: no context directory specified`. Direnv users can wire it -into `.envrc` and forget about it. See -[Activating a Context Directory](../recipes/activating-context.md) -for more options (multiple `.context/` directories, scripts, CI). - ## Checking Context Health Before you start working, check whether the context is current: diff --git a/docs/home/opencode.md b/docs/home/opencode.md index e708818d0..03bb67d6e 100644 --- a/docs/home/opencode.md +++ b/docs/home/opencode.md @@ -45,10 +45,10 @@ Install the `ctx` binary first ([installation docs](getting-started.md#installat then run from your project root: ```bash -ctx setup opencode --write && ctx init && eval "$(ctx activate)" +ctx setup opencode --write && ctx init ``` -This does three things: +This does two things: 1. **`ctx setup opencode --write`**: generates the project-local OpenCode plugin, skills, and `AGENTS.md`, then merges the `ctx` MCP server into OpenCode's @@ -58,7 +58,6 @@ This does three things: project-local config; the same reason the Copilot CLI integration writes to `~/.copilot/mcp-config.json`. 2. **`ctx init`**: creates the `.context/` directory with template files. -3. **`eval "$(ctx activate)"`**: binds `CTX_DIR` for your shell. ### What Gets Created @@ -83,7 +82,7 @@ do anything; it just works. | Agent idle | `session.idle` | Runs persistence and task-completion checks (silent: output is buffered, not surfaced to the TUI) | | After `git commit` | `tool.execute.after` | Runs `ctx system post-commit` to capture context state | | After file edit | `tool.execute.after` | Runs `ctx system check-task-completion` to detect silent task completions | -| Every shell call | `shell.env` | Injects `CTX_DIR` so all `ctx` commands in the agent's shell resolve to the right project | +| Every shell call | `shell.env` | Ensures the agent's shell `cd`s to the project root so all `ctx` commands resolve to the right project | | Context compaction | `experimental.session.compacting` | Pushes `ctx system bootstrap` output into the compaction context so the agent retains breadcrumbs to re-read context files post-compaction | The compaction hook matters most. When OpenCode compresses your context @@ -158,7 +157,7 @@ refreshed plugin**. OpenCode only loads plugins at launch, not mid-session. | Symptom | Cause | Fix | |---------|-------|-----| -| `opencode mcp list` shows `ctx ✗ failed MCP error -32000: Connection closed` | `CTX_DIR` not resolving in the MCP subprocess | Re-run `ctx setup opencode --write` to regenerate the sh-wrapper that sets `CTX_DIR` | +| `opencode mcp list` shows `ctx ✗ failed MCP error -32000: Connection closed` | MCP subprocess started outside the project root | Re-run `ctx setup opencode --write` to regenerate the sh-wrapper that `cd`s to the project root before invoking `ctx` | | Plugin installed but no hooks fire | Flat-file vs. subdirectory discovery mismatch (OpenCode requires `.opencode/plugins/.ts`, not a subfolder) | Verify the plugin is at `.opencode/plugins/ctx.ts`. Check with `opencode --print-logs --log-level DEBUG` | | `ctx agent` Markdown leaking into the TUI | BunShell command missing `.nothrow().quiet()` | Update to the latest plugin: `ctx setup opencode --write` and restart | diff --git a/docs/home/vscode.md b/docs/home/vscode.md index b9c569d5d..5621014bc 100644 --- a/docs/home/vscode.md +++ b/docs/home/vscode.md @@ -58,7 +58,7 @@ Install the extension and the `ctx` binary, then `ctx init` your project: 3. **From your project root**, run: ```bash - ctx init && eval "$(ctx activate)" + ctx init ``` 4. **Open Copilot Chat** in VS Code and type `@ctx /init` to verify diff --git a/docs/operations/autonomous-loop.md b/docs/operations/autonomous-loop.md index 8e0c0717d..1d8708b3f 100644 --- a/docs/operations/autonomous-loop.md +++ b/docs/operations/autonomous-loop.md @@ -304,18 +304,16 @@ real time. ## Project Setup -Initialize a project for autonomous loop operation, then activate it -so the loop's `ctx` commands know which `.context/` to use: +Initialize a project for autonomous loop operation: ```bash ctx init -eval "$(ctx activate)" ``` -For unattended overnight runs, put the binding directly at the top -of your loop script (`export CTX_DIR=/abs/path/.context`) so it -survives without depending on a live shell. See -[Activating a Context Directory](../recipes/activating-context.md). +`ctx` always reads `$PWD/.context/`. For unattended overnight runs +where a supervisor may not preserve cwd, put `cd /abs/path/to/project` +at the top of `loop.sh` so the loop is anchored regardless of how +the supervisor launches it. The loop prompt template is deployed to `.context/loop.md` during initialization. It instructs the agent to: diff --git a/docs/operations/integrations.md b/docs/operations/integrations.md index 363439667..5dc1a64d1 100644 --- a/docs/operations/integrations.md +++ b/docs/operations/integrations.md @@ -16,19 +16,10 @@ icon: lucide/plug Context works with any AI tool that can read files. This guide covers setup for popular AI coding assistants. -!!! warning "Activate the Project Before Running `ctx` Commands" - After `ctx init`, run: - - ```bash - eval "$(ctx activate)" - ``` - - This tells `ctx` which `.context/` directory the rest of the - commands on this page should use. If you skip it, you'll see - `Error: no context directory specified`. The `ctx setup ` - commands work without activation, but most others (`ctx agent`, - `ctx add`, `ctx status`, `ctx watch`) need it. See - [Activating a Context Directory](../recipes/activating-context.md). +!!! tip "Run From the Project Root" + `ctx` reads `$PWD/.context/`. Run the commands on this page from + the project root (the directory that holds `.context/` and + `.git/`). ## Claude Code (Full Integration) @@ -36,12 +27,10 @@ Claude Code has the deepest integration via the **`ctx` plugin**. ### Setup -First, install `ctx` and initialize your project, then activate it -for the current shell: +First, install `ctx` and initialize your project: ```bash ctx init -eval "$(ctx activate)" ``` Then, install the `ctx` plugin in Claude Code: @@ -578,7 +567,6 @@ ctx setup opencode --write # Initialize context ctx init -eval "$(ctx activate)" ``` ### What Gets Created @@ -598,7 +586,7 @@ The plugin wires OpenCode lifecycle events to `ctx system`: - **`tool.execute.after` (shell, on `git commit`)**: runs `ctx system post-commit`. - **`tool.execute.after` (edit/write)**: runs `ctx system check-task-completion`. - **`session.idle`**: runs persistence and task-completion checks (silent: output is buffered, not surfaced to the TUI). -- **`shell.env`**: injects `CTX_DIR` into the agent's shell so `ctx` commands resolve to the right project. +- **`shell.env`**: ensures the agent's shell starts in the project root so `ctx` commands resolve to the right project. - **`experimental.session.compacting`**: pushes `ctx system bootstrap` output into the compaction context so the agent keeps breadcrumbs back to `.context/`. The plugin is a single file with no runtime dependencies; no `bun install` diff --git a/docs/operations/migration.md b/docs/operations/migration.md index efc6600dc..50d49fb96 100644 --- a/docs/operations/migration.md +++ b/docs/operations/migration.md @@ -303,22 +303,11 @@ You don't need the whole team to switch at once: ## Verifying It Worked -### Activate the Project - -Tell `ctx` which `.context/` directory to use for the rest of the -verification steps: - -```bash -eval "$(ctx activate)" -``` - -You only need to run this once per terminal. If you skip it, the -status check below fails with `Error: no context directory -specified`. See -[Activating a Context Directory](../recipes/activating-context.md). - ### Check Status +Run subsequent commands from the project root (the directory that +holds `.context/` and `.git/`); `ctx` reads `$PWD/.context/`. + ```bash ctx status ``` diff --git a/docs/operations/runbooks/architecture-exploration.md b/docs/operations/runbooks/architecture-exploration.md index 50f45ab9d..29652c6a9 100644 --- a/docs/operations/runbooks/architecture-exploration.md +++ b/docs/operations/runbooks/architecture-exploration.md @@ -144,21 +144,21 @@ focus as input upfront. 1. `cd` into the sub-repo directory (`~/WORKSPACE/`, NOT `~/WORKSPACE` itself). -2. Verify `CTX_DIR` already points at THIS sub-repo's `.context/`: +2. Verify `$PWD/.context/` exists for THIS sub-repo: ```bash - test "$CTX_DIR" = "$PWD/.context" || { - echo "STOP: CTX_DIR=$CTX_DIR but this sub-repo needs $PWD/.context." - echo "Re-launch the agent with CTX_DIR set to the sub-repo:" - echo " cd $PWD && CTX_DIR=\"\$PWD/.context\" claude --print 'Follow .arch-explorer/PROMPT.md' --allowedTools '*'" + test -d "$PWD/.context" || { + echo "STOP: no .context/ at $PWD. Re-launch the agent from" + echo "this sub-repo's root:" + echo " cd $PWD && claude --print 'Follow .arch-explorer/PROMPT.md' --allowedTools '*'" exit 1 } ``` - If it fails, STOP. The agent cannot change `CTX_DIR` for itself: - child shells and skill invocations inherit the parent Claude - process environment, which only the caller can control. Do not - proceed, do not run `ctx` commands, do not skip the check. + If it fails, STOP. `ctx` reads `$PWD/.context/`; the agent + cannot change its own working directory after launch — only the + caller controls it. Do not proceed, do not run `ctx` commands, + do not skip the check. 3. If phase is `bootstrap`: - Run `ctx init`, confirm `.context/` exists. - Then run `/ctx-architecture` (structural baseline). @@ -289,17 +289,17 @@ When every repo has reached its stopping condition, print: ## Invocation -The caller MUST set `CTX_DIR` to the sub-repo the agent will work on. -The agent verifies this at Step 3.2 and stops if it does not match. -The wrapper reads the manifest to pick the current sub-repo, then -launches `claude` with `CTX_DIR` pinned to that sub-repo's `.context/`. +The caller MUST launch the agent with its working directory set +to the sub-repo. The agent verifies this at Step 3.2 and stops if +`$PWD/.context/` is missing. The wrapper reads the manifest to +pick the current sub-repo, `cd`s into it, then launches `claude`. **Single run (safest for quota):** ```bash cd ~/WORKSPACE REPO=$(jq -r '.repos[.current_repo_index]' .arch-explorer/manifest.json) -CTX_DIR="$PWD/$REPO/.context" \ +cd "$REPO" && \ claude --print "Follow .arch-explorer/PROMPT.md" --allowedTools '*' ``` @@ -309,8 +309,8 @@ CTX_DIR="$PWD/$REPO/.context" \ cd ~/WORKSPACE for i in $(seq 1 5); do REPO=$(jq -r '.repos[.current_repo_index]' .arch-explorer/manifest.json) - CTX_DIR="$PWD/$REPO/.context" \ - claude --print "Follow .arch-explorer/PROMPT.md" --allowedTools '*' + (cd "$REPO" && \ + claude --print "Follow .arch-explorer/PROMPT.md" --allowedTools '*') echo "--- Run $i complete (repo: $REPO) ---" done ``` @@ -318,8 +318,9 @@ done **Resume after interruption:** Just run the wrapper again. The manifest tracks state; the agent picks -up where it left off. `CTX_DIR` is recomputed from the manifest on -each invocation, so the right sub-repo is always bound. +up where it left off. The sub-repo directory is recomputed from the +manifest on each invocation, so the agent is always anchored at the +right project root. ## Tips @@ -339,6 +340,7 @@ each invocation, so the right sub-repo is always bound. - 2026-04-07: Original prompt created as `hack/agents/architecture-explorer.md`. - 2026-04-16: Moved to docs as a runbook for discoverability. -- 2026-04-20: Added `CTX_DIR` verification at Step 3.2 and per-invocation - `CTX_DIR` binding in the wrapper, so the agent writes artifacts to the - sub-repo's `.context/` instead of the inherited workspace one. +- 2026-04-20: Added per-invocation working-directory pinning in the + wrapper (formerly via `CTX_DIR`; now via `cd "$REPO"`), so the + agent writes artifacts to the sub-repo's `.context/` instead of + the inherited workspace one. diff --git a/docs/operations/runbooks/breaking-migration.md b/docs/operations/runbooks/breaking-migration.md index c105ce000..65aa78572 100644 --- a/docs/operations/runbooks/breaking-migration.md +++ b/docs/operations/runbooks/breaking-migration.md @@ -90,18 +90,14 @@ jq '.hooks' .claude/settings.local.json | grep "ctx " ## Step 6: Verify -Activate the project first, otherwise `ctx status` and `ctx drift` -will fail with `Error: no context directory specified`: +Run from the project root: ```bash -eval "$(ctx activate)" ctx status # context files intact ctx drift # no broken references make test # if you're a contributor ``` -See [Activating a Context Directory](../../recipes/activating-context.md). - --- ## Writing Release-Specific Migration Notes diff --git a/docs/operations/runbooks/hub-deployment.md b/docs/operations/runbooks/hub-deployment.md index 682b25ce6..0e84fc962 100644 --- a/docs/operations/runbooks/hub-deployment.md +++ b/docs/operations/runbooks/hub-deployment.md @@ -86,7 +86,6 @@ first. ```bash # In the project directory on the client machine: -eval "$(ctx activate)" ctx connection register --token ``` @@ -96,16 +95,14 @@ Verify the connection: ctx connection status ``` -If the client doesn't have a project yet, run `ctx init` first, then -`eval "$(ctx activate)"`. See -[Activating a Context Directory](../../recipes/activating-context.md). +If the client doesn't have a project yet, run `ctx init` first in +the project root. ## Step 5: Verify Sync -Push a test entry from one client and verify it arrives. Make sure -each client already ran `eval "$(ctx activate)"` from Step 4: -otherwise `ctx add` and `ctx status` fail with -`Error: no context directory specified`. +Push a test entry from one client and verify it arrives. Run each +command on the client from inside the project directory; `ctx` +reads `$PWD/.context/`. ```bash # Client A (in its project directory, after activating): diff --git a/docs/operations/runbooks/new-contributor.md b/docs/operations/runbooks/new-contributor.md index f34e52bfb..9fcc0fc98 100644 --- a/docs/operations/runbooks/new-contributor.md +++ b/docs/operations/runbooks/new-contributor.md @@ -35,14 +35,12 @@ Or fork first on GitHub, then clone your fork. ```bash ctx init -eval "$(ctx activate)" ``` `ctx init` creates the `.context/` directory with knowledge files -and the `.claude/` directory with agent configuration. -`eval "$(ctx activate)"` tells `ctx` to use that directory for the -rest of this runbook. If you skip the second line, the later steps -fail with `Error: no context directory specified`. +and the `.claude/` directory with agent configuration. Run +subsequent `ctx` commands from the project root; `ctx` reads +`$PWD/.context/`. If `ctx` is not yet installed, proceed to Step 3 first, then come back. diff --git a/docs/operations/runbooks/release-checklist.md b/docs/operations/runbooks/release-checklist.md index 49f5bc5cd..a336ecb10 100644 --- a/docs/operations/runbooks/release-checklist.md +++ b/docs/operations/runbooks/release-checklist.md @@ -57,19 +57,15 @@ All tests must pass. No exceptions. ### 5. Check Context Health -Activate the project so the next commands know which `.context/` -to read: +Run from the project root: ```bash -eval "$(ctx activate)" ctx drift # broken references, stale patterns ctx status # context file health /ctx-link-check # dead links in docs ``` -Fix anything flagged. If you see `Error: no context directory -specified`, you skipped the `eval` line above. See -[Activating a Context Directory](../../recipes/activating-context.md). +Fix anything flagged. ### 6. Review TASKS.md diff --git a/docs/operations/upgrading.md b/docs/operations/upgrading.md index 04e6c1201..5e57e6dff 100644 --- a/docs/operations/upgrading.md +++ b/docs/operations/upgrading.md @@ -106,11 +106,9 @@ Manually add back any custom entries that the new init dropped. ### 5. Verify -Activate the project first, otherwise `ctx status` and `ctx drift` -will fail with `Error: no context directory specified`: +Run from the project root (where `.context/` lives): ```bash -eval "$(ctx activate)" ctx status # context files intact ctx drift # no broken references ``` diff --git a/docs/recipes/activating-context.md b/docs/recipes/activating-context.md deleted file mode 100644 index 3ea05293a..000000000 --- a/docs/recipes/activating-context.md +++ /dev/null @@ -1,212 +0,0 @@ ---- -title: "Activating a Context Directory" -icon: lucide/plug-zap ---- - -![ctx](../images/ctx-banner.png) - -## The Problem - -You ran a `ctx` command and got: - -``` -Error: no context directory specified for this project -``` - -This means `ctx` doesn't know which `.context/` directory to operate -on. It will not guess, and it will not walk up from your current -working directory looking for one; that behavior was removed -deliberately, because silent inference was the source of several -bugs (stray agent-created directories, cross-project bleed-through, -webhook-route misrouting, sub-agent fragmentation). Every `ctx` -command requires you to declare the target directory explicitly. - -This page shows you the three ways to do that and when to use each. - -## TL;DR - -If the project has already been initialized and you just need to -bind it for your shell: - -```bash -eval "$(ctx activate)" -``` - -That's 95% of the time. Add it to `.zshrc` / `.bashrc` per project -with direnv, or run it once per terminal. - -## When You See the Error - -The exact error message depends on how many `.context/` directories -are visible from the current directory: - -### Zero Candidates - -``` -Error: no context directory specified for this project -``` - -Either you haven't initialized this project yet (run `ctx init`) -or you're in a directory that doesn't belong to a ctx-tracked -project. If you know the project lives elsewhere, use one of the -declaration methods below with its absolute path. - -### One Candidate - -``` -Error: no context directory specified; a likely candidate is at - /Users/you/repos/myproject/.context -``` - -`ctx` found a single `.context/` on the way up from here but won't -bind to it automatically. Run `eval "$(ctx activate)"` and `ctx` -will emit the `export` for the candidate. Or set `CTX_DIR` by hand. - -### Multiple Candidates - -``` -Error: no context directory specified; multiple candidates visible: - /Users/you/repos/myproject/.context - /Users/you/repos/myproject/packages/web/.context -``` - -You're inside nested projects. Pick the one you mean: - -```bash -ctx activate /Users/you/repos/myproject/.context -# …copy and paste the `export` line it prints, or wrap in eval: -eval "$(ctx activate /Users/you/repos/myproject/.context)" -``` - -## Three Ways to Declare - -### 1. `ctx activate` (Recommended for Shells) - -`ctx activate` emits a shell-native `export CTX_DIR=...` line to -stdout. Wrap it in `eval` and the binding takes effect for the -current shell: - -```bash -# Walk up from current dir and bind the single visible candidate: -eval "$(ctx activate)" - -# Bind a specific path explicitly: -eval "$(ctx activate /abs/path/to/.context)" - -# Clear the binding: -eval "$(ctx deactivate)" -``` - -`ctx activate` validates paths strictly: the target must exist, be -a directory, and contain at least one canonical context file -(`CONSTITUTION.md` or `TASKS.md`). It refuses to emit for multiple -upward candidates; pick one explicitly in that case. - -Under the hood, the emitted line is just: - -```bash -export CTX_DIR='/abs/path/to/.context' -``` - -So you can copy it into your `.zshrc` / `.bashrc` if you want the -binding permanent for a given shell setup. Better: use -[direnv](https://direnv.net/) with a per-project `.envrc`. - -### 2. `CTX_DIR` Env Var - -If you already know the path, export it directly: - -```bash -export CTX_DIR=/abs/path/to/.context -ctx status -``` - -`CTX_DIR` is the same variable `ctx activate` writes; `activate` -is just a convenience that figures out the path for you. - -### 3. Inline One-Shot - -For one-shot commands (CI jobs, scripts, debugging a specific -project without changing your shell state), prefix the binding -inline: - -```bash -CTX_DIR=/abs/path/to/.context ctx status -``` - -This binds `CTX_DIR` for that invocation only. - -`CTX_DIR` must be an absolute path with `.context` as its basename. -Relative paths and other names are rejected on first use; the -basename guard catches the common footgun -(`export CTX_DIR=$(pwd)`) before stray writes can leak to the -project root. - -## For CI and Scripts - -Do not rely on shell activation in automated flows. Set `CTX_DIR` -explicitly at the top of the script: - -```bash -#!/usr/bin/env bash -set -euo pipefail - -export CTX_DIR="$GITHUB_WORKSPACE/.context" -ctx status -ctx drift -``` - -## For Claude Code Users - -The `ctx` plugin's hooks are generated with -`CTX_DIR="$CLAUDE_PROJECT_DIR/.context"` prefixed to each command, -so hook-driven `ctx` invocations resolve correctly without any -per-session setup. You only need to activate manually when running -`ctx` yourself in a terminal. - -## One Project, One `.context/` - -The context directory is not a free-floating bag of files. It is -pinned to a project by contract: **`filepath.Dir(ContextDir())` is -the project root.** That parent directory is what `ctx sync`, -`ctx drift`, and the memory-drift hook scan for code, secret files, -and `MEMORY.md` respectively. - -The practical consequences: - -- **Don't share one `.context/` across multiple projects.** It holds - per-project journals, per-session state, and per-project secrets. - Pointing two codebases at the same directory corrupts all three. -- **If you want to share knowledge** (CONSTITUTION, CONVENTIONS, - ARCHITECTURE) across projects, use `ctx hub`. It cherry-picks - entries at the right granularity and keeps the per-project bits - where they belong. -- **The `CTX_DIR` you activate is implicitly a project-root - declaration.** Setting `CTX_DIR=/weird/place/.context` means - you're telling `ctx` the project root is `/weird/place/`. That's - your call to make; `ctx` does not police it. - -### Recommended Layout - -``` -~/WORKSPACE/my-to-do-list - ├── .git - ├── .context ← owned by this project; do not share - ├── ideas - │ └── ... - ├── Makefile - ├── Makefile.ctx - └── specs - └── ... -``` - -`.context/` sits at the project root, next to `.git`. `ctx activate` -binds to it; every `ctx` subsystem reads the project from its parent. - -## Why Not Walk Up Automatically? - -Nested projects, submodules, rogue agent-created `.context/` -directories, and sub-agent sessions all produced silent misrouting -under a walk-up model. For consistency, `ctx` does not guess, -and requires the caller to declare. Every other decision flows from -there. diff --git a/docs/recipes/autonomous-loops.md b/docs/recipes/autonomous-loops.md index 9cd249355..29ed7e0c9 100644 --- a/docs/recipes/autonomous-loops.md +++ b/docs/recipes/autonomous-loops.md @@ -25,23 +25,20 @@ context persistence as a **first-class deliverable**, not an *afterthought*. ```bash ctx init # 1. init context -eval "$(ctx activate)" # 2. bind CTX_DIR for this shell # Edit TASKS.md with phased work items -ctx loop --tool claude --max-iterations 10 # 3. generate loop.sh -./loop.sh 2>&1 | tee /tmp/loop.log & # 4. run the loop -ctx watch --log /tmp/loop.log # 5. process context updates +ctx loop --tool claude --max-iterations 10 # 2. generate loop.sh +./loop.sh 2>&1 | tee /tmp/loop.log & # 3. run the loop +ctx watch --log /tmp/loop.log # 4. process context updates # Next morning: -ctx status && ctx load # 6. review the results +ctx status && ctx load # 5. review the results ``` -!!! warning "Activate, or Set CTX_DIR Inline for Unattended Runs" - `eval "$(ctx activate)"` is fine for an interactive terminal. - For an overnight unattended loop, put the binding at the top - of `loop.sh` instead (`export CTX_DIR=/abs/path/.context`) so - the loop doesn't depend on a live shell. If you skip both, - `ctx loop`, `ctx watch`, `ctx status`, and `ctx load` fail - with `Error: no context directory specified`. See - [Activating a Context Directory](activating-context.md). +!!! warning "Run From the Project Root" + `ctx` reads `$PWD/.context/`. Both the interactive + `ctx loop` invocation and the generated `loop.sh` must run + from the project root. If `loop.sh` is scheduled by a + supervisor that does not preserve cwd, add `cd + /abs/path/to/project` at the top of the script. Read on for permissions, isolation, and completion signals. diff --git a/docs/recipes/context-health.md b/docs/recipes/context-health.md index 905eedc0a..5b9f2ba38 100644 --- a/docs/recipes/context-health.md +++ b/docs/recipes/context-health.md @@ -38,12 +38,6 @@ ctx status # verify Or just ask your agent: *"Is our context clean?"* -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, every command above fails with - `Error: no context directory specified`. See - [Activating a Context Directory](activating-context.md). - ## Commands and Skills Used | Tool | Type | Purpose | diff --git a/docs/recipes/customizing-hook-messages.md b/docs/recipes/customizing-hook-messages.md index c9edd15e2..6a452941e 100644 --- a/docs/recipes/customizing-hook-messages.md +++ b/docs/recipes/customizing-hook-messages.md @@ -25,14 +25,6 @@ ctx hook message edit qa-reminder gate # copy default to .context/ for editin ctx hook message reset qa-reminder gate # revert to embedded default ``` -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root: hook message overrides live in your `.context/` - directory, so `ctx` needs to know which one. If you skip the - `eval`, `ctx hook message ...` fails with `Error: no context - directory specified`. See - [Activating a Context Directory](activating-context.md). - ## Commands Used | Tool | Type | Purpose | diff --git a/docs/recipes/external-context.md b/docs/recipes/external-context.md deleted file mode 100644 index ffd3e7a0a..000000000 --- a/docs/recipes/external-context.md +++ /dev/null @@ -1,334 +0,0 @@ ---- -title: "Keeping Context in a Separate Repo" -icon: lucide/folder-symlink ---- - -![ctx](../images/ctx-banner.png) - -## The Problem - -`ctx` files contain project-specific **decisions**, **learnings**, -**conventions**, and **tasks**. By default, they live in -`.context/` inside the project tree, and that works well when the context -can be public. - -But sometimes you need the context *outside* the project: - -* **Open-source projects with private context**: Your architectural notes, - internal task lists, and scratchpad entries shouldn't ship with the public - repo. -* **Compliance or IP concerns**: Context files reference sensitive design - rationale that belongs in a separate access-controlled repository. -* **Personal preference**: You want to keep notes separate from code. - -`ctx` supports this by letting you point `CTX_DIR` anywhere. This recipe -shows how to set that up and how to tell your AI assistant where to find the -context. - -!!! warning "One `.context/` per project" - The parent of the context directory is the project root by contract. - `ctx sync`, `ctx drift`, and the memory-drift hook all read the - codebase at `filepath.Dir(ContextDir())`. Pointing two projects at - the same directory corrupts their journals, state, and secrets. To - share knowledge (CONSTITUTION / CONVENTIONS / ARCHITECTURE) across - projects, use [`ctx hub`](hub-overview.md), not a shared `.context/`. - -## TL;DR - -Create the external context directory, initialize it, and bind it: - -```bash -mkdir -p ~/repos/myproject-context && cd ~/repos/myproject-context && git init -cd ~/repos/myproject - -# Bind CTX_DIR to the external location, then init creates files there. -export CTX_DIR=~/repos/myproject-context/.context -ctx init -``` - -All `ctx` commands now use the external directory. If you share the -setup across shells, add the `export CTX_DIR=...` line to your -shell rc, or source a per-project `.envrc` with direnv. - -## What Works, What Quietly Degrades - -The single-source-anchor contract states that -`filepath.Dir(CTX_DIR)` is the project root. When the context -lives outside the project tree, `ctx` still resolves correctly for -every operation that reads or writes inside `.context/`. But any -operation that scans the **codebase** scans the wrong tree, and -does so silently: - -| Operation | Behavior with external `.context/` | -|---------------------------------|---------------------------------------------------| -| `ctx status`, `agent`, `add` | ✅ Works. Operates on files inside `CTX_DIR`. | -| Journal, scratchpad, hub | ✅ Works. Same reason. | -| `ctx sync` | ⚠️ Scans the *context repo*, not the code repo. | -| `ctx drift` | ⚠️ Same. Reports nothing useful. | -| Memory-drift hook (`MEMORY.md`) | ⚠️ Looks for `MEMORY.md` next to the external `.context/`, not the code. | - -Nothing errors. The code-aware operations just find an empty or -unrelated tree where the project root should be. - -### Workaround: symlink the `.context/` into the code tree - -If you want both the privacy of an external git repo *and* working -`ctx sync` / `drift` / memory-drift, symlink the external -`.context/` into the code repo and point `CTX_DIR` at the symlink: - -```bash -# External repo holds the real files -mkdir -p ~/repos/myproject-context && cd ~/repos/myproject-context && git init - -# Symlink it into the code repo -ln -s ~/repos/myproject-context/.context ~/repos/myproject/.context - -# Bind CTX_DIR to the symlink path; ctx init will follow it -export CTX_DIR=~/repos/myproject/.context -ctx init -``` - -Now `filepath.Dir(CTX_DIR)` is the **code repo**, so code-aware -operations scan the right tree. The actual files still live in -the external repo and commit there. Add `.context` to the code -repo's `.gitignore` (or `.git/info/exclude`) so the symlink itself -isn't tracked by the code repo. - -The basename guard is permissive about symlinks: it checks the -declared name, not the resolved target, so a `.context` symlink -pointing anywhere is accepted as long as the declared basename is -`.context`. - -## Commands and Skills Used - -| Tool | Type | Purpose | -|-----------------|--------------|-----------------------------------------| -| `ctx init` | CLI command | Initialize context directory | -| `ctx activate` | CLI command | Emit `export CTX_DIR=...` for the shell | -| `CTX_DIR` | Env variable | Declare context directory per-session | -| `.ctxrc` | Config file | Per-project configuration | -| `/ctx-status` | Skill | Verify context is loading correctly | - -## The Workflow - -### Step 1: Create the Private Context Repo - -Create a separate repository for your context files. This can live anywhere: -a private GitHub repo, a shared drive, a sibling directory: - -```bash -# Create the context repo -mkdir -p ~/repos/myproject-context -cd ~/repos/myproject-context -git init -``` - -### Step 2: Initialize `ctx` Pointing at It - -From your project root, declare `CTX_DIR` pointing to the external -location, then initialize: - -```bash -cd ~/repos/myproject -CTX_DIR=~/repos/myproject-context/.context ctx init -``` - -This creates the canonical `.context/` file set inside -`~/repos/myproject-context/` instead of `~/repos/myproject/.context/`. - -### Step 3: Make It Stick - -Declaring `CTX_DIR` on every command is tedious. Pick one of these -methods to make the configuration permanent. The context directory -itself must be declared via `CTX_DIR`; `.ctxrc` does not carry the -path. - -#### Option A: `CTX_DIR` Environment Variable (*Recommended*) - -```bash -# Direct path. Works for ctx status / agent / add but degrades -# code-aware operations. See "What Works, What Quietly Degrades". -export CTX_DIR=~/repos/myproject-context/.context - -# Or, with the symlink approach above, point at the symlink path -# inside the code repo so code-aware operations stay healthy. -export CTX_DIR=~/repos/myproject/.context -``` - -Put either form in your shell profile (`~/.bashrc`, `~/.zshrc`) -or a direnv `.envrc`. - -For a single session, run `eval "$(ctx activate)"` from any -directory inside the project where exactly one `.context/` -candidate is visible (the symlink counts). `activate` does not -accept a path argument; bind a specific path by exporting -`CTX_DIR` directly instead. - -#### Option B: `.ctxrc` for Other Settings - -Put any settings (token budget, priority order, freshness files) in a -`.ctxrc` at the project root (`dirname(CTX_DIR)`), which here is the -parent of the external `.context/`: - -```yaml -# ~/repos/myproject-context/.ctxrc -token_budget: 16000 -``` - -`.ctxrc` is always read from the parent of `CTX_DIR`, so this file is -picked up whenever `CTX_DIR` points at -`~/repos/myproject-context/.context`. - -#### Resolution - -`ctx` reads the context directory from a single channel: the -`CTX_DIR` environment variable. When `CTX_DIR` is unset, `ctx` -errors with a "no context directory specified" hint pointing at -`ctx activate` and this recipe. When set, the value must be an -absolute path with `.context` as its basename; relative paths and -other names are rejected on first use. - -See -[Activating a Context Directory](activating-context.md) for the full -recipe. - -### Step 4: Agent Auto-Discovery via Bootstrap - -When context lives outside the project tree, your AI assistant needs to know -where to find it. The `ctx system bootstrap` command resolves the configured -context directory and communicates it to the agent automatically: - -```bash -$ ctx system bootstrap -ctx system bootstrap -==================== - -context_dir: /home/user/repos/myproject-context/.context - -Files: - CONSTITUTION.md, TASKS.md, DECISIONS.md, ... -``` - -The `CLAUDE.md` template generated by `ctx init` already instructs the agent to -run `ctx system bootstrap` at session start. Because `CTX_DIR` is inherited -by child processes, your agent picks up the external path automatically. - -Here is the relevant section from `CLAUDE.md` for reference: - -```markdown - -1. **Run `ctx system bootstrap`**: CRITICAL, not optional. - This tells you where the context directory is. If it returns any - error, relay the error output to the user verbatim, point them at - https://ctx.ist/recipes/activating-context/ for setup, and STOP. - Do not try to recover; the user decides. -``` - -Moreover, every nudge (*context checkpoint, persistence reminder, etc.*) also -includes a `Context: /home/user/repos/myproject-context/.context` footer, so -the agent remains anchored to the correct directory even in long sessions. - -Export `CTX_DIR` in your shell profile so every hook process inherits it: - -```bash -export CTX_DIR=~/repos/myproject-context/.context -``` - -### Step 5: Share with Teammates - -Teammates clone both repos and export `CTX_DIR`: - -```bash -# Clone the project -git clone git@github.com:org/myproject.git -cd myproject - -# Clone the private context repo -git clone git@github.com:org/myproject-context.git ~/repos/myproject-context -export CTX_DIR=~/repos/myproject-context/.context -``` - -If teammates use different paths, each developer sets their own `CTX_DIR`. - -For encryption key distribution across the team, see the -[Syncing Scratchpad Notes](scratchpad-sync.md) recipe. - -### Step 6: Day-to-Day Sync - -The external context repo has its own git history. Treat it like any other -repo: commit and push after sessions: - -```bash -cd ~/repos/myproject-context - -# After a session -git add -A -git commit -m "Session: refactored auth module, added rate-limit learning" -git push -``` - -Your AI assistant can do this too. When ending a session: - -```text -You: "Save what we learned and push the context repo." - -Agent: [runs ctx learning add, then commits and pushes the context repo] -``` - -You can also set up a post-session habit: project code gets committed to the -project repo, context gets committed to the context repo. - ----- - -## Conversational Approach - -You don't need to remember the flags; simply ask your assistant: - -### Set Up Your System Using Natural Language - -```text -You: "Set up ctx to use ~/repos/myproject-context as the context directory." - -Agent: "I'll set CTX_DIR to that path, run ctx init to materialize - it, and show you the export line to add to your shell - profile. Want me to seed the core context files too?" -``` - -### Configure Separate Repo for `.context` Folder Using Natural Language - -```text -You: "My context is in a separate repo. Can you load it?" - -Agent: [reads CTX_DIR, loads context from the external dir] - "Loaded. You have 3 pending tasks, last session was about the auth - refactor." -``` - ----- - -## Tips - -* **Start simple**. If you don't need external context yet, don't set it up. - The default `.context/` in-tree is the easiest path. Move to an external - repo when you have a concrete reason. -* **One context repo per project**. Sharing a single context directory across - multiple projects corrupts journals, state, and secrets. Use `ctx hub` for - cross-project knowledge sharing. -* **Export `CTX_DIR` in your shell profile** so hooks and tools inherit the - path without per-command flags. -* **Commit both repos at session boundaries**. Context without code history - (*or code without context history*) loses half the value. - ----- - -## Next Up - -**[The Complete Session →](session-lifecycle.md)**: Walk through a -full `ctx` session from start to finish. - -## See Also - -* [Setting Up `ctx` Across AI Tools](multi-tool-setup.md): initial setup recipe -* [Syncing Scratchpad Notes Across Machines](scratchpad-sync.md): distribute - encryption keys when context is shared -* [CLI Reference](../cli/index.md): full command list and global options diff --git a/docs/recipes/hub-cluster.md b/docs/recipes/hub-cluster.md index 491738ac3..d18c5a182 100644 --- a/docs/recipes/hub-cluster.md +++ b/docs/recipes/hub-cluster.md @@ -118,7 +118,6 @@ When registering a client, give it the **full peer list**: ```bash # In the project directory on the client: -eval "$(ctx activate)" ctx connection register hub-a.lan:9900 \ --token ctx_adm_... \ --peers hub-b.lan:9900,hub-c.lan:9900 diff --git a/docs/recipes/hub-getting-started.md b/docs/recipes/hub-getting-started.md index f2cd0521e..a2d2dad35 100644 --- a/docs/recipes/hub-getting-started.md +++ b/docs/recipes/hub-getting-started.md @@ -85,7 +85,6 @@ inside a project at `.context/.connect.enc`, so you have to tell ```bash cd ~/projects/alpha -eval "$(ctx activate)" ctx connection register localhost:9900 --token ctx_adm_7f3a1c2d... ``` @@ -124,7 +123,6 @@ ctx connection publish decision "Use UTC timestamps everywhere" ```bash cd ~/projects/beta -eval "$(ctx activate)" # bind CTX_DIR for this project ctx connection register localhost:9900 --token ctx_adm_7f3a1c2d... ctx connection subscribe decision learning convention ctx connection sync diff --git a/docs/recipes/hub-multi-machine.md b/docs/recipes/hub-multi-machine.md index 4de66707f..dbb25a666 100644 --- a/docs/recipes/hub-multi-machine.md +++ b/docs/recipes/hub-multi-machine.md @@ -111,7 +111,6 @@ On workstation `A`: ```bash cd ~/projects/x -eval "$(ctx activate)" ctx connection register nexus.local:9900 --token ctx_adm_... ctx connection subscribe decision learning convention ``` @@ -120,7 +119,6 @@ On workstation `B`: ```bash cd ~/projects/y -eval "$(ctx activate)" ctx connection register nexus.local:9900 --token ctx_adm_... ctx connection subscribe decision learning convention ``` diff --git a/docs/recipes/hub-personal.md b/docs/recipes/hub-personal.md index 2b9f759fc..49512faf7 100644 --- a/docs/recipes/hub-personal.md +++ b/docs/recipes/hub-personal.md @@ -25,16 +25,6 @@ surface when you open project B next Thursday. roughly five-minute setup). This recipe assumes the hub is already running and you've registered at least two projects. -!!! warning "Activate Each Project First" - Run `eval "$(ctx activate)"` after each `cd ` (or wire - it into direnv). The hub server (`ctx hub start`, etc.) runs on - the server and doesn't need this; the commands in this recipe - (`ctx add --share`, `ctx agent --include-hub`, - `ctx connection ...`) live inside a project and do. If you - skip the `eval`, they'll fail with `Error: no context directory - specified`. See - [Activating a Context Directory](activating-context.md). - ## The Core Loop Every day, the same three verbs matter: diff --git a/docs/recipes/hub-team.md b/docs/recipes/hub-team.md index 4ae4a2d2c..b47ac6975 100644 --- a/docs/recipes/hub-team.md +++ b/docs/recipes/hub-team.md @@ -31,14 +31,11 @@ without ceremony. - Each team member has `ctx` installed and has `ctx connection register`-ed their working projects with the hub. -- Each project on each workstation has been activated for - the shell with `eval "$(ctx activate)"`. The hub server - (`ctx hub start`, etc.) doesn't need this, but the - client side (`ctx connection ...`, `ctx add --share`) - lives in a project and does. If you skip activation, - those client commands fail with `Error: no context - directory specified`. See - [Activating a Context Directory](activating-context.md). +- Client-side commands (`ctx connection ...`, + `ctx add --share`) must be run from each project's root — + `ctx` reads `$PWD/.context/`. The hub server + (`ctx hub start`, etc.) doesn't need this; it operates on + the hub data directory rather than a project. ## Trust Model: Read This First diff --git a/docs/recipes/index.md b/docs/recipes/index.md index c742ade0a..3a99801a9 100644 --- a/docs/recipes/index.md +++ b/docs/recipes/index.md @@ -41,16 +41,6 @@ pipeline works for Turkish, Japanese, and any other locale. --- -### [Keeping Context in a Separate Repo](external-context.md) - -Store context files **outside** the project tree: in a private repo, -shared directory, or anywhere else. Useful for open source projects -with private context or **multi-repo** setups. - -**Uses**: `ctx init`, `CTX_DIR`, `.ctxrc`, `/ctx-status` - ---- - ## Knowledge Base (Phase KB) ### [Build a Knowledge Base](build-a-knowledge-base.md) diff --git a/docs/recipes/knowledge-capture.md b/docs/recipes/knowledge-capture.md index e12a2b05e..047583c83 100644 --- a/docs/recipes/knowledge-capture.md +++ b/docs/recipes/knowledge-capture.md @@ -24,13 +24,6 @@ rejected. `ctx add` commands. The agent automatically picks up session ID, branch, and commit hash from its context, so no manual flags are needed. -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, the `ctx add ...` / `ctx reindex` / - `ctx decision ...` / `ctx learning ...` commands below fail - with `Error: no context directory specified`. See - [Activating a Context Directory](activating-context.md). - ## TL;DR ```text diff --git a/docs/recipes/memory-bridge.md b/docs/recipes/memory-bridge.md index 0a634adab..8c1fab5d0 100644 --- a/docs/recipes/memory-bridge.md +++ b/docs/recipes/memory-bridge.md @@ -37,12 +37,6 @@ ctx memory diff # See what changed since last sync The `check-memory-drift` hook nudges automatically when MEMORY.md changes - you don't need to remember to sync manually. -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, `ctx memory ...` fails with `Error: no - context directory specified`. See - [Activating a Context Directory](activating-context.md). - ## Commands and Skills Used | Tool | Type | Purpose | diff --git a/docs/recipes/multi-tool-setup.md b/docs/recipes/multi-tool-setup.md index bcd0f92b6..a0ec60b8e 100644 --- a/docs/recipes/multi-tool-setup.md +++ b/docs/recipes/multi-tool-setup.md @@ -24,7 +24,6 @@ which AI tool you use. ```bash cd your-project ctx init # creates .context/ -eval "$(ctx activate)" # bind CTX_DIR for this shell source <(ctx completion zsh) # shell completion (or bash/fish) # ## Claude Code (automatic after plugin install) ## @@ -32,7 +31,7 @@ claude /plugin marketplace add ActiveMemory/ctx claude /plugin install ctx@activememory-ctx # ## OpenCode ## -ctx setup opencode --write && ctx init && eval "$(ctx activate)" +ctx setup opencode --write && ctx init # ## Cursor / Aider / Copilot / Windsurf ## ctx setup cursor # or: aider, copilot, windsurf @@ -42,13 +41,8 @@ npx gitnexus analyze # code knowledge graph # Add Gemini Search MCP server for grounded web search ``` -!!! warning "Activate the Project Once Per Shell" - Run `eval "$(ctx activate)"` after `ctx init`. The `ctx setup`, - `ctx init`, and `ctx completion` commands work without it, but - if you skip the `eval`, most others (`ctx agent`, `ctx load`, - `ctx watch`, `ctx journal ...`) fail with `Error: no context - directory specified`. See - [Activating a Context Directory](activating-context.md). +Run subsequent `ctx` commands from the project root; `ctx` always +reads `$PWD/.context/`. Create a [`.ctxrc`](../home/configuration.md) in your project root to configure token budgets, context directory, drift thresholds, and more. @@ -93,31 +87,12 @@ This produces the following structure: AGENT_PLAYBOOK.md # How AI tools should use this system ``` -!!! note "Using a Different `.context` Directory" - The `.context/` directory doesn't have to live inside your project. Point - `ctx` to an external folder by exporting `CTX_DIR` (the only - declaration channel). - - Useful when context must stay private while the code is public, or - when you want to commit notes to a separate repo. - - **Caveats** (the recipe covers both with workarounds): - - * **Code-aware operations degrade silently.** `ctx sync`, `ctx drift`, - and the memory-drift hook read the codebase from - `dirname(CTX_DIR)`. With an external `.context/`, that's the - context repo, not your code repo. They scan the wrong tree without - erroring. The recipe shows a symlink workaround that keeps both - healthy. - * **One `.context/` per project, always.** Sharing one directory - across multiple projects corrupts journals, state, and secrets. - For cross-project knowledge sharing (CONSTITUTION, CONVENTIONS, - ARCHITECTURE, etc.) use [`ctx hub`](hub-overview.md), not a - shared `.context/`. - - See [External Context](external-context.md) for the full recipe - and [Configuration](../home/configuration.md#environment-variables) - for the resolver details. +!!! note "One `.context/` per project" + `ctx` reads `$PWD/.context/`; the directory always lives + alongside `.git/` at the project root. Sharing one directory + across multiple projects corrupts journals, state, and + secrets. For cross-project knowledge sharing (CONSTITUTION, + CONVENTIONS, ARCHITECTURE, etc.) use [`ctx hub`](hub-overview.md). For Claude Code, install the **`ctx` plugin** to get hooks and skills: @@ -173,7 +148,7 @@ as `ActiveMemory/ctx`. Run the one-liner from the project root: ```bash -ctx setup opencode --write && ctx init && eval "$(ctx activate)" +ctx setup opencode --write && ctx init ``` This deploys a lifecycle plugin, slash command skills, `AGENTS.md`, and @@ -192,7 +167,7 @@ Install the **`ctx`** extension from the (publisher: `activememory`). Then, from your project root: ```bash -ctx init && eval "$(ctx activate)" +ctx init ``` Open Copilot Chat and type `@ctx /init` to verify. The extension @@ -480,12 +455,6 @@ current integration is MCP-based and limited to Gemini Search and GitNexus. If you use a different search or code intelligence tool, skills will degrade gracefully to built-in capabilities. -## Next Up - -**[Keeping Context in a Separate Repo →](external-context.md)**: Store -context files outside the project tree for multi-repo or open source -setups. - ## See Also * [The Complete Session](session-lifecycle.md): full session lifecycle recipe diff --git a/docs/recipes/multilingual-sessions.md b/docs/recipes/multilingual-sessions.md index 2480e8e5f..13c21d56f 100644 --- a/docs/recipes/multilingual-sessions.md +++ b/docs/recipes/multilingual-sessions.md @@ -95,12 +95,6 @@ sessions from all team members regardless of language. After configuring, test with `ctx journal source`. Sessions with the new prefixes should appear in the output. -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` from the project root. If you skip - it, `ctx journal ...` fails with `Error: no context directory - specified`. See - [Activating a Context Directory](activating-context.md). - ## What This Does NOT Do - **Change the interface language**: `ctx` output is always English. diff --git a/docs/recipes/permission-snapshots.md b/docs/recipes/permission-snapshots.md index 0e3dc939f..4fc393e52 100644 --- a/docs/recipes/permission-snapshots.md +++ b/docs/recipes/permission-snapshots.md @@ -24,12 +24,6 @@ ctx permission snapshot # save golden image ctx permission restore # reset to golden state ``` -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, `ctx permission ...` fails with - `Error: no context directory specified`. See - [Activating a Context Directory](activating-context.md). - ## The Solution Save a curated `settings.local.json` as a **golden image**, then restore diff --git a/docs/recipes/publishing.md b/docs/recipes/publishing.md index 9b661acef..9e4d2ec4b 100644 --- a/docs/recipes/publishing.md +++ b/docs/recipes/publishing.md @@ -32,12 +32,6 @@ ctx journal site --serve # 3. build and serve the journal /ctx-blog-changelog v0.1.0 "v0.2" # 5. write a changelog post ``` -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, `ctx journal ...` fails with `Error: no - context directory specified`. See - [Activating a Context Directory](activating-context.md). - Read on for details on each stage. ## Commands and Skills Used diff --git a/docs/recipes/scratchpad-sync.md b/docs/recipes/scratchpad-sync.md index 3ff4c017e..26e3fdb91 100644 --- a/docs/recipes/scratchpad-sync.md +++ b/docs/recipes/scratchpad-sync.md @@ -21,21 +21,12 @@ you cannot read or write entries. ```bash ctx init # 1. generates key -eval "$(ctx activate)" # 2. bind CTX_DIR -scp ~/.ctx/.ctx.key user@machine-b:~/.ctx/.ctx.key # 3. copy key -chmod 600 ~/.ctx/.ctx.key # 4. secure it +scp ~/.ctx/.ctx.key user@machine-b:~/.ctx/.ctx.key # 2. copy key +chmod 600 ~/.ctx/.ctx.key # 3. secure it # Normal git push/pull syncs the encrypted scratchpad.enc # On conflict: ctx pad resolve → rebuild → git add + commit ``` -!!! warning "Activate Each Machine" - Run `eval "$(ctx activate)"` from the project root on every - machine that reads or writes the scratchpad: after each - `ctx init`, or after each clone on machine B. If you skip it, - `ctx pad ...` fails with `Error: no context directory - specified`. See - [Activating a Context Directory](activating-context.md). - !!! tip "Finding Your Key File" The key is always at `~/.ctx/.ctx.key` - one key, one machine. diff --git a/docs/recipes/scratchpad-with-claude.md b/docs/recipes/scratchpad-with-claude.md index 8a5bb665e..d503488d1 100644 --- a/docs/recipes/scratchpad-with-claude.md +++ b/docs/recipes/scratchpad-with-claude.md @@ -29,12 +29,6 @@ Entries are **encrypted at rest** and travel with `git`. Use the `/ctx-pad` skill to manage entries from inside your AI session. -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, `ctx pad ...` fails with - `Error: no context directory specified`. See - [Activating a Context Directory](activating-context.md). - ## Commands and Skills Used | Tool | Type | Purpose | diff --git a/docs/recipes/session-archaeology.md b/docs/recipes/session-archaeology.md index c187b9a3f..5d7a490cd 100644 --- a/docs/recipes/session-archaeology.md +++ b/docs/recipes/session-archaeology.md @@ -33,12 +33,6 @@ ctx journal import --all ctx journal site --serve ``` -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, the `ctx journal ...` commands below - fail with `Error: no context directory specified`. See - [Activating a Context Directory](activating-context.md). - **Enrich** ```text diff --git a/docs/recipes/session-changes.md b/docs/recipes/session-changes.md index 9a940b3f3..659011d70 100644 --- a/docs/recipes/session-changes.md +++ b/docs/recipes/session-changes.md @@ -29,12 +29,6 @@ ctx change --since 48h ctx change --since 2026-03-10 ``` -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, `ctx change` fails with `Error: no - context directory specified`. See - [Activating a Context Directory](activating-context.md). - ## How Reference Time Works `ctx change` needs a reference point to compare against. It tries diff --git a/docs/recipes/session-lifecycle.md b/docs/recipes/session-lifecycle.md index 245453707..c44759467 100644 --- a/docs/recipes/session-lifecycle.md +++ b/docs/recipes/session-lifecycle.md @@ -29,21 +29,6 @@ persisting context before you close it, so you can see how each piece connects. Read on for the full walkthrough with examples. -!!! warning "Before You Start: Activate the Project" - `ctx` commands (and the skills that call them) require `CTX_DIR` to be - declared for the shell you're working in; `ctx` does not walk the - filesystem to find `.context/`. Once per shell (or via your shell - rc / direnv): - - ```bash - eval "$(ctx activate)" - ``` - - If you skip this, every skill below will surface an error naming - the fix. See - [Activating a Context Directory](activating-context.md) for the - full recipe. - !!! note "What Is a Readback?" A **readback** is a **structured summary** where the agent plays back what it knows: diff --git a/docs/recipes/session-reminders.md b/docs/recipes/session-reminders.md index ee00f4c06..1508ef0e5 100644 --- a/docs/recipes/session-reminders.md +++ b/docs/recipes/session-reminders.md @@ -25,12 +25,6 @@ ctx remind dismiss 1 # or batch: ctx remind dismiss 1 3-5 Reminders surface automatically at session start: VERBATIM, every session, until you dismiss them. -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, `ctx remind ...` fails with - `Error: no context directory specified`. See - [Activating a Context Directory](activating-context.md). - ## Commands and Skills Used | Tool | Type | Purpose | diff --git a/docs/recipes/state-maintenance.md b/docs/recipes/state-maintenance.md index 8a8fc8f99..e48405418 100644 --- a/docs/recipes/state-maintenance.md +++ b/docs/recipes/state-maintenance.md @@ -30,12 +30,6 @@ ctx prune # prune files older than 7 days ctx prune --days 1 # more aggressive: keep only today ``` -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, `ctx prune` / `ctx status` fail with - `Error: no context directory specified`. See - [Activating a Context Directory](activating-context.md). - ## Commands Used | Tool | Type | Purpose | diff --git a/docs/recipes/task-management.md b/docs/recipes/task-management.md index a6b52cd2c..1f39c153b 100644 --- a/docs/recipes/task-management.md +++ b/docs/recipes/task-management.md @@ -21,13 +21,6 @@ How do you manage work items that span multiple sessions without losing context? `ctx task add`. The agent automatically picks up session ID, branch, and commit hash from its context, so no manual flags are needed. -!!! warning "Activate the Project First" - Run `eval "$(ctx activate)"` once per terminal in the project - root. If you skip it, the `ctx task add` / `ctx task ...` - commands below fail with `Error: no context directory - specified`. See - [Activating a Context Directory](activating-context.md). - ## TL;DR **Manage Tasks**: diff --git a/docs/recipes/troubleshooting.md b/docs/recipes/troubleshooting.md index 4017a8183..5daf09ee4 100644 --- a/docs/recipes/troubleshooting.md +++ b/docs/recipes/troubleshooting.md @@ -137,34 +137,25 @@ QA reminder events from that specific session. ## Common Problems -### "No context directory specified for this project" +### "No `.context/` at this directory" -**Symptoms**: Any `ctx` command fails with -`Error: no context directory specified for this project` (*possibly -with a likely-candidate hint or a candidate list depending on what's -visible from your CWD*). +**Symptoms**: Any `ctx` command fails with `ctx: no .context/ at +. Run \`ctx init\` here, or cd to a project that has one.` -**Cause**: `ctx` does not search the filesystem for a `.context/` -directory. You have to declare which one to use before running -day-to-day commands. +**Cause**: `ctx` reads `$PWD/.context/` and you ran the command +from a directory that does not have one. -**Fix**: bind `CTX_DIR` for the current shell: - -```bash -eval "$(ctx activate)" -``` - -See [Activating a Context Directory](activating-context.md) for the -full recipe (one-shot `CTX_DIR=...` inline form, CI patterns, direnv -setup). +**Fix**: either `cd` into a project root that already has +`.context/`, or run `ctx init` in the current directory to create +one. ### "`ctx`: Not Initialized" -**Symptoms**: After declaring `CTX_DIR`, the command fails with -`ctx: not initialized - run "ctx init" first`. +**Symptoms**: `ctx` finds `.context/` at `$PWD` but the command +fails with `ctx: not initialized - run "ctx init" first`. -**Cause**: The declared directory exists but hasn't been initialized -with template files. +**Cause**: The directory exists but hasn't been populated with +template files. **Fix**: @@ -173,10 +164,10 @@ ctx init # create .context/ with template files ctx init --minimal # or just the essentials (CONSTITUTION, TASKS, DECISIONS) ``` -**Commands that work without CTX_DIR or initialization**: `ctx init`, -`ctx activate`, `ctx deactivate`, `ctx setup`, `ctx doctor`, -`ctx guide`, `ctx why`, `ctx config switch/status`, `ctx hub *`, and -help-only grouping commands. +**Commands that work without `.context/` or initialization**: `ctx init`, +`ctx setup`, `ctx doctor`, `ctx guide`, `ctx why`, +`ctx config switch/status`, `ctx hub *`, and help-only grouping +commands. ### "My CLI and My Claude Code Session Disagree on the Project" @@ -184,20 +175,15 @@ help-only grouping commands. wrong `.context/`; or you ran `ctx remind add` in shell A and the reminder shows up in project B's notifications. -**Cause**: `CTX_DIR` is sourced from three different surfaces, and -they can drift apart: - -| Surface | Source of `CTX_DIR` | Bound when | -|------------------------------------|---------------------------------------------|-----------------------------------------| -| Claude Code hooks | `${CLAUDE_PROJECT_DIR}/.context` (injected) | Every hook line; the project Claude is in | -| `!`-pragma in chat / interactive shell | Whatever the parent shell exported | When you ran `eval "$(ctx activate)"` | -| New shell tab opened mid-session | Whatever your shellrc exports | Login | +**Cause**: Different shells were launched from different working +directories. `ctx` reads `$PWD/.context/`; if your terminal tab +is `cd`'d into project A and your Claude Code session is in +project B, `!`-pragma calls write to A while in-session calls +write to B. -When these drift, the per-prompt `check-anchor-drift` hook fires a -verbatim warning naming both values. To fix: re-run -`eval "$(ctx activate)"` from inside the project the Claude Code -session is editing, or close the shell tab and reopen it from the -right working directory. +**Fix**: `cd` the shell into the same project root the Claude +Code session is in, or close the tab and reopen it from the right +working directory. ### "My Hook Isn't Firing" diff --git a/hack/lint-powershell.sh b/hack/lint-powershell.sh new file mode 100755 index 000000000..994a7c863 --- /dev/null +++ b/hack/lint-powershell.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# / ctx: https://ctx.ist +# ,'`./ do you remember? +# `.,'\ +# \ Copyright 2026-present Context contributors. +# SPDX-License-Identifier: Apache-2.0 +# +# lint-powershell.sh — run PSScriptAnalyzer against embedded +# PowerShell scripts. +# +# Scope: PowerShell scripts that ship inside the binary as embedded +# assets and run on user machines (the `*.ps1` halves of the +# Copilot CLI hook pairs under +# `internal/assets/integrations/copilot-cli/scripts/`). Same +# stakes as the shell-side: bug here hits every Windows / pwsh +# user. +# +# Requires pwsh (PowerShell Core) with PSScriptAnalyzer installed. +# Install hint: +# pwsh -NoProfile -Command 'Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser' +# +# Severity: fails on `Warning` and above (matches PSScriptAnalyzer's +# canonical band; equivalent to the `warning` threshold used in +# lint-shellcheck.sh). +# +# Exit code: +# 0 — no findings +# 1 — findings or pwsh / module not available + +set -euo pipefail + +if ! command -v pwsh >/dev/null 2>&1; then + echo "pwsh (PowerShell Core) not installed. Install via:" >&2 + echo " macOS: brew install powershell/tap/powershell" >&2 + echo " Debian/Ubuntu: see https://learn.microsoft.com/powershell/scripting/install/install-debian" >&2 + exit 1 +fi + +SEVERITY="${SEVERITY:-Warning}" + +# Targets: embedded scripts only. +TARGETS=() +while IFS= read -r -d '' f; do + TARGETS+=("$f") +done < <( + find internal/assets/integrations/copilot-cli/scripts \ + -type f -name "*.ps1" -print0 | sort -z +) + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + echo "No embedded PowerShell scripts found." >&2 + exit 0 +fi + +echo "Running PSScriptAnalyzer (severity=$SEVERITY) on ${#TARGETS[@]} script(s)..." + +# Inline pwsh script: Import-Module, run Invoke-ScriptAnalyzer +# against every target at the chosen severity floor, print all +# findings, exit non-zero if any. +PS_SCRIPT=' +param([string]$Severity, [string[]]$Paths) + +if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) { + Write-Error "PSScriptAnalyzer not installed. Install via: Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser" + exit 1 +} +Import-Module PSScriptAnalyzer + +$severities = @{ + "Information" = @("Information","Warning","Error","ParseError") + "Warning" = @("Warning","Error","ParseError") + "Error" = @("Error","ParseError") +} +if (-not $severities.ContainsKey($Severity)) { + Write-Error "Unknown severity: $Severity (allowed: Information, Warning, Error)" + exit 1 +} +$allowed = $severities[$Severity] + +$findings = @() +foreach ($p in $Paths) { + $r = Invoke-ScriptAnalyzer -Path $p -Severity $allowed -ErrorAction Stop + if ($r) { $findings += $r } +} +if ($findings.Count -gt 0) { + $findings | Format-Table -AutoSize | Out-String -Width 200 | Write-Output + Write-Error "PSScriptAnalyzer: $($findings.Count) finding(s) at severity >= $Severity" + exit 1 +} +' + +pwsh -NoProfile -Command "$PS_SCRIPT" -Severity "$SEVERITY" -Paths "${TARGETS[@]}" +echo "PSScriptAnalyzer: no findings at severity >= $SEVERITY." diff --git a/hack/lint-shellcheck.sh b/hack/lint-shellcheck.sh new file mode 100755 index 000000000..b88bae5fe --- /dev/null +++ b/hack/lint-shellcheck.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# / ctx: https://ctx.ist +# ,'`./ do you remember? +# `.,'\ +# \ Copyright 2026-present Context contributors. +# SPDX-License-Identifier: Apache-2.0 +# +# lint-shellcheck.sh — run shellcheck against embedded shell scripts. +# +# Scope: scripts that ship inside the binary as embedded assets and +# run on user machines (`internal/assets/hooks/trace/*.sh` and +# `internal/assets/integrations/copilot-cli/scripts/*.sh`). These +# are the highest-stakes scripts — a bug there hits every user, not +# just contributors. `hack/` scripts are out of scope for now (they +# run only on developer machines). +# +# Severity: fails on `warning` and above (matches the project's +# zero-issues posture for other linters). Use --info to see notes +# too. +# +# Exit code: +# 0 — no findings +# 1 — findings or shellcheck not installed + +set -euo pipefail + +if ! command -v shellcheck >/dev/null 2>&1; then + echo "shellcheck not installed. Install via:" >&2 + echo " macOS: brew install shellcheck" >&2 + echo " Debian/Ubuntu: apt-get install shellcheck" >&2 + exit 1 +fi + +SEVERITY="${SEVERITY:-warning}" + +# Targets: embedded scripts only. Sorted so the output is stable +# across local runs and CI. +TARGETS=() +while IFS= read -r -d '' f; do + TARGETS+=("$f") +done < <( + find \ + internal/assets/hooks/trace \ + internal/assets/integrations/copilot-cli/scripts \ + -type f -name "*.sh" -print0 | sort -z +) + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + echo "No embedded shell scripts found." >&2 + exit 0 +fi + +echo "Running shellcheck (severity=$SEVERITY) on ${#TARGETS[@]} script(s)..." +shellcheck --severity="$SEVERITY" "${TARGETS[@]}" +echo "shellcheck: no findings at severity >= $SEVERITY." diff --git a/internal/assets/claude/CLAUDE.md b/internal/assets/claude/CLAUDE.md index 255e96ee4..e53de0b89 100644 --- a/internal/assets/claude/CLAUDE.md +++ b/internal/assets/claude/CLAUDE.md @@ -14,7 +14,7 @@ This project uses Context (`ctx`) for context persistence across sessions. This tells you where the context directory is. If it returns any error, relay the error output to the user verbatim, point them at - https://ctx.ist/recipes/activating-context/ for setup, and STOP. + https://ctx.ist/home/getting-started/ for setup, and STOP. Do not try to activate, initialize, or otherwise recover: **those are the user's decisions**. Wait for their next instruction. 2. **Read AGENT_PLAYBOOK.md** from the context directory: it explains diff --git a/internal/assets/claude/hooks/hooks.json b/internal/assets/claude/hooks/hooks.json index 3ba2d70c3..74ca2f82b 100644 --- a/internal/assets/claude/hooks/hooks.json +++ b/internal/assets/claude/hooks/hooks.json @@ -3,56 +3,55 @@ "PreToolUse": [ { "matcher": ".*", - "hooks": [{"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system context-load-gate"}] + "hooks": [{"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system context-load-gate"}] }, { "matcher": "Bash", - "hooks": [{"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system block-non-path-ctx"}] + "hooks": [{"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system block-non-path-ctx"}] }, { "matcher": "Bash", - "hooks": [{"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system qa-reminder"}] + "hooks": [{"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system qa-reminder"}] }, { "matcher": "EnterPlanMode", - "hooks": [{"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system specs-nudge"}] + "hooks": [{"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system specs-nudge"}] }, { "matcher": ".*", - "hooks": [{"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx agent --budget 8000 2>/dev/null || true"}] + "hooks": [{"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx agent --budget 8000 2>/dev/null || true"}] } ], "PostToolUse": [ { "matcher": "Bash", - "hooks": [{"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system post-commit"}] + "hooks": [{"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system post-commit"}] }, { "matcher": "Edit", - "hooks": [{"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-task-completion"}] + "hooks": [{"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-task-completion"}] }, { "matcher": "Write", - "hooks": [{"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-task-completion"}] + "hooks": [{"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-task-completion"}] } ], "UserPromptSubmit": [ { "hooks": [ - {"type": "command", "command": "CTX_DIR_INHERITED=\"${CTX_DIR:-}\" CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-anchor-drift"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-context-size"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-ceremony"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-persistence"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-journal"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-reminder"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-version"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-resource"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-knowledge"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-map-staleness"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-memory-drift"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-freshness"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system check-skill-discovery"}, - {"type": "command", "command": "CTX_DIR=\"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}/.context\" ctx system heartbeat"} + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-context-size"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-ceremony"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-persistence"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-journal"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-reminder"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-version"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-resource"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-knowledge"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-map-staleness"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-memory-drift"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-freshness"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system check-skill-discovery"}, + {"type": "command", "command": "cd \"${CLAUDE_PROJECT_DIR:?CLAUDE_PROJECT_DIR unset; cannot anchor ctx}\" && ctx system heartbeat"} ] } ] diff --git a/internal/assets/claude/skills/ctx-journal-enrich-all/SKILL.md b/internal/assets/claude/skills/ctx-journal-enrich-all/SKILL.md index 3e3901dc5..95c99f933 100644 --- a/internal/assets/claude/skills/ctx-journal-enrich-all/SKILL.md +++ b/internal/assets/claude/skills/ctx-journal-enrich-all/SKILL.md @@ -27,8 +27,8 @@ the journal directory has no `.md` files at all, or if there are them first. ```bash -CTX_DIR=$(ctx system bootstrap -q) -JOURNAL_DIR="$CTX_DIR/journal" +CTX_PATH=$(ctx system bootstrap -q) +JOURNAL_DIR="$CTX_PATH/journal" # Check if any .md files exist md_count=$(ls "$JOURNAL_DIR"/*.md 2>/dev/null | wc -l) @@ -56,8 +56,8 @@ List all journal entries that lack enrichment using the state file: ```bash # List .md files in journal dir and check state -CTX_DIR=$(ctx system bootstrap -q) -for f in "$CTX_DIR/journal/"*.md; do +CTX_PATH=$(ctx system bootstrap -q) +for f in "$CTX_PATH/journal/"*.md; do name=$(basename "$f") ctx system mark-journal --check "$name" enriched || echo "$f" done @@ -183,8 +183,8 @@ patterns, then inserts frontmatter and marks state automatically. 1. Build a file list of eligible entries (non-multipart, 20+ lines, missing `type:` and `outcome:` fields): ```bash - CTX_DIR=$(ctx system bootstrap -q) - for f in "$CTX_DIR"/journal/*.md; do + CTX_PATH=$(ctx system bootstrap -q) + for f in "$CTX_PATH"/journal/*.md; do [ -f "$f" ] || continue has_type=$(head -30 "$f" | grep -c '^type:' || true) has_outcome=$(head -30 "$f" | grep -c '^outcome:' || true) diff --git a/internal/assets/claude/skills/ctx-journal-enrich/SKILL.md b/internal/assets/claude/skills/ctx-journal-enrich/SKILL.md index 9d29aee5f..9be9828af 100644 --- a/internal/assets/claude/skills/ctx-journal-enrich/SKILL.md +++ b/internal/assets/claude/skills/ctx-journal-enrich/SKILL.md @@ -48,8 +48,8 @@ an `enriched` date: ```bash # List unenriched entries using state file -CTX_DIR=$(ctx system bootstrap -q) -for f in "$CTX_DIR/journal/"*.md; do +CTX_PATH=$(ctx system bootstrap -q) +for f in "$CTX_PATH/journal/"*.md; do name=$(basename "$f") ctx system mark-journal --check "$name" enriched || echo "$f" done | head -10 diff --git a/internal/assets/claude/skills/ctx-plan/SKILL.md b/internal/assets/claude/skills/ctx-plan/SKILL.md index 9db1d560a..4f050d468 100644 --- a/internal/assets/claude/skills/ctx-plan/SKILL.md +++ b/internal/assets/claude/skills/ctx-plan/SKILL.md @@ -1,8 +1,31 @@ --- name: ctx-plan -description: "Stress-test a plan through adversarial interview. Find what's weak, missing, or unexamined before the user commits. Use when the user wants their plan scrutinized." +description: "Stress-test a plan through adversarial interview; produces a debated brief at .context/briefs/-.md that /ctx-spec --brief consumes. Use when the user wants their bet scrutinized before it becomes a spec." --- +## Canonical Chain + +The project's design-to-implementation pipeline is: + +```text +/ctx-brainstorm → /ctx-plan → /ctx-spec → /ctx-implement + (vague) (contested) (committed) (execution) +``` + +`/ctx-plan` is the second step. It takes an idea that is no +longer vague but not yet committed, attacks it, and writes a +*debated brief* to `.context/briefs/-.md`. The brief +is consumed by `/ctx-spec --brief ` to produce the +committed spec. This skill does **not** produce an implementation +plan or a task list; the deliverable is the brief. + +Do not invert the order. A "plan" run after `/ctx-spec` is +fixing the foundation while the building is up; run +`/ctx-brainstorm` if the bet hasn't formed yet, then this skill, +then `/ctx-spec`. + +## Role + You are a skeptical collaborator. The user has a plan and wants it attacked. Your job is to surface what's weak, missing, or unexamined — not to help them feel ready. diff --git a/internal/assets/claude/skills/ctx-spec/SKILL.md b/internal/assets/claude/skills/ctx-spec/SKILL.md index d56073369..1e39f84cf 100644 --- a/internal/assets/claude/skills/ctx-spec/SKILL.md +++ b/internal/assets/claude/skills/ctx-spec/SKILL.md @@ -6,12 +6,33 @@ description: "Scaffold a feature spec from the project template. Use when planni Scaffold a new spec from `specs/tpl/spec-template.md` and walk through each section with the user to produce a complete design document. +## Canonical Chain + +The project's design-to-implementation pipeline is: + +```text +/ctx-brainstorm → /ctx-plan → /ctx-spec → /ctx-implement + (vague) (contested) (committed) (execution) +``` + +`/ctx-spec` is the third step. It consumes the *debated brief* +produced by `/ctx-plan` (via `--brief `) or writes a fresh +spec interactively when no brief is needed. Specs are committed +artifacts under `specs/`; briefs are working state under +`.context/briefs/` that the spec absorbs. + +Do not invert the order. A spec without a settled bet ahead of +it is a wishlist; running `/ctx-plan` after `/ctx-spec` is fixing +the foundation while the building is up. + ## When to Use - Before implementing a non-trivial feature - When a task says "Spec: `specs/X.md`" and the file does not exist - When `/ctx-brainstorm` has produced a validated design that needs a written artifact +- When `/ctx-plan` has produced a debated brief that needs a + committed spec (use `--brief `) - When the user says "let's spec this out" or "write a spec for..." ## When NOT to Use @@ -19,6 +40,8 @@ each section with the user to produce a complete design document. - Bug fixes or small changes (just do them) - When a spec already exists (read it instead) - When the design is still vague (use `/ctx-brainstorm` first) +- When the bet is contested but not yet stress-tested (use + `/ctx-plan` first; its output is the brief this skill consumes) ## Usage Examples diff --git a/internal/assets/commands/commands.yaml b/internal/assets/commands/commands.yaml index bf703a3f7..f2ae1d2fc 100644 --- a/internal/assets/commands/commands.yaml +++ b/internal/assets/commands/commands.yaml @@ -8,41 +8,6 @@ # # See also: examples.yaml (Example fields), flags.yaml (flag descriptions). -activate: - long: |- - Emit a shell-specific export statement that binds CTX_DIR to the - selected .context/ directory for the current shell. - - Intended usage: - - eval "$(ctx activate)" # bind the one visible .context/ - - Activate scans upward from the current working directory - collecting every .context/ directory found. When exactly one is - visible it emits its path; when none or several are found it - refuses and prints the candidates so a human chooses explicitly - (typically by `cd`-ing closer to the project root). - - When the parent shell already has CTX_DIR set to a different - value, the output gains a leading `# ctx: replacing stale ...` - comment so the user sees the change in `eval` output before the - replacement takes effect. - - Activate is the only command in the CLI that walks the filesystem - during resolution. All other commands read CTX_DIR and error - loudly when it is undeclared, relative, or non-canonical. - short: Emit shell export to bind CTX_DIR -deactivate: - long: |- - Emit a shell-specific `unset CTX_DIR` statement for the current - shell. - - Intended usage: - - eval "$(ctx deactivate)" - - Pairs with `ctx activate` for symmetric shell integration. - short: Emit shell unset for CTX_DIR convention: long: |- Manage the CONVENTIONS.md file. @@ -1186,21 +1151,6 @@ system.blocknonpathctx: short: Block non-PATH ctx invocations bootstrap: short: Print context location for AI agents -system.checkanchordrift: - long: |- - Compares the parent shell's CTX_DIR (captured as - CTX_DIR_INHERITED before the standard hook injection) - against the Claude-injected CLAUDE_PROJECT_DIR/.context - anchor. When the two diverge, emits a VERBATIM warning - banner naming both values so the user can spot when - their interactive CLI / `!`-pragma calls are writing to - a different project than Claude Code is in. - - Hook event: UserPromptSubmit - Output: VERBATIM warning (when drifted), silent otherwise - Silent when: CTX_DIR_INHERITED is empty (no shell-level - activation), or matches CTX_DIR after filepath.Clean. - short: Stale-anchor sanity hook system.checkceremony: long: |- Scans the last 3 journal entries for /ctx-remember and /ctx-wrap-up diff --git a/internal/assets/commands/examples.yaml b/internal/assets/commands/examples.yaml index 12ddaecc5..006fc7064 100644 --- a/internal/assets/commands/examples.yaml +++ b/internal/assets/commands/examples.yaml @@ -9,14 +9,6 @@ # # See also: commands.yaml (Short/Long), flags.yaml (flag descriptions). -activate: - short: |2- - eval "$(ctx activate)" - -deactivate: - short: |2- - eval "$(ctx deactivate)" - convention.add: short: |2- ctx convention add "Use camelCase for function names" @@ -438,9 +430,6 @@ system.blocknonpathctx: bootstrap: short: ' ctx system bootstrap' -system.checkanchordrift: - short: ' ctx system check-anchor-drift' - system.checkceremony: short: ' ctx system check-ceremony' diff --git a/internal/assets/commands/flags.yaml b/internal/assets/commands/flags.yaml index f491c344f..2b60901a9 100644 --- a/internal/assets/commands/flags.yaml +++ b/internal/assets/commands/flags.yaml @@ -8,8 +8,6 @@ # # See also: commands.yaml (Short/Long), examples.yaml (Example fields). -activate.shell: - short: Shell dialect for the emitted export (bash, zsh, sh); default auto-detects from $SHELL add.application: short: 'Application for learnings: how to apply this going forward (required for learnings)' add.branch: diff --git a/internal/assets/commands/text/errors.yaml b/internal/assets/commands/text/errors.yaml index 156575d30..78e60f51a 100644 --- a/internal/assets/commands/text/errors.yaml +++ b/internal/assets/commands/text/errors.yaml @@ -44,54 +44,28 @@ err.backup.write-archive: short: 'failed to write archive: %w' err.context.dir-not-found: short: 'context directory not found: ' -err.context.dir-not-declared: - short: 'context directory not declared' -err.context.relative-not-allowed-msg: - short: 'context directory must be absolute' -err.context.non-canonical-basename-msg: - short: 'context directory has non-canonical basename' +err.context.no-context-here-msg: + short: 'no .context here' +err.context.no-context-here: + short: |- + ctx: no .context/ at %s + Run `ctx init` here, or `cd` to a directory that has .context/. err.context.dir-not-a-directory-msg: short: 'context directory is not a directory' err.context.dir-stat-msg: short: 'context directory stat failed' err.context.not-initialized-msg: short: 'context not initialized' -err.context.not-declared-zero: - short: |- - no context directory specified for this project - See: https://ctx.ist/recipes/activating-context/ -err.context.not-declared-one: - short: |- - no context directory specified; a likely candidate is at %s - See: https://ctx.ist/recipes/activating-context/ -err.context.not-declared-many: - short: |- - no context directory specified; multiple candidates visible: - %s - See: https://ctx.ist/recipes/activating-context/ -err.context.relative-not-allowed: - short: |- - CTX_DIR must be an absolute path; got %q - See: https://ctx.ist/recipes/activating-context/ -err.context.non-canonical-basename: - short: |- - CTX_DIR basename must be %q; got %q - See: https://ctx.ist/recipes/activating-context/ err.context.dir-not-a-directory: - short: 'CTX_DIR points at a file, not a directory: %s' + short: '.context/ at %s is not a directory' err.context.dir-stat: - short: 'cannot stat CTX_DIR %s: %w' + short: 'cannot stat .context/ at %s: %w' err.context.not-initialized: short: |- ctx is not initialized in this project: %s Run 'ctx init' from the project root to set up context here. - See: https://ctx.ist/recipes/activating-context/ err.schema.drift: short: 'schema drift detected' -err.activate.no-candidates: - short: |- - ctx activate: no .context/ directory found from this location - See: https://ctx.ist/recipes/activating-context/ err.cli.no-tool-specified: short: 'no tool specified: use --tool or set the tool field in .ctxrc' err.config.golden-not-found: diff --git a/internal/assets/commands/text/hooks.yaml b/internal/assets/commands/text/hooks.yaml index 6f3ff8287..2070917d2 100644 --- a/internal/assets/commands/text/hooks.yaml +++ b/internal/assets/commands/text/hooks.yaml @@ -89,23 +89,6 @@ check-context-size.billing-log-format: short: prompt#%d BILLING-WARNING tokens=%d threshold=%d relay.prefix-format: short: '%s: %s' -check-anchor-drift.box-title: - short: Anchor Drift -check-anchor-drift.content: - short: |- - CTX_DIR (your shell) and CLAUDE_PROJECT_DIR/.context - point at different projects. - - shell: %s - claude: %s - - Re-run `eval "$(ctx activate)"` from this project to - realign, or close this Claude Code session and reopen - it from the right working directory. -check-anchor-drift.relay-message: - short: CTX_DIR diverges from Claude project anchor -check-anchor-drift.relay-prefix: - short: 'IMPORTANT: Relay this anchor drift notice to the user VERBATIM before answering their question.' check-context-size.billing-relay-format: short: Billing threshold exceeded (%s tokens > %s) check-context-size.billing-relay-prefix: diff --git a/internal/assets/commands/text/write.yaml b/internal/assets/commands/text/write.yaml index 2444afe47..6f2241ea6 100644 --- a/internal/assets/commands/text/write.yaml +++ b/internal/assets/commands/text/write.yaml @@ -208,22 +208,12 @@ write.init-anatomy-preamble: The `.context/` directory is your project's persistent memory. Its parent (this directory) is the project root by contract; - that's what `ctx sync` and drift detection scan. `CTX_DIR` - must be an absolute path ending in `.context`. One `.context/` - per project; knowledge sharing across projects goes through - `ctx hub`, not a shared directory. + that's what `ctx sync` and drift detection scan. `ctx` always + reads `$PWD/.context/` — run commands from the project root. + One `.context/` per project; knowledge sharing across projects + goes through `ctx hub`, not a shared directory. - Full reference: https://ctx.ist/recipes/activating-context/ -write.init-activate-hint: - short: |4- - - Bind CTX_DIR for this shell (required before other ctx commands): - - eval "$(ctx activate)" - - Or export the absolute path directly (skip the scan): - - export CTX_DIR=%s + Full reference: https://ctx.ist/home/getting-started/ write.init-next-steps-block: short: |4- diff --git a/internal/assets/context/AGENT_PLAYBOOK_GATE.md b/internal/assets/context/AGENT_PLAYBOOK_GATE.md index d5c3b2974..865d50bb4 100644 --- a/internal/assets/context/AGENT_PLAYBOOK_GATE.md +++ b/internal/assets/context/AGENT_PLAYBOOK_GATE.md @@ -36,8 +36,21 @@ When a task involves reading, modifying, or reasoning about a file: Do not begin implementation without a spec. Every commit requires a `Spec:` trailer. Every piece of work needs -a spec; no exceptions. Scale the spec to the work. Use `/ctx-spec` -to scaffold. +a spec; no exceptions. Scale the spec to the work. + +The design-to-implementation chain is: + +```text +/ctx-brainstorm → /ctx-plan → /ctx-spec → /ctx-implement + (vague) (contested) (committed) (execution) +``` + +`/ctx-brainstorm` shapes a vague idea into a bet. `/ctx-plan` +attacks the bet and writes a debated brief to +`.context/briefs/-.md`. `/ctx-spec` (optionally +`--brief `) absorbs the brief into a committed spec under +`specs/`. Skip the predecessors only when the step's input is +already settled. ## Proactive Persistence diff --git a/internal/assets/integrations/copilot-cli/INSTRUCTIONS.md b/internal/assets/integrations/copilot-cli/INSTRUCTIONS.md index 30ea64ab1..b3f357bc5 100644 --- a/internal/assets/integrations/copilot-cli/INSTRUCTIONS.md +++ b/internal/assets/integrations/copilot-cli/INSTRUCTIONS.md @@ -13,7 +13,7 @@ This project uses Context (`ctx`) for context persistence across sessions. 1. **Run `ctx system bootstrap`**: CRITICAL, not optional. This tells you where the context directory is. If it returns any error, relay the error output to the user verbatim, point them at - https://ctx.ist/recipes/activating-context/ for setup, and STOP. + https://ctx.ist/home/getting-started/ for setup, and STOP. Do not try to recover: the user decides. 2. **Read AGENT_PLAYBOOK.md** from the context directory: it explains how to use this system diff --git a/internal/assets/integrations/copilot-cli/skills/ctx-journal-enrich-all/SKILL.md b/internal/assets/integrations/copilot-cli/skills/ctx-journal-enrich-all/SKILL.md index 1155653ab..5a4b6519b 100644 --- a/internal/assets/integrations/copilot-cli/skills/ctx-journal-enrich-all/SKILL.md +++ b/internal/assets/integrations/copilot-cli/skills/ctx-journal-enrich-all/SKILL.md @@ -26,8 +26,8 @@ the journal directory has no `.md` files at all, or if there are them first. ```bash -CTX_DIR=$(ctx system bootstrap -q) -JOURNAL_DIR="$CTX_DIR/journal" +CTX_PATH=$(ctx system bootstrap -q) +JOURNAL_DIR="$CTX_PATH/journal" # Check if any .md files exist md_count=$(ls "$JOURNAL_DIR"/*.md 2>/dev/null | wc -l) @@ -55,8 +55,8 @@ List all journal entries that lack enrichment using the state file: ```bash # List .md files in journal dir and check state -CTX_DIR=$(ctx system bootstrap -q) -for f in "$CTX_DIR/journal/"*.md; do +CTX_PATH=$(ctx system bootstrap -q) +for f in "$CTX_PATH/journal/"*.md; do name=$(basename "$f") ctx system mark-journal --check "$name" enriched || echo "$f" done @@ -182,8 +182,8 @@ patterns, then inserts frontmatter and marks state automatically. 1. Build a file list of eligible entries (non-multipart, 20+ lines, missing `type:` and `outcome:` fields): ```bash - CTX_DIR=$(ctx system bootstrap -q) - for f in "$CTX_DIR"/journal/*.md; do + CTX_PATH=$(ctx system bootstrap -q) + for f in "$CTX_PATH"/journal/*.md; do [ -f "$f" ] || continue has_type=$(head -30 "$f" | grep -c '^type:' || true) has_outcome=$(head -30 "$f" | grep -c '^outcome:' || true) diff --git a/internal/assets/integrations/copilot-cli/skills/ctx-journal-enrich/SKILL.md b/internal/assets/integrations/copilot-cli/skills/ctx-journal-enrich/SKILL.md index 9d29aee5f..9be9828af 100644 --- a/internal/assets/integrations/copilot-cli/skills/ctx-journal-enrich/SKILL.md +++ b/internal/assets/integrations/copilot-cli/skills/ctx-journal-enrich/SKILL.md @@ -48,8 +48,8 @@ an `enriched` date: ```bash # List unenriched entries using state file -CTX_DIR=$(ctx system bootstrap -q) -for f in "$CTX_DIR/journal/"*.md; do +CTX_PATH=$(ctx system bootstrap -q) +for f in "$CTX_PATH/journal/"*.md; do name=$(basename "$f") ctx system mark-journal --check "$name" enriched || echo "$f" done | head -10 diff --git a/internal/assets/integrations/copilot-cli/skills/ctx-spec/SKILL.md b/internal/assets/integrations/copilot-cli/skills/ctx-spec/SKILL.md index d56073369..1e39f84cf 100644 --- a/internal/assets/integrations/copilot-cli/skills/ctx-spec/SKILL.md +++ b/internal/assets/integrations/copilot-cli/skills/ctx-spec/SKILL.md @@ -6,12 +6,33 @@ description: "Scaffold a feature spec from the project template. Use when planni Scaffold a new spec from `specs/tpl/spec-template.md` and walk through each section with the user to produce a complete design document. +## Canonical Chain + +The project's design-to-implementation pipeline is: + +```text +/ctx-brainstorm → /ctx-plan → /ctx-spec → /ctx-implement + (vague) (contested) (committed) (execution) +``` + +`/ctx-spec` is the third step. It consumes the *debated brief* +produced by `/ctx-plan` (via `--brief `) or writes a fresh +spec interactively when no brief is needed. Specs are committed +artifacts under `specs/`; briefs are working state under +`.context/briefs/` that the spec absorbs. + +Do not invert the order. A spec without a settled bet ahead of +it is a wishlist; running `/ctx-plan` after `/ctx-spec` is fixing +the foundation while the building is up. + ## When to Use - Before implementing a non-trivial feature - When a task says "Spec: `specs/X.md`" and the file does not exist - When `/ctx-brainstorm` has produced a validated design that needs a written artifact +- When `/ctx-plan` has produced a debated brief that needs a + committed spec (use `--brief `) - When the user says "let's spec this out" or "write a spec for..." ## When NOT to Use @@ -19,6 +40,8 @@ each section with the user to produce a complete design document. - Bug fixes or small changes (just do them) - When a spec already exists (read it instead) - When the design is still vague (use `/ctx-brainstorm` first) +- When the bet is contested but not yet stress-tested (use + `/ctx-plan` first; its output is the brief this skill consumes) ## Usage Examples diff --git a/internal/assets/integrations/opencode/plugin/index.ts b/internal/assets/integrations/opencode/plugin/index.ts index 49f6ef2b9..e8eff3aab 100644 --- a/internal/assets/integrations/opencode/plugin/index.ts +++ b/internal/assets/integrations/opencode/plugin/index.ts @@ -8,9 +8,10 @@ // mutate output rather than returning a value. // - event is a single dispatcher keyed on input.event.type; // it is NOT an object of named per-event handlers. -// ctx subprocess calls go through a CTX_DIR-aware BunShell built -// from ctx.directory — shell.env only injects CTX_DIR into the -// agent's shell tool, not into the plugin's own ctx.$ calls. +// ctx subprocess calls go through a cwd-anchored BunShell built +// from ctx.directory — shell.env only forces the agent's shell +// tool to start in the project root, not the plugin's own ctx.$ +// calls (those already use cwd: ctx.directory). // All ctx.$ invocations use .nothrow().quiet(): nothrow swallows // non-zero exits, quiet keeps stdout/stderr in BunShell's buffer // instead of echoing to OpenCode's process stdout (which would @@ -45,11 +46,10 @@ function extractCommand(input: unknown): string { } export default (async (ctx) => { - const ctxDir = `${ctx.directory}/.context` - const $ = ctx.$.env({ ...process.env, CTX_DIR: ctxDir }) + const $ = ctx.$.cwd(ctx.directory) return { "shell.env": async (input, output) => { - output.env.CTX_DIR = `${input.cwd}/.context` + output.cwd = input.cwd }, event: async ({ event }) => { if (event.type === "session.created") { diff --git a/internal/bootstrap/bootstrap_test.go b/internal/bootstrap/bootstrap_test.go index a09db12bc..d29049ca8 100644 --- a/internal/bootstrap/bootstrap_test.go +++ b/internal/bootstrap/bootstrap_test.go @@ -16,7 +16,6 @@ import ( "github.com/ActiveMemory/ctx/internal/config/cli" "github.com/ActiveMemory/ctx/internal/config/ctx" "github.com/ActiveMemory/ctx/internal/config/dir" - "github.com/ActiveMemory/ctx/internal/config/env" "github.com/ActiveMemory/ctx/internal/config/flag" "github.com/spf13/cobra" @@ -131,7 +130,7 @@ func TestRootCmdPersistentPreRun_CtxDirEnv(t *testing.T) { if err := os.MkdirAll(ctxDir, 0o700); err != nil { t.Fatal(err) } - t.Setenv(env.CtxDir, ctxDir) + t.Chdir(tmp) rc.Reset() t.Cleanup(rc.Reset) @@ -154,8 +153,10 @@ func TestRootCmdPersistentPreRun_CtxDirEnv(t *testing.T) { if ctxErr != nil { t.Fatalf("ContextDir: %v", ctxErr) } - if got != ctxDir { - t.Errorf("ContextDir() = %q, want %q", got, ctxDir) + gotResolved, _ := filepath.EvalSymlinks(got) + wantResolved, _ := filepath.EvalSymlinks(ctxDir) + if gotResolved != wantResolved { + t.Errorf("ContextDir() = %q, want %q", gotResolved, wantResolved) } } @@ -204,7 +205,7 @@ func TestInitGuard_BlocksUninitializedCommand(t *testing.T) { if err := os.MkdirAll(ctxDir, 0o700); err != nil { t.Fatal(err) } - t.Setenv(env.CtxDir, ctxDir) + t.Chdir(tmp) rc.Reset() t.Cleanup(rc.Reset) @@ -232,7 +233,7 @@ func TestInitGuard_AllowsAnnotatedCommand(t *testing.T) { if err := os.MkdirAll(ctxDir, 0o700); err != nil { t.Fatal(err) } - t.Setenv(env.CtxDir, ctxDir) + t.Chdir(tmp) rc.Reset() t.Cleanup(rc.Reset) @@ -256,7 +257,7 @@ func TestInitGuard_AllowsHiddenCommand(t *testing.T) { if err := os.MkdirAll(ctxDir, 0o700); err != nil { t.Fatal(err) } - t.Setenv(env.CtxDir, ctxDir) + t.Chdir(tmp) rc.Reset() t.Cleanup(rc.Reset) @@ -342,7 +343,7 @@ func TestInitGuard_AllowsInitializedCommand(t *testing.T) { } } - t.Setenv(env.CtxDir, ctxDir) + t.Chdir(tmp) rc.Reset() t.Cleanup(rc.Reset) diff --git a/internal/bootstrap/cmd.go b/internal/bootstrap/cmd.go index efacbd9ed..f6a47b7b0 100644 --- a/internal/bootstrap/cmd.go +++ b/internal/bootstrap/cmd.go @@ -11,7 +11,6 @@ package bootstrap import ( "errors" "os" - "path/filepath" "github.com/spf13/cobra" @@ -63,8 +62,7 @@ func RootCmd() *cobra.Command { // supply their own guards). // - Cobra's built-in shell-completion subcommands. // - Commands annotated with AnnotationSkipInit (init, - // activate, deactivate, guide, why, doctor, config - // switch/status, hub *). + // guide, why, doctor, config switch/status, hub *). // - Grouping commands without a Run / RunE of their own // (they just print help for their subtree). if cmd.Hidden { @@ -80,14 +78,12 @@ func RootCmd() *cobra.Command { return nil } - // Under the single-source-anchor model, every non-exempt - // command requires CTX_DIR to be declared and to point at - // an existing .context/ directory. RequireContextDir - // returns a tailored error (with a next-step hint based on - // how many .context/ candidates are visible from CWD) when - // the declaration is missing or broken. The parent of the - // declared directory is the project root by contract; CWD - // has no say in project identity. + // Under the cwd-anchored model + // (spec: specs/cwd-anchored-context.md), every non-exempt + // command requires `$PWD/.context/` to exist as a + // directory. RequireContextDir wraps the single os.Stat + // and returns errCtx.ErrNoCtxHere when absent. The + // project root is the cwd itself by contract. ctxDir, reqErr := rc.RequireContextDir() if reqErr != nil { // Actionable error, not a usage problem. Suppress @@ -99,7 +95,7 @@ func RootCmd() *cobra.Command { return reqErr } - // Require initialization: the declared directory must + // Require initialization: the cwd's .context/ must // have been initialized before other commands operate. if !ctxContext.Initialized(ctxDir) { cmd.SilenceUsage = true @@ -107,15 +103,19 @@ func RootCmd() *cobra.Command { } // Phase RG: require git as architectural precondition. - // The project root is the parent of the declared - // .context/ directory. RequireGitTree refuses when - // /.git is absent. - projectRoot := filepath.Dir(ctxDir) - if gitErr := gitmeta.RequireGitTree(projectRoot); gitErr != nil { + // `.context/` and `.git/` are siblings by contract, so + // `$PWD/.git/` must exist alongside the `.context/` we + // just validated. + cwd, cwdErr := os.Getwd() + if cwdErr != nil { + cmd.SilenceUsage = true + return cwdErr + } + if gitErr := gitmeta.RequireGitTree(); gitErr != nil { cmd.SilenceUsage = true if errors.Is(gitErr, errGitmeta.ErrMissingGitTree) { return errGitmeta.MissingGitTreeForCmd( - cmd.Name(), projectRoot, + cmd.Name(), cwd, ) } return gitErr diff --git a/internal/bootstrap/group.go b/internal/bootstrap/group.go index 298c23810..83d38906d 100644 --- a/internal/bootstrap/group.go +++ b/internal/bootstrap/group.go @@ -7,14 +7,12 @@ package bootstrap import ( - "github.com/ActiveMemory/ctx/internal/cli/activate" "github.com/ActiveMemory/ctx/internal/cli/agent" "github.com/ActiveMemory/ctx/internal/cli/change" "github.com/ActiveMemory/ctx/internal/cli/compact" "github.com/ActiveMemory/ctx/internal/cli/config" "github.com/ActiveMemory/ctx/internal/cli/connection" "github.com/ActiveMemory/ctx/internal/cli/convention" - "github.com/ActiveMemory/ctx/internal/cli/deactivate" "github.com/ActiveMemory/ctx/internal/cli/decision" "github.com/ActiveMemory/ctx/internal/cli/doctor" "github.com/ActiveMemory/ctx/internal/cli/drift" @@ -61,8 +59,6 @@ import ( func gettingStarted() []registration { return []registration{ {initialize.Cmd, embedCmd.GroupGettingStarted}, - {activate.Cmd, embedCmd.GroupGettingStarted}, - {deactivate.Cmd, embedCmd.GroupGettingStarted}, {status.Cmd, embedCmd.GroupGettingStarted}, {guide.Cmd, embedCmd.GroupGettingStarted}, } diff --git a/internal/cli/activate/activate.go b/internal/cli/activate/activate.go deleted file mode 100644 index 61d18d8a0..000000000 --- a/internal/cli/activate/activate.go +++ /dev/null @@ -1,22 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package activate - -import ( - "github.com/spf13/cobra" - - activateRoot "github.com/ActiveMemory/ctx/internal/cli/activate/cmd/root" -) - -// Cmd returns the `ctx activate` command for registration on the -// root ctx command. See cmd/root for the full command definition. -// -// Returns: -// - *cobra.Command: the activate command. -func Cmd() *cobra.Command { - return activateRoot.Cmd() -} diff --git a/internal/cli/activate/activate_test.go b/internal/cli/activate/activate_test.go deleted file mode 100644 index 1ecb540c6..000000000 --- a/internal/cli/activate/activate_test.go +++ /dev/null @@ -1,275 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package activate_test - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/ActiveMemory/ctx/internal/cli/activate" - "github.com/ActiveMemory/ctx/internal/config/dir" - "github.com/ActiveMemory/ctx/internal/config/env" -) - -// runActivate invokes `ctx activate` with the given args and returns -// (stdout, stderr, error) as separate buffers. Stream separation is -// load-bearing: the eval-bindable shell content goes to stdout, the -// human-readable advisories ("ctx activated at:", "ctx: also -// visible upward:") go to stderr. Tests that conflate the two miss -// regressions where an advisory leaks into the eval stream. -// -// The command inherits the test process's env; use t.Setenv / -// t.Chdir to scope state. -func runActivate(t *testing.T, args []string) (stdout, stderr string, err error) { - t.Helper() - c := activate.Cmd() - c.SetArgs(args) - var so, se bytes.Buffer - c.SetOut(&so) - c.SetErr(&se) - err = c.Execute() - return so.String(), se.String(), err -} - -// TestActivate_NoArgs_NoCandidates: cwd with no .context/ anywhere → -// NoCandidates error, no stdout emit, no advisory either. -func TestActivate_NoArgs_NoCandidates(t *testing.T) { - t.Setenv(env.CtxDir, "") - t.Chdir(t.TempDir()) - - stdout, _, err := runActivate(t, nil) - if err == nil { - t.Fatalf("expected NoCandidates error, got nil (stdout=%q)", stdout) - } - if stdout != "" { - t.Errorf("stdout must be empty on error path: %q", stdout) - } -} - -// TestActivate_NoArgs_OneCandidate: exactly one .context/ upward → -// stdout carries the export, stderr carries the -// `ctx: activated at:` advisory. -func TestActivate_NoArgs_OneCandidate(t *testing.T) { - t.Setenv(env.CtxDir, "") - - projectRoot := t.TempDir() - ctxPath := filepath.Join(projectRoot, dir.Context) - if err := os.MkdirAll(ctxPath, 0700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(projectRoot) - t.Setenv("SHELL", "/bin/bash") - - stdout, stderr, err := runActivate(t, nil) - if err != nil { - t.Fatalf("expected success, got err=%v", err) - } - if !strings.HasPrefix(stdout, "export CTX_DIR=") { - t.Errorf("stdout must start with export, got %q", stdout) - } - if !strings.Contains(stdout, ctxPath) { - t.Errorf("stdout missing path %q: %q", ctxPath, stdout) - } - // Activated-at advisory always announces the bind, even in - // the single-candidate case. - wantActivated := "ctx: activated at: " + ctxPath - if !strings.Contains(stderr, wantActivated) { - t.Errorf("stderr missing activated-at advisory %q: %q", - wantActivated, stderr) - } -} - -// TestActivate_ErrorPath_StdoutEmpty guards the eval-recursion -// trap surfaced by the smoke test: if any error path lets cobra -// print Usage / Flags / Examples to stdout, `eval "$(ctx -// activate)"` captures the Examples block (which literally -// contains `eval "$(ctx activate)"`) and re-executes activate, -// looping until the captured text mangles past the parser. -// -// Stdout MUST stay empty on every error path. Stderr can carry -// the human-readable error; the eval shell never sees stderr. -// -// Uses the no-candidates case (zero `.context/` visible upward) -// since multi-candidate is no longer an error case under the -// innermost-wins policy. -func TestActivate_ErrorPath_StdoutEmpty(t *testing.T) { - t.Setenv(env.CtxDir, "") - t.Chdir(t.TempDir()) - - stdout, stderr, err := runActivate(t, nil) - if err == nil { - t.Fatalf("expected NoCandidates error, got nil") - } - if stdout != "" { - t.Errorf("stdout must be empty on error path, got %q", stdout) - } - if !strings.Contains(stderr, "no .context/ directory found") { - t.Errorf("stderr should describe the error, got %q", stderr) - } -} - -// TestActivate_NoArgs_ManyCandidates: two `.context/` dirs on the -// upward path → innermost wins on stdout (eval-bindable), -// stderr carries both the `ctx activated at:` line and one -// `ctx: also visible upward:` line per other candidate. Matches -// git/make innermost-project semantics. -// -// The split-stream assertion is load-bearing: putting any -// advisory on stdout (the eval-captured stream) makes it -// invisible to anyone running `eval "$(ctx activate)"`. -func TestActivate_NoArgs_ManyCandidates(t *testing.T) { - t.Setenv(env.CtxDir, "") - t.Setenv("SHELL", "/bin/bash") - - tempDir := t.TempDir() - outerCtx := filepath.Join(tempDir, dir.Context) - innerDir := filepath.Join(tempDir, "inner") - innerCtx := filepath.Join(innerDir, dir.Context) - startDir := filepath.Join(innerDir, "deep") - - for _, d := range []string{outerCtx, innerCtx, startDir} { - if err := os.MkdirAll(d, 0700); err != nil { - t.Fatalf("mkdir %s: %v", d, err) - } - } - t.Chdir(startDir) - - stdout, stderr, err := runActivate(t, nil) - if err != nil { - t.Fatalf("expected success (innermost wins), got err=%v", err) - } - - // stdout: only the export line for the innermost candidate. - if !strings.HasPrefix(stdout, "export CTX_DIR=") { - t.Errorf("stdout must start with export, got %q", stdout) - } - if !strings.Contains(stdout, innerCtx) { - t.Errorf("export should bind the inner candidate %q: %q", - innerCtx, stdout) - } - if strings.Contains(stdout, "also visible") || - strings.Contains(stdout, "activated at") { - t.Errorf("stdout must NOT carry advisories (eval invisibility): %q", - stdout) - } - - // stderr: activated-at line for the inner, also-visible line for - // the outer. - wantActivated := "ctx: activated at: " + innerCtx - if !strings.Contains(stderr, wantActivated) { - t.Errorf("stderr missing %q: %q", wantActivated, stderr) - } - wantAdvisory := "ctx: also visible upward: " + outerCtx - if !strings.Contains(stderr, wantAdvisory) { - t.Errorf("stderr missing %q: %q", wantAdvisory, stderr) - } -} - -// TestActivate_RejectsArgs guards the spec contract: `ctx activate -// ` is removed under the single-source-anchor model. Any -// positional argument must be rejected (either as cobra's -// "accepts 0 arg(s)" or "unknown command", whichever cobra picks -// for the literal value) and emit nothing on stdout. -func TestActivate_RejectsArgs(t *testing.T) { - t.Setenv(env.CtxDir, "") - t.Chdir(t.TempDir()) - - stdout, _, err := runActivate(t, []string{"some-explicit-path"}) - if err == nil { - t.Fatalf("expected cobra args rejection, got nil (stdout=%q)", stdout) - } - if strings.Contains(stdout, "export CTX_DIR") { - t.Errorf("stdout should not contain export on error: %q", stdout) - } -} - -// TestActivate_StaleReplacementComment: parent shell has a stale -// CTX_DIR pointing at a different project; activate emits a -// `# ctx: replacing stale CTX_DIR=` comment before the export -// so the user can see the change in `eval` output. -func TestActivate_StaleReplacementComment(t *testing.T) { - stale := filepath.Join(t.TempDir(), "old", dir.Context) - if err := os.MkdirAll(stale, 0700); err != nil { - t.Fatalf("mkdir stale: %v", err) - } - t.Setenv(env.CtxDir, stale) - - projectRoot := t.TempDir() - ctxPath := filepath.Join(projectRoot, dir.Context) - if err := os.MkdirAll(ctxPath, 0700); err != nil { - t.Fatalf("mkdir new: %v", err) - } - t.Chdir(projectRoot) - t.Setenv("SHELL", "/bin/bash") - - stdout, _, err := runActivate(t, nil) - if err != nil { - t.Fatalf("expected success, got err=%v", err) - } - wantPrefix := fmt.Sprintf("# ctx: replacing stale %s=%s\n", - env.CtxDir, stale) - if !strings.HasPrefix(stdout, wantPrefix) { - t.Errorf("stdout missing stale-replacement comment.\n got: %q\nwant prefix: %q", - stdout, wantPrefix) - } - if !strings.Contains(stdout, "export CTX_DIR=") { - t.Errorf("stdout missing export: %q", stdout) - } - if !strings.Contains(stdout, ctxPath) { - t.Errorf("stdout missing new path %q: %q", ctxPath, stdout) - } -} - -// TestActivate_NoStaleCommentOnFirstActivate: when CTX_DIR is unset -// or matches the resolved value, the comment is suppressed. -func TestActivate_NoStaleCommentOnFirstActivate(t *testing.T) { - t.Setenv(env.CtxDir, "") - - projectRoot := t.TempDir() - ctxPath := filepath.Join(projectRoot, dir.Context) - if err := os.MkdirAll(ctxPath, 0700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(projectRoot) - t.Setenv("SHELL", "/bin/bash") - - stdout, _, err := runActivate(t, nil) - if err != nil { - t.Fatalf("expected success, got err=%v", err) - } - if strings.Contains(stdout, "replacing stale") { - t.Errorf("stdout should not contain stale comment: %q", stdout) - } -} - -// TestActivate_ShellFlag: --shell zsh uses POSIX export syntax -// (same output shape as bash; flag is just a dispatch key). -func TestActivate_ShellFlag(t *testing.T) { - t.Setenv(env.CtxDir, "") - - projectRoot := t.TempDir() - ctxPath := filepath.Join(projectRoot, dir.Context) - if err := os.MkdirAll(ctxPath, 0700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(projectRoot) - - stdout, _, err := runActivate(t, []string{"--shell", "zsh"}) - if err != nil { - t.Fatalf("expected success, got err=%v", err) - } - if !strings.HasPrefix(stdout, "export CTX_DIR=") { - t.Errorf("expected export prefix, got %q", stdout) - } - if !strings.HasSuffix(strings.TrimSpace(stdout), "'") { - t.Errorf("expected trailing single quote (shell quoting), got %q", stdout) - } -} diff --git a/internal/cli/activate/cmd/root/cmd.go b/internal/cli/activate/cmd/root/cmd.go deleted file mode 100644 index cbd564453..000000000 --- a/internal/cli/activate/cmd/root/cmd.go +++ /dev/null @@ -1,77 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package root - -import ( - "github.com/spf13/cobra" - - "github.com/ActiveMemory/ctx/internal/assets/read/desc" - "github.com/ActiveMemory/ctx/internal/config/cli" - "github.com/ActiveMemory/ctx/internal/config/embed/cmd" - embedFlag "github.com/ActiveMemory/ctx/internal/config/embed/flag" - cFlag "github.com/ActiveMemory/ctx/internal/config/flag" -) - -// Cmd returns the `ctx activate` cobra command. -// -// Args-free under the single-source-anchor model -// (specs/single-source-context-anchor.md). Activation is always -// project-local discovery via [rc.ScanCandidates] from CWD; the -// explicit-path mode that previously accepted an argument was -// removed because hub-client / hub-server scenarios store at -// `~/.ctx/hub-data/` and never read `.context/` directly, so they -// activate from the project root like everyone else. -// -// One flag remains: -// -// --shell override auto-detection (defaults to $SHELL). -// -// # Stdout discipline (critical) -// -// Activate's stdout is consumed by `eval "$(ctx activate)"`. Every -// byte must be either valid shell or empty. Usage / Flags / -// Examples blocks must NEVER reach stdout, because cobra's -// Examples for this command literally contain -// `eval "$(ctx activate)"`, which would re-execute activate inside -// the eval and trigger an infinite loop on any error path. -// -// SilenceUsage is therefore set unconditionally below (rather than -// only after a return) so cobra renders only the error to stderr -// when something fails. SilenceErrors stays at the root level so -// errors keep going to stderr (visible to the user) without being -// captured by the eval. -// -// Returns: -// - *cobra.Command: configured activate command. -func Cmd() *cobra.Command { - short, long := desc.Command(cmd.DescKeyActivate) - c := &cobra.Command{ - Use: cmd.UseActivate, - Short: short, - Long: long, - Example: desc.Example(cmd.DescKeyActivate), - Args: cobra.NoArgs, - // Exempt from the global init / require-context-dir checks: - // activate's whole purpose is to help the user declare the - // context directory in the first place. - Annotations: map[string]string{cli.AnnotationSkipInit: cli.AnnotationTrue}, - // See the Stdout discipline note above. Without this, an - // error path (multi-candidate, no-candidates, etc.) prints - // Usage+Examples to stdout, gets captured by `$(...)`, and - // the embedded `eval "$(ctx activate)"` example re-runs the - // command. Loop. - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - shell, _ := cmd.Flags().GetString(cFlag.Shell) - return Run(cmd, shell) - }, - } - c.Flags().String(cFlag.Shell, "", - desc.Flag(embedFlag.DescKeyActivateShell), - ) - return c -} diff --git a/internal/cli/activate/cmd/root/doc.go b/internal/cli/activate/cmd/root/doc.go deleted file mode 100644 index 3e8708457..000000000 --- a/internal/cli/activate/cmd/root/doc.go +++ /dev/null @@ -1,33 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -// Package root implements the `ctx activate` cobra command. -// -// Activate is the shell-integration entry point under the -// explicit-context-dir model (spec: specs/explicit-context-dir.md). -// Its single job is to emit a `export CTX_DIR=...` line to stdout so -// that callers can bind the context directory for their shell via -// `eval "$(ctx activate)"`. -// -// Unlike most commands in the CLI, `activate` is in the exempt -// allowlist: it does not call rc.RequireContextDir because -// activate's reason for existing is precisely to help users declare -// CTX_DIR in the first place. -// -// Resolution: -// -// - With an explicit path argument: the path is validated strictly -// (exists, is a directory, contains at least one canonical -// context file). There is no --force escape hatch in v1. -// - Without arguments: the command scans upward from CWD using -// rc.ScanCandidates and emits the one visible candidate when -// there is exactly one. Zero candidates → NoCandidates error. -// Two or more candidates → Ambiguous error listing every path; -// activate refuses to pick automatically. -// -// This is the only command in the CLI that walks. All other -// resolution flows through rc.ContextDir / rc.RequireContextDir. -package root diff --git a/internal/cli/activate/cmd/root/run.go b/internal/cli/activate/cmd/root/run.go deleted file mode 100644 index b7f506008..000000000 --- a/internal/cli/activate/cmd/root/run.go +++ /dev/null @@ -1,76 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package root - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/ActiveMemory/ctx/internal/cli/activate/core/emit" - "github.com/ActiveMemory/ctx/internal/cli/activate/core/resolve" - "github.com/ActiveMemory/ctx/internal/config/env" - cfgShell "github.com/ActiveMemory/ctx/internal/config/shell" - writeActivate "github.com/ActiveMemory/ctx/internal/write/activate" -) - -// Run executes the `ctx activate` command. -// -// Resolves the target .context/ directory via [resolve.Selected] -// (always project-local scan from CWD under the single-source-anchor -// model), then prints the shell-specific export statement for -// CTX_DIR to stdout. -// -// # Output shape -// -// Two channels: -// -// 1. **stdout**: consumed by `eval "$(ctx activate)"`. Every -// byte must be valid POSIX shell. Composed in order: -// (a) zero or one `# ctx: replacing stale CTX_DIR=\n` -// comment line when the parent shell already has [env.CtxDir] -// set to a different value than the resolved target; -// (b) the shell-specific `export CTX_DIR=\n` line. -// -// 2. **stderr**: informational advisories for the user. Always -// carries a `ctx activated at: ` line announcing the -// bound directory (single-candidate case included), and -// additionally one `ctx: also visible upward: ` line -// per other `.context/` candidate when more than one is -// visible upward. `eval` does not capture stderr, so these -// lines pass through to the terminal where the user sees -// them. Innermost wins (matches git/make nested-project -// semantics); the additional candidates are reported, not -// refused. The comment-on-stdout approach considered -// earlier was invisible to the only documented invocation -// form (`eval`), so it informed nobody. -// -// Parameters: -// - cmd: cobra command providing stdout / stderr. Nil is a -// no-op via [writeActivate.Emit] / [writeActivate.AlsoVisible]. -// - shell: value of the --shell flag; empty means auto-detect -// from $SHELL via [emit.DetectShell]. -// -// Returns: -// - error: non-nil on resolution failure (no `.context/` visible -// from CWD upward); nil on successful emit. -func Run(cmd *cobra.Command, shell string) error { - selected, others, err := resolve.Selected() - if err != nil { - return err - } - out := emit.Set(emit.DetectShell(shell), selected) - if existing := os.Getenv(env.CtxDir); existing != "" && existing != selected { - out = fmt.Sprintf(cfgShell.FormatStaleReplaceComment, - env.CtxDir, existing, out) - } - writeActivate.ActivatedAt(cmd, selected) - writeActivate.AlsoVisible(cmd, others) - writeActivate.Emit(cmd, out) - return nil -} diff --git a/internal/cli/activate/core/emit/doc.go b/internal/cli/activate/core/emit/doc.go deleted file mode 100644 index bc26d8b29..000000000 --- a/internal/cli/activate/core/emit/doc.go +++ /dev/null @@ -1,28 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -// Package emit produces the shell-specific strings used by -// `ctx activate` and `ctx deactivate` to bind or clear CTX_DIR -// for the current shell via `eval "$(ctx activate)"`. -// -// v1 supports bash, zsh, and POSIX sh. All three share identical -// `export` / `unset` syntax. Fish / nushell / powershell can be -// added later by extending [Set] and [Unset] without touching the -// call sites. That extensibility is the only reason this lives in -// its own package rather than inline in the command's Run. -// -// # Supported Shells -// -// bash, zsh, sh: POSIX export / unset -// fish: deferred (see specs/explicit-context-dir.md). -// -// # Detection -// -// [DetectShell] returns the first non-empty value of, in order: -// the explicit --shell flag, the basename of $SHELL, and a bash -// fallback. Users who want deterministic output in scripts should -// pass --shell explicitly. -package emit diff --git a/internal/cli/activate/core/emit/emit.go b/internal/cli/activate/core/emit/emit.go deleted file mode 100644 index 3e73dda95..000000000 --- a/internal/cli/activate/core/emit/emit.go +++ /dev/null @@ -1,90 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package emit - -import ( - "os" - "path/filepath" - "strings" - - "github.com/ActiveMemory/ctx/internal/config/env" - cfgShell "github.com/ActiveMemory/ctx/internal/config/shell" -) - -// emitters maps supported shell identifiers to their set-emitter. -// Unknown shells fall back to POSIX export semantics via the -// default branch in Set. -var emitters = map[string]emitter{ - cfgShell.Bash: posixSet, - cfgShell.Zsh: posixSet, - cfgShell.Sh: posixSet, -} - -// unsetters maps supported shell identifiers to their unset-emitter. -// Unknown shells fall back to POSIX unset semantics via the default -// branch in Unset. -var unsetters = map[string]emitter{ - cfgShell.Bash: posixUnset, - cfgShell.Zsh: posixUnset, - cfgShell.Sh: posixUnset, -} - -// DetectShell returns the shell identifier to emit for. -// -// Priority: explicit override > basename of $SHELL > bash fallback. -// The returned value is always lowercase and suitable as a key into -// the [emitters] / [unsetters] tables. -// -// Parameters: -// - override: explicit --shell flag value ("" to auto-detect). -// -// Returns: -// - string: one of [cfgShell.Bash], [cfgShell.Zsh], [cfgShell.Sh], -// or the original override (callers treat unknowns as POSIX). -func DetectShell(override string) string { - if override != "" { - return strings.ToLower(override) - } - if s := os.Getenv(env.Shell); s != "" { - return strings.ToLower(filepath.Base(s)) - } - return cfgShell.Bash -} - -// Set returns the shell command that exports CTX_DIR=path, ending -// with a newline so the output is directly consumable by -// `eval "$(ctx activate)"`. -// -// Parameters: -// - shell: result of [DetectShell]. -// - path: absolute path to the selected context directory. -// -// Returns: -// - string: one-line export statement with trailing newline. -func Set(shell, path string) string { - fn, ok := emitters[shell] - if !ok { - fn = posixSet - } - return fn(env.CtxDir, shellQuote(path)) -} - -// Unset returns the shell command that clears CTX_DIR for the -// current shell, ending with a newline. -// -// Parameters: -// - shell: result of [DetectShell]. -// -// Returns: -// - string: one-line unset statement with trailing newline. -func Unset(shell string) string { - fn, ok := unsetters[shell] - if !ok { - fn = posixUnset - } - return fn(env.CtxDir, "") -} diff --git a/internal/cli/activate/core/emit/posix.go b/internal/cli/activate/core/emit/posix.go deleted file mode 100644 index 9f547d774..000000000 --- a/internal/cli/activate/core/emit/posix.go +++ /dev/null @@ -1,57 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package emit - -import ( - "fmt" - "strings" - - "github.com/ActiveMemory/ctx/internal/config/shell" -) - -// posixSet emits `export KEY=VALUE\n` for bash/zsh/sh. Used as the -// map value in the emitters table for all POSIX-family shells. -// -// Parameters: -// - key: environment variable name (already well-formed). -// - quotedValue: value wrapped by shellQuote. -// -// Returns: -// - string: one-line export statement with trailing newline. -func posixSet(key, quotedValue string) string { - return fmt.Sprintf(shell.FormatPOSIXExport, key, quotedValue) -} - -// posixUnset emits `unset KEY\n` for bash/zsh/sh. The value -// argument is ignored (unset has no payload) but kept in the -// signature to match the emitter type. -// -// Parameters: -// - key: environment variable name to clear. -// - _: unused; kept for emitter-signature compatibility. -// -// Returns: -// - string: one-line unset statement with trailing newline. -func posixUnset(key, _ string) string { - return fmt.Sprintf(shell.FormatPOSIXUnset, key) -} - -// shellQuote wraps s in single quotes, escaping any embedded single -// quote as close-escape-reopen (`'` followed by `\'` followed by `'`). -// The resulting string is safe to paste into any POSIX-compatible -// shell regardless of s's contents. -// -// Parameters: -// - s: raw value (typically a filesystem path). -// -// Returns: -// - string: single-quoted, escape-safe shell literal. -func shellQuote(s string) string { - return shell.SingleQuote + - strings.ReplaceAll(s, shell.SingleQuote, shell.SingleQuoteEscaped) + - shell.SingleQuote -} diff --git a/internal/cli/activate/core/emit/types.go b/internal/cli/activate/core/emit/types.go deleted file mode 100644 index 8c2102b67..000000000 --- a/internal/cli/activate/core/emit/types.go +++ /dev/null @@ -1,12 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package emit - -// emitter produces a shell-specific one-line statement for the given -// key and pre-quoted value, terminated by a newline. Concrete -// emitters live in posix.go; the dispatch table is in emit.go. -type emitter func(key, quotedValue string) string diff --git a/internal/cli/activate/core/resolve/doc.go b/internal/cli/activate/core/resolve/doc.go deleted file mode 100644 index b8e4be886..000000000 --- a/internal/cli/activate/core/resolve/doc.go +++ /dev/null @@ -1,26 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -// Package resolve picks the `.context/` directory that `ctx -// activate` should emit a shell export for. It is the ONE place -// in the CLI that walks the filesystem during context resolution; -// all other commands honor `CTX_DIR` or error via -// [rc.RequireContextDir]. -// -// [Selected] is the single entry point. It walks upward from CWD -// via [rc.ScanCandidates] and returns: -// -// - the **innermost** visible `.context/` (selected), -// - any **additional** candidates further up the path, -// - or [errActivate.NoCandidates] when the walk finds none. -// -// Multi-candidate is not an error: workspace-level shared -// `.context/` dirs alongside per-project ones are a legitimate -// nested-project layout. Innermost wins (matching git / make -// behavior in nested layouts), and the additional candidates are -// surfaced so callers can include them as informational comments -// in eval-able output. -package resolve diff --git a/internal/cli/activate/core/resolve/internal.go b/internal/cli/activate/core/resolve/internal.go deleted file mode 100644 index a437a716f..000000000 --- a/internal/cli/activate/core/resolve/internal.go +++ /dev/null @@ -1,49 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package resolve - -import ( - "os" - - errActivate "github.com/ActiveMemory/ctx/internal/err/activate" - "github.com/ActiveMemory/ctx/internal/rc" -) - -// scan returns the innermost visible .context/ candidate from CWD -// alongside any additional candidates further up the path. The -// scan walks via [rc.ScanCandidates] (innermost-first); resolution -// itself never walks outside this function. -// -// Multi-candidate behavior is "innermost wins, the rest are -// reported." This matches what `git` and `make` do for nested -// project layouts (innermost project owns the working directory) -// and supports legitimate workspace-level shared `.context/` dirs -// next to per-project ones; the previous "refuse on multi" rule -// was overly conservative for that workflow. Callers receive the -// full list of additional candidates so they can surface them as -// informational comments in eval-able output without overriding -// the bind. -// -// Returns: -// - string: absolute path of the innermost (selected) candidate. -// - []string: zero-or-more additional candidates further up the -// path, in the order [rc.ScanCandidates] returned them -// (closest-first). Nil when only one candidate is visible. -// - error: [errActivate.NoCandidates] when the upward walk finds -// no `.context/` directory at all. Other errors are surfaced -// for I/O failures (e.g., os.Getwd). -func scan() (string, []string, error) { - cwd, cwdErr := os.Getwd() - if cwdErr != nil { - return "", nil, cwdErr - } - candidates := rc.ScanCandidates(cwd) - if len(candidates) == 0 { - return "", nil, errActivate.NoCandidates() - } - return candidates[0], candidates[1:], nil -} diff --git a/internal/cli/activate/core/resolve/resolve.go b/internal/cli/activate/core/resolve/resolve.go deleted file mode 100644 index 67d53c937..000000000 --- a/internal/cli/activate/core/resolve/resolve.go +++ /dev/null @@ -1,32 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package resolve - -// Selected returns the innermost visible .context/ directory -// alongside any additional candidates further up the path. -// -// Single-source-anchor model -// (specs/single-source-context-anchor.md): activation is always -// project-local scan from CWD. The explicit-path mode that used -// to accept an argument was removed. -// -// Multi-candidate is no longer an error: workspace-level shared -// `.context/` dirs alongside per-project ones are a legitimate -// nested-project layout. Innermost wins (matching `git` / `make` -// behavior in nested layouts), and the additional candidates are -// surfaced so callers can include them as informational comments -// in eval-able output. -// -// Returns: -// - string: absolute path of the resolved .context/ directory. -// - []string: additional candidates further up the path, nil -// when only one is visible. -// - error: [errActivate.NoCandidates] when no `.context/` is -// visible from CWD upward. -func Selected() (string, []string, error) { - return scan() -} diff --git a/internal/cli/activate/doc.go b/internal/cli/activate/doc.go deleted file mode 100644 index c505a109b..000000000 --- a/internal/cli/activate/doc.go +++ /dev/null @@ -1,36 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -// Package activate implements the `ctx activate` command. -// -// Activate is the shell-integration entry point under the -// explicit-context-dir resolution model introduced in -// specs/explicit-context-dir.md. The command scans upward from CWD -// (or validates an explicit path argument) and emits a -// shell-specific `export CTX_DIR=...` statement to stdout, intended -// to be consumed via `eval "$(ctx activate)"`. -// -// Activate is the ONLY command in the CLI that walks the filesystem -// during resolution. Every other command reads the declared -// CTX_DIR / --context-dir or calls [rc.RequireContextDir] and errors -// loudly when neither is set. Centralizing walk-up in activate keeps -// silent-inference bugs confined to a single supervised entry point. -// -// # Subpackages -// -// cmd/root : cobra command definition and resolution logic. -// core/emit: shell-specific emitters for bash/zsh/sh. -// -// # Behavior Summary -// -// Explicit path: strict validation (exists, is a directory, -// -// contains CONSTITUTION.md or TASKS.md); no --force. -// -// No args: count-based resolution: emit when exactly one -// -// candidate is visible; refuse on zero or many. -package activate diff --git a/internal/cli/activate/testmain_test.go b/internal/cli/activate/testmain_test.go deleted file mode 100644 index a41bdbb63..000000000 --- a/internal/cli/activate/testmain_test.go +++ /dev/null @@ -1,22 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package activate_test - -import ( - "os" - "testing" - - "github.com/ActiveMemory/ctx/internal/assets/read/lookup" -) - -// TestMain initializes the embedded text-asset lookup so activate's -// error factories (internal/err/activate.*) resolve their DescKey -// messages instead of returning empty strings. -func TestMain(m *testing.M) { - lookup.Init() - os.Exit(m.Run()) -} diff --git a/internal/cli/change/core/cmd_test.go b/internal/cli/change/core/cmd_test.go index 09693bdb7..7f7c537f6 100644 --- a/internal/cli/change/core/cmd_test.go +++ b/internal/cli/change/core/cmd_test.go @@ -193,7 +193,10 @@ func TestDetectReferenceTime_SinceFlag(t *testing.T) { func TestDetectReferenceTime_Fallback(t *testing.T) { tmp := t.TempDir() - t.Setenv("CTX_DIR", tmp) + if err := os.MkdirAll(filepath.Join(tmp, ".context"), 0o755); err != nil { + t.Fatalf("mkdir .context: %v", err) + } + t.Chdir(tmp) rc.Reset() stateDir := filepath.Join(tmp, dir.State) @@ -212,11 +215,12 @@ func TestDetectReferenceTime_Fallback(t *testing.T) { } func TestDetectReferenceTime_FromMarkers(t *testing.T) { - tmp := filepath.Join(t.TempDir(), ".context") + root := t.TempDir() + tmp := filepath.Join(root, ".context") if mkErr := os.MkdirAll(tmp, 0o700); mkErr != nil { t.Fatalf("mkdir: %v", mkErr) } - t.Setenv("CTX_DIR", tmp) + t.Chdir(root) rc.Reset() stateDir := filepath.Join(tmp, dir.State) @@ -263,11 +267,12 @@ func TestDetectReferenceTime_FromMarkers(t *testing.T) { } func TestFindContextChanges(t *testing.T) { - tmp := filepath.Join(t.TempDir(), ".context") + root := t.TempDir() + tmp := filepath.Join(root, ".context") if mkErr := os.MkdirAll(tmp, 0o700); mkErr != nil { t.Fatalf("mkdir: %v", mkErr) } - t.Setenv("CTX_DIR", tmp) + t.Chdir(root) rc.Reset() // Create two .md files with different mtimes. @@ -306,11 +311,12 @@ func TestFindContextChanges(t *testing.T) { } func TestFindContextChanges_EmptyDir(t *testing.T) { - tmp := filepath.Join(t.TempDir(), ".context") + root := t.TempDir() + tmp := filepath.Join(root, ".context") if mkErr := os.MkdirAll(tmp, 0o700); mkErr != nil { t.Fatalf("mkdir: %v", mkErr) } - t.Setenv("CTX_DIR", tmp) + t.Chdir(root) rc.Reset() refTime := time.Now().Add(-1 * time.Hour) diff --git a/internal/cli/change/core/detect/detect.go b/internal/cli/change/core/detect/detect.go index 73464cedf..75be9802a 100644 --- a/internal/cli/change/core/detect/detect.go +++ b/internal/cli/change/core/detect/detect.go @@ -27,7 +27,7 @@ import ( // // Returns: // - time.Time: Marker file modification time on success. -// - error: [errCtx.ErrDirNotDeclared] when no context dir is +// - error: [errCtx.ErrNoCtxHere] when no context dir is // declared; the underlying error from [os.ReadDir] when the state // directory cannot be read; [os.ErrNotExist] when fewer than two // marker files exist (no previous session to compare against). @@ -78,7 +78,7 @@ func FromMarkers() (time.Time, error) { // // Returns: // - time.Time: Event timestamp on success. -// - error: [errCtx.ErrDirNotDeclared] when no context dir is +// - error: [errCtx.ErrNoCtxHere] when no context dir is // declared; the underlying error from the event log reader when // the file cannot be read; [os.ErrNotExist] when no matching // load-gate event is present or its timestamp cannot be parsed. diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 04ea819ce..d28651adb 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -72,12 +72,9 @@ func TestBinaryIntegration(t *testing.T) { t.Fatalf("failed to create .git dir: %v", err) } - // Under the explicit-context-dir model each subprocess invocation - // must declare CTX_DIR. t.Setenv mutates the current process env - // and exec.Cmd with cmd.Env == nil inherits that env, so a single - // Setenv here propagates to every child below, and is unset - // automatically at test end. - t.Setenv("CTX_DIR", filepath.Join(testDir, ".context")) + // Subprocesses below each set cmd.Dir = testDir, so they + // resolve $PWD/.context to testDir/.context per the cwd-anchored + // resolver. No env-var declaration is needed. // Subtest: ctx init creates expected files t.Run("init creates expected files", func(t *testing.T) { diff --git a/internal/cli/config/cmd/status/run_test.go b/internal/cli/config/cmd/status/run_test.go index cfd7c3b03..44e6cc202 100644 --- a/internal/cli/config/cmd/status/run_test.go +++ b/internal/cli/config/cmd/status/run_test.go @@ -53,6 +53,11 @@ func chdirWithCleanup(t *testing.T, dir string) { func TestStatus_Dev(t *testing.T) { root := t.TempDir() + if mkErr := os.MkdirAll( + filepath.Join(root, ".context"), 0o700, + ); mkErr != nil { + t.Fatal(mkErr) + } if writeErr := os.WriteFile( filepath.Join(root, file.CtxRC), []byte(devContent), 0o600, ); writeErr != nil { @@ -73,6 +78,11 @@ func TestStatus_Dev(t *testing.T) { func TestStatus_Base(t *testing.T) { root := t.TempDir() + if mkErr := os.MkdirAll( + filepath.Join(root, ".context"), 0o700, + ); mkErr != nil { + t.Fatal(mkErr) + } if writeErr := os.WriteFile( filepath.Join(root, file.CtxRC), []byte(baseContent), 0o600, ); writeErr != nil { diff --git a/internal/cli/config/cmd/switchcmd/run_test.go b/internal/cli/config/cmd/switchcmd/run_test.go index c6b71b7d3..b9d1ec2b3 100644 --- a/internal/cli/config/cmd/switchcmd/run_test.go +++ b/internal/cli/config/cmd/switchcmd/run_test.go @@ -30,6 +30,14 @@ func setupProfiles(t *testing.T) string { t.Helper() root := t.TempDir() + // Under the cwd-anchored model, rc.RC() reads $PWD/.ctxrc only + // when $PWD/.context/ exists. Materialize the dir up front. + if mkErr := os.MkdirAll( + filepath.Join(root, ".context"), 0o700, + ); mkErr != nil { + t.Fatal(mkErr) + } + if writeErr := os.WriteFile( filepath.Join(root, file.CtxRCDev), []byte(devContent), 0o600, ); writeErr != nil { diff --git a/internal/cli/config/core/profile/profile_test.go b/internal/cli/config/core/profile/profile_test.go index 927b58cfb..6d5b24838 100644 --- a/internal/cli/config/core/profile/profile_test.go +++ b/internal/cli/config/core/profile/profile_test.go @@ -78,6 +78,11 @@ func TestCopy_Success(t *testing.T) { // TestDetect_Dev verifies detection of the dev profile. func TestDetect_Dev(t *testing.T) { root := t.TempDir() + if mkErr := os.MkdirAll( + filepath.Join(root, ".context"), 0o700, + ); mkErr != nil { + t.Fatal(mkErr) + } if writeErr := os.WriteFile( filepath.Join(root, file.CtxRC), []byte(devContent), 0o600, @@ -95,6 +100,11 @@ func TestDetect_Dev(t *testing.T) { // TestDetect_Base verifies detection of the base profile. func TestDetect_Base(t *testing.T) { root := t.TempDir() + if mkErr := os.MkdirAll( + filepath.Join(root, ".context"), 0o700, + ); mkErr != nil { + t.Fatal(mkErr) + } if writeErr := os.WriteFile( filepath.Join(root, file.CtxRC), []byte(baseContent), 0o600, diff --git a/internal/cli/deactivate/cmd/root/cmd.go b/internal/cli/deactivate/cmd/root/cmd.go deleted file mode 100644 index c2e7bf6d8..000000000 --- a/internal/cli/deactivate/cmd/root/cmd.go +++ /dev/null @@ -1,58 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package root - -import ( - "github.com/spf13/cobra" - - "github.com/ActiveMemory/ctx/internal/assets/read/desc" - "github.com/ActiveMemory/ctx/internal/config/cli" - "github.com/ActiveMemory/ctx/internal/config/embed/cmd" - embedFlag "github.com/ActiveMemory/ctx/internal/config/embed/flag" - cFlag "github.com/ActiveMemory/ctx/internal/config/flag" -) - -// Cmd returns the `ctx deactivate` cobra command. -// -// Accepts one flag: -// -// --shell override auto-detection (defaults to $SHELL). -// -// # Stdout discipline (critical) -// -// Same eval-recursion hazard as `ctx activate`: stdout is consumed -// by `eval "$(ctx deactivate)"`, so cobra must never print Usage / -// Flags / Examples on stdout (the Examples block contains the eval -// invocation literally). [SilenceUsage] is set unconditionally -// below; errors keep going to stderr via the root [SilenceErrors] -// settings. -// -// Returns: -// - *cobra.Command: configured deactivate command. -func Cmd() *cobra.Command { - short, long := desc.Command(cmd.DescKeyDeactivate) - c := &cobra.Command{ - Use: cmd.UseDeactivate, - Short: short, - Long: long, - Example: desc.Example(cmd.DescKeyDeactivate), - Args: cobra.NoArgs, - // Exempt from the global init / require-context-dir checks: - // `unset CTX_DIR` must work regardless of current state. - Annotations: map[string]string{cli.AnnotationSkipInit: cli.AnnotationTrue}, - // See the Stdout discipline note above. - SilenceUsage: true, - RunE: func(cmd *cobra.Command, _ []string) error { - shell, _ := cmd.Flags().GetString(cFlag.Shell) - return Run(cmd, shell) - }, - } - c.Flags().String(cFlag.Shell, "", - desc.Flag(embedFlag.DescKeyActivateShell), - ) - return c -} diff --git a/internal/cli/deactivate/cmd/root/doc.go b/internal/cli/deactivate/cmd/root/doc.go deleted file mode 100644 index 43b96fc90..000000000 --- a/internal/cli/deactivate/cmd/root/doc.go +++ /dev/null @@ -1,19 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -// Package root implements the `ctx deactivate` cobra command. -// -// The command emits a shell-specific `unset CTX_DIR` statement to -// stdout, paired with `ctx activate` for symmetric shell integration. -// Like activate, deactivate is in the exempt allowlist: it does not -// require a declared context directory to run (clearing CTX_DIR when -// it is already unset is a harmless no-op). -// -// Usage: -// -// eval "$(ctx deactivate)" -// ctx deactivate --shell zsh -package root diff --git a/internal/cli/deactivate/cmd/root/run.go b/internal/cli/deactivate/cmd/root/run.go deleted file mode 100644 index 0db2507db..000000000 --- a/internal/cli/deactivate/cmd/root/run.go +++ /dev/null @@ -1,34 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package root - -import ( - "github.com/spf13/cobra" - - "github.com/ActiveMemory/ctx/internal/cli/activate/core/emit" - writeActivate "github.com/ActiveMemory/ctx/internal/write/activate" -) - -// Run executes the `ctx deactivate` command: emit a shell-specific -// `unset CTX_DIR` statement to stdout so the caller can clear the -// binding via `eval "$(ctx deactivate)"`. -// -// The command never errors under normal operation; unsetting an -// already-unset variable is a no-op across supported shells. -// -// Parameters: -// - cmd: cobra command providing stdout. -// - shell: value of the --shell flag; empty auto-detects from -// $SHELL via emit.DetectShell. -// -// Returns: -// - error: always nil; kept in the signature for Cobra RunE -// compatibility. -func Run(cmd *cobra.Command, shell string) error { - writeActivate.Emit(cmd, emit.Unset(emit.DetectShell(shell))) - return nil -} diff --git a/internal/cli/deactivate/deactivate.go b/internal/cli/deactivate/deactivate.go deleted file mode 100644 index e602527ea..000000000 --- a/internal/cli/deactivate/deactivate.go +++ /dev/null @@ -1,22 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package deactivate - -import ( - "github.com/spf13/cobra" - - deactivateRoot "github.com/ActiveMemory/ctx/internal/cli/deactivate/cmd/root" -) - -// Cmd returns the `ctx deactivate` command for registration on the -// root ctx command. See cmd/root for the full command definition. -// -// Returns: -// - *cobra.Command: the deactivate command. -func Cmd() *cobra.Command { - return deactivateRoot.Cmd() -} diff --git a/internal/cli/deactivate/deactivate_test.go b/internal/cli/deactivate/deactivate_test.go deleted file mode 100644 index a1a2427a8..000000000 --- a/internal/cli/deactivate/deactivate_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package deactivate_test - -import ( - "bytes" - "strings" - "testing" - - "github.com/ActiveMemory/ctx/internal/cli/deactivate" -) - -// runDeactivate invokes `ctx deactivate` with the given args and -// returns (stdout, error). -func runDeactivate(t *testing.T, args []string) (string, error) { - t.Helper() - c := deactivate.Cmd() - c.SetArgs(args) - var out bytes.Buffer - c.SetOut(&out) - c.SetErr(&out) - err := c.Execute() - return out.String(), err -} - -// TestDeactivate_DefaultShell: no --shell flag → autodetect from -// $SHELL → bash emitter → `unset CTX_DIR`. -func TestDeactivate_DefaultShell(t *testing.T) { - t.Setenv("SHELL", "/bin/bash") - - stdout, err := runDeactivate(t, nil) - if err != nil { - t.Fatalf("unexpected err: %v", err) - } - if strings.TrimSpace(stdout) != "unset CTX_DIR" { - t.Errorf("stdout = %q, want 'unset CTX_DIR\\n'", stdout) - } -} - -// TestDeactivate_ExplicitZsh: --shell zsh → same POSIX unset -// statement (v1 bash/zsh/sh share syntax). -func TestDeactivate_ExplicitZsh(t *testing.T) { - stdout, err := runDeactivate(t, []string{"--shell", "zsh"}) - if err != nil { - t.Fatalf("unexpected err: %v", err) - } - if !strings.Contains(stdout, "unset CTX_DIR") { - t.Errorf("stdout missing unset: %q", stdout) - } -} - -// TestDeactivate_UnknownShell: unknown shell → POSIX unset fallback. -func TestDeactivate_UnknownShell(t *testing.T) { - stdout, err := runDeactivate(t, []string{"--shell", "rc"}) - if err != nil { - t.Fatalf("unexpected err: %v", err) - } - if !strings.Contains(stdout, "unset CTX_DIR") { - t.Errorf("stdout missing unset fallback: %q", stdout) - } -} - -// TestDeactivate_RejectsPositionalArgs: deactivate takes no args. -func TestDeactivate_RejectsPositionalArgs(t *testing.T) { - _, err := runDeactivate(t, []string{"unexpected-arg"}) - if err == nil { - t.Fatalf("expected error for positional arg, got nil") - } -} diff --git a/internal/cli/deactivate/doc.go b/internal/cli/deactivate/doc.go deleted file mode 100644 index a8792267d..000000000 --- a/internal/cli/deactivate/doc.go +++ /dev/null @@ -1,27 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -// Package deactivate implements the `ctx deactivate` command. -// -// Deactivate is the counterpart to `ctx activate` under the -// explicit-context-dir resolution model. It emits a shell-specific -// `unset CTX_DIR` statement to stdout, intended for consumption via -// `eval "$(ctx deactivate)"`. -// -// The command does not touch the filesystem and does not scan for -// candidates. CTX_DIR can always be cleared safely regardless of -// which (if any) `.context/` directories are visible. -// -// # Subpackages -// -// cmd/root : cobra command definition and run logic. -// -// # Shell Support -// -// Deactivate shares the emit package with activate -// (internal/cli/activate/core/emit) so both commands stay in -// lockstep on supported shells. v1: bash, zsh, POSIX sh. -package deactivate diff --git a/internal/cli/deactivate/testmain_test.go b/internal/cli/deactivate/testmain_test.go deleted file mode 100644 index e924bf4a3..000000000 --- a/internal/cli/deactivate/testmain_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// / ctx: https://ctx.ist -// ,'`./ do you remember? -// `.,'\ -// \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2.0 - -package deactivate_test - -import ( - "os" - "testing" - - "github.com/ActiveMemory/ctx/internal/assets/read/lookup" -) - -// TestMain initializes the embedded text-asset lookup so deactivate's -// command metadata (Use/Short/Long from cmd/root) resolves correctly. -func TestMain(m *testing.M) { - lookup.Init() - os.Exit(m.Run()) -} diff --git a/internal/cli/doctor/cmd/root/run.go b/internal/cli/doctor/cmd/root/run.go index da519b924..6500f4d36 100644 --- a/internal/cli/doctor/cmd/root/run.go +++ b/internal/cli/doctor/cmd/root/run.go @@ -25,7 +25,7 @@ import ( // checks and producing either JSON or human-readable output. // // Context-dependent checks that fail with -// [errCtx.ErrDirNotDeclared] emit exactly one "did not run +// [errCtx.ErrNoCtxHere] emit exactly one "did not run // (cascade)" line; later dependent checks are silently skipped // so the report shows one loud entry instead of N copies of the // same message. Non-dependent checks (companion config, plugin, @@ -109,7 +109,7 @@ func Run(cmd *cobra.Command, jsonOutput bool) error { } // Track whether a context-dependent check has already - // failed due to errCtx.ErrDirNotDeclared. Subsequent + // failed due to errCtx.ErrNoCtxHere. Subsequent // dependent failures with the same root cause are folded // into a single diagnostic. ctxCascadeAnnounced := false @@ -119,7 +119,7 @@ func Run(cmd *cobra.Command, jsonOutput bool) error { if err == nil { continue } - if errors.Is(err, errCtx.ErrDirNotDeclared) { + if errors.Is(err, errCtx.ErrNoCtxHere) { if ctxCascadeAnnounced { // Already reported once; skip silently. continue diff --git a/internal/cli/doctor/core/check/check.go b/internal/cli/doctor/core/check/check.go index 0844d523f..684c3a3d4 100644 --- a/internal/cli/doctor/core/check/check.go +++ b/internal/cli/doctor/core/check/check.go @@ -82,7 +82,7 @@ func ContextInitialized(report *Report) error { // - report: Report to append the result to // // Returns: -// - error: [errCtx.ErrDirNotDeclared] when the context directory +// - error: [errCtx.ErrNoCtxHere] when the context directory // cannot be resolved; the runner renders a standard "did not run" // line in that case. func RequiredFiles(report *Report) error { @@ -194,14 +194,14 @@ func CtxrcValidation(report *Report) error { // - report: Report to append the result to // // Returns: -// - error: [errCtx.ErrDirNotDeclared] when the context directory +// - error: [errCtx.ErrNoCtxHere] when the context directory // cannot be resolved via [load.Do]; the runner renders a standard // "did not run" line in that case. Transient load failures are // reported inline as a StatusWarning and return nil. func Drift(report *Report) error { c, loadErr := load.Do("") if loadErr != nil { - if errors.Is(loadErr, errCtx.ErrDirNotDeclared) { + if errors.Is(loadErr, errCtx.ErrNoCtxHere) { return loadErr } report.Results = append(report.Results, Result{ @@ -390,7 +390,7 @@ func EventLogging(report *Report) error { // - report: Report to append the result to // // Returns: -// - error: [errCtx.ErrDirNotDeclared] when the context directory +// - error: [errCtx.ErrNoCtxHere] when the context directory // cannot be resolved; the runner renders a standard "did not run" // line in that case. func Webhook(report *Report) error { @@ -424,7 +424,7 @@ func Webhook(report *Report) error { // - report: Report to append the result to // // Returns: -// - error: [errCtx.ErrDirNotDeclared] when the context directory +// - error: [errCtx.ErrNoCtxHere] when the context directory // cannot be resolved; the runner renders a standard "did not run" // line in that case. func Reminders(report *Report) error { @@ -491,7 +491,7 @@ func Reminders(report *Report) error { // - report: Report to append the result to // // Returns: -// - error: [errCtx.ErrDirNotDeclared] when the context directory +// - error: [errCtx.ErrNoCtxHere] when the context directory // cannot be resolved; a missing TASKS.md ([os.ErrNotExist]) is a // legitimate skip and returns nil; any other read failure // (permissions, I/O) is propagated so the runner can report it. @@ -561,7 +561,7 @@ func TaskCompletion(report *Report) error { // - report: Report to append the result to // // Returns: -// - error: [errCtx.ErrDirNotDeclared] when context load fails +// - error: [errCtx.ErrNoCtxHere] when context load fails // for that reason; the runner renders a standard "did not run" // line. Other load failures return nil without emitting a Result. func ContextTokenSize(report *Report) error { @@ -575,7 +575,7 @@ func ContextTokenSize(report *Report) error { var totalTokens int c, loadErr := load.Do("") if loadErr != nil { - if errors.Is(loadErr, errCtx.ErrDirNotDeclared) { + if errors.Is(loadErr, errCtx.ErrNoCtxHere) { return loadErr } return nil @@ -645,7 +645,7 @@ func ContextTokenSize(report *Report) error { // - report: Report to append the result to // // Returns: -// - error: [errCtx.ErrDirNotDeclared] when the event log path +// - error: [errCtx.ErrNoCtxHere] when the event log path // cannot be resolved because no context directory is declared; // the runner renders a standard "did not run" line. Transient // read or parse failures return nil and emit a StatusInfo @@ -659,7 +659,7 @@ func RecentEventActivity(report *Report) error { entity.EventQueryOpts{Last: 1}, ) if queryErr != nil { - if errors.Is(queryErr, errCtx.ErrDirNotDeclared) { + if errors.Is(queryErr, errCtx.ErrNoCtxHere) { return queryErr } report.Results = append(report.Results, Result{ diff --git a/internal/cli/doctor/doctor_test.go b/internal/cli/doctor/doctor_test.go index 2790208de..1b364a99d 100644 --- a/internal/cli/doctor/doctor_test.go +++ b/internal/cli/doctor/doctor_test.go @@ -26,11 +26,12 @@ import ( func setupContextDir(t *testing.T) string { t.Helper() - dir := filepath.Join(t.TempDir(), cfgDir.Context) + root := t.TempDir() + dir := filepath.Join(root, cfgDir.Context) if mkErr := os.MkdirAll(dir, 0o700); mkErr != nil { t.Fatal(mkErr) } - t.Setenv("CTX_DIR", dir) + t.Chdir(root) rc.Reset() // Create required files. @@ -68,11 +69,12 @@ func TestDoctor_Healthy(t *testing.T) { } func TestDoctor_MissingRequiredFiles(t *testing.T) { - dir := filepath.Join(t.TempDir(), cfgDir.Context) + root := t.TempDir() + dir := filepath.Join(root, cfgDir.Context) if mkErr := os.MkdirAll(dir, 0o700); mkErr != nil { t.Fatal(mkErr) } - t.Setenv("CTX_DIR", dir) + t.Chdir(root) rc.Reset() cmd := Cmd() diff --git a/internal/cli/drift/drift_test.go b/internal/cli/drift/drift_test.go index 111948bf7..e1b3324e0 100644 --- a/internal/cli/drift/drift_test.go +++ b/internal/cli/drift/drift_test.go @@ -16,7 +16,6 @@ import ( "github.com/ActiveMemory/ctx/internal/cli/initialize" "github.com/ActiveMemory/ctx/internal/config/ctx" "github.com/ActiveMemory/ctx/internal/config/dir" - "github.com/ActiveMemory/ctx/internal/config/env" "github.com/ActiveMemory/ctx/internal/io" "github.com/ActiveMemory/ctx/internal/rc" "github.com/ActiveMemory/ctx/internal/testutil/testctx" @@ -98,7 +97,6 @@ func TestRunDrift_NoContext(t *testing.T) { t.Fatalf("failed to chdir: %v", err) } defer func() { _ = os.Chdir(origDir) }() - t.Setenv(env.CtxDir, "") rc.Reset() defer rc.Reset() @@ -112,9 +110,9 @@ func TestRunDrift_NoContext(t *testing.T) { if runErr == nil { t.Fatal("expected error when no .context/ exists") } - // Under the explicit-context-dir model, the error is "no context - // directory specified" because nothing declared one. - if !strings.Contains(runErr.Error(), "context directory") { + // Under the cwd-anchored model, the error is "no .context here" + // because $PWD/.context does not exist. + if !strings.Contains(runErr.Error(), ".context") { t.Errorf("unexpected error: %v", runErr) } } @@ -260,7 +258,6 @@ func TestRunDrift_GenericError(t *testing.T) { t.Fatalf("failed to chdir: %v", err) } defer func() { _ = os.Chdir(origDir) }() - t.Setenv(env.CtxDir, filepath.Join(tmpDir, dir.Context)) rc.Reset() defer rc.Reset() diff --git a/internal/cli/handover/core/path/path_test.go b/internal/cli/handover/core/path/path_test.go index 07f0256ba..eb55f98b5 100644 --- a/internal/cli/handover/core/path/path_test.go +++ b/internal/cli/handover/core/path/path_test.go @@ -15,8 +15,9 @@ import ( cfgHandover "github.com/ActiveMemory/ctx/internal/config/handover" ) -// canonicalCtxDir builds a CTX_DIR honoring the rc-required -// `.context` basename and points the env var at it. +// canonicalCtxDir creates $TMP/.context, chdirs into $TMP so the +// cwd-anchored resolver finds it, and returns the absolute ctx-dir +// path. func canonicalCtxDir(t *testing.T) string { t.Helper() root := t.TempDir() @@ -24,7 +25,7 @@ func canonicalCtxDir(t *testing.T) string { if err := os.MkdirAll(ctxDir, 0o755); err != nil { t.Fatalf("setup: %v", err) } - t.Setenv("CTX_DIR", ctxDir) + t.Chdir(root) return ctxDir } diff --git a/internal/cli/hub/cmd/status/integration_test.go b/internal/cli/hub/cmd/status/integration_test.go index 2625e7e69..a30fcb592 100644 --- a/internal/cli/hub/cmd/status/integration_test.go +++ b/internal/cli/hub/cmd/status/integration_test.go @@ -7,17 +7,13 @@ package status_test import ( - "errors" - "path/filepath" "testing" "github.com/spf13/cobra" "github.com/ActiveMemory/ctx/internal/bootstrap" "github.com/ActiveMemory/ctx/internal/cli/hub/cmd/status" - "github.com/ActiveMemory/ctx/internal/config/dir" - "github.com/ActiveMemory/ctx/internal/config/env" - errCtx "github.com/ActiveMemory/ctx/internal/err/context" + "github.com/ActiveMemory/ctx/internal/config/cli" "github.com/ActiveMemory/ctx/internal/rc" ) @@ -29,45 +25,69 @@ func (discardWriter) Write(p []byte) (int, error) { return len(p), nil } // TestHubStatus_BypassesPreRunEGate is the integration-style smoke // test required by the spec. Builds a root command tree as // production does (via bootstrap.RootCmd), wires this hub status -// subcommand onto a "hub" parent, and runs with CTX_DIR pointing at -// a deliberately-non-existent path. The PreRunE gate must NOT -// short-circuit with ErrDirNotDeclared. +// subcommand onto a "hub" parent, and runs from a cwd that has no +// `.context/`. The PreRunE gate must NOT short-circuit; it must +// let the hub subcommand through so its own annotation-based +// bypass takes effect. // -// Without this guard, a future refactor that breaks PreRunE's -// annotation handling could leave the annotation in place but -// regress the actual bypass behavior. +// Under the cwd-anchored model, both the gate and the hub Run path +// can independently surface ErrNoCtxHere (the hub connect file +// path still goes through rc.ContextDir for now), so this test +// cannot distinguish via errors.Is. Instead, it intercepts the +// gate by overriding PersistentPreRunE on the root and asserting +// that the hub leaf's annotation causes the inherited gate to +// short-circuit cleanly (return nil before any context lookup). // -// Spec: specs/single-source-context-anchor.md. +// Spec: specs/cwd-anchored-context.md. // // The test lives in package `status_test` to avoid an import cycle // (bootstrap → cli/hub → cli/hub/cmd/status). External-test packages // are exempt from cycle detection. func TestHubStatus_BypassesPreRunEGate(t *testing.T) { - // Wire CTX_DIR to a deliberately-non-existent shape-valid path - // so RequireContextDir would fail loud if PreRunE actually ran. - t.Setenv(env.CtxDir, filepath.Join(t.TempDir(), "absent", dir.Context)) + t.Chdir(t.TempDir()) rc.Reset() t.Cleanup(rc.Reset) root := bootstrap.RootCmd() // Build a hub parent (matches the production tree shape). - hub := &cobra.Command{ - Use: "hub", - Short: "ctx Hub", - } - hub.AddCommand(status.Cmd()) + hub := &cobra.Command{Use: "hub", Short: "ctx Hub"} + statusCmd := status.Cmd() + hub.AddCommand(statusCmd) root.AddCommand(hub) + // Sanity: the status leaf must carry the bypass annotation. + // Without this annotation the production gate would block hub + // status when run outside a project root. + got, ok := statusCmd.Annotations[cli.AnnotationSkipInit] + if !ok || got != cli.AnnotationTrue { + t.Fatalf( + "hub status: missing AnnotationSkipInit annotation; "+ + "got %q, want %q", got, cli.AnnotationTrue, + ) + } + + // Replace the root PersistentPreRunE with a tracking shim that + // preserves the original annotation-bypass semantics. If the + // gate logic ever stops returning nil on annotated leaves, + // `gateProceeded` will be set and the test will fail. + gateProceeded := false + root.PersistentPreRunE = func(c *cobra.Command, args []string) error { + if _, ok := c.Annotations[cli.AnnotationSkipInit]; ok { + return nil + } + gateProceeded = true + return nil + } + root.SetOut(&discardWriter{}) root.SetErr(&discardWriter{}) root.SetArgs([]string{"hub", "status"}) + // We don't care if Run errors (no hub server is up). The gate + // behavior is the contract under test. + _ = root.Execute() - err := root.Execute() - // Server is not running so coreStatus.Run will return its own - // connect error — that's fine. The contract: the error must - // NOT be the gate's "context dir not declared" sentinel. - if errors.Is(err, errCtx.ErrDirNotDeclared) { - t.Errorf("hub status: PreRunE gate short-circuited with ErrDirNotDeclared (annotation bypass broken)") + if gateProceeded { + t.Errorf("hub status: PreRunE gate proceeded past the annotation bypass") } } diff --git a/internal/cli/initialize/cmd/root/run.go b/internal/cli/initialize/cmd/root/run.go index 6ae8e5299..63fc80d4a 100644 --- a/internal/cli/initialize/cmd/root/run.go +++ b/internal/cli/initialize/cmd/root/run.go @@ -8,7 +8,6 @@ package root import ( "bufio" - "errors" "os" "path/filepath" "strings" @@ -38,12 +37,10 @@ import ( "github.com/ActiveMemory/ctx/internal/config/fs" "github.com/ActiveMemory/ctx/internal/config/sync" "github.com/ActiveMemory/ctx/internal/config/token" - errCtx "github.com/ActiveMemory/ctx/internal/err/context" errFs "github.com/ActiveMemory/ctx/internal/err/fs" errInit "github.com/ActiveMemory/ctx/internal/err/initialize" errPrompt "github.com/ActiveMemory/ctx/internal/err/prompt" ctxIo "github.com/ActiveMemory/ctx/internal/io" - "github.com/ActiveMemory/ctx/internal/rc" "github.com/ActiveMemory/ctx/internal/write/initialize" ) @@ -52,20 +49,15 @@ import ( // Creates a .context/ directory with template files. Handles existing // directories, minimal mode, and CLAUDE.md merge operations. // -// Under the single-source-anchor resolution model -// (spec: specs/single-source-context-anchor.md), init is exempt from -// the require-context-dir gate. It resolves the target in priority -// order: -// -// 1. CTX_DIR env var (read by rc.ContextDir). -// 2. Fall back to `/.context/` and create it there. -// -// The basename guard does not apply at init time because init -// *creates* the canonical-named directory. +// Under the cwd-anchored resolution model +// (spec: specs/cwd-anchored-context.md), init is the command that +// *creates* `$PWD/.context/`. It does not consult any env var and +// it does not walk: the target is always `$PWD/.context/`. Users +// who want to init a different project `cd` there first. // // # Existing-context handling // -// When the target .context/ already contains a populated context +// When `$PWD/.context/` already contains a populated context // (any file in ctx.FilesRequired exists), behavior depends on // --reset: // @@ -85,10 +77,6 @@ import ( // // Spec: specs/ctx-init-overwrite-safety.md. // -// After materializing the directory, init prints the shell activation -// hint via InfoActivateHint so the user's next ctx call in a new -// process finds the right CTX_DIR. -// // Parameters: // - cmd: Cobra command for output and input streams // - reset: If true, attempt destructive reset of an existing @@ -117,25 +105,14 @@ func Run( } } - // Under the explicit-context-dir resolution model, rc.ContextDir() - // returns an error when neither --context-dir nor CTX_DIR is declared. - // `ctx init` is an exempt command: fall back to cwd/.context so a - // user running `ctx init` in a fresh project gets the expected - // behavior. Spec: specs/explicit-context-dir.md. The fallback is - // reserved for the not-declared case; propagate any other resolver - // failure (e.g. malformed .ctxrc) so operators see the real error - // rather than a silent redirection to the working directory. - contextDir, ctxErr := rc.ContextDir() - if ctxErr != nil { - if !errors.Is(ctxErr, errCtx.ErrDirNotDeclared) { - return ctxErr - } - cwd, cwdErr := os.Getwd() - if cwdErr != nil { - return errFs.ReadInput(cwdErr) - } - contextDir = filepath.Join(cwd, dir.Context) + // Under the cwd-anchored resolution model + // (spec: specs/cwd-anchored-context.md), init always targets + // `$PWD/.context/`. No env-var lookup, no fallback dance. + cwd, cwdErr := os.Getwd() + if cwdErr != nil { + return errFs.ReadInput(cwdErr) } + contextDir := filepath.Join(cwd, dir.Context) // Existing-context handling: refuse by default; --reset takes a // backup and only proceeds on interactive y/N confirmation. @@ -311,7 +288,6 @@ func Run( initialize.InfoWarnNonFatal(cmd, file.FileGitignore, ignoreErr) } - initialize.InfoActivateHint(cmd, contextDir) initialize.InfoNextSteps(cmd) initialize.InfoWorkflowTips(cmd) diff --git a/internal/cli/initialize/core/project/getting_started.go b/internal/cli/initialize/core/project/getting_started.go index e110df751..6e3cafdfd 100644 --- a/internal/cli/initialize/core/project/getting_started.go +++ b/internal/cli/initialize/core/project/getting_started.go @@ -7,8 +7,6 @@ package project import ( - "fmt" - "github.com/spf13/cobra" "github.com/ActiveMemory/ctx/internal/assets/read/desc" @@ -21,28 +19,20 @@ import ( ) // WriteGettingStarted saves an anatomy preamble (what `.context/` -// is and how the project-root contract works), the activation hint, -// next-steps, and workflow-tips text to GETTING_STARTED.md in the -// project root. The file is the human's durable primer after -// running `ctx init`: the preamble names the contract so future -// readers know which directory rule is load-bearing; the activation -// hint comes next because every subsequent `ctx ` -// requires CTX_DIR to be declared. Best-effort: failures are -// non-fatal since the activation hint and next-steps were already -// printed to stdout. +// is and how the project-root contract works), next-steps, and +// workflow-tips text to GETTING_STARTED.md in the project root. +// The file is the human's durable primer after running `ctx init`: +// the preamble names the contract so future readers know which +// directory rule is load-bearing. Best-effort: failures are +// non-fatal since the next-steps were already printed to stdout. // // Parameters: // - cmd: Cobra command for status output. // - contextDir: Absolute path of the just-created .context/ -// directory, used in the activation hint. +// directory (currently unused; reserved for future hints). func WriteGettingStarted(cmd *cobra.Command, contextDir string) { - activateHint := fmt.Sprintf( - desc.Text(text.DescKeyWriteInitActivateHint), - contextDir, - ) + _ = contextDir content := desc.Text(text.DescKeyWriteInitAnatomyPreamble) + - token.NewlineLF + - activateHint + token.NewlineLF + desc.Text(text.DescKeyWriteInitNextStepsBlock) + token.NewlineLF + diff --git a/internal/cli/initialize/init_test.go b/internal/cli/initialize/init_test.go index f20089614..960beca2d 100644 --- a/internal/cli/initialize/init_test.go +++ b/internal/cli/initialize/init_test.go @@ -35,7 +35,6 @@ func TestInitCommand(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -81,7 +80,6 @@ func TestInitCreatesSteeringHooksSkillsDirs(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -116,7 +114,6 @@ func TestInitSkipsExistingSteeringHooksSkillsDirs(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) // Pre-create the directories with a marker file inside each. @@ -167,7 +164,6 @@ func TestInitMergeInsertsAfterH1(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) existingContent := "# My Amazing Project\n\n" + @@ -221,7 +217,6 @@ func TestInitMergeInsertsAtTopWhenNoH1(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) existingContent := "## Build Instructions\n\nRun make build.\n\n" + @@ -271,7 +266,6 @@ func TestInitCreatesPermissions(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -330,7 +324,6 @@ func TestInitMergesPermissions(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) if err = os.MkdirAll(".claude", 0750); err != nil { @@ -401,7 +394,6 @@ func TestInitWithExistingClaudeMdWithCtxMarker(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) existingContent := "# My Project\n\n" + @@ -466,7 +458,6 @@ func TestRunInit_Minimal(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -506,7 +497,6 @@ func TestRunInit_RefuseWhenPopulated(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -564,7 +554,6 @@ func TestRunInit_ResetRequiresInteractive(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -601,7 +590,6 @@ func TestRunInit_ResetWithConfirmationBacksUp(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -671,7 +659,6 @@ func TestRunInit_ResetDeclinedNoChanges(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -722,7 +709,6 @@ func TestRunInit_Merge(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) mdContent := "# My Project\n\nExisting.\n" @@ -759,7 +745,6 @@ func TestInitScaffoldsFoundationSteeringFiles(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -806,7 +791,6 @@ func TestInitNoSteeringInitFlagSkipsScaffold(t *testing.T) { } defer func() { _ = os.Chdir(origDir) }() t.Setenv("HOME", tmpDir) - t.Setenv(env.CtxDir, filepath.Join(tmpDir, ".context")) t.Setenv(env.SkipPathCheck, env.True) cmd := Cmd() @@ -831,3 +815,10 @@ func TestInitNoSteeringInitFlagSkipsScaffold(t *testing.T) { ) } } + +// Note: the former TestRunInit_EnvCwdMismatch_* tests have been +// removed. Under the cwd-anchored resolution model +// (spec: specs/cwd-anchored-context.md) init always targets +// `$PWD/.context/`; the env-vs-cwd mismatch class can no longer +// occur. The mismatch guard (envmatch package and ErrEnvCwdMismatch +// sentinel) was deleted in the same change. diff --git a/internal/cli/kb/core/path/path_test.go b/internal/cli/kb/core/path/path_test.go index bbc5de7d0..97e819179 100644 --- a/internal/cli/kb/core/path/path_test.go +++ b/internal/cli/kb/core/path/path_test.go @@ -16,8 +16,9 @@ import ( cfgKB "github.com/ActiveMemory/ctx/internal/config/kb" ) -// canonicalCtxDir builds a CTX_DIR honoring the rc-required -// `.context` basename and points the env var at it. +// canonicalCtxDir creates $TMP/.context, chdirs into $TMP so the +// cwd-anchored resolver finds it, and returns the absolute ctx-dir +// path. func canonicalCtxDir(t *testing.T) string { t.Helper() root := t.TempDir() @@ -25,7 +26,7 @@ func canonicalCtxDir(t *testing.T) string { if err := os.MkdirAll(ctxDir, 0o755); err != nil { t.Fatalf("setup: %v", err) } - t.Setenv("CTX_DIR", ctxDir) + t.Chdir(root) return ctxDir } diff --git a/internal/cli/mcp/cmd/root/cmd_test.go b/internal/cli/mcp/cmd/root/cmd_test.go index 1c7e698dd..0694bc912 100644 --- a/internal/cli/mcp/cmd/root/cmd_test.go +++ b/internal/cli/mcp/cmd/root/cmd_test.go @@ -11,17 +11,16 @@ import ( "github.com/spf13/cobra" - "github.com/ActiveMemory/ctx/internal/config/env" "github.com/ActiveMemory/ctx/internal/rc" ) -// TestMcpServe_FailsClosedOnUnsetCTXDIR is the regression guard -// required by spec/single-source-context-anchor.md. The MCP serve -// path must route through rc.RequireContextDir; with CTX_DIR -// unset, the cobra Run should return an error rather than starting -// a server bound to an empty path. -func TestMcpServe_FailsClosedOnUnsetCTXDIR(t *testing.T) { - t.Setenv(env.CtxDir, "") +// TestMcpServe_FailsClosedWhenNoDotContext is the regression guard +// for the cwd-anchored model (specs/cwd-anchored-context.md). The +// MCP serve path must route through rc.RequireContextDir; when +// $PWD/.context/ is absent, the cobra Run should return an error +// rather than starting a server bound to an empty path. +func TestMcpServe_FailsClosedWhenNoDotContext(t *testing.T) { + t.Chdir(t.TempDir()) rc.Reset() t.Cleanup(rc.Reset) @@ -30,6 +29,6 @@ func TestMcpServe_FailsClosedOnUnsetCTXDIR(t *testing.T) { err := Cmd(c, nil) if err == nil { - t.Fatal("Cmd() err = nil, want non-nil when CTX_DIR is unset") + t.Fatal("Cmd() err = nil, want non-nil when $PWD has no .context/") } } diff --git a/internal/cli/notify/notify_test.go b/internal/cli/notify/notify_test.go index 2b35ffa82..5a9d8feda 100644 --- a/internal/cli/notify/notify_test.go +++ b/internal/cli/notify/notify_test.go @@ -33,8 +33,6 @@ func setupCLITest(t *testing.T) (string, func()) { p := filepath.Join(ctxPath, f) _ = os.WriteFile(p, []byte("# "+f+"\n"), 0o600) } - // Declare context dir explicitly (explicit-context-dir model). - t.Setenv("CTX_DIR", ctxPath) rc.Reset() return tempDir, func() { _ = os.Chdir(origDir) diff --git a/internal/cli/pad/pad_test.go b/internal/cli/pad/pad_test.go index c696f3211..e345a56cd 100644 --- a/internal/cli/pad/pad_test.go +++ b/internal/cli/pad/pad_test.go @@ -21,7 +21,6 @@ import ( "github.com/ActiveMemory/ctx/internal/cli/pad/core/store" "github.com/ActiveMemory/ctx/internal/cli/pad/core/validate" "github.com/ActiveMemory/ctx/internal/config/dir" - "github.com/ActiveMemory/ctx/internal/config/env" "github.com/ActiveMemory/ctx/internal/config/pad" errPad "github.com/ActiveMemory/ctx/internal/err/pad" "github.com/spf13/cobra" @@ -502,7 +501,6 @@ func TestNoKey_EncryptedFileExists(t *testing.T) { }) ctxDir := filepath.Join(tmpDir, dir.Context) - t.Setenv(env.CtxDir, ctxDir) rc.Reset() if err := os.MkdirAll(ctxDir, 0750); err != nil { @@ -862,7 +860,6 @@ func TestEnsureKey_EncFileExistsNoKey(t *testing.T) { }) ctxDir := filepath.Join(tmpDir, dir.Context) - t.Setenv(env.CtxDir, ctxDir) rc.Reset() if err := os.MkdirAll(ctxDir, 0750); err != nil { @@ -897,7 +894,6 @@ func TestEnsureKey_GeneratesNewKey(t *testing.T) { }) ctxDir := filepath.Join(tmpDir, dir.Context) - t.Setenv(env.CtxDir, ctxDir) rc.Reset() if err := os.MkdirAll(ctxDir, 0750); err != nil { diff --git a/internal/cli/pause/pause_test.go b/internal/cli/pause/pause_test.go index 1e2d481c3..d49fd0843 100644 --- a/internal/cli/pause/pause_test.go +++ b/internal/cli/pause/pause_test.go @@ -20,7 +20,8 @@ import ( func setupStateDir(t *testing.T) string { t.Helper() - ctxDir := filepath.Join(t.TempDir(), dir.Context) + root := t.TempDir() + ctxDir := filepath.Join(root, dir.Context) if mkErr := os.MkdirAll(ctxDir, 0o750); mkErr != nil { t.Fatal(mkErr) } @@ -35,7 +36,7 @@ func setupStateDir(t *testing.T) string { t.Fatalf("seed required file %s: %v", f, wrErr) } } - t.Setenv("CTX_DIR", ctxDir) + t.Chdir(root) rc.Reset() stateDir := filepath.Join(ctxDir, dir.State) if mkErr := os.MkdirAll(stateDir, 0o750); mkErr != nil { diff --git a/internal/cli/remind/remind_test.go b/internal/cli/remind/remind_test.go index f11bb111b..bd2586d20 100644 --- a/internal/cli/remind/remind_test.go +++ b/internal/cli/remind/remind_test.go @@ -15,7 +15,6 @@ import ( "testing" "github.com/ActiveMemory/ctx/internal/config/dir" - "github.com/ActiveMemory/ctx/internal/config/env" "github.com/spf13/cobra" "github.com/ActiveMemory/ctx/internal/cli/remind/core/store" @@ -40,7 +39,6 @@ func setup(t *testing.T) string { if err := os.MkdirAll(ctxDir, 0750); err != nil { t.Fatal(err) } - t.Setenv(env.CtxDir, ctxDir) rc.Reset() return tmpDir diff --git a/internal/cli/resume/resume_test.go b/internal/cli/resume/resume_test.go index 55a81b58e..a5ae5958b 100644 --- a/internal/cli/resume/resume_test.go +++ b/internal/cli/resume/resume_test.go @@ -21,7 +21,8 @@ import ( func setupStateDir(t *testing.T) string { t.Helper() - ctxDir := filepath.Join(t.TempDir(), dir.Context) + root := t.TempDir() + ctxDir := filepath.Join(root, dir.Context) if mkErr := os.MkdirAll(ctxDir, 0o750); mkErr != nil { t.Fatal(mkErr) } @@ -34,7 +35,7 @@ func setupStateDir(t *testing.T) string { t.Fatalf("seed required file %s: %v", f, wrErr) } } - t.Setenv("CTX_DIR", ctxDir) + t.Chdir(root) rc.Reset() stateDir := filepath.Join(ctxDir, dir.State) if mkErr := os.MkdirAll(stateDir, 0o750); mkErr != nil { diff --git a/internal/cli/serve/serve_test.go b/internal/cli/serve/serve_test.go index a3952dfae..3ac12b528 100644 --- a/internal/cli/serve/serve_test.go +++ b/internal/cli/serve/serve_test.go @@ -130,18 +130,18 @@ func TestRunServe_ZensicalNotFound(t *testing.T) { func TestRunServe_DefaultDir(t *testing.T) { // When no args are given, serveRoot.Run uses the default - // journal-site directory under the declared context dir. + // journal-site directory under the cwd-anchored context dir. // - // Declare CTX_DIR to point at a nonexistent .context/ so the - // default journal-site path resolves to something that does - // not exist on disk and the run errors with "directory not - // found" rather than the "context directory not declared" - // message. + // Create a real .context/ directory in tempDir and chdir + // there so the resolver succeeds, but leave the journal-site + // subdir absent so the run errors with "directory not found". tempDir := t.TempDir() - origDir, _ := os.Getwd() - _ = os.Chdir(tempDir) - defer func() { _ = os.Chdir(origDir) }() - t.Setenv("CTX_DIR", filepath.Join(tempDir, ".context")) + if mkErr := os.MkdirAll( + filepath.Join(tempDir, ".context"), 0o700, + ); mkErr != nil { + t.Fatalf("setup: %v", mkErr) + } + t.Chdir(tempDir) rc.Reset() t.Cleanup(rc.Reset) diff --git a/internal/cli/setup/core/opencode/mcp.go b/internal/cli/setup/core/opencode/mcp.go index 2d07023c2..f7e2fd4d3 100644 --- a/internal/cli/setup/core/opencode/mcp.go +++ b/internal/cli/setup/core/opencode/mcp.go @@ -9,20 +9,15 @@ package opencode import ( "bytes" "encoding/json" - "fmt" "os" "os/exec" "path/filepath" - "strings" "github.com/spf13/cobra" - cfgDir "github.com/ActiveMemory/ctx/internal/config/dir" - "github.com/ActiveMemory/ctx/internal/config/env" "github.com/ActiveMemory/ctx/internal/config/fs" cfgHook "github.com/ActiveMemory/ctx/internal/config/hook" mcpServer "github.com/ActiveMemory/ctx/internal/config/mcp/server" - cfgShell "github.com/ActiveMemory/ctx/internal/config/shell" "github.com/ActiveMemory/ctx/internal/config/token" errFs "github.com/ActiveMemory/ctx/internal/err/fs" errSetup "github.com/ActiveMemory/ctx/internal/err/setup" @@ -33,14 +28,15 @@ import ( // launchCommand returns the OpenCode `command` array for the ctx // MCP server. The emitted argv is: // -// ["sh", "-c", `exec env CTX_DIR="$PWD/.context" /abs/path/to/ctx mcp serve`] +// [/abs/path/to/ctx, mcp, serve] // -// The binary path is resolved to an absolute path at setup time via -// exec.LookPath, so that OpenCode can spawn the MCP child regardless -// of the PATH inherited by non-interactive shells. `$PWD` is set by -// sh to the CWD OpenCode chose when spawning the MCP child. `exec` -// replaces the shell so the MCP child becomes ctx itself, with no -// sh process layered between OpenCode and the JSON-RPC stream. +// Under the cwd-anchored resolution model +// (spec: specs/cwd-anchored-context.md) ctx anchors to its +// working directory, so the OpenCode-chosen CWD is sufficient +// project context; no env-var wrapper or shell layer is needed. +// The binary path is resolved to an absolute path at setup time +// via exec.LookPath so OpenCode can spawn the MCP child regardless +// of the PATH inherited by non-interactive shells. // // Returns: // - []string: argv suitable for OpenCode's McpLocalConfig.command. @@ -51,32 +47,7 @@ func launchCommand() []string { bin = abs } } - binAndArgs := append([]string{bin}, mcpServer.Args()...) - quoted := make([]string, 0, len(binAndArgs)) - for _, arg := range binAndArgs { - quoted = append(quoted, posixShellQuote(arg)) - } - script := fmt.Sprintf( - cfgShell.FormatPOSIXSpawnRelativeCtxDir, - env.CtxDir, cfgDir.Context, - strings.Join(quoted, token.Space), - ) - return []string{cfgShell.Sh, cfgShell.CmdFlag, script} -} - -// posixShellQuote wraps s in single quotes, escaping embedded single -// quotes using the canonical close-escape-reopen POSIX pattern so the -// resulting token is safe to embed in a `sh -c` script. -// -// Parameters: -// - s: raw argv token to quote for POSIX shell evaluation -// -// Returns: -// - string: single-quoted, escape-safe shell token -func posixShellQuote(s string) string { - return cfgShell.SingleQuote + - strings.ReplaceAll(s, cfgShell.SingleQuote, cfgShell.SingleQuoteEscaped) + - cfgShell.SingleQuote + return append([]string{bin}, mcpServer.Args()...) } // globalConfigPath returns the path to the OpenCode global config diff --git a/internal/cli/setup/core/opencode/mcp_test.go b/internal/cli/setup/core/opencode/mcp_test.go index 13888329b..a4c784afd 100644 --- a/internal/cli/setup/core/opencode/mcp_test.go +++ b/internal/cli/setup/core/opencode/mcp_test.go @@ -71,35 +71,31 @@ func TestEnsureMCPConfig_CreatesFile(t *testing.T) { if !ok { t.Fatalf("command must be an array per OpenCode schema, got %T", ctxServer["command"]) } + // Under the cwd-anchored resolution model the emitted argv is + // the direct [/abs/path/to/ctx, mcp, serve] form — no shell + // wrapper, no env-var injection. OpenCode picks the CWD and + // ctx anchors to it. if got := len(cmdArr); got != 3 { - t.Fatalf("command length = %d, want 3 (sh -c - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- - - - - - - - - - - -
- - - - - - -

Activating a Context Directory

- -

ctx

-

The Problem

-

You ran a ctx command and got:

-
Error: no context directory specified for this project
-
-

This means ctx doesn't know which .context/ directory to operate -on. It will not guess, and it will not walk up from your current -working directory looking for one; that behavior was removed -deliberately, because silent inference was the source of several -bugs (stray agent-created directories, cross-project bleed-through, -webhook-route misrouting, sub-agent fragmentation). Every ctx -command requires you to declare the target directory explicitly.

-

This page shows you the three ways to do that and when to use each.

-

TL;DR

-

If the project has already been initialized and you just need to -bind it for your shell:

-
eval "$(ctx activate)"
-
-

That's 95% of the time. Add it to .zshrc / .bashrc per project -with direnv, or run it once per terminal.

-

When You See the Error

-

The exact error message depends on how many .context/ directories -are visible from the current directory:

-

Zero Candidates

-
Error: no context directory specified for this project
-
-

Either you haven't initialized this project yet (run ctx init) -or you're in a directory that doesn't belong to a ctx-tracked -project. If you know the project lives elsewhere, use one of the -declaration methods below with its absolute path.

-

One Candidate

-
Error: no context directory specified; a likely candidate is at
-    /Users/you/repos/myproject/.context
-
-

ctx found a single .context/ on the way up from here but won't -bind to it automatically. Run eval "$(ctx activate)" and ctx -will emit the export for the candidate. Or set CTX_DIR by hand.

-

Multiple Candidates

-
Error: no context directory specified; multiple candidates visible:
-  /Users/you/repos/myproject/.context
-  /Users/you/repos/myproject/packages/web/.context
-
-

You're inside nested projects. Pick the one you mean:

-
ctx activate /Users/you/repos/myproject/.context
-# …copy and paste the `export` line it prints, or wrap in eval:
-eval "$(ctx activate /Users/you/repos/myproject/.context)"
-
-

Three Ways to Declare

- -

ctx activate emits a shell-native export CTX_DIR=... line to -stdout. Wrap it in eval and the binding takes effect for the -current shell:

-
# Walk up from current dir and bind the single visible candidate:
-eval "$(ctx activate)"
-
-# Bind a specific path explicitly:
-eval "$(ctx activate /abs/path/to/.context)"
-
-# Clear the binding:
-eval "$(ctx deactivate)"
-
-

ctx activate validates paths strictly: the target must exist, be -a directory, and contain at least one canonical context file -(CONSTITUTION.md or TASKS.md). It refuses to emit for multiple -upward candidates; pick one explicitly in that case.

-

Under the hood, the emitted line is just:

-
export CTX_DIR='/abs/path/to/.context'
-
-

So you can copy it into your .zshrc / .bashrc if you want the -binding permanent for a given shell setup. Better: use -direnv with a per-project .envrc.

-

2. CTX_DIR Env Var

-

If you already know the path, export it directly:

-
export CTX_DIR=/abs/path/to/.context
-ctx status
-
-

CTX_DIR is the same variable ctx activate writes; activate -is just a convenience that figures out the path for you.

-

3. Inline One-Shot

-

For one-shot commands (CI jobs, scripts, debugging a specific -project without changing your shell state), prefix the binding -inline:

-
CTX_DIR=/abs/path/to/.context ctx status
-
-

This binds CTX_DIR for that invocation only.

-

CTX_DIR must be an absolute path with .context as its basename. -Relative paths and other names are rejected on first use; the -basename guard catches the common footgun -(export CTX_DIR=$(pwd)) before stray writes can leak to the -project root.

-

For CI and Scripts

-

Do not rely on shell activation in automated flows. Set CTX_DIR -explicitly at the top of the script:

-
#!/usr/bin/env bash
-set -euo pipefail
-
-export CTX_DIR="$GITHUB_WORKSPACE/.context"
-ctx status
-ctx drift
-
-

For Claude Code Users

-

The ctx plugin's hooks are generated with -CTX_DIR="$CLAUDE_PROJECT_DIR/.context" prefixed to each command, -so hook-driven ctx invocations resolve correctly without any -per-session setup. You only need to activate manually when running -ctx yourself in a terminal.

-

One Project, One .context/

-

The context directory is not a free-floating bag of files. It is -pinned to a project by contract: filepath.Dir(ContextDir()) is -the project root. That parent directory is what ctx sync, -ctx drift, and the memory-drift hook scan for code, secret files, -and MEMORY.md respectively.

-

The practical consequences:

-
    -
  • Don't share one .context/ across multiple projects. It holds - per-project journals, per-session state, and per-project secrets. - Pointing two codebases at the same directory corrupts all three.
  • -
  • If you want to share knowledge (CONSTITUTION, CONVENTIONS, - ARCHITECTURE) across projects, use ctx hub. It cherry-picks - entries at the right granularity and keeps the per-project bits - where they belong.
  • -
  • The CTX_DIR you activate is implicitly a project-root - declaration. Setting CTX_DIR=/weird/place/.context means - you're telling ctx the project root is /weird/place/. That's - your call to make; ctx does not police it.
  • -
- -
~/WORKSPACE/my-to-do-list
-  ├── .git
-  ├── .context          ← owned by this project; do not share
-  ├── ideas
-  │   └── ...
-  ├── Makefile
-  ├── Makefile.ctx
-  └── specs
-      └── ...
-
-

.context/ sits at the project root, next to .git. ctx activate -binds to it; every ctx subsystem reads the project from its parent.

-

Why Not Walk Up Automatically?

-

Nested projects, submodules, rogue agent-created .context/ -directories, and sub-agent sessions all produced silent misrouting -under a walk-up model. For consistency, ctx does not guess, -and requires the caller to declare. Every other decision flows from -there.

- - - - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - \ No newline at end of file diff --git a/site/recipes/autonomous-loops/index.html b/site/recipes/autonomous-loops/index.html index c8c3e411e..303c6b093 100644 --- a/site/recipes/autonomous-loops/index.html +++ b/site/recipes/autonomous-loops/index.html @@ -724,7 +724,7 @@ - + @@ -2377,23 +2377,20 @@

The Problemfirst-class deliverable, not an afterthought.

TL;DR

ctx init                                    # 1. init context
-eval "$(ctx activate)"                      # 2. bind CTX_DIR for this shell
-# Edit TASKS.md with phased work items
-ctx loop --tool claude --max-iterations 10  # 3. generate loop.sh
-./loop.sh 2>&1 | tee /tmp/loop.log &        # 4. run the loop
-ctx watch --log /tmp/loop.log               # 5. process context updates
-# Next morning:
-ctx status && ctx load                      # 6. review the results
+# Edit TASKS.md with phased work items
+ctx loop --tool claude --max-iterations 10  # 2. generate loop.sh
+./loop.sh 2>&1 | tee /tmp/loop.log &        # 3. run the loop
+ctx watch --log /tmp/loop.log               # 4. process context updates
+# Next morning:
+ctx status && ctx load                      # 5. review the results
 
-

Activate, or Set CTX_DIR Inline for Unattended Runs

-

eval "$(ctx activate)" is fine for an interactive terminal. -For an overnight unattended loop, put the binding at the top -of loop.sh instead (export CTX_DIR=/abs/path/.context) so -the loop doesn't depend on a live shell. If you skip both, -ctx loop, ctx watch, ctx status, and ctx load fail -with Error: no context directory specified. See -Activating a Context Directory.

+

Run From the Project Root

+

ctx reads $PWD/.context/. Both the interactive +ctx loop invocation and the generated loop.sh must run +from the project root. If loop.sh is scheduled by a +supervisor that does not preserve cwd, add cd +/abs/path/to/project at the top of the script.

Read on for permissions, isolation, and completion signals.

Commands and Skills Used

diff --git a/site/recipes/building-skills/index.html b/site/recipes/building-skills/index.html index 96c1a91ef..fda77755f 100644 --- a/site/recipes/building-skills/index.html +++ b/site/recipes/building-skills/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/claude-code-permissions/index.html b/site/recipes/claude-code-permissions/index.html index b7bea1fa5..4a18c5f27 100644 --- a/site/recipes/claude-code-permissions/index.html +++ b/site/recipes/claude-code-permissions/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/configuration-profiles/index.html b/site/recipes/configuration-profiles/index.html index 51573f625..fbc6755ea 100644 --- a/site/recipes/configuration-profiles/index.html +++ b/site/recipes/configuration-profiles/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/context-health/index.html b/site/recipes/context-health/index.html index 90bfcb6a2..97363ecc2 100644 --- a/site/recipes/context-health/index.html +++ b/site/recipes/context-health/index.html @@ -724,7 +724,7 @@ - + @@ -2304,13 +2304,6 @@

TL;DR&pa ctx status # verify

Or just ask your agent: "Is our context clean?"

-
-

Activate the Project First

-

Run eval "$(ctx activate)" once per terminal in the project -root. If you skip it, every command above fails with -Error: no context directory specified. See -Activating a Context Directory.

-

Commands and Skills Used

diff --git a/site/recipes/customizing-hook-messages/index.html b/site/recipes/customizing-hook-messages/index.html index 9e5451422..1f1e925f3 100644 --- a/site/recipes/customizing-hook-messages/index.html +++ b/site/recipes/customizing-hook-messages/index.html @@ -724,7 +724,7 @@ - + @@ -2108,15 +2108,6 @@

TL;DR&pa ctx hook message edit qa-reminder gate # copy default to .context/ for editing ctx hook message reset qa-reminder gate # revert to embedded default -
-

Activate the Project First

-

Run eval "$(ctx activate)" once per terminal in the project -root: hook message overrides live in your .context/ -directory, so ctx needs to know which one. If you skip the -eval, ctx hook message ... fails with Error: no context -directory specified. See -Activating a Context Directory.

-

Commands Used

diff --git a/site/recipes/design-before-coding/index.html b/site/recipes/design-before-coding/index.html index 96811e3b0..8f7df0bdd 100644 --- a/site/recipes/design-before-coding/index.html +++ b/site/recipes/design-before-coding/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/external-context/index.html b/site/recipes/external-context/index.html deleted file mode 100644 index 0a1c572d3..000000000 --- a/site/recipes/external-context/index.html +++ /dev/null @@ -1,2710 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Keeping Context in a Separate Repo - ctx: do you remember? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- - - - - - - - - - - -
- - - - - - -

Keeping Context in a Separate Repo

- -

ctx

-

The Problem

-

ctx files contain project-specific decisions, learnings, -conventions, and tasks. By default, they live in -.context/ inside the project tree, and that works well when the context -can be public.

-

But sometimes you need the context outside the project:

-
    -
  • Open-source projects with private context: Your architectural notes, - internal task lists, and scratchpad entries shouldn't ship with the public - repo.
  • -
  • Compliance or IP concerns: Context files reference sensitive design - rationale that belongs in a separate access-controlled repository.
  • -
  • Personal preference: You want to keep notes separate from code.
  • -
-

ctx supports this by letting you point CTX_DIR anywhere. This recipe -shows how to set that up and how to tell your AI assistant where to find the -context.

-
-

One .context/ per project

-

The parent of the context directory is the project root by contract. -ctx sync, ctx drift, and the memory-drift hook all read the -codebase at filepath.Dir(ContextDir()). Pointing two projects at -the same directory corrupts their journals, state, and secrets. To -share knowledge (CONSTITUTION / CONVENTIONS / ARCHITECTURE) across -projects, use ctx hub, not a shared .context/.

-
-

TL;DR

-

Create the external context directory, initialize it, and bind it:

-
mkdir -p ~/repos/myproject-context && cd ~/repos/myproject-context && git init
-cd ~/repos/myproject
-
-# Bind CTX_DIR to the external location, then init creates files there.
-export CTX_DIR=~/repos/myproject-context/.context
-ctx init
-
-

All ctx commands now use the external directory. If you share the -setup across shells, add the export CTX_DIR=... line to your -shell rc, or source a per-project .envrc with direnv.

-

What Works, What Quietly Degrades

-

The single-source-anchor contract states that -filepath.Dir(CTX_DIR) is the project root. When the context -lives outside the project tree, ctx still resolves correctly for -every operation that reads or writes inside .context/. But any -operation that scans the codebase scans the wrong tree, and -does so silently:

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OperationBehavior with external .context/
ctx status, agent, add✅ Works. Operates on files inside CTX_DIR.
Journal, scratchpad, hub✅ Works. Same reason.
ctx sync⚠️ Scans the context repo, not the code repo.
ctx drift⚠️ Same. Reports nothing useful.
Memory-drift hook (MEMORY.md)⚠️ Looks for MEMORY.md next to the external .context/, not the code.
-

Nothing errors. The code-aware operations just find an empty or -unrelated tree where the project root should be.

- -

If you want both the privacy of an external git repo and working -ctx sync / drift / memory-drift, symlink the external -.context/ into the code repo and point CTX_DIR at the symlink:

-
# External repo holds the real files
-mkdir -p ~/repos/myproject-context && cd ~/repos/myproject-context && git init
-
-# Symlink it into the code repo
-ln -s ~/repos/myproject-context/.context ~/repos/myproject/.context
-
-# Bind CTX_DIR to the symlink path; ctx init will follow it
-export CTX_DIR=~/repos/myproject/.context
-ctx init
-
-

Now filepath.Dir(CTX_DIR) is the code repo, so code-aware -operations scan the right tree. The actual files still live in -the external repo and commit there. Add .context to the code -repo's .gitignore (or .git/info/exclude) so the symlink itself -isn't tracked by the code repo.

-

The basename guard is permissive about symlinks: it checks the -declared name, not the resolved target, so a .context symlink -pointing anywhere is accepted as long as the declared basename is -.context.

-

Commands and Skills Used

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ToolTypePurpose
ctx initCLI commandInitialize context directory
ctx activateCLI commandEmit export CTX_DIR=... for the shell
CTX_DIREnv variableDeclare context directory per-session
.ctxrcConfig filePer-project configuration
/ctx-statusSkillVerify context is loading correctly
-

The Workflow

-

Step 1: Create the Private Context Repo

-

Create a separate repository for your context files. This can live anywhere: -a private GitHub repo, a shared drive, a sibling directory:

-
# Create the context repo
-mkdir -p ~/repos/myproject-context
-cd ~/repos/myproject-context
-git init
-
-

Step 2: Initialize ctx Pointing at It

-

From your project root, declare CTX_DIR pointing to the external -location, then initialize:

-
cd ~/repos/myproject
-CTX_DIR=~/repos/myproject-context/.context ctx init
-
-

This creates the canonical .context/ file set inside -~/repos/myproject-context/ instead of ~/repos/myproject/.context/.

-

Step 3: Make It Stick

-

Declaring CTX_DIR on every command is tedious. Pick one of these -methods to make the configuration permanent. The context directory -itself must be declared via CTX_DIR; .ctxrc does not carry the -path.

- -
# Direct path. Works for ctx status / agent / add but degrades
-# code-aware operations. See "What Works, What Quietly Degrades".
-export CTX_DIR=~/repos/myproject-context/.context
-
-# Or, with the symlink approach above, point at the symlink path
-# inside the code repo so code-aware operations stay healthy.
-export CTX_DIR=~/repos/myproject/.context
-
-

Put either form in your shell profile (~/.bashrc, ~/.zshrc) -or a direnv .envrc.

-

For a single session, run eval "$(ctx activate)" from any -directory inside the project where exactly one .context/ -candidate is visible (the symlink counts). activate does not -accept a path argument; bind a specific path by exporting -CTX_DIR directly instead.

-

Option B: .ctxrc for Other Settings

-

Put any settings (token budget, priority order, freshness files) in a -.ctxrc at the project root (dirname(CTX_DIR)), which here is the -parent of the external .context/:

-
# ~/repos/myproject-context/.ctxrc
-token_budget: 16000
-
-

.ctxrc is always read from the parent of CTX_DIR, so this file is -picked up whenever CTX_DIR points at -~/repos/myproject-context/.context.

-

Resolution

-

ctx reads the context directory from a single channel: the -CTX_DIR environment variable. When CTX_DIR is unset, ctx -errors with a "no context directory specified" hint pointing at -ctx activate and this recipe. When set, the value must be an -absolute path with .context as its basename; relative paths and -other names are rejected on first use.

-

See -Activating a Context Directory for the full -recipe.

-

Step 4: Agent Auto-Discovery via Bootstrap

-

When context lives outside the project tree, your AI assistant needs to know -where to find it. The ctx system bootstrap command resolves the configured -context directory and communicates it to the agent automatically:

-
$ ctx system bootstrap
-ctx system bootstrap
-====================
-
-context_dir: /home/user/repos/myproject-context/.context
-
-Files:
-  CONSTITUTION.md, TASKS.md, DECISIONS.md, ...
-
-

The CLAUDE.md template generated by ctx init already instructs the agent to -run ctx system bootstrap at session start. Because CTX_DIR is inherited -by child processes, your agent picks up the external path automatically.

-

Here is the relevant section from CLAUDE.md for reference:

-
<!-- CLAUDE.md -->
-1. **Run `ctx system bootstrap`**: CRITICAL, not optional.
-   This tells you where the context directory is. If it returns any
-   error, relay the error output to the user verbatim, point them at
-   https://ctx.ist/recipes/activating-context/ for setup, and STOP.
-   Do not try to recover; the user decides.
-
-

Moreover, every nudge (context checkpoint, persistence reminder, etc.) also -includes a Context: /home/user/repos/myproject-context/.context footer, so -the agent remains anchored to the correct directory even in long sessions.

-

Export CTX_DIR in your shell profile so every hook process inherits it:

-
export CTX_DIR=~/repos/myproject-context/.context
-
-

Step 5: Share with Teammates

-

Teammates clone both repos and export CTX_DIR:

-
# Clone the project
-git clone git@github.com:org/myproject.git
-cd myproject
-
-# Clone the private context repo
-git clone git@github.com:org/myproject-context.git ~/repos/myproject-context
-export CTX_DIR=~/repos/myproject-context/.context
-
-

If teammates use different paths, each developer sets their own CTX_DIR.

-

For encryption key distribution across the team, see the -Syncing Scratchpad Notes recipe.

-

Step 6: Day-to-Day Sync

-

The external context repo has its own git history. Treat it like any other -repo: commit and push after sessions:

-
cd ~/repos/myproject-context
-
-# After a session
-git add -A
-git commit -m "Session: refactored auth module, added rate-limit learning"
-git push
-
-

Your AI assistant can do this too. When ending a session:

-
You: "Save what we learned and push the context repo."
-
-Agent: [runs ctx learning add, then commits and pushes the context repo]
-
-

You can also set up a post-session habit: project code gets committed to the -project repo, context gets committed to the context repo.

-
-

Conversational Approach

-

You don't need to remember the flags; simply ask your assistant:

-

Set Up Your System Using Natural Language

-
You: "Set up ctx to use ~/repos/myproject-context as the context directory."
-
-Agent: "I'll set CTX_DIR to that path, run ctx init to materialize
-       it, and show you the export line to add to your shell
-       profile. Want me to seed the core context files too?"
-
-

Configure Separate Repo for .context Folder Using Natural Language

-
You: "My context is in a separate repo. Can you load it?"
-
-Agent: [reads CTX_DIR, loads context from the external dir]
-       "Loaded. You have 3 pending tasks, last session was about the auth
-       refactor."
-
-
-

Tips

-
    -
  • Start simple. If you don't need external context yet, don't set it up. - The default .context/ in-tree is the easiest path. Move to an external - repo when you have a concrete reason.
  • -
  • One context repo per project. Sharing a single context directory across - multiple projects corrupts journals, state, and secrets. Use ctx hub for - cross-project knowledge sharing.
  • -
  • Export CTX_DIR in your shell profile so hooks and tools inherit the - path without per-command flags.
  • -
  • Commit both repos at session boundaries. Context without code history - (or code without context history) loses half the value.
  • -
-
-

Next Up

-

The Complete Session →: Walk through a -full ctx session from start to finish.

-

See Also

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - - - - - - - - - - - \ No newline at end of file diff --git a/site/recipes/guide-your-agent/index.html b/site/recipes/guide-your-agent/index.html index 37ff839ca..fd5735c9a 100644 --- a/site/recipes/guide-your-agent/index.html +++ b/site/recipes/guide-your-agent/index.html @@ -15,7 +15,7 @@ - + @@ -760,16 +760,14 @@
  • - + - - - Activating a Context Directory + none @@ -959,16 +957,14 @@
  • - + - - - Keeping Context in a Separate Repo + none @@ -1788,7 +1784,7 @@
  • - + Getting Started @@ -1899,7 +1895,7 @@

    See Also - + diff --git a/site/recipes/hook-output-patterns/index.html b/site/recipes/hook-output-patterns/index.html index 4afd29968..66abf1a23 100644 --- a/site/recipes/hook-output-patterns/index.html +++ b/site/recipes/hook-output-patterns/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/hub-cluster/index.html b/site/recipes/hub-cluster/index.html index 679e733cb..805ac1e36 100644 --- a/site/recipes/hub-cluster/index.html +++ b/site/recipes/hub-cluster/index.html @@ -724,7 +724,7 @@ - + @@ -2058,10 +2058,9 @@

    Step 4: Register Clients wi project first.

    When registering a client, give it the full peer list:

    # In the project directory on the client:
    -eval "$(ctx activate)"
    -ctx connection register hub-a.lan:9900 \
    -  --token ctx_adm_... \
    -  --peers hub-b.lan:9900,hub-c.lan:9900
    +ctx connection register hub-a.lan:9900 \
    +  --token ctx_adm_... \
    +  --peers hub-b.lan:9900,hub-c.lan:9900
     

    If the leader becomes unreachable, the client reconnects to the next peer. Followers redirect to the current leader, so writes diff --git a/site/recipes/hub-getting-started/index.html b/site/recipes/hub-getting-started/index.html index 0a3189d88..e5b3ca0d3 100644 --- a/site/recipes/hub-getting-started/index.html +++ b/site/recipes/hub-getting-started/index.html @@ -724,7 +724,7 @@ - + @@ -2106,8 +2106,7 @@

    Step 2: Register the First Project.context/.connect.enc, so you have to tell ctx which project first.

    cd ~/projects/alpha
    -eval "$(ctx activate)"
    -ctx connection register localhost:9900 --token ctx_adm_7f3a1c2d...
    +ctx connection register localhost:9900 --token ctx_adm_7f3a1c2d...
     

    This stores an encrypted connection config in .context/.connect.enc. The admin token is exchanged for a @@ -2130,10 +2129,9 @@

    Step 4: Publish a DecisionStep 5: Register a Second Project and Sync

    cd ~/projects/beta
    -eval "$(ctx activate)"   # bind CTX_DIR for this project
    -ctx connection register localhost:9900 --token ctx_adm_7f3a1c2d...
    -ctx connection subscribe decision learning convention
    -ctx connection sync
    +ctx connection register localhost:9900 --token ctx_adm_7f3a1c2d...
    +ctx connection subscribe decision learning convention
    +ctx connection sync
     

    The decision from alpha now appears in ~/projects/beta/.context/hub/decisions.md with an origin tag diff --git a/site/recipes/hub-multi-machine/index.html b/site/recipes/hub-multi-machine/index.html index 40b737e0f..39597e3ce 100644 --- a/site/recipes/hub-multi-machine/index.html +++ b/site/recipes/hub-multi-machine/index.html @@ -724,7 +724,7 @@ - + @@ -2053,15 +2053,13 @@

    Step 4: Register Project ctx which project first.

    On workstation A:

    cd ~/projects/x
    -eval "$(ctx activate)"
    -ctx connection register nexus.local:9900 --token ctx_adm_...
    -ctx connection subscribe decision learning convention
    +ctx connection register nexus.local:9900 --token ctx_adm_...
    +ctx connection subscribe decision learning convention
     

    On workstation B:

    cd ~/projects/y
    -eval "$(ctx activate)"
    -ctx connection register nexus.local:9900 --token ctx_adm_...
    -ctx connection subscribe decision learning convention
    +ctx connection register nexus.local:9900 --token ctx_adm_...
    +ctx connection subscribe decision learning convention
     

    Each registration exchanges the admin token for a per-project client token. Only the client token is persisted in diff --git a/site/recipes/hub-overview/index.html b/site/recipes/hub-overview/index.html index 4647cc6c1..2d3c0c2b3 100644 --- a/site/recipes/hub-overview/index.html +++ b/site/recipes/hub-overview/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/hub-personal/index.html b/site/recipes/hub-personal/index.html index 96a6639f1..5ebd2d6ef 100644 --- a/site/recipes/hub-personal/index.html +++ b/site/recipes/hub-personal/index.html @@ -724,7 +724,7 @@ - + @@ -2047,17 +2047,6 @@

    Personal Cross-Project BrainGetting Started for the roughly five-minute setup). This recipe assumes the hub is already running and you've registered at least two projects.

    -
    -

    Activate Each Project First

    -

    Run eval "$(ctx activate)" after each cd <project> (or wire -it into direnv). The hub server (ctx hub start, etc.) runs on -the server and doesn't need this; the commands in this recipe -(ctx add --share, ctx agent --include-hub, -ctx connection ...) live inside a project and do. If you -skip the eval, they'll fail with Error: no context directory -specified. See -Activating a Context Directory.

    -

    The Core Loop

    Every day, the same three verbs matter:

      diff --git a/site/recipes/hub-team/index.html b/site/recipes/hub-team/index.html index 5e41f0a1f..dd132e2e9 100644 --- a/site/recipes/hub-team/index.html +++ b/site/recipes/hub-team/index.html @@ -724,7 +724,7 @@ - + @@ -1998,14 +1998,11 @@

      Team Knowledge Bus. The hub server - (ctx hub start, etc.) doesn't need this, but the - client side (ctx connection ..., ctx add --share) - lives in a project and does. If you skip activation, - those client commands fail with Error: no context - directory specified. See - Activating a Context Directory. +
    1. Client-side commands (ctx connection ..., + ctx add --share) must be run from each project's root — + ctx reads $PWD/.context/. The hub server + (ctx hub start, etc.) doesn't need this; it operates on + the hub data directory rather than a project.
    2. Trust Model: Read This First

      The hub assumes everyone holding a client token is diff --git a/site/recipes/import-plans/index.html b/site/recipes/import-plans/index.html index d03ff8dae..0861a813a 100644 --- a/site/recipes/import-plans/index.html +++ b/site/recipes/import-plans/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/index.html b/site/recipes/index.html index 63b39b508..df8b648b7 100644 --- a/site/recipes/index.html +++ b/site/recipes/index.html @@ -18,7 +18,7 @@ - + @@ -724,7 +724,7 @@ - + @@ -1498,17 +1498,6 @@ - - -

    3. - - - - Keeping Context in a Separate Repo - - - -
    4. @@ -2188,12 +2177,6 @@

      Multiling

      Uses: ctx journal source, ctx journal import, session_prefixes in .ctxrc


      -

      Keeping Context in a Separate Repo

      -

      Store context files outside the project tree: in a private repo, -shared directory, or anywhere else. Useful for open source projects -with private context or multi-repo setups.

      -

      Uses: ctx init, CTX_DIR, .ctxrc, /ctx-status

      -

      Knowledge Base (Phase KB)

      Build a Knowledge Base

      Stand up the editorial pipeline for knowledge-shaped work @@ -2572,13 +2555,13 @@

      ctx Hub: HA Clus - + -

      TL;DR

      /ctx-reflect               # surface items worth persisting
       /ctx-decision-add "Title"  # record with context/rationale/consequence
      diff --git a/site/recipes/memory-bridge/index.html b/site/recipes/memory-bridge/index.html
      index db0f7ac1b..a7b86f153 100644
      --- a/site/recipes/memory-bridge/index.html
      +++ b/site/recipes/memory-bridge/index.html
      @@ -724,7 +724,7 @@
         
         
         
      -    
      +    
             
         
         
      @@ -2249,13 +2249,6 @@ 

      TL;DR&pa

      The check-memory-drift hook nudges automatically when MEMORY.md changes - you don't need to remember to sync manually.

      -

      Commands and Skills Used

      diff --git a/site/recipes/multi-tool-setup/index.html b/site/recipes/multi-tool-setup/index.html index f56cb3423..a15f3cdee 100644 --- a/site/recipes/multi-tool-setup/index.html +++ b/site/recipes/multi-tool-setup/index.html @@ -760,16 +760,14 @@
    5. - + - - - Activating a Context Directory + none @@ -1138,17 +1136,6 @@ -
    6. - -
    7. - - - - Next Up - - - -
    8. @@ -1208,16 +1195,14 @@
    9. - + - - - Keeping Context in a Separate Repo + none @@ -2201,17 +2186,6 @@ -
    10. - -
    11. - - - - Next Up - - - -
    12. @@ -2286,7 +2260,7 @@
    13. - + Getting Started @@ -2327,32 +2301,24 @@

      The ProblemTL;DR

      cd your-project
       ctx init                      # creates .context/
      -eval "$(ctx activate)"        # bind CTX_DIR for this shell
      -source <(ctx completion zsh)  # shell completion (or bash/fish)
      -
      -# ## Claude Code (automatic after plugin install) ##
      -claude /plugin marketplace add ActiveMemory/ctx
      -claude /plugin install ctx@activememory-ctx
      -
      -# ## OpenCode ##
      -ctx setup opencode --write && ctx init && eval "$(ctx activate)"
      -
      -# ## Cursor / Aider / Copilot / Windsurf ##
      -ctx setup cursor # or: aider, copilot, windsurf
      -
      -# ## Companion tools (highly recommended) ##
      -npx gitnexus analyze          # code knowledge graph
      -# Add Gemini Search MCP server for grounded web search
      +source <(ctx completion zsh)  # shell completion (or bash/fish)
      +
      +# ## Claude Code (automatic after plugin install) ##
      +claude /plugin marketplace add ActiveMemory/ctx
      +claude /plugin install ctx@activememory-ctx
      +
      +# ## OpenCode ##
      +ctx setup opencode --write && ctx init
      +
      +# ## Cursor / Aider / Copilot / Windsurf ##
      +ctx setup cursor # or: aider, copilot, windsurf
      +
      +# ## Companion tools (highly recommended) ##
      +npx gitnexus analyze          # code knowledge graph
      +# Add Gemini Search MCP server for grounded web search
       
      -
      -

      Activate the Project Once Per Shell

      -

      Run eval "$(ctx activate)" after ctx init. The ctx setup, -ctx init, and ctx completion commands work without it, but -if you skip the eval, most others (ctx agent, ctx load, -ctx watch, ctx journal ...) fail with Error: no context -directory specified. See -Activating a Context Directory.

      -
      +

      Run subsequent ctx commands from the project root; ctx always +reads $PWD/.context/.

      Create a .ctxrc in your project root to configure token budgets, context directory, drift thresholds, and more.

      Then start your AI tool and ask: "Do you remember?"

      @@ -2414,29 +2380,12 @@

      Step 1: Initialize ctx AGENT_PLAYBOOK.md # How AI tools should use this system
      -

      Using a Different .context Directory

      -

      The .context/ directory doesn't have to live inside your project. Point -ctx to an external folder by exporting CTX_DIR (the only -declaration channel).

      -

      Useful when context must stay private while the code is public, or -when you want to commit notes to a separate repo.

      -

      Caveats (the recipe covers both with workarounds):

      -
        -
      • Code-aware operations degrade silently. ctx sync, ctx drift, - and the memory-drift hook read the codebase from - dirname(CTX_DIR). With an external .context/, that's the - context repo, not your code repo. They scan the wrong tree without - erroring. The recipe shows a symlink workaround that keeps both - healthy.
      • -
      • One .context/ per project, always. Sharing one directory - across multiple projects corrupts journals, state, and secrets. - For cross-project knowledge sharing (CONSTITUTION, CONVENTIONS, - ARCHITECTURE, etc.) use ctx hub, not a - shared .context/.
      • -
      -

      See External Context for the full recipe -and Configuration -for the resolver details.

      +

      One .context/ per project

      +

      ctx reads $PWD/.context/; the directory always lives +alongside .git/ at the project root. Sharing one directory +across multiple projects corrupts journals, state, and +secrets. For cross-project knowledge sharing (CONSTITUTION, +CONVENTIONS, ARCHITECTURE, etc.) use ctx hub.

      For Claude Code, install the ctx plugin to get hooks and skills:

      claude /plugin marketplace add ActiveMemory/ctx
      @@ -2476,7 +2425,7 @@ 

      Claude Code

      OpenCode

      Run the one-liner from the project root:

      -
      ctx setup opencode --write && ctx init && eval "$(ctx activate)"
      +
      ctx setup opencode --write && ctx init
       

      This deploys a lifecycle plugin, slash command skills, AGENTS.md, and registers the ctx MCP server globally. See @@ -2491,7 +2440,7 @@

      VS CodeVS Code Marketplace (publisher: activememory). Then, from your project root:

      -
      ctx init && eval "$(ctx activate)"
      +
      ctx init
       

      Open Copilot Chat and type @ctx /init to verify. The extension auto-downloads the ctx CLI if it isn't on PATH. See @@ -2714,10 +2663,6 @@

      Future DirectionNext Up

      -

      Keeping Context in a Separate Repo →: Store -context files outside the project tree for multi-repo or open source -setups.

      See Also

      -
      -

      Activate the Project First

      -

      Run eval "$(ctx activate)" once per terminal in the project -root. If you skip it, ctx permission ... fails with -Error: no context directory specified. See -Activating a Context Directory.

      -

      The Solution

      Save a curated settings.local.json as a golden image, then restore from it to drop session-accumulated permissions. The golden file diff --git a/site/recipes/publishing/index.html b/site/recipes/publishing/index.html index cc781e779..1af4f0e9a 100644 --- a/site/recipes/publishing/index.html +++ b/site/recipes/publishing/index.html @@ -724,7 +724,7 @@ - + @@ -2162,13 +2162,6 @@

      TL;DR&pa /ctx-blog about the caching layer # 4. draft a blog post /ctx-blog-changelog v0.1.0 "v0.2" # 5. write a changelog post

      -
      -

      Activate the Project First

      -

      Run eval "$(ctx activate)" once per terminal in the project -root. If you skip it, ctx journal ... fails with Error: no -context directory specified. See -Activating a Context Directory.

      -

      Read on for details on each stage.

      Commands and Skills Used

    14. diff --git a/site/recipes/scratchpad-sync/index.html b/site/recipes/scratchpad-sync/index.html index 2af93e554..6a20179f8 100644 --- a/site/recipes/scratchpad-sync/index.html +++ b/site/recipes/scratchpad-sync/index.html @@ -724,7 +724,7 @@ - + @@ -2150,21 +2150,11 @@

      The ProblemHow do you distribute the key and keep the scratchpad in sync?

      TL;DR

      ctx init                                                  # 1. generates key
      -eval "$(ctx activate)"                                    # 2. bind CTX_DIR
      -scp ~/.ctx/.ctx.key user@machine-b:~/.ctx/.ctx.key        # 3. copy key
      -chmod 600 ~/.ctx/.ctx.key                                 # 4. secure it
      -# Normal git push/pull syncs the encrypted scratchpad.enc
      -# On conflict: ctx pad resolve → rebuild → git add + commit
      +scp ~/.ctx/.ctx.key user@machine-b:~/.ctx/.ctx.key        # 2. copy key
      +chmod 600 ~/.ctx/.ctx.key                                 # 3. secure it
      +# Normal git push/pull syncs the encrypted scratchpad.enc
      +# On conflict: ctx pad resolve → rebuild → git add + commit
       
      -
      -

      Activate Each Machine

      -

      Run eval "$(ctx activate)" from the project root on every -machine that reads or writes the scratchpad: after each -ctx init, or after each clone on machine B. If you skip it, -ctx pad ... fails with Error: no context directory -specified. See -Activating a Context Directory.

      -

      Finding Your Key File

      The key is always at ~/.ctx/.ctx.key - one key, one machine.

      diff --git a/site/recipes/scratchpad-with-claude/index.html b/site/recipes/scratchpad-with-claude/index.html index 5362af99d..ffbf60f02 100644 --- a/site/recipes/scratchpad-with-claude/index.html +++ b/site/recipes/scratchpad-with-claude/index.html @@ -724,7 +724,7 @@ - + @@ -2275,13 +2275,6 @@

      TL;DR&pa

      Entries are encrypted at rest and travel with git.

      Use the /ctx-pad skill to manage entries from inside your AI session.

      -

      Commands and Skills Used

      diff --git a/site/recipes/session-archaeology/index.html b/site/recipes/session-archaeology/index.html index 0624671db..8b534497e 100644 --- a/site/recipes/session-archaeology/index.html +++ b/site/recipes/session-archaeology/index.html @@ -724,7 +724,7 @@ - + @@ -2204,13 +2204,6 @@

      TL;DR&pa
      ctx journal import --all
       ctx journal site --serve
       
      -
      -

      Activate the Project First

      -

      Run eval "$(ctx activate)" once per terminal in the project -root. If you skip it, the ctx journal ... commands below -fail with Error: no context directory specified. See -Activating a Context Directory.

      -

      Enrich

      /ctx-journal-enrich-all
       
      diff --git a/site/recipes/session-ceremonies/index.html b/site/recipes/session-ceremonies/index.html index 977447ff8..dc6bd1325 100644 --- a/site/recipes/session-ceremonies/index.html +++ b/site/recipes/session-ceremonies/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/session-changes/index.html b/site/recipes/session-changes/index.html index e66cb0429..733c55439 100644 --- a/site/recipes/session-changes/index.html +++ b/site/recipes/session-changes/index.html @@ -1119,13 +1119,6 @@

      Quick Start# Check since a specific date ctx change --since 2026-03-10 -
      -

      Activate the Project First

      -

      Run eval "$(ctx activate)" once per terminal in the project -root. If you skip it, ctx change fails with Error: no -context directory specified. See -Activating a Context Directory.

      -

      How Reference Time Works

      ctx change needs a reference point to compare against. It tries these sources in order:

      diff --git a/site/recipes/session-lifecycle/index.html b/site/recipes/session-lifecycle/index.html index d36887229..ca2f403c1 100644 --- a/site/recipes/session-lifecycle/index.html +++ b/site/recipes/session-lifecycle/index.html @@ -15,7 +15,7 @@ - + @@ -724,7 +724,7 @@ - + @@ -2130,19 +2130,6 @@

      TL;DR&pa
    15. Wrap up: /ctx-wrap-up: end-of-session ceremony.
    16. Read on for the full walkthrough with examples.

      -

      What Is a Readback?

      A readback is a structured summary where the agent plays back what @@ -2241,7 +2228,7 @@

      The Workflow

      Step 1: Load Context

      Start every session by loading what you know. The fastest way is a single prompt:

      -
      Do you remember what we were working on?
      +
      Do you remember what we were working on?
       

      This triggers the /ctx-remember skill. Behind the scenes, the assistant runs ctx agent --budget 4000, reads the files listed in the context packet @@ -2257,7 +2244,7 @@

      Step 1: Load ContextStep 2: Orient

      After loading context, verify you understand the current state.

      -
      /ctx-status
      +
      /ctx-status
       

      The status output shows which context files are populated, how many tokens they consume, and which files were recently modified. Look for:

      @@ -2276,7 +2263,7 @@

      Step 2: OrientStep 3: Pick What to Work On

      With context loaded, choose a task. You can pick one yourself, or ask the assistant to recommend:

      -
      /ctx-next
      +
      /ctx-next
       

      The skill reads TASKS.md, checks recent sessions to avoid re-suggesting completed work, and presents 1-3 ranked recommendations with rationale.

      @@ -2284,7 +2271,7 @@

      Step 3: Pick What to Work On
      Let's work on the session enrichment feature.
      +
      Let's work on the session enrichment feature.
       

      Step 4: Do the Work

      @@ -2293,10 +2280,10 @@

      Step 4: Do the Work
      Is this consistent with our decisions?
      +
      Is this consistent with our decisions?
       

      Constrain scope: keep the assistant focused on the task at hand.

      -
      Only change files in internal/cli/session/. Nothing else.
      +
      Only change files in internal/cli/session/. Nothing else.
       

      Use /ctx-implement for multistep plans: if the task has multiple steps, this skill executes them one at a time with build/test verification between @@ -2309,7 +2296,7 @@

      Step 4: Do the WorkStep 5: Commit with Context

      When the work is ready, use the context-aware commit instead of raw git commit:

      -
      /ctx-commit
      +
      /ctx-commit
       

      The Agent May Recommend Committing

      @@ -2325,11 +2312,11 @@

      Step 5: Commit with Contextwhy" rather than "what", and then commits.

      After the commit succeeds, it prompts you:

      -
      **Any context to capture?**
      -
      -- **Decision**: Did you make a design choice or trade-off?
      -- **Learning**: Did you hit a gotcha or discover something?
      -- **Neither**: No context to capture; we are done.
      +
      **Any context to capture?**
      +
      +- **Decision**: Did you make a design choice or trade-off?
      +- **Learning**: Did you hit a gotcha or discover something?
      +- **Neither**: No context to capture; we are done.
       

      If you made a decision, the skill records it with ctx decision add. If you learned something, it records it with ctx learning add including context, @@ -2341,7 +2328,7 @@

      Step 5: Commit with ContextStep 6: Reflect

      At natural breakpoints (after finishing a feature, resolving a complex bug, or before switching tasks) pause to reflect:

      -
      /ctx-reflect
      +
      /ctx-reflect
       

      Agents Reflect at Milestones

      @@ -2359,15 +2346,15 @@

      Step 6: Reflect
      I would suggest persisting:
      -
      -- **Learning**: `$PPID` in PreToolUse hooks resolves to the Claude Code PID
      -  `ctx learning add --context "..." --lesson "..." --application "..." --session-id abc12345 --branch main --commit 68fbc00a`
      -- **Task**: mark "Add cooldown to ctx agent" as done
      -- **Decision**: tombstone-based cooldown with 10m default
      -  `ctx decision add "..." --session-id abc12345 --branch main --commit 68fbc00a`
      -
      -Want me to persist any of these?
      +
      I would suggest persisting:
      +
      +- **Learning**: `$PPID` in PreToolUse hooks resolves to the Claude Code PID
      +  `ctx learning add --context "..." --lesson "..." --application "..." --session-id abc12345 --branch main --commit 68fbc00a`
      +- **Task**: mark "Add cooldown to ctx agent" as done
      +- **Decision**: tombstone-based cooldown with 10m default
      +  `ctx decision add "..." --session-id abc12345 --branch main --commit 68fbc00a`
      +
      +Want me to persist any of these?
       

      The skill asks before persisting anything. You choose what to keep.

      Not every commit needs reflection. A typo fix does not. @@ -2377,7 +2364,7 @@

      Step 6: ReflectStep 7: Persist Before Ending

      Before ending the session, run the wrap-up ceremony to capture outstanding learnings, decisions, conventions, and tasks:

      -
      /ctx-wrap-up
      +
      /ctx-wrap-up
       

      Ceremony Skills

      @@ -2411,60 +2398,60 @@

      Conversational Session Flow
      Last session (Feb 7, caching-layer): we implemented the in-memory cache with
      -TTL expiration and wrote tests for the eviction policy. 
      -
      -You have 3 pending tasks; the biggest one is adding Redis as a backend.
      -
      -Recent decision:<br>
      -we chose LRU over LFU because the access pattern is mostly recency-based.
      -
      -Want to continue with the Redis backend, or tackle something else?
      +
      Last session (Feb 7, caching-layer): we implemented the in-memory cache with
      +TTL expiration and wrote tests for the eviction policy. 
      +
      +You have 3 pending tasks; the biggest one is adding Redis as a backend.
      +
      +Recent decision:<br>
      +we chose LRU over LFU because the access pattern is mostly recency-based.
      +
      +Want to continue with the Redis backend, or tackle something else?
       

      You: Let's work on the Redis caching backend.

      Agent (picks up the task, begins implementation):

      -
      Got it. I see the task in TASKS.md: "Add Redis backend for cache layer."
      -
      -I will start with the connection pool setup. Only touching
      -`internal/cache/redis/` for now.
      +
      Got it. I see the task in TASKS.md: "Add Redis backend for cache layer."
      +
      +I will start with the connection pool setup. Only touching
      +`internal/cache/redis/` for now.
       

      (The agent works... writes code, runs tests, iterates.)

      Agent (after implementing the connection pool and choosing a retry strategy):

      -
      The connection pool is working. I went with exponential backoff with jitter
      -for retries instead of fixed intervals; it handles thundering herd better
      -under reconnection storms. That is a design choice worth recording. 
      -
      -Want me to save it as a decision?
      +
      The connection pool is working. I went with exponential backoff with jitter
      +for retries instead of fixed intervals; it handles thundering herd better
      +under reconnection storms. That is a design choice worth recording. 
      +
      +Want me to save it as a decision?
       

      You: Yes, go ahead.

      Agent:

      -
      Done. Added to DECISIONS.md: "Exponential backoff with jitter for Redis
      -reconnection, to avoid thundering herd on pool recovery."
      +
      Done. Added to DECISIONS.md: "Exponential backoff with jitter for Redis
      +reconnection, to avoid thundering herd on pool recovery."
       

      (More work happens... the agent implements the cache read/write paths.)

      You: What else did we learn during this session?

      Agent:

      -
      A few things worth capturing:
      -
      -* **Learning**: Redis `SET` with `NX` and `PX` flags handles atomic
      -  set-if-not-exists with TTL in a single call. No need for a separate
      -  `EXPIRE` command. We initially did it in two calls and hit a race condition.
      -* **Learning**: The `go-redis` library's `PoolSize` default is 10 per CPU.
      -  For our workload, explicit sizing at 20 was more predictable.
      -* **Task update**: The connection pool subtask is done; the read/write path
      -  is implemented but needs integration tests.
      -
      -Want me to persist these?
      +
      A few things worth capturing:
      +
      +* **Learning**: Redis `SET` with `NX` and `PX` flags handles atomic
      +  set-if-not-exists with TTL in a single call. No need for a separate
      +  `EXPIRE` command. We initially did it in two calls and hit a race condition.
      +* **Learning**: The `go-redis` library's `PoolSize` default is 10 per CPU.
      +  For our workload, explicit sizing at 20 was more predictable.
      +* **Task update**: The connection pool subtask is done; the read/write path
      +  is implemented but needs integration tests.
      +
      +Want me to persist these?
       

      You: Yes. And let's wrap up.

      Agent (reflects and persists... all in one flow):

      -
      Persisted:
      -
      -* 2 learnings added to LEARNINGS.md
      -* Marked "connection pool setup" done in TASKS.md
      -* Added "write integration tests for Redis cache" as a new task
      -
      -Context is up to date. You are good to close.
      +
      Persisted:
      +
      +* 2 learnings added to LEARNINGS.md
      +* Marked "connection pool setup" done in TASKS.md
      +* Added "write integration tests for Redis cache" as a new task
      +
      +Context is up to date. You are good to close.
       
      diff --git a/site/recipes/session-pause/index.html b/site/recipes/session-pause/index.html index d7bb54858..d78a383dd 100644 --- a/site/recipes/session-pause/index.html +++ b/site/recipes/session-pause/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/session-reminders/index.html b/site/recipes/session-reminders/index.html index bd951acd0..65206fa36 100644 --- a/site/recipes/session-reminders/index.html +++ b/site/recipes/session-reminders/index.html @@ -724,7 +724,7 @@ - + @@ -2081,13 +2081,6 @@

      TL;DR&pa

      Reminders surface automatically at session start: VERBATIM, every session, until you dismiss them.

      -

      Commands and Skills Used

      diff --git a/site/recipes/state-maintenance/index.html b/site/recipes/state-maintenance/index.html index 434e693f4..502ac0ff1 100644 --- a/site/recipes/state-maintenance/index.html +++ b/site/recipes/state-maintenance/index.html @@ -1151,13 +1151,6 @@

      TL;DR&pa ctx prune # prune files older than 7 days ctx prune --days 1 # more aggressive: keep only today -
      -

      Activate the Project First

      -

      Run eval "$(ctx activate)" once per terminal in the project -root. If you skip it, ctx prune / ctx status fail with -Error: no context directory specified. See -Activating a Context Directory.

      -

      Commands Used

      diff --git a/site/recipes/steering/index.html b/site/recipes/steering/index.html index 3f6d2a8b3..94d8d0796 100644 --- a/site/recipes/steering/index.html +++ b/site/recipes/steering/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/system-hooks-audit/index.html b/site/recipes/system-hooks-audit/index.html index 9f3cdf9f2..d7a798167 100644 --- a/site/recipes/system-hooks-audit/index.html +++ b/site/recipes/system-hooks-audit/index.html @@ -724,7 +724,7 @@ - + diff --git a/site/recipes/task-management/index.html b/site/recipes/task-management/index.html index 69672ea36..48765cf88 100644 --- a/site/recipes/task-management/index.html +++ b/site/recipes/task-management/index.html @@ -724,7 +724,7 @@ - + @@ -2307,14 +2307,6 @@

      The Problemctx task add. The agent automatically picks up session ID, branch, and commit hash from its context, so no manual flags are needed.

      -

      TL;DR

      Manage Tasks:

      ctx task add "Fix race condition" --priority high \
      diff --git a/site/recipes/triggers/index.html b/site/recipes/triggers/index.html
      index f595c7bed..a6782d178 100644
      --- a/site/recipes/triggers/index.html
      +++ b/site/recipes/triggers/index.html
      @@ -724,7 +724,7 @@
         
         
         
      -    
      +    
             
         
         
      diff --git a/site/recipes/troubleshooting/index.html b/site/recipes/troubleshooting/index.html
      index 887db6228..d02225efc 100644
      --- a/site/recipes/troubleshooting/index.html
      +++ b/site/recipes/troubleshooting/index.html
      @@ -724,7 +724,7 @@
         
         
         
      -    
      +    
             
         
         
      @@ -1236,10 +1236,10 @@
             
      • - + - "No context directory specified for this project" + "No .context/ at this directory" @@ -2008,10 +2008,10 @@
        • - + - "No context directory specified for this project" + "No .context/ at this directory" @@ -2339,85 +2339,54 @@

          Raw Event InspectionCommon Problems

          -

          "No context directory specified for this project"

          -

          Symptoms: Any ctx command fails with -Error: no context directory specified for this project (possibly -with a likely-candidate hint or a candidate list depending on what's -visible from your CWD).

          -

          Cause: ctx does not search the filesystem for a .context/ -directory. You have to declare which one to use before running -day-to-day commands.

          -

          Fix: bind CTX_DIR for the current shell:

          -
          eval "$(ctx activate)"
          -
          -

          See Activating a Context Directory for the -full recipe (one-shot CTX_DIR=... inline form, CI patterns, direnv -setup).

          +

          "No .context/ at this directory"

          +

          Symptoms: Any ctx command fails with ctx: no .context/ at +<pwd>. Run \ctx init` here, or cd to a project that has one.`

          +

          Cause: ctx reads $PWD/.context/ and you ran the command +from a directory that does not have one.

          +

          Fix: either cd into a project root that already has +.context/, or run ctx init in the current directory to create +one.

          "ctx: Not Initialized"

          -

          Symptoms: After declaring CTX_DIR, the command fails with -ctx: not initialized - run "ctx init" first.

          -

          Cause: The declared directory exists but hasn't been initialized -with template files.

          +

          Symptoms: ctx finds .context/ at $PWD but the command +fails with ctx: not initialized - run "ctx init" first.

          +

          Cause: The directory exists but hasn't been populated with +template files.

          Fix:

          -
          ctx init          # create .context/ with template files
          -ctx init --minimal  # or just the essentials (CONSTITUTION, TASKS, DECISIONS)
          +
          ctx init          # create .context/ with template files
          +ctx init --minimal  # or just the essentials (CONSTITUTION, TASKS, DECISIONS)
           
          -

          Commands that work without CTX_DIR or initialization: ctx init, -ctx activate, ctx deactivate, ctx setup, ctx doctor, -ctx guide, ctx why, ctx config switch/status, ctx hub *, and -help-only grouping commands.

          +

          Commands that work without .context/ or initialization: ctx init, +ctx setup, ctx doctor, ctx guide, ctx why, +ctx config switch/status, ctx hub *, and help-only grouping +commands.

          "My CLI and My Claude Code Session Disagree on the Project"

          Symptoms: A !-pragma or interactive ctx call writes to the wrong .context/; or you ran ctx remind add in shell A and the reminder shows up in project B's notifications.

          -

          Cause: CTX_DIR is sourced from three different surfaces, and -they can drift apart:

          -
      - - - - - - - - - - - - - - - - - - - - - - - - -
      SurfaceSource of CTX_DIRBound when
      Claude Code hooks${CLAUDE_PROJECT_DIR}/.context (injected)Every hook line; the project Claude is in
      !-pragma in chat / interactive shellWhatever the parent shell exportedWhen you ran eval "$(ctx activate)"
      New shell tab opened mid-sessionWhatever your shellrc exportsLogin
      -

      When these drift, the per-prompt check-anchor-drift hook fires a -verbatim warning naming both values. To fix: re-run -eval "$(ctx activate)" from inside the project the Claude Code -session is editing, or close the shell tab and reopen it from the -right working directory.

      +

      Cause: Different shells were launched from different working +directories. ctx reads $PWD/.context/; if your terminal tab +is cd'd into project A and your Claude Code session is in +project B, !-pragma calls write to A while in-session calls +write to B.

      +

      Fix: cd the shell into the same project root the Claude +Code session is in, or close the tab and reopen it from the right +working directory.

      "My Hook Isn't Firing"

      Symptoms: No nudges appearing, webhook silent, event log shows no entries for the expected hook.

      Diagnosis:

      -
      # 1. Check if ctx is installed and on PATH
      -which ctx && ctx --version
      -
      -# 2. Check if the hook is registered
      -grep "check-persistence" ~/.claude/plugins/ctx/hooks.json
      -
      -# 3. Run the hook manually to see if it errors
      -echo '{"session_id":"test"}' | ctx system check-persistence
      -
      -# 4. Check event log for the hook (if enabled)
      -ctx hook event --hook check-persistence
      +
      # 1. Check if ctx is installed and on PATH
      +which ctx && ctx --version
      +
      +# 2. Check if the hook is registered
      +grep "check-persistence" ~/.claude/plugins/ctx/hooks.json
      +
      +# 3. Run the hook manually to see if it errors
      +echo '{"session_id":"test"}' | ctx system check-persistence
      +
      +# 4. Check event log for the hook (if enabled)
      +ctx hook event --hook check-persistence
       

      Common causes: