feat: agent instructions editor in Settings #22

Manually merged
lz merged 17 commits from feat/agent-instructions into main 2026-05-26 18:31:56 +02:00
Owner

Summary

A discoverable Settings tab for editing the shared ~/.claude/CLAUDE.md file that every claude session sees as system context. Nexus ships a sensible default documenting the affordances agents need to know about (notify-preview, the worktree/branch model, what they can't do), seeds it on first boot, and respects operator deletions thereafter.

  • New module nexus/src/lib/server/agent-instructions/ with read / write / resetToDefault against a single hardcoded path under SHARED_CONFIG_DIR.
  • Boot-time seedOnBoot is one-shot, gated by an instance_settings flag — deleting the file post-seed stays deleted; "Reset to default" in the UI is the explicit way back.
  • Three REST routes (GET / PUT + POST /reset), all auth-gated by the existing hooks.
  • Third subtab on /settings with a monospace textarea, Save, Reset to default, byte counter, beforeunload guard against accidental loss.
  • Future-proofed naming ("agent-instructions" not "claude-md") for eventual multi-orchestrator support.

Spec: docs/superpowers/specs/2026-05-25-agent-instructions-editor.md
Plan: docs/superpowers/plans/2026-05-25-agent-instructions-editor.md

Depends on

PR #21 (operator-approved port previews). The bundled default CLAUDE.md documents the notify-preview helper that PR introduces. Land port previews first, then rebase or merge main into this branch before merge.

Implementation notes

  • The default file is shipped in nexus/static/default-CLAUDE.md and pinned to LF via .gitattributes (so Windows contributors can't accidentally check in CRLF that would break the isDefault byte-equality check on every fresh install).
  • seed.ts writes both agent_instructions_seeded='true' and agent_instructions_last_seeded_hash=<sha256> to instance_settings. The hash is unused by v1 logic but enables future drift detection (v2) without needing a migration.
  • service.ts:resetToDefault deliberately bypasses the max_file_bytes size check because the bundled default is trusted, code-shipped content.
  • The file is also visible in the Config Explorer at the same path — both UIs read/write the same bytes; the Settings tab is the discoverable shortcut.

Review status

Per-task TDD + code-quality reviews ran during implementation. The pure helpers (paths, default, hash), service, seed, route handlers all got both spec and quality review with follow-up fix commits where reviewers flagged issues (path arithmetic in the prod-adapter-node resolver, ENOENT discrimination in seed's existence check, error-type pinning in tests, etc.). A final cross-cutting review confirmed end-to-end coherence. The Settings UI panel and AGENTS.md fact skipped per-task quality review to keep cadence.

Test plan

  • CI runs pnpm --dir nexus test on node:22-alpine. Locally on Windows, seed tests skip via the sqliteAvailable pattern; CI exercises them fully.
  • pnpm --dir nexus exec tsc --noEmit passes locally; CI re-runs.
  • Manual smoke per the spec's Verification section — not yet run:
    • Fresh install: textarea loads with the bundled default; "showing the shipped default" hint visible; worker's ~/.claude/CLAUDE.md matches.
    • Edit + save propagates: append text in the textarea, save, spawn a worker; the new session's CLAUDE.md contains the edit.
    • Reset to default: edit + save, then click Reset; textarea reverts to bundled default; hint reappears.
    • Operator deletes via Config Explorer: empty textarea on next GET; isDefault: false; Reset recreates the file.
    • Edit + close tab: browser shows "Leave site?"; cancel keeps edits; confirm discards.
    • Edit + subtab switch: silently discards (documented behavior).
    • Oversize write: paste >256 KB; byte counter turns red; Save disabled; server returns 400 if forced.
    • Hash recorded: SELECT * FROM instance_settings WHERE key LIKE 'agent_instructions%' shows two rows after first boot.
    • LF normalization: git check-attr eol nexus/static/default-CLAUDE.md reports lf.
## Summary A discoverable Settings tab for editing the shared `~/.claude/CLAUDE.md` file that every claude session sees as system context. Nexus ships a sensible default documenting the affordances agents need to know about (notify-preview, the worktree/branch model, what they can't do), seeds it on first boot, and respects operator deletions thereafter. - New module `nexus/src/lib/server/agent-instructions/` with `read` / `write` / `resetToDefault` against a single hardcoded path under `SHARED_CONFIG_DIR`. - Boot-time `seedOnBoot` is one-shot, gated by an `instance_settings` flag — deleting the file post-seed stays deleted; "Reset to default" in the UI is the explicit way back. - Three REST routes (`GET` / `PUT` + `POST /reset`), all auth-gated by the existing hooks. - Third subtab on `/settings` with a monospace textarea, Save, Reset to default, byte counter, `beforeunload` guard against accidental loss. - Future-proofed naming ("agent-instructions" not "claude-md") for eventual multi-orchestrator support. Spec: docs/superpowers/specs/2026-05-25-agent-instructions-editor.md Plan: docs/superpowers/plans/2026-05-25-agent-instructions-editor.md ## Depends on **PR #21 (operator-approved port previews).** The bundled default CLAUDE.md documents the `notify-preview` helper that PR introduces. Land port previews first, then rebase or merge `main` into this branch before merge. ## Implementation notes - The default file is shipped in `nexus/static/default-CLAUDE.md` and pinned to LF via `.gitattributes` (so Windows contributors can't accidentally check in CRLF that would break the `isDefault` byte-equality check on every fresh install). - `seed.ts` writes both `agent_instructions_seeded='true'` and `agent_instructions_last_seeded_hash=<sha256>` to `instance_settings`. The hash is unused by v1 logic but enables future drift detection (v2) without needing a migration. - `service.ts:resetToDefault` deliberately bypasses the `max_file_bytes` size check because the bundled default is trusted, code-shipped content. - The file is also visible in the Config Explorer at the same path — both UIs read/write the same bytes; the Settings tab is the discoverable shortcut. ## Review status Per-task TDD + code-quality reviews ran during implementation. The pure helpers (paths, default, hash), service, seed, route handlers all got both spec and quality review with follow-up fix commits where reviewers flagged issues (path arithmetic in the prod-adapter-node resolver, ENOENT discrimination in seed's existence check, error-type pinning in tests, etc.). A final cross-cutting review confirmed end-to-end coherence. The Settings UI panel and AGENTS.md fact skipped per-task quality review to keep cadence. ## Test plan - [ ] CI runs `pnpm --dir nexus test` on `node:22-alpine`. Locally on Windows, seed tests skip via the `sqliteAvailable` pattern; CI exercises them fully. - [ ] `pnpm --dir nexus exec tsc --noEmit` passes locally; CI re-runs. - [ ] Manual smoke per the spec's Verification section — **not yet run**: - [x] Fresh install: textarea loads with the bundled default; "showing the shipped default" hint visible; worker's `~/.claude/CLAUDE.md` matches. - [x] Edit + save propagates: append text in the textarea, save, spawn a worker; the new session's CLAUDE.md contains the edit. - [x] Reset to default: edit + save, then click Reset; textarea reverts to bundled default; hint reappears. - [ ] Operator deletes via Config Explorer: empty textarea on next GET; `isDefault: false`; Reset recreates the file. - [ ] Edit + close tab: browser shows "Leave site?"; cancel keeps edits; confirm discards. - [x] Edit + subtab switch: silently discards (documented behavior). - [ ] Oversize write: paste >256 KB; byte counter turns red; Save disabled; server returns 400 if forced. - [ ] Hash recorded: `SELECT * FROM instance_settings WHERE key LIKE 'agent_instructions%'` shows two rows after first boot. - [ ] LF normalization: `git check-attr eol nexus/static/default-CLAUDE.md` reports `lf`.
lz added 16 commits 2026-05-26 07:34:38 +02:00
Documents a built-in Settings tab for editing the shared CLAUDE.md
that ships seeded with Nexus. Bundled default, atomic write, reset-to-
default, beforeunload guard against accidental loss.

Depends on PR #21 (port previews) — the bundled default documents the
notify-preview helper introduced by that PR.
11 tasks: bundled default + .gitattributes pin, paths/default/service/seed
modules with TDD, boot wiring, REST routes, API client, Settings subtab
panel, AGENTS.md fact, smoke verification, PR open (depends on #21).
The default is shipped in nexus/static/ so it's bundled with adapter-node's
build output and readable at runtime via fs.readFileSync. The .gitattributes
entry prevents Windows contributors from accidentally committing CRLF that
would make isDefault checks fail on every fresh install.
Previous commit accidentally wrote '@AGENTS.md' (an import directive) into
the file instead of the actual default content from the spec's Appendix A.
- paths.ts: hardcoded path under SHARED_CONFIG_DIR
- default.ts: reads nexus/static/default-CLAUDE.md once at module init,
  exports DEFAULT_CONTENT (LF-normalized) + DEFAULT_HASH (sha256 hex)
- Path resolution tries dev (vitest) and prod (adapter-node) layouts
- 5 tests including hash correctness and LF-only invariant
Per code review:
- The prod adapter-node candidate path walked one '..' too few. Chunks
  live at build/server/chunks/, so reaching build/client/ needs two
  '..' hops. Previously masked by the CWD fallback; now correct.
- LF normalization regex was /\r\n/g, which misses bare \r. Widened
  to /\r\n?/g to also collapse lone CR (classic-Mac line endings).
- read: returns {content, isDefault}; missing file is empty + isDefault=false
- write: atomic .tmp + rename; rejects content over max_file_bytes
- resetToDefault: writes DEFAULT_CONTENT, returns it
- 10 tests with mocked paths/singletons/default + real FS in a tmpdir
- Runs on Windows (no sqlite dependency)
Per code review:
- service.ts: drop redundant Number() wrap; destructure getInstanceSetting's
  value field to match every other callsite in the codebase.
- service.test.ts: tighten the 'rethrows non-ENOENT' assertion from a bare
  .rejects.toThrow() to .rejects.toMatchObject({ code: 'EISDIR' }) so the
  test can't silently pass on a future mock-induced ENOENT regression.
- Writes DEFAULT_CONTENT to disk only if file missing (respects pre-placed files)
- Upserts agent_instructions_seeded='true' + agent_instructions_last_seeded_hash
- No-op when flag is already 'true' (operator deletion stays deleted)
- 4 tests; skip on Windows per the sqliteAvailable pattern
Per code review:
- The bare catch on fs.access was treating EACCES/EISDIR/EIO as 'file
  absent' and proceeding to overwrite. Non-ENOENT errors now propagate
  to seedOnBoot's caller (server.ts wraps in .catch + logger.warn),
  giving operators a clear diagnostic instead of a downstream writeFile
  failure masking the real cause. Matches the discrimination pattern
  used in service.ts read().
- GET /api/settings/agent-instructions → {content, isDefault, maxBytes}
- PUT /api/settings/agent-instructions {content} → 204 / 400 on oversize / 500 on IO
- POST /api/settings/agent-instructions/reset → {content, isDefault: true}
- Auth-gated by hooks.server.ts (no carve-out needed; operator-only by default)
- /settings adds a third subtab 'Agent instructions'
- AgentInstructionsPanel: plain textarea, Save/Reset, byte counter,
  isDefault hint, beforeunload guard, monospace, vertically resizable
- Subtab switches silently discard (documented in panel description)
- api.getAgentInstructions / setAgentInstructions / resetAgentInstructions
  added to client.ts to wire the panel to existing API routes
Calls seedOnBoot after runMigrations, best-effort .catch so a seed
failure doesn't block startup.
getAgentInstructions / setAgentInstructions / resetAgentInstructions —
plain JSON body for content (not base64).
fix(agent-instructions): dedup client methods from cherry-pick auto-merge
Some checks failed
ci / nexus (pull_request) Failing after 1m40s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
8d94c3afdf
Cherry-picking 73e261e on top of 0e56ae9 (which had inadvertently also
added the three client methods during the Settings UI task) left duplicate
property declarations. Removed the second copy; kept the first because
its resetAgentInstructions return type pins isDefault: true (literal)
instead of isDefault: boolean.
Author
Owner

When developing through the proxy from PR #21, agents currently have to be told about it after something breaks, e.g. a Next.js dev server starts fine, but the browser-side HMR socket hits ws://NEXUS_SERVER:8080/... and Next.js refuses it as a cross-origin dev request. The fix (allowedDevOrigins: ['devops.lan'] in next.config.js) is trivial once you know the hostname; the cost is the round-trip of discovery.

The proxy hostname should be injected into CLAUDE.md so agents see it from turn one. That means dev-server setups (Next.js allowedDevOrigins, Vite server.hmr.host, CRA WDS_SOCKET_HOST, etc.) can be configured up front.

When developing through the proxy from PR #21, agents currently have to be told about it after something breaks, e.g. a Next.js dev server starts fine, but the browser-side HMR socket hits ws://NEXUS_SERVER:8080/... and Next.js refuses it as a cross-origin dev request. The fix (allowedDevOrigins: ['devops.lan'] in next.config.js) is trivial once you know the hostname; the cost is the round-trip of discovery. The proxy hostname should be injected into CLAUDE.md so agents see it from turn one. That means dev-server setups (Next.js allowedDevOrigins, Vite server.hmr.host, CRA WDS_SOCKET_HOST, etc.) can be configured up front.
fix(agent-instructions): add esbuild-bundle candidate to path resolver
Some checks failed
ci / nexus (pull_request) Failing after 1m31s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
f9a746472a
Production adapter-node uses esbuild to inline everything into a single
/app/build/server.js (not Rollup chunks at build/server/chunks/). So
import.meta.url resolves to file:///app/build/server.js, dirname=/app/build,
and the bundled default file is at /app/build/client/default-CLAUDE.md
(no .. hops needed).

Caught at runtime: 'could not locate default-CLAUDE.md; tried: /static/...,
/client/..., /app/static/...'. Added <dir>/client/default-CLAUDE.md as the
first candidate; kept the rollup-chunks and dev/vitest candidates as
fallbacks.
lz manually merged commit f9a4d56a1a into main 2026-05-26 18:31:56 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
lz/agent-nexus!22
No description provided.