Skip to content

Commit 58b1c0f

Browse files
committed
feat: ctxctl module, OS-native memory pressure, typed audit errors
Branch wrap-up commit for feat/pad-undo-snapshot (pad-undo Phase 1 shipped earlier on this branch). It bundles several arcs developed together under the "we own the branch, one commit" plan, plus a few unrelated items captured at the user's request. Grouped by area, with the spec each serves. ctxctl: separate maintainer module (per specs/ctxctl-bootstrap.md) The out-of-band audit channel leaves the shipped `ctx` binary entirely. Six logic trees (cli/audit, system/cmd/checkaudit, system/core/audit, config/audit, err/audit, write/audit) were relocated under internal/ctxctl/... with imports rewritten. A new separate Go module lives at tools/ctxctl/ (module path github.com/ActiveMemory/ctx/tools/ctxctl; its go.mod requires the ctx module via `replace ../..`; the repo-root go.work links both), so the ctx module never requires ctxctl and can never import it. tools/ctxctl wires `ctxctl audit list|show|dismiss` plus the `ctxctl audit-relay` hook (renamed from check-audit). The audit channel's English lives as plain Go constants under tools/ctxctl (no YAML i18n, no desc engine); the ctx-side audit YAML keys and DescKey* constants were deleted. The Makefile gains ctxctl, install-ctxctl and reinstall-ctxctl. An import-graph guard (internal/compliance/ctxctl_isolation_test.go) asserts cmd/ctx depends on no internal/ctxctl/... package and that the shipped hooks.json carries no check-audit. The repo-local UserPromptSubmit hook invokes `ctxctl audit-relay`. Typed audit errors (per specs/ctxctl-bootstrap.md) internal/ctxctl/err/audit now returns typed, text-free errors (ReadReportError, ParseReportError, WriteDismissalError, ReadDismissalError, UnknownIDError, plus the ErrIDRequired, ErrNoFrontmatter and ErrUnterminatedFrontmatter sentinels). The user-facing English moved to tools/ctxctl, which renders the errors at the command edge through a sole printer (SilenceErrors on the root, mirroring ctx's internal/write/err.With). This closes the one place ctxctl text still lived in internal/, bringing the code into full alignment with the spec. OS-native memory pressure (correctness fix; no dedicated spec, see DECISIONS.md 2026-05-27) Replaces the broken occupancy-percentage DANGER triggers (swap_used/total, memory_used/total) with the kernel's own pressure signal. macOS reads kern.memorystatus_vm_pressure_level; Linux parses /proc/pressure/memory PSI (some.avg10 >= 10 -> Warning, full.avg10 >= 10 -> Danger); other platforms report PressureSupported=false. The dead, always-OK doctor swap row and its orphaned constants are removed. PSI thresholds are named in config/stats for one-place retuning. check-anchor-drift cleanup (per specs/experiments/acdl-session-start.md) Primary-source archaeology proved check-anchor-drift was a deliberately-retired feature (deleted in fc7db22), not a phantom; its stale commands.yaml row is pruned. Follow-up tasks were filed: plugin hooks.json version-skew fix, `ctx system` unknown-subcommand verbatim relay, and Windows memory-pressure exploration. Docs docs/recipes/audit-channel.md and the recipes index reframe the channel as maintainer-only via ctxctl with a repo-local hook; a new "Maintainer Tooling: ctxctl" section in docs/home/contributing.md documents build and install (make ctxctl / install-ctxctl / reinstall-ctxctl). The rendered docs site (site/**) and feed were regenerated. Specs added specs/ctx-add-json-ingest.md (design for `ctx <add> --json` ingest, parked) and specs/experiments/acdl-session-start.md. Tooling and context (dogfooding; unrelated items bundled per request) .gitignore un-ignores go.work/go.work.sum (now tracked for the workspace) and ignores the built ctxctl binaries (/ctxctl, tools/ctxctl/ctxctl). CLAUDE.md and AGENTS.md gain a GitNexus code-intelligence section. TASKS, DECISIONS, LEARNINGS and CONVENTIONS were updated. Verification: both modules pass `make lint` (0 issues) and `make test` (green); `go build` is clean on each. Spec: specs/ctxctl-bootstrap.md Spec: specs/experiments/acdl-session-start.md Signed-off-by: Jose Alekhinne <jose@ctx.ist>
1 parent b9ce72e commit 58b1c0f

126 files changed

Lines changed: 4944 additions & 1198 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/_ctx-surface-audit/SKILL.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
---
2-
name: ctx-surface-audit
3-
description: "Out-of-band audit: scan a git ref range for user-facing surfaces that landed without matching SKILL.md, recipe, or docs/cli updates. Run me from a SEPARATE Claude Code session, not the one that wrote the code. Drops a structured report at .context/audit/surface.md for the next interactive session's check-audit hook to relay verbatim."
2+
name: _ctx-surface-audit
3+
description: "ctx-repo-internal (note the _ prefix; sibling of _ctx-command-audit / _ctx-audit). Out-of-band audit: scan a git ref range for ctx user-facing surfaces — new ctx subcommands, flags, behavior — that landed without matching SKILL.md, recipe, or docs/cli updates. Run from a SEPARATE Claude Code session, not the one that wrote the code. Drops a report at .context/audit/surface.md for the ctxctl audit-relay hook to relay verbatim."
44
allowed-tools: Bash(git:*), Bash(rg:*), Bash(grep:*), Bash(find:*), Read, Glob, Grep, Write
55
---
66

77
You are the **surface audit**: an out-of-band reviewer that
88
catches user-facing changes landing without matching agent
99
SKILL.md, recipe, or `docs/cli` updates.
1010

11+
This skill is **internal to the ctx repository** (the `_`
12+
prefix marks it as repo-only dev tooling, like
13+
`_ctx-command-audit` and `_ctx-audit`; it is not bundled into
14+
end-user installs). It hard-codes ctx's own directory layout
15+
(`internal/cli/`, `internal/assets/commands/`,
16+
`internal/config/embed/`, `docs/recipes/`). It is the
17+
reference *producer* for the generic audit channel
18+
(`ctxctl audit` + `ctxctl audit-relay`), which lives in the
19+
maintainer-only `ctxctl` binary (not the shipped `ctx`
20+
binary); a downstream project that wants the pattern writes
21+
its own audit skill targeting its own conventions and drops
22+
reports into the same `.context/audit/` channel.
23+
1124
The whole point of this skill is **fresh-context judgment**.
1225
The agent that just shipped a feature has tunnel vision; you
1326
do not. You read the diff cold and ask: "if a user runs `ctx
@@ -165,7 +178,7 @@ Surfaces scanned: <N>
165178
Coverage checked: SKILL.md, recipes, docs/cli, integrations
166179
```
167180

168-
A `clean` report is still useful — `ctx audit list` shows
181+
A `clean` report is still useful — `ctxctl audit list` shows
169182
it with a timestamp, so the user knows the audit ran.
170183

171184
### Digest
@@ -189,7 +202,7 @@ the dismissal.
189202
frontmatter + body.
190203
8. Print a one-line summary to the user: report path,
191204
surface count, finding count, and the next-step hint
192-
("Open a working session — the check-audit hook will
205+
("Open a working session — the audit-relay hook will
193206
relay the findings on the next prompt.").
194207

195208
## Important Notes
@@ -213,10 +226,10 @@ the dismissal.
213226

214227
- `specs/audit-channel.md`: design rationale, retention
215228
policy, naming-collision notes.
216-
- `internal/cli/audit/`: CLI for `ctx audit list / show /
217-
dismiss`.
218-
- `internal/cli/system/cmd/checkaudit/`: the
219-
UserPromptSubmit hook that relays your reports.
229+
- `internal/ctxctl/cli/audit/`: logic behind `ctxctl audit
230+
list / show / dismiss`.
231+
- `internal/ctxctl/cli/checkaudit/`: the `ctxctl audit-relay`
232+
hook logic that relays your reports.
220233
- `.context/CONVENTIONS.md`
221234
*User-Facing Surface Completeness*: the canonical rule
222235
this audit enforces.

.context/CONVENTIONS.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,36 @@ The Self-check before declaring a feature commit complete is:
344344
*"If a user runs `ctx help` or asks `/ctx-<area>` to do this
345345
new thing today, will the help text / skill / recipe match
346346
what the code does?"* If no, the commit is not complete.
347+
348+
## Maintainer-Only Binaries (Layout and Installation)
349+
350+
Maintainer-only binaries — tooling that must never ship to end
351+
users — live in `tools/<name>/` as separate Go modules. The
352+
module path is lexically nested under the main ctx module
353+
(`github.com/ActiveMemory/ctx/tools/<name>`) so the new module
354+
CAN import the parent's `internal/` packages (Go's
355+
internal-import rule is path-lexical, not module-scoped — see
356+
LEARNINGS.md), reusing `rc`, `desc`, `nudge`, `config`
357+
primitives without duplication.
358+
359+
Build and install:
360+
361+
- Built to `dist/<name>` via `make <name>` (keeps the repo
362+
root clean).
363+
- PATH-installed to `/usr/local/bin/<name>` via
364+
`make install-<name>` / `make reinstall-<name>` —
365+
mirroring ctx's `install` / `reinstall` targets so one
366+
binary serves every worktree and repo copy.
367+
- The shipped `ctx` binary's `go.mod` must NOT `require` the
368+
maintainer module, giving a **hard module-graph guarantee**
369+
that the maintainer code can never leak into `ctx`.
370+
371+
Repo-local hooks calling the maintainer binary live in the
372+
gitignored `.claude/settings.local.json`, **not** in the
373+
shipped `internal/assets/claude/hooks/hooks.json`. The hook
374+
command shape is `cd "$CLAUDE_PROJECT_DIR" && <name>
375+
<subcommand>` (PATH binary, project-root cwd so `.context/`
376+
resolves correctly under cwd-anchoring).
377+
378+
`tools/ctxctl/` is the first inhabitant. Future maintainer
379+
binaries follow the same shape.

.context/DECISIONS.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
<!-- INDEX:START -->
44
| Date | Decision |
55
|----|--------|
6+
| 2026-05-28 | ctxctl PATH-installed alongside ctx for clean roots and one binary across worktrees |
7+
| 2026-05-28 | Memory pressure detection uses OS-native signals (macOS pressure level + Linux PSI), not occupancy |
8+
| 2026-05-27 | ctxctl is a separate Go module at tools/ctxctl (own go.mod), not cmd/ctxctl in the same module |
69
| 2026-05-24 | ctxctl lives at cmd/ctxctl in the same Go module, not a separate go.mod |
710
| 2026-05-24 | Discipline enforcement belongs on the verbatim-relay channel, run out-of-band |
811
| 2026-05-24 | Pad snapshot-on-mutate at the store.WriteEntries choke point |
@@ -154,6 +157,48 @@ For significant decisions:
154157
155158
-->
156159

160+
## [2026-05-28-201000] ctxctl PATH-installed alongside ctx for clean roots and one binary across worktrees
161+
162+
**Status**: Accepted
163+
164+
**Context**: Initial ctxctl design wired the hook to `./ctxctl` at repo root, forcing a per-worktree build, dirtying the root, and contradicting the project's PATH-only convention (`block-non-path-ctx` enforces it for ctx).
165+
166+
**Decision**: ctxctl PATH-installed alongside ctx for clean roots and one binary across worktrees
167+
168+
**Rationale**: Mirror ctx's install pattern: build to `dist/`, install to `/usr/local/bin/ctxctl`. One binary serves all worktrees and repo copies; the local hook calls `ctxctl` from PATH so no repo-root binary is needed. Defensive `/ctxctl` + `tools/ctxctl/ctxctl` gitignores stay so stray binaries can never be committed.
169+
170+
**Consequence**: New Makefile targets `install-ctxctl` and `reinstall-ctxctl` mirror `install`/`reinstall`. Hook in `.claude/settings.local.json`: `cd "$CLAUDE_PROJECT_DIR" && ctxctl audit-relay`. Sets the convention for future maintainer-only binaries (`tools/<name>/` separate module, `dist/` build, PATH install). `specs/ctxctl-bootstrap.md` Interface section updated to match.
171+
172+
---
173+
174+
## [2026-05-28-200500] Memory pressure detection uses OS-native signals (macOS pressure level + Linux PSI), not occupancy
175+
176+
**Status**: Accepted
177+
178+
**Context**: `check-resource` alerted DANGER at swap-used ≥ 75% / memory-used ≥ 90% — pure occupancy. macOS swap is sticky (never recedes); post-hibernation swap stays >75% with idle RAM, producing false "wrap up the session" DANGER at session start. Memory occupancy on macOS includes reclaimable cache — also a poor pressure proxy.
179+
180+
**Decision**: Memory pressure detection uses OS-native signals (macOS pressure level + Linux PSI), not occupancy
181+
182+
**Rationale**: Occupancy is a level; pressure is a derivative. Only the kernel's derivative reflects current struggle. macOS: `sysctl kern.memorystatus_vm_pressure_level` (1/2/4 → OK/Warning/Danger). Linux: `/proc/pressure/memory` (PSI) `some.avg10 ≥ 10.0` → warn, `full.avg10 ≥ 10.0` → danger. Windows: filed as an exploratory task; unsupported for now ("other" platform falls through to `PressureSupported=false`, no alert).
183+
184+
**Consequence**: `MemInfo` gains `Pressure` + `PressureSupported`; `threshold.go` drops both occupancy `byteCheck`s and emits a single pressure alert. Doctor swap row removed (no longer a health signal); occupancy fields retained for `ctx stats` display. PSI 10.0 defaults named in `config/stats` — retunable in one place. `make lint` 0 issues, `make test` ok on the change.
185+
186+
---
187+
188+
## [2026-05-27-161302] ctxctl is a separate Go module at tools/ctxctl (own go.mod), not cmd/ctxctl in the same module
189+
190+
**Status**: Accepted
191+
192+
**Context**: Migrating the maintainer-only audit channel out of the ctx binary (specs/ctxctl-bootstrap.md). The prior decision (handover 2026-05-26) chose same-module cmd/ctxctl, on the belief that a separate go.mod could not import ctx's internal/ packages and would force relocating/duplicating ~25 files.
193+
194+
**Decision**: ctxctl is a separate Go module at tools/ctxctl (own go.mod), not cmd/ctxctl in the same module
195+
196+
**Rationale**: That blocker was empirically disproved this session: a nested module whose path is lexically under github.com/ActiveMemory/ctx CAN import the parent module's internal/ packages (verified by build test; a non-nested 'outsider' module path is rejected). Given that, a hard module boundary beats an in-module import-graph test for the asymmetric requirement that actually matters: ctx must never break because of ctxctl. ctx's go.mod will not require tools/ctxctl, so ctx literally cannot import ctxctl; the one-directional ctxctl->ctx coupling is acceptable because ctxctl is disposable maintainer tooling ('nobody whines if ctxctl breaks; everyone suffers if ctxctl leaks into ctx'). Full self-containment (duplicating the ~20 shared internal foundations: rc, desc, config, nudge, io...) was rejected as a DRY catastrophe and a worse broken window than the one being fixed.
197+
198+
**Consequence**: New module tools/ctxctl (module path github.com/ActiveMemory/ctx/tools/ctxctl) reuses ctx's internal/ foundations in place; audit-channel-specific logic relocates to internal/ctxctl/; ctxctl owns its relay/CLI text as plain English Go constants under tools/ctxctl (no YAML localization, no desc/i18n engine for its own output -- no French ctxctl); a repo-root go.work (committed) wires the workspace; an import-graph guard test asserts cmd/ctx never imports internal/ctxctl. Supersedes the same-module cmd/ctxctl decision. specs/ctxctl-bootstrap.md is rewritten to match.
199+
200+
---
201+
157202
## [2026-05-24-123908] ctxctl lives at cmd/ctxctl in the same Go module, not a separate go.mod
158203

159204
**Status**: Accepted

.context/LEARNINGS.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ DO NOT UPDATE FOR:
1717
<!-- INDEX:START -->
1818
| Date | Learning |
1919
|----|--------|
20+
| 2026-05-28 | Swap occupancy is not memory pressure — use the kernel's derivative |
21+
| 2026-05-28 | A non-root Go module nested under the main module's path CAN import its internal/ packages |
22+
| 2026-05-28 | cobra's legacyArgs lets unknown subcommands silently succeed on non-root groups |
2023
| 2026-05-25 | Skill shipping location: _ctx- prefix is repo-internal, internal/assets/claude/skills/ctx-* is bundled and shipped |
2124
| 2026-05-24 | Audit gates that bite when introducing new packages and helpers |
2225
| 2026-05-23 | Spec-trailer improvisation is heuristic drift — when no spec genuinely fits, the failure mode is reaching for the most-recent one |
@@ -162,6 +165,36 @@ DO NOT UPDATE FOR:
162165

163166
---
164167

168+
## [2026-05-28-201500] Swap occupancy is not memory pressure — use the kernel's derivative
169+
170+
**Context**: ctx's `check-resource` UserPromptSubmit hook alerted DANGER at swap-used ≥ 75% / memory-used ≥ 90%, generating false "wrap up the session" warnings at session start after hibernation. On macOS, swap doesn't recede when pressure ends — it's a sticky high-water mark, so static occupancy carries zero current information about whether the system is actually struggling.
171+
172+
**Lesson**: macOS and Windows swap proactively, and swap occupancy is STICKY — it doesn't recede when pressure ends. After hibernation, swap can be >75% full with zero current pressure. Any alert keyed on `SwapUsed/SwapTotal ≥ X%` will false-positive at session start. The signal isn't the *level*, it's the *derivative* — pages actively being pushed out, or the kernel's own pressure metric.
173+
174+
**Application**: For host-pressure detection, key on OS-native pressure signals (macOS `kern.memorystatus_vm_pressure_level` 1/2/4 → OK/Warning/Danger; Linux PSI `/proc/pressure/memory` `some.avg10` and `full.avg10`). These are kernel-computed derivatives — no snapshot state needed and they collapse to zero when the pressure ends. If native is unavailable, fall back to swap-out RATE (snapshot delta) gated on low available memory; never to occupancy alone. (Decision recorded same date; Windows exploratory task filed under Phase CLI-FIX.)
175+
176+
---
177+
178+
## [2026-05-28-201400] A non-root Go module nested under the main module's path CAN import its internal/ packages
179+
180+
**Context**: While designing the ctxctl module split, the initial spec (and a lot of online consensus) claimed a separate `go.mod` cannot import the parent module's `internal/` packages, which would have forced relocating or duplicating ~25 foundation packages (`rc`, `desc`, `nudge`, `config/*`, …). The "obvious" reading made same-module the only viable option.
181+
182+
**Lesson**: Go's internal-import rule is **lexical on import paths, not module-scoped**. A separate module whose path is `github.com/<owner>/<main>/tools/<x>` CAN import `github.com/<owner>/<main>/internal/...` — verified by an empirical build experiment this session. An outsider path (`example.com/...`) is rejected with `use of internal package … not allowed`. The rule fires on the import-path prefix relative to the `internal/` directory's parent, not on module boundaries.
183+
184+
**Application**: For monorepo splits (maintainer-only tooling, isolated experiments, ancillary CLIs), choose a module path nested under the main module so the new module reuses the parent's foundations via the lexical-internal allowance. Full self-containment of a maintainer module would be a DRY catastrophe; the lexical allowance is the correct shape. Prove it with a throwaway `go build` against a representative `internal/` import before designing around the *wrong* constraint.
185+
186+
---
187+
188+
## [2026-05-28-201300] cobra's legacyArgs lets unknown subcommands silently succeed on non-root groups
189+
190+
**Context**: Every prompt of this session injected 52 lines of `ctx system` help text into agent context, labeled "hook success." Investigation traced it to the 0.8.1 plugin's `hooks.json` wiring `ctx system check-anchor-drift` as the first UserPromptSubmit hook — a command the 0.8.1 binary no longer has (the command was deleted by the cwd-anchored migration in `fc7db228`, but the plugin's hook config wasn't updated). The harness reported "hook success" because cobra exits 0 on the unknown subcommand.
191+
192+
**Lesson**: cobra's `legacyArgs` only raises "unknown command" for the **root** command (`!cmd.HasParent()`); any non-root group (built with `parent.Cmd`) treats an unknown subcommand as non-error: it falls through to `Help()` and returns nil → exit 0. In a UserPromptSubmit hook this is **invisible** — the harness logs "hook success" and injects the whole help text into agent context every prompt. The 0.8.1 plugin's stale wiring of the retired `check-anchor-drift` caused exactly this for the entire session.
193+
194+
**Application**: Non-root cobra groups must have an explicit unknown-subcommand guard. Two routes: (a) `Args: cobra.NoArgs` so unknown subcommands error loud (non-zero exit + "unknown command" stderr); (b) a `RunE` that emits a **verbatim relay** — which is what actually reaches the user in a UserPromptSubmit hook context where a non-zero exit alone is invisible. Tracked under Phase CLI-FIX as the verbatim-relay guard on `ctx system`.
195+
196+
---
197+
165198
## [2026-05-25-221357] Skill shipping location: _ctx- prefix is repo-internal, internal/assets/claude/skills/ctx-* is bundled and shipped
166199

167200
**Context**: Created /ctx-surface-audit under internal/assets/claude/skills/ (the shipped path), but it audits ctx's own internal/ source layout — useless in an end-user project that installs ctx. There is an established _ctx-* family (_ctx-command-audit, _ctx-audit, _ctx-release, _ctx-qa, etc.) in .claude/skills/ for repo-only dev skills; the user caught the misplacement.

0 commit comments

Comments
 (0)