Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .context/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<!-- INDEX:START -->
| Date | Decision |
|----|--------|
| 2026-06-02 | Remove the implicit project-local .ctx.key resolution tier |
| 2026-05-30 | Name the add JSON-ingest flag --json-file, not --json |
| 2026-05-28 | ctxctl PATH-installed alongside ctx for clean roots and one binary across worktrees |
| 2026-05-28 | Memory pressure detection uses OS-native signals (macOS pressure level + Linux PSI), not occupancy |
Expand Down Expand Up @@ -158,6 +159,61 @@ For significant decisions:

-->

## [2026-06-02-051330] Remove the implicit project-local .ctx.key resolution tier

**Status**: Accepted

**Context**: Picking up TASKS.md P0.8.5 ("notify fails in worktrees"), we
found `crypto.ResolveKeyPath` still auto-detects a project-local
`<contextDir>/.ctx.key` (a stat-gated tier) and prefers it over the global
`~/.ctx/.ctx.key`. That file is gitignored, so it is absent in a fresh
worktree checkout: resolution silently falls back to the (different) global
key and webhook/pad decryption fails. The v0.8.0 global-encryption-key spec
already collapsed per-project keys into one global key — calling project-local
keys "a security antipattern [key next to ciphertext]" that "broke in
worktrees" — but left the implicit auto-detect tier in place. Empirically
(built binary + isolated repo + fake webhook sink): the default global key
works in worktrees; only a project-local key reproduces the failure, and the
fire path swallows it silently.

**Alternatives Considered**:
- Approach A — worktree-aware key fallback via `git rev-parse --git-common-dir`
to resolve the main checkout's key from inside a worktree: keeps project-local
keys working / but adds git-awareness to key resolution, contradicts the
CWD-anchored model, larger blast radius, and props up a deprecated mechanism.
- Approach B — copy the key into the worktree at creation (ctx-worktree skill):
no resolver change / but agent-driven and unenforceable, widens skill
permissions, and is redundant under a global key.
- Keep the tier, only fix the silent failure: smallest change / but leaves the
documented security antipattern and the worktree divergence in place.

**Decision**: Remove the implicit project-local `.context/.ctx.key`
auto-detection tier from `ResolveKeyPath`. Resolution becomes: (1) explicit
`.ctxrc key_path` override, (2) global `~/.ctx/.ctx.key`, (3) project-local
path only as a degenerate fallback when the home dir is unavailable. Genuine
per-project isolation stays available via the explicit `key_path` override.
Paired with surfacing the silent fire-path failure so any stranded-key decrypt
failure is visible, not silent.

**Rationale**: The project-local key is the only thing that makes a worktree
behave differently from N side-by-side terminals in the same directory;
removing it makes them indistinguishable (the desired model) and deletes a
security antipattern the project already named. It is net deletion, consistent
with the global-key and cwd-anchored simplifications. The explicit override
covers the rare real isolation need without the ciphertext-adjacent footgun.

**Consequence**: Projects on the default global key are unaffected. Projects
with a project-local `.context/.ctx.key` resolve to the global key; their
existing local-key-encrypted `.notify.enc` / pad will fail to decrypt — now
visibly (a warning on the fire path, a surfaced error on pad/test paths)
instead of silently. Documented remedy: back up the local key, then re-key to
global or set an explicit `key_path`. No auto-migration (none exists in-tree).

**Related**: Spec specs/notify-resolution-hardening.md | Supersedes the
project-local auto-detection portion of
specs/released/v0.8.0/global-encryption-key.md | Relates to
specs/cwd-anchored-context.md and [2026-03-01] Global encryption key

## [2026-05-30-114429] Name the add JSON-ingest flag --json-file, not --json

**Status**: Accepted
Expand Down
11 changes: 11 additions & 0 deletions .context/LEARNINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ DO NOT UPDATE FOR:
<!-- INDEX:START -->
| Date | Learning |
|----|--------|
| 2026-06-02 | os.IsNotExist doesn't unwrap; detect file absence with os.Stat + errors.Is(os.ErrNotExist) |
| 2026-06-01 | An error-discard catalogue is an inventory, not a verdict — verify each site by reading before fixing |
| 2026-06-01 | Guard managed blocks before regenerating; don't trust the span to be machine-owned |
| 2026-05-30 | Capture golden fixtures from the live legacy code path before deleting it |
Expand Down Expand Up @@ -171,6 +172,16 @@ DO NOT UPDATE FOR:

---

## [2026-06-02-051330] os.IsNotExist doesn't unwrap — detect file absence with os.Stat + errors.Is

**Context**: Hardening notify (P0.8.5), `LoadWebhook` needed to tell "encrypted file genuinely absent" (silent: not configured) from "present but broken" (surface it). `os.IsNotExist(loadErr)` on `crypto.LoadKey`'s error is always false: `LoadKey` wraps the os error via `errCrypto.ReadKey` → `fmt.Errorf(desc.Text(...), cause)`, and `os.IsNotExist` does not unwrap. The subtle part is `errors.Is(loadErr, os.ErrNotExist)`: it is **registry-dependent**. `errCrypto.ReadKey`'s format string comes from the externalized text registry (`'read key: %w'`); `fmt.Errorf` honors `%w` at runtime regardless of where the string came from, so in production (registry loaded) `errors.Is` correctly unwraps to `fs.ErrNotExist`. But in a unit-test binary that never initializes the text registry (verified: a probe in `internal/notify`), `desc.Text` returns a string with **no** `%w`, the cause is never wrapped, the error prints `%!(EXTRA *fs.PathError=...)`, and `errors.Is` also returns false. So the same call can behave differently in prod vs. a bare test binary.

**Lesson**: `os.IsNotExist` is the legacy, non-unwrapping check — false on any `fmt.Errorf("…%w…", …)` error; always prefer `errors.Is`. But `errors.Is(err, os.ErrNotExist)` only holds if the wrap actually carries `%w` at runtime, and a wrap whose format string is fetched from a text/i18n registry only carries `%w` when that registry is initialized. `go vet`'s wrap check sees only literal format strings, so a registry-templated wrap is vet-invisible and its wrapping is environment-dependent.

**Application**: To detect file absence reliably, stat the path directly: `os.Stat` returns an unwrapped `*fs.PathError`, so `errors.Is(statErr, os.ErrNotExist)` is dependable in every context. Branch on the stat (absent → not-configured; present → proceed to read/decrypt and surface any error). Reserve `errors.Is(…, os.ErrNotExist)` on a *returned library* error for chains you have confirmed wrap with `%w` independent of registry state.

---

## [2026-06-01-195111] An error-discard catalogue is an inventory, not a verdict — verify each site by reading before fixing

**Context**: Phase EH audited ~184 silent error-discard sites under internal/. The catalogue was built by grep + pattern/name classification (e.g. 'x, _ := SomethingMarshal' => B-marshal). When fixing, several name-inferred verdicts were wrong.
Expand Down
25 changes: 18 additions & 7 deletions .context/TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,22 @@ Important things that agent (or human) yeeted to the future.
+ static Zensical + LoopScript + Tier-2 recall HTML (metaTable/details)
migrated to embedded templates behind handles; Tier-3 single-line format
strings, pure joins, and the RecallListRow meta-format kept as fmt.Sprintf.
- [ ] P0.8.5: Enable webhook notifications in worktrees. Currently `ctx notify`
silently fails because `.context.key` is gitignored and absent in
worktrees. For autonomous runs with opaque worktree agents, notifications
are the one feature that would genuinely be useful. Possible approaches:
resolve the key via `git rev-parse --git-common-dir` to find the main
checkout, or copy the key into worktrees at creation time (ctx-worktree
skill). #priority:medium #added:2026-02-22
- [x] P0.8.5: Harden notify resolution (reframed 2026-06-02). The original
premise ("`ctx notify` silently fails in worktrees because the key is
gitignored and absent") was investigated and largely disproven: with the
default global key, notify works in worktrees (verified against a built
binary + isolated repo + fake webhook sink). The failure only reproduces
with a deprecated project-local key. Real defects to fix: (1) remove the
implicit `.context/.ctx.key` resolution tier — the sole worktree-divergence
and a documented security antipattern; (2) surface the silent fire-path
failure when a CONFIGURED webhook can't be delivered (decrypt/read/POST),
while keeping legitimate silences (not-configured, event-not-subscribed).
Whether config reaches a worktree is the user's call via `.ctxrc`
git-tracking — ctx does not special-case worktrees (it cannot distinguish a
worktree from N side-by-side terminals). Approaches A (--git-common-dir key
fallback) and B (copy key at worktree creation) rejected; see DECISIONS.
Spec: specs/notify-resolution-hardening.md
#priority:medium #added:2026-02-22 #reframed:2026-06-02
- [ ] P0.9.2: Split cli-reference.md (1633 lines) into command group pages:
cli-overview, cli-init-status, cli-context, cli-recall, cli-tools,
cli-system —
Expand Down Expand Up @@ -434,6 +443,8 @@ Important things that agent (or human) yeeted to the future.

### Phase CT: Companion Tool Integration

- [ ] Add a 'make strip-gitnexus' target (backed by a hack/ script) that mechanically removes the GitNexus auto-injected block — delimited by <!-- gitnexus:start --> / <!-- gitnexus:end --> markers — from AGENTS.md and CLAUDE.md. Marker-bounded delete (sed range or awk between markers). Must: (1) leave AGENTS.md as the redirect stub and CLAUDE.md ending at its Companion Tools / GITNEXUS.md pointer; (2) NOT touch GITNEXUS.md (the intended managed home for that content); (3) be idempotent (no-op when markers absent). Run it after 'npx gitnexus analyze'. Upstream-preferred guard is 'analyze --skip-agents-md'; this script is the belt-and-suspenders cleanup when analyze runs without that flag. Manual removal was done in 8da165a3; this automates it. #priority:medium #session:74c94e3a #branch:fix/notify-resolution-hardening #commit:8da165a3 #added:2026-06-02-085625

Session-start checks, suppressibility, and registry for companion MCP tools.

- [ ] ctx-remember preflight: verify ctx binary in PATH,
Expand Down
122 changes: 0 additions & 122 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,3 @@
# Agent Instructions

Read and follow [CLAUDE.md](CLAUDE.md).

<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **ctx** (19319 symbols, 100435 relationships, 188 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

## Always Do

- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.

## When Debugging

1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
3. `READ gitnexus://repo/ctx/process/{processName}` — trace the full execution flow step by step
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed

## When Refactoring

- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.

## Never Do

- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.

## Tools Quick Reference

| Tool | When to use | Command |
|------|-------------|---------|
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |

## Impact Risk Levels

| Depth | Meaning | Action |
|-------|---------|--------|
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
| d=3 | MAY NEED TESTING — transitive | Test if critical path |

## Resources

| Resource | Use for |
|----------|---------|
| `gitnexus://repo/ctx/context` | Codebase overview, check index freshness |
| `gitnexus://repo/ctx/clusters` | All functional areas |
| `gitnexus://repo/ctx/processes` | All execution flows |
| `gitnexus://repo/ctx/process/{name}` | Step-by-step execution trace |

## Self-Check Before Finishing

Before completing any code modification task, verify:
1. `gitnexus_impact` was run for all modified symbols
2. No HIGH/CRITICAL risk warnings were ignored
3. `gitnexus_detect_changes()` confirms changes match expected scope
4. All d=1 (WILL BREAK) dependents were updated

## Keeping the Index Fresh

After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:

```bash
npx gitnexus analyze
```

If the index previously included embeddings, preserve them by adding `--embeddings`:

```bash
npx gitnexus analyze --embeddings
```

To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**

> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.

## CLI

| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
| Work in the Pad area (250 symbols) | `.claude/skills/generated/pad/SKILL.md` |
| Work in the Skill area (222 symbols) | `.claude/skills/generated/skill/SKILL.md` |
| Work in the Audit area (155 symbols) | `.claude/skills/generated/audit/SKILL.md` |
| Work in the Format area (139 symbols) | `.claude/skills/generated/format/SKILL.md` |
| Work in the Rc area (128 symbols) | `.claude/skills/generated/rc/SKILL.md` |
| Work in the Steering area (104 symbols) | `.claude/skills/generated/steering/SKILL.md` |
| Work in the Initialize area (104 symbols) | `.claude/skills/generated/initialize/SKILL.md` |
| Work in the Hub area (99 symbols) | `.claude/skills/generated/hub/SKILL.md` |
| Work in the Memory area (95 symbols) | `.claude/skills/generated/memory/SKILL.md` |
| Work in the Server area (92 symbols) | `.claude/skills/generated/server/SKILL.md` |
| Work in the Journal area (85 symbols) | `.claude/skills/generated/journal/SKILL.md` |
| Work in the Nudge area (82 symbols) | `.claude/skills/generated/nudge/SKILL.md` |
| Work in the Parser area (79 symbols) | `.claude/skills/generated/parser/SKILL.md` |
| Work in the Root area (77 symbols) | `.claude/skills/generated/root/SKILL.md` |
| Work in the Trigger area (76 symbols) | `.claude/skills/generated/trigger/SKILL.md` |
| Work in the Store area (73 symbols) | `.claude/skills/generated/store/SKILL.md` |
| Work in the Trace area (72 symbols) | `.claude/skills/generated/trace/SKILL.md` |
| Work in the Assets area (68 symbols) | `.claude/skills/generated/assets/SKILL.md` |
| Work in the Flagbind area (64 symbols) | `.claude/skills/generated/flagbind/SKILL.md` |
| Work in the Add area (63 symbols) | `.claude/skills/generated/add/SKILL.md` |

<!-- gitnexus:end -->
Loading
Loading