diff --git a/apps/backend/Makefile b/apps/backend/Makefile index 1e56cf8e62..9a400e6cc2 100644 --- a/apps/backend/Makefile +++ b/apps/backend/Makefile @@ -7,6 +7,16 @@ GO := go GOFLAGS := -v LDFLAGS := -s -w +# Debug launches get a distinct browser identity by default. An explicit +# environment or command-line value still wins through Make's `?=` semantics. +KANDEV_WEB_TITLE_PREFIX ?= Debug + +# Quote the debug title prefix for the POSIX recipe. The replacement keeps +# embedded single quotes safe while preserving spaces and shell metacharacters. +single_quote := ' +single_quote_escape := '\'' +shell_quote = $(single_quote)$(subst $(single_quote),$(single_quote_escape),$(1))$(single_quote) + # Cross-platform commands # # Two independent axes decide these variables; conflating them breaks Git @@ -50,8 +60,8 @@ ifeq ($(OS)$(MSYSTEM),Windows_NT) GO_DARWIN_AMD64 = set CGO_ENABLED=0& set GOOS=darwin& set GOARCH=amd64& # Run-target env prefixes and binary invocation. cmd.exe has no `NAME=VAL cmd` # syntax (use `set NAME=VAL&`) and no `exec`; the binary path needs backslashes. - DEV_ENV = set KANDEV_DEBUG_PPROF_ENABLED=true& - DEBUG_ENV = set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& + DEV_ENV = set KANDEV_DEBUG_DEV_MODE=true& + DEBUG_ENV = set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=$(KANDEV_WEB_TITLE_PREFIX)"& EXEC = RUN_BIN = "$(subst /,\,$(BUILD_DIR)/$(BINARY_NAME)$(EXE))" # check-make-shells is a bash script; native cmd.exe can't run its shebang, so @@ -75,8 +85,8 @@ else GO_LINUX_ARM64 = CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO_DARWIN_ARM64 = CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 GO_DARWIN_AMD64 = CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 - DEV_ENV = KANDEV_DEBUG_PPROF_ENABLED=true - DEBUG_ENV = KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug + DEV_ENV = KANDEV_DEBUG_DEV_MODE=true + DEBUG_ENV = KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX=$(call shell_quote,$(KANDEV_WEB_TITLE_PREFIX)) EXEC = exec RUN_BIN = ./$(BUILD_DIR)/$(BINARY_NAME)$(EXE) CHECK_SHELLS = @../../scripts/check-make-shells diff --git a/apps/backend/internal/launcher/env.go b/apps/backend/internal/launcher/env.go index 1c32b53d51..9c6e2e86f9 100644 --- a/apps/backend/internal/launcher/env.go +++ b/apps/backend/internal/launcher/env.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "os" + "strings" ) const healthTokenBytes = 32 @@ -30,6 +31,7 @@ func backendEnv(ports portConfig, logLevel, consoleLogLevel string, debug bool, if debug { env = upsertEnv(env, "KANDEV_DEBUG_AGENT_MESSAGES", "true") env = upsertEnv(env, "KANDEV_DEBUG_PPROF_ENABLED", "true") + env = setEnvIfUnset(env, "KANDEV_WEB_TITLE_PREFIX", "Debug") } return env } @@ -37,10 +39,20 @@ func backendEnv(ports portConfig, logLevel, consoleLogLevel string, debug bool, func upsertEnv(env []string, key, value string) []string { prefix := key + "=" for i, item := range env { - if len(item) >= len(prefix) && item[:len(prefix)] == prefix { + if strings.HasPrefix(item, prefix) { env[i] = prefix + value return env } } return append(env, prefix+value) } + +func setEnvIfUnset(env []string, key, value string) []string { + prefix := key + "=" + for _, item := range env { + if strings.HasPrefix(item, prefix) { + return env + } + } + return append(env, prefix+value) +} diff --git a/apps/backend/internal/launcher/start_test.go b/apps/backend/internal/launcher/start_test.go index be9f1349ce..d2f79fd4b1 100644 --- a/apps/backend/internal/launcher/start_test.go +++ b/apps/backend/internal/launcher/start_test.go @@ -56,6 +56,24 @@ func TestBackendEnvCarriesIndependentThresholds(t *testing.T) { } } +func TestBackendEnvDebugDefaultsToDebugTitle(t *testing.T) { + unsetEnvForTest(t, "KANDEV_WEB_TITLE_PREFIX") + + env := backendEnv(portConfig{BackendPort: 1234, AgentctlPort: 5678}, "debug", "warn", true, "health-token") + if got := envValue(env, "KANDEV_WEB_TITLE_PREFIX"); got != "Debug" { + t.Fatalf("KANDEV_WEB_TITLE_PREFIX = %q for debug launch; want %q", got, "Debug") + } +} + +func TestBackendEnvDebugPreservesExplicitTitle(t *testing.T) { + t.Setenv("KANDEV_WEB_TITLE_PREFIX", "Custom") + + env := backendEnv(portConfig{BackendPort: 1234, AgentctlPort: 5678}, "debug", "warn", true, "health-token") + if got := envValue(env, "KANDEV_WEB_TITLE_PREFIX"); got != "Custom" { + t.Fatalf("KANDEV_WEB_TITLE_PREFIX = %q for explicit debug launch; want %q", got, "Custom") + } +} + func TestNewHealthTokenIsFreshAndOpaque(t *testing.T) { first, err := newHealthToken() if err != nil { @@ -192,3 +210,28 @@ func TestRunManagedAppAttachesSignalsBeforeBackendLaunch(t *testing.T) { t.Fatalf("health token launched=%q waited=%q, want one shared non-empty token", launchedHealthToken, waitedHealthToken) } } + +func envValue(env []string, key string) string { + prefix := key + "=" + for _, item := range env { + if strings.HasPrefix(item, prefix) { + return strings.TrimPrefix(item, prefix) + } + } + return "" +} + +func unsetEnvForTest(t *testing.T, key string) { + t.Helper() + value, wasSet := os.LookupEnv(key) + if err := os.Unsetenv(key); err != nil { + t.Fatalf("unset %s: %v", key, err) + } + t.Cleanup(func() { + if wasSet { + _ = os.Setenv(key, value) + return + } + _ = os.Unsetenv(key) + }) +} diff --git a/apps/backend/internal/launcher/supervisor.go b/apps/backend/internal/launcher/supervisor.go index f2336ece83..9880d58a44 100644 --- a/apps/backend/internal/launcher/supervisor.go +++ b/apps/backend/internal/launcher/supervisor.go @@ -176,6 +176,7 @@ func allowedSupervisorEnv(env []string) map[string]string { "KANDEV_AGENT_STANDALONE_PORT": true, "KANDEV_LOG_LEVEL": true, "KANDEV_CONSOLE_LOG_LEVEL": true, + "KANDEV_WEB_TITLE_PREFIX": true, "KANDEV_DEBUG_DEV_MODE": true, "KANDEV_DEBUG_AGENT_MESSAGES": true, "KANDEV_DEBUG_PPROF_ENABLED": true, diff --git a/apps/backend/internal/launcher/supervisor_test.go b/apps/backend/internal/launcher/supervisor_test.go index 6067aaa6f9..1bcab3f41a 100644 --- a/apps/backend/internal/launcher/supervisor_test.go +++ b/apps/backend/internal/launcher/supervisor_test.go @@ -13,6 +13,7 @@ func TestBuildManifestUsesSameBinaryBackendMode(t *testing.T) { "KANDEV_SERVER_PORT=1234", "KANDEV_DESKTOP_HEALTH_TOKEN=health-token", "KANDEV_CONSOLE_LOG_LEVEL=warn", + "KANDEV_WEB_TITLE_PREFIX=Debug", "UNRELATED=value", } manifest := buildManifest("/opt/kandev/bin/kandev", []string{"__backend"}, "/opt/kandev/bin", env, "/tmp/home", 1234, "run") @@ -35,6 +36,9 @@ func TestBuildManifestUsesSameBinaryBackendMode(t *testing.T) { if manifest.Env["KANDEV_DESKTOP_HEALTH_TOKEN"] != "health-token" { t.Fatalf("KANDEV_DESKTOP_HEALTH_TOKEN = %q", manifest.Env["KANDEV_DESKTOP_HEALTH_TOKEN"]) } + if manifest.Env["KANDEV_WEB_TITLE_PREFIX"] != "Debug" { + t.Fatalf("KANDEV_WEB_TITLE_PREFIX = %q", manifest.Env["KANDEV_WEB_TITLE_PREFIX"]) + } } func TestPrepareSupervisorEnvWritesExpectedPaths(t *testing.T) { diff --git a/apps/backend/internal/profiles/profiles.go b/apps/backend/internal/profiles/profiles.go index fce283c184..e171994f34 100644 --- a/apps/backend/internal/profiles/profiles.go +++ b/apps/backend/internal/profiles/profiles.go @@ -69,7 +69,7 @@ func DetectEnvironment() Environment { if isTruthy(os.Getenv("KANDEV_E2E_MOCK")) { return EnvE2E } - if isTruthy(os.Getenv("KANDEV_DEBUG_DEV_MODE")) || isTruthy(os.Getenv("KANDEV_DEBUG_PPROF_ENABLED")) { + if isTruthy(os.Getenv("KANDEV_DEBUG_DEV_MODE")) { return EnvDev } return EnvProd diff --git a/apps/backend/internal/profiles/profiles.yaml b/apps/backend/internal/profiles/profiles.yaml index 5f5c6c3c9d..0019ab7b16 100644 --- a/apps/backend/internal/profiles/profiles.yaml +++ b/apps/backend/internal/profiles/profiles.yaml @@ -134,8 +134,8 @@ debug: dev: "true" e2e: "" - # Legacy alias for KANDEV_DEBUG_DEV_MODE — kept for compatibility - # with older deploy scripts that grep for "pprof". + # Legacy debug behavior variable — kept for compatibility with older + # deploy scripts that enable pprof. It does not select the dev profile. KANDEV_DEBUG_PPROF_ENABLED: prod: "" dev: "true" diff --git a/apps/backend/internal/profiles/profiles_test.go b/apps/backend/internal/profiles/profiles_test.go index 1e4c5769ed..18df55a27d 100644 --- a/apps/backend/internal/profiles/profiles_test.go +++ b/apps/backend/internal/profiles/profiles_test.go @@ -94,6 +94,26 @@ func TestApplyProfile_DevUsesDevelopmentDefaults(t *testing.T) { } } +func TestApplyProfile_PprofDebugKeepsProductionProfile(t *testing.T) { + clearProfileSelectors(t) + clearProfilesYAMLVars(t) + t.Setenv("KANDEV_DEBUG_PPROF_ENABLED", "true") + + _, env, err := ApplyProfile() + if err != nil { + t.Fatalf("ApplyProfile: %v", err) + } + if env != EnvProd { + t.Fatalf("env = %q with pprof-only debug; want %q", env, EnvProd) + } + if v := os.Getenv("KANDEV_FEATURES_OFFICE"); v != "false" { + t.Errorf("KANDEV_FEATURES_OFFICE = %q with pprof-only debug; want %q", v, "false") + } + if _, ok := os.LookupEnv("KANDEV_WEB_TITLE_PREFIX"); ok { + t.Error("KANDEV_WEB_TITLE_PREFIX was set by the production profile; want it unset") + } +} + func TestApplyProfile_ExplicitTitlePrefixWinsInDev(t *testing.T) { clearProfileSelectors(t) clearProfilesYAMLVars(t) diff --git a/apps/web/e2e/tests/system/title-prefix.spec.ts b/apps/web/e2e/tests/system/title-prefix.spec.ts index fa4a357288..56dd6abd3c 100644 --- a/apps/web/e2e/tests/system/title-prefix.spec.ts +++ b/apps/web/e2e/tests/system/title-prefix.spec.ts @@ -1,6 +1,22 @@ import { expect, test } from "../../fixtures/test-base"; test.describe.serial("environment browser title prefixes", () => { + test("uses Debug for a diagnostics-only production profile", async ({ backend, testPage }) => { + await backend.restart({ + KANDEV_E2E_MOCK: "", + KANDEV_DEBUG_DEV_MODE: "", + KANDEV_DEBUG_PPROF_ENABLED: "true", + KANDEV_WEB_TITLE_PREFIX: "Debug", + }); + + try { + await testPage.goto("/"); + await expect(testPage).toHaveTitle("Debug Kandev"); + } finally { + await backend.restart(); + } + }); + test("uses Dev for the development profile", async ({ backend, testPage }) => { await backend.restart({ KANDEV_E2E_MOCK: "", KANDEV_DEBUG_DEV_MODE: "true" }); diff --git a/docs/configuration.md b/docs/configuration.md index 320cbd22f8..1fed3caf90 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -34,9 +34,10 @@ All env vars use the `KANDEV_` prefix. Nested keys map by replacing `.` with `_` The aliases on the right are explicit bindings - see `LoadWithPath` in `config.go` for the full list. New keys should follow the deterministic rule (`KANDEV_
_`) unless there is a reason to add an alias. -The `make dev` launcher uses `Dev` as the default browser title prefix, and PR -preview environments use `Preview`. An explicit `KANDEV_WEB_TITLE_PREFIX` -value overrides those environment defaults. +The `make dev` launcher uses `Dev` as the default browser title prefix. The +`make start-debug` launcher keeps production defaults, enables diagnostics, and +uses `Debug`. PR preview environments use `Preview`. An explicit +`KANDEV_WEB_TITLE_PREFIX` value overrides those environment defaults. ## Full `config.yaml` example diff --git a/docs/decisions/0007-runtime-feature-flags.md b/docs/decisions/0007-runtime-feature-flags.md index 15683de668..4e0bc065f7 100644 --- a/docs/decisions/0007-runtime-feature-flags.md +++ b/docs/decisions/0007-runtime-feature-flags.md @@ -57,9 +57,15 @@ Top-level sections are purely for human readability (the loader walks every leaf | signal | profile | who sets it | |---|---|---| | `KANDEV_E2E_MOCK=true` | `e2e` | `apps/web/e2e/fixtures/backend.ts` | -| `KANDEV_DEBUG_DEV_MODE=true` (or `…_PPROF_ENABLED=true`) | `dev` | `apps/cli/src/dev.ts` | +| `KANDEV_DEBUG_DEV_MODE=true` | `dev` | `apps/cli/src/dev.ts`, the `make dev` backend target | | (neither) | `prod` | the default for any other launch | +`KANDEV_DEBUG_PPROF_ENABLED` is a legacy debug behavior variable. It enables +pprof when a launcher sets it, but it does not select the `dev` profile. This +keeps `make start-debug` on the production profile while it retains debug +diagnostics; that launcher supplies the explicit `Debug` title prefix. See +[ADR-2026-08-10-debug-launcher-profile-selection](2026-08-10-debug-launcher-profile-selection.md). + `profiles.ApplyProfile()` then walks every leaf and calls `os.Setenv` **only when the var is not already set** and **only when the resolved value is non-empty**. Empty means "leave unset." This produces the precedence chain: diff --git a/docs/decisions/2026-08-10-debug-launcher-profile-selection.md b/docs/decisions/2026-08-10-debug-launcher-profile-selection.md new file mode 100644 index 0000000000..7fc8dd48c7 --- /dev/null +++ b/docs/decisions/2026-08-10-debug-launcher-profile-selection.md @@ -0,0 +1,52 @@ +# ADR-2026-08-10: Keep Debug Launches on the Production Profile + +**Status:** accepted +**Date:** 2026-08-10 +**Area:** backend, frontend, cli + +## Context + +The native `--debug` launcher sets `KANDEV_DEBUG_PPROF_ENABLED=true` and +`KANDEV_DEBUG_AGENT_MESSAGES=true`. Profile detection also treated the pprof +variable as the development-profile selector. As a result, `make start-debug` +received development defaults and the `Dev Kandev` browser title. + +The debug launcher needs diagnostic behavior without development-only defaults. +The source-checkout `make dev` path already has a canonical development +selector, `KANDEV_DEBUG_DEV_MODE=true`. + +## Decision + +Use only `KANDEV_DEBUG_DEV_MODE=true` as the development-profile selector. +Keep `KANDEV_DEBUG_PPROF_ENABLED` as a legacy behavior variable for pprof and +related debug compatibility. It does not select the `dev` profile. + +The launch contracts are: + +- `make dev` selects `dev` and uses `Dev Kandev`. +- `make start-debug` enables debug diagnostics, selects `prod`, and uses + `Debug Kandev` by default. An explicit title prefix still wins. +- `KANDEV_E2E_MOCK=true` keeps its existing priority and selects `e2e`. + +The browser title continues to use `KANDEV_WEB_TITLE_PREFIX`. The debug +launcher supplies `Debug` as its default prefix without adding a new profile. + +## Consequences + +Debugging a release-style build no longer enables development feature defaults +or the development browser identity. It has the distinct `Debug Kandev` title +while retaining production defaults. Contributors who need the development +profile use `make dev` or set `KANDEV_DEBUG_DEV_MODE=true`. + +The legacy pprof variable remains available for older launch scripts. Tests and +launcher checks must keep the selector and debug behavior separate. + +## Alternatives Considered + +1. Set only an empty title prefix in `make start-debug`. Rejected because it + fixes the browser label but leaves other development defaults enabled and + does not identify a diagnostics-only launch. +2. Keep pprof as a profile selector. Rejected because pprof is a diagnostic + behavior, not a development-environment identity. +3. Remove the pprof variable. Rejected because existing launchers and config + users depend on the compatibility alias. diff --git a/docs/decisions/INDEX.md b/docs/decisions/INDEX.md index 70b3880e2a..18bfdeaa60 100644 --- a/docs/decisions/INDEX.md +++ b/docs/decisions/INDEX.md @@ -142,3 +142,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or | 2026-08-08-task-owned-worktree-lifetime | [Keep Worktree Ownership at the Task Lifecycle](2026-08-08-task-owned-worktree-lifetime.md) | accepted | backend, frontend | 2026-08-08 | | 2026-08-09-bind-automation-mutations-to-event-targets | [Bind Automation Mutations to Event Targets](2026-08-09-bind-automation-mutations-to-event-targets.md) | accepted | backend, agentctl, protocol, security, workflow | 2026-08-09 | | 2026-08-09-exclusive-runtime-state-ownership | [Lock Runtime State Before Backend Startup](2026-08-09-exclusive-runtime-state-ownership.md) | accepted | backend, infra | 2026-08-09 | +| 2026-08-10-debug-launcher-profile-selection | [Keep Debug Launches on the Production Profile](2026-08-10-debug-launcher-profile-selection.md) | accepted | backend, frontend, cli | 2026-08-10 | diff --git a/docs/plans/debug-start-production-title/plan.md b/docs/plans/debug-start-production-title/plan.md new file mode 100644 index 0000000000..2e8a2593da --- /dev/null +++ b/docs/plans/debug-start-production-title/plan.md @@ -0,0 +1,153 @@ +--- +spec: docs/specs/dev-preview-title-prefixes/spec.md +created: 2026-08-10 +status: complete +--- + +# Implementation Plan: Keep Debug Start on the Production Profile + +## Overview + +`make start-debug` sets the legacy pprof debug variable. The profile loader +currently treats that variable as a development selector, so it applies the +`Dev` title prefix. Make `KANDEV_DEBUG_DEV_MODE` the only development selector, +keep pprof as debug behavior, and preserve `Dev Kandev` for `make dev`. The +debug launcher supplies `Debug` as the default title prefix, producing +`Debug Kandev` without selecting the development profile. + +The work proceeds from the backend profile contract to the launcher checks and +then to the browser regression test and public documentation. + +The confirmed root cause is in `profiles.DetectEnvironment`: its dev branch +checks `KANDEV_DEBUG_PPROF_ENABLED` even though the release debug launcher sets +that variable without selecting the development profile. + +## Backend and launchers + +### Profile selection + +- `apps/backend/internal/profiles/profiles.go`: make + `KANDEV_DEBUG_DEV_MODE=true` the only non-e2e signal for the `dev` profile. + Keep `KANDEV_DEBUG_PPROF_ENABLED` available to the debug configuration and + pprof routes. +- `apps/backend/internal/profiles/profiles_test.go`: add a pprof-only + regression case that resolves to `prod`, keeps the title prefix out of the + profile defaults, and keeps production feature defaults. + +### Debug title default + +- `apps/backend/internal/launcher/env.go`: when the native debug launcher is + used, supply `KANDEV_WEB_TITLE_PREFIX=Debug` only when the caller has not + already supplied a prefix. Keep the explicit environment override contract. +- `apps/backend/internal/launcher/start_test.go`: verify the debug launcher + supplies `Debug` by default and preserves an explicit prefix. +- `apps/backend/internal/launcher/supervisor.go`: preserve the title prefix in + the restart manifest so a supervised restart keeps `Debug Kandev`. +- `apps/backend/internal/launcher/supervisor_test.go`: cover the allowlisted + title prefix in the restart manifest. + +### Make targets + +- `apps/backend/Makefile`: make the direct `dev` target export the canonical + `KANDEV_DEBUG_DEV_MODE=true` selector. Make the direct `start-debug` target + default to the `Debug` prefix while preserving an explicitly supplied + `KANDEV_WEB_TITLE_PREFIX`. The debug target continues to export pprof and + debug logging without the dev selector. +- `scripts/check-make-shells`: update the expected `dev` command for Unix, + native Windows, and Git Bash/MSYS, plus the direct `start-debug` title + environment. + +### Durable decision record + +- `docs/decisions/0007-runtime-feature-flags.md`: reconcile the profile + selector table with the corrected contract. +- `docs/decisions/2026-08-10-debug-launcher-profile-selection.md`: preserve the + reason for separating diagnostic behavior from environment identity. +- `docs/decisions/INDEX.md`: index the new decision. + +## Frontend + +No frontend production code changes are required. The existing server-shell +and SPA title composition already uses `KANDEV_WEB_TITLE_PREFIX` correctly. + +The existing title-prefix browser test will cover the debug-start result. This +is browser metadata, not a layout or interaction surface. Desktop and phone +viewports therefore have the same expected title, and no mobile-specific +interaction test is required. + +## Public documentation + +- `docs/public/configuration.md`: state that `make start-debug` keeps the + production profile and uses the `Debug Kandev` title while enabling + diagnostics. +- `docs/public/cli.md`: explain that `--debug` does not select the development + profile; `make dev` is the development-profile launcher. +- `docs/configuration.md`: keep the internal configuration reference aligned. + +## Tests + +- **What:** pprof-only startup selects `prod` and does not apply `Dev`. + **File:** `apps/backend/internal/profiles/profiles_test.go`. + **How:** isolated environment unit test through `ApplyProfile`. +- **What:** the native debug launcher supplies the `Debug` prefix by default + and preserves an explicit prefix. + **File:** `apps/backend/internal/launcher/start_test.go`. + **How:** inspect the environment assembled for debug and non-debug starts. +- **What:** supervised restarts preserve the debug title prefix. + **File:** `apps/backend/internal/launcher/supervisor_test.go`. + **How:** assert the debug prefix survives the restart manifest allowlist. +- **What:** direct `make dev` emits the canonical dev selector across shells. + **File:** `scripts/check-make-shells`. + **How:** run the existing deterministic `make -n` shell matrix. +- **What:** the debug-start launcher uses the distinct debug title. + **File:** `apps/web/e2e/tests/system/title-prefix.spec.ts`. + **How:** restart the fixture with pprof enabled and both profile selectors + disabled plus the launcher's `Debug` prefix, navigate to `/`, and assert + `Debug Kandev`. +- **What:** development and preview title behavior remains unchanged. + **File:** `apps/web/e2e/tests/system/title-prefix.spec.ts`. + **How:** retain the existing `Dev Kandev` and `Preview Kandev` assertions. + +## E2E Tests + +- **Scenario:** GIVEN a backend restarted with pprof/debug behavior and no + development or e2e selector, WHEN the browser opens Kandev, THEN the title is + `Debug Kandev`. +- **Scenario:** GIVEN a backend restarted with the development selector, WHEN + the browser opens Kandev, THEN the title is `Dev Kandev`. +- **Scenario:** GIVEN a backend with an explicit `Preview` prefix, WHEN the + browser opens Kandev, THEN the title is `Preview Kandev`. +- **File:** `apps/web/e2e/tests/system/title-prefix.spec.ts`. +- **Mobile note:** `document.title` is viewport-independent. The existing + browser metadata assertion covers the same user outcome on a phone, so no + `mobile-*.spec.ts` file is needed. + +## Verification Results + +- `make test-backend` — passed (full backend test suite). +- `make lint-backend` — passed (0 issues). +- `make build-backend` — passed (backend binaries built). +- `cd apps/backend && make check-make-shells` — passed for Unix, native + Windows, and Git Bash/MSYS dispatch, including explicit prefixes with spaces. +- `cd apps/web && pnpm e2e:run --project chromium tests/system/title-prefix.spec.ts` + — passed (3 tests). +- `node --test scripts/validate-public-docs.test.mjs` — passed (58 tests). +- `node scripts/validate-public-docs.mjs` — passed (41 published docs pages). +- `git diff --check` — passed. + +## Implementation Waves And Parallel Candidates + +Wave 1 (sequential): + +- [x] [task-01-profile-selector-contract](task-01-profile-selector-contract.md) + +Wave 2 (sequential, depends on Wave 1): + +- [x] [task-02-title-regression-docs](task-02-title-regression-docs.md) + +Parallel delegation is not authorized by this plan. The work stays in the +primary session and follows the dependency order above. + +## Open Questions + +None. diff --git a/docs/plans/debug-start-production-title/task-01-profile-selector-contract.md b/docs/plans/debug-start-production-title/task-01-profile-selector-contract.md new file mode 100644 index 0000000000..91776000bf --- /dev/null +++ b/docs/plans/debug-start-production-title/task-01-profile-selector-contract.md @@ -0,0 +1,77 @@ +--- +id: "01-profile-selector-contract" +title: "Separate debug profile selector" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +spec: "../../specs/dev-preview-title-prefixes/spec.md" +--- + +# Task 01: Separate debug profile selector + +## Acceptance + +- `KANDEV_DEBUG_PPROF_ENABLED=true` without `KANDEV_DEBUG_DEV_MODE=true` selects + `prod` and does not receive the `Dev` profile prefix. +- `KANDEV_DEBUG_DEV_MODE=true` still selects `dev` and supplies the `Dev` + title prefix. +- The native and direct backend `start-debug` launchers supply the `Debug` + prefix by default, while preserving an explicit prefix. +- A supervised backend restart preserves the configured title prefix. +- The direct backend `dev` target exports `KANDEV_DEBUG_DEV_MODE=true`, while + `start-debug` retains pprof and debug logging without that selector. +- The profile ADR and its index describe the corrected boundary. + +## Verification + +- `make test-backend` +- `make lint-backend` +- `make build-backend` +- `make check-make-shells` +- `git diff --check` + +## Files likely touched + +- `apps/backend/internal/profiles/profiles.go` +- `apps/backend/internal/profiles/profiles_test.go` +- `apps/backend/internal/launcher/env.go` +- `apps/backend/internal/launcher/start_test.go` +- `apps/backend/internal/launcher/supervisor.go` +- `apps/backend/internal/launcher/supervisor_test.go` +- `apps/backend/Makefile` +- `scripts/check-make-shells` +- `docs/decisions/0007-runtime-feature-flags.md` +- `docs/decisions/2026-08-10-debug-launcher-profile-selection.md` +- `docs/decisions/INDEX.md` + +## Dependencies + +None. + +## Parallelism + +Sequential. The profile selector and launcher environment are one shared +startup contract. + +## Inputs + +- The `What`, API surface, failure modes, and scenarios in the repair spec. +- The confirmed root cause in `plan.md`. +- `profiles.DetectEnvironment`, `profiles.ApplyProfile`, and the existing shell + dispatch harness. + +## Output contract + +Report the changed files, exact test commands and results, any risks, and the +updated task and plan statuses in the same conversation. Do not delegate this +task to a subagent without explicit user authorization. + +## Results + +- `make test-backend` — passed (full backend test suite). +- `make lint-backend` — passed (0 issues). +- `make build-backend` — passed (backend binaries built). +- `make check-make-shells` — passed (Unix, native Windows, and Git Bash/MSYS + dispatch, including explicit prefixes with spaces). +- `git diff --check` — passed. diff --git a/docs/plans/debug-start-production-title/task-02-title-regression-docs.md b/docs/plans/debug-start-production-title/task-02-title-regression-docs.md new file mode 100644 index 0000000000..119eeeaa61 --- /dev/null +++ b/docs/plans/debug-start-production-title/task-02-title-regression-docs.md @@ -0,0 +1,67 @@ +--- +id: "02-title-regression-docs" +title: "Prove debug title behavior" +status: done +wave: 2 +depends_on: ["01-profile-selector-contract"] +plan: "plan.md" +spec: "../../specs/dev-preview-title-prefixes/spec.md" +--- + +# Task 02: Prove debug title behavior + +## Acceptance + +- The title-prefix E2E test asserts `Debug Kandev` for a pprof/debug restart + without development or e2e profile selectors. +- The existing `Dev Kandev` and `Preview Kandev` assertions continue to pass. +- Public and internal configuration docs explain the different `make dev` and + `make start-debug` title behavior. +- The viewport-independent metadata behavior has an explicit mobile-parity + justification. No touch or layout surface changes. + +## Verification + +- `cd apps/web && pnpm e2e:run --project chromium tests/system/title-prefix.spec.ts` +- `node --test scripts/validate-public-docs.test.mjs` +- `node scripts/validate-public-docs.mjs` +- `git diff --check` + +If the fresh worktree has no frontend dependencies, first run +`cd apps && pnpm install --frozen-lockfile`. + +## Files likely touched + +- `apps/web/e2e/tests/system/title-prefix.spec.ts` +- `docs/public/configuration.md` +- `docs/public/cli.md` +- `docs/configuration.md` + +## Dependencies + +Task 01 must pass before this task starts. + +## Parallelism + +Sequential. The browser assertion depends on the profile-selector behavior. + +## Inputs + +- The title scenarios in `docs/specs/dev-preview-title-prefixes/spec.md`. +- The mobile-parity conclusion in `plan.md`. +- Existing fixture restart behavior in `apps/web/e2e/fixtures/backend.ts`. + +## Output contract + +Report the changed files, exact E2E and documentation checks, cleanup status, +and the updated task and plan statuses in the same conversation. + +## Results + +- `cd apps/web && pnpm e2e:run --project chromium tests/system/title-prefix.spec.ts` + — passed (3 tests: Debug, Dev, and Preview titles). +- `node --test scripts/validate-public-docs.test.mjs` — passed (58 tests). +- `node scripts/validate-public-docs.mjs` — passed (41 published docs pages). +- `cd apps && pnpm exec prettier --check web/e2e/tests/system/title-prefix.spec.ts` + — passed. +- `git diff --check` — passed. diff --git a/docs/public/cli.md b/docs/public/cli.md index 36b565f002..42148ecaa1 100644 --- a/docs/public/cli.md +++ b/docs/public/cli.md @@ -251,7 +251,15 @@ Flags take precedence over the equivalent port variables. `KANDEV_BACKEND_PORT` The launcher also sets the selected server and `agentctl` ports for the backend. Treat its supervisor socket and manifest under `/supervisor/` as private implementation state, not a control API. -`--debug` sets `KANDEV_DEBUG_AGENT_MESSAGES=true` and `KANDEV_DEBUG_PPROF_ENABLED=true`. Debug events go to `/logs/backend-logs.log`; warn and above still appear on stdout. ACP logs can contain full prompts, file content, and tool calls, while diagnostic endpoints expose process details. Use debug mode only on a trusted machine and remove retained debug logs afterward. [Configuration](./configuration.md) lists locations and retention. +`--debug` sets `KANDEV_DEBUG_AGENT_MESSAGES=true` and +`KANDEV_DEBUG_PPROF_ENABLED=true` without selecting the `dev` profile. The +`make start-debug` launcher also defaults the browser title to `Debug Kandev`; +`make dev` selects the development profile and uses `Dev Kandev`. Debug events +go to `/logs/backend-logs.log`; warn and above still appear on stdout. +ACP logs can contain full prompts, file content, and tool calls, while +diagnostic endpoints expose process details. Use debug mode only on a trusted +machine and remove retained debug logs afterward. [Configuration](./configuration.md) +lists locations and retention. ## Data and cleanup diff --git a/docs/public/configuration.md b/docs/public/configuration.md index 7f83d33ec6..386d598bd8 100644 --- a/docs/public/configuration.md +++ b/docs/public/configuration.md @@ -58,7 +58,7 @@ Some common camelCase keys have explicit compatibility aliases. Use the document | `server.readTimeout` | `KANDEV_SERVER_READTIMEOUT` | `30` | HTTP read timeout in seconds. | | `server.writeTimeout` | `KANDEV_SERVER_WRITETIMEOUT` | `30` | HTTP write timeout in seconds. | | `server.webInternalUrl` | `KANDEV_WEB_INTERNAL_URL` | empty | Development reverse-proxy target for a separately running web app. Installed releases normally serve embedded assets. | -| `server.webTitlePrefix` | `KANDEV_WEB_TITLE_PREFIX` | empty | Prefixes the browser tab title as ` Kandev` (for example `TEST` renders `TEST Kandev`), so several instances stay distinguishable in adjacent tabs. `make dev` defaults to `Dev`; PR previews use `Preview`. An explicit value overrides these defaults. Empty keeps the plain `Kandev` title. | +| `server.webTitlePrefix` | `KANDEV_WEB_TITLE_PREFIX` | empty | Prefixes the browser tab title as ` Kandev` (for example `TEST` renders `TEST Kandev`), so several instances stay distinguishable in adjacent tabs. `make dev` defaults to `Dev`; `make start-debug` keeps production defaults, enables diagnostics, and defaults to `Debug`; PR previews use `Preview`. An explicit value overrides these defaults. Empty keeps the plain `Kandev` title. | The default host exposes the server on every interface even though the CLI prints a `localhost` URL. The current local product path must not be treated as an authenticated multi-user perimeter. For remote access, bind to loopback and use a trusted authenticated tunnel/proxy, or isolate the network at the deployment layer. @@ -170,9 +170,17 @@ Discovery roots bound automatic filesystem traversal, so scope them narrowly. Th | YAML key | Environment variable | Default | Current behavior | |---|---|---|---| | `debug.devMode` | `KANDEV_DEBUG_DEV_MODE` | `false` | Enables diagnostic endpoints and agent-message debug logging. | -| `debug.pprofEnabled` | `KANDEV_DEBUG_PPROF_ENABLED` | `false` | Legacy alias; also enables debug mode. | +| `debug.pprofEnabled` | `KANDEV_DEBUG_PPROF_ENABLED` | `false` | Legacy diagnostics switch. It enables pprof behavior but does not select the `dev` profile. | -Debug mode is high risk. It enables local diagnostic surfaces and implies `KANDEV_DEBUG_AGENT_MESSAGES=true` and `KANDEV_DEBUG_PPROF_ENABLED=true` when not explicitly locked by the environment. ACP JSONL frames include complete prompts, file content, and tool calls. Do not enable it on a shared or network-exposed backend. +`KANDEV_DEBUG_DEV_MODE=true` selects the `dev` profile. `make dev` sets that +selector and defaults the browser title to `Dev Kandev`. `make start-debug` +enables pprof and debug logging without selecting the `dev` profile, and +defaults the browser title to `Debug Kandev`. Debug mode is high risk. It +enables local diagnostic surfaces and implies +`KANDEV_DEBUG_AGENT_MESSAGES=true` and `KANDEV_DEBUG_PPROF_ENABLED=true` when +not explicitly locked by the environment. ACP JSONL frames include complete +prompts, file content, and tool calls. Do not enable it on a shared or +network-exposed backend. ## Minimal examples @@ -304,7 +312,7 @@ Copying this entire file is unnecessary and can freeze old defaults in a deploym | `features.auth` | `KANDEV_FEATURES_AUTH` | off | Authentication and per-user workspaces for the whole install. | | `features.claudeBackgroundPromptHandoff` | `KANDEV_FEATURES_CLAUDE_BACKGROUND_PROMPT_HANDOFF` | off | High-risk Claude Code experiment that exposes recognized background-only activity and admits a successor prompt. | | `features.claudeMidTurnSteering` | `KANDEV_FEATURES_CLAUDE_MID_TURN_STEERING` | off | High-risk Claude Code experiment that delivers a prompt into a still-generating turn (mid-turn steering) instead of queuing it, for agents advertising prompt queueing. | -| `debug.devMode` | `KANDEV_DEBUG_DEV_MODE` (also locked by explicit legacy/debug-message vars) | off | High-risk diagnostic endpoints and ACP frame logging. | +| `debug.devMode` | `KANDEV_DEBUG_DEV_MODE` | off | High-risk diagnostic endpoints and ACP frame logging. | UI changes are persisted in the database and require a restart. An explicitly set environment value wins and locks the UI control. Otherwise a database override wins over the embedded profile/default. Resetting a toggle removes its database override. diff --git a/docs/specs/dev-preview-title-prefixes/spec.md b/docs/specs/dev-preview-title-prefixes/spec.md index 58ab677248..e57fa1d9d6 100644 --- a/docs/specs/dev-preview-title-prefixes/spec.md +++ b/docs/specs/dev-preview-title-prefixes/spec.md @@ -6,6 +6,8 @@ owner: Kandev # Environment-specific browser tab title prefixes +The profile-selection boundary follows [ADR-2026-08-10-debug-launcher-profile-selection](../../decisions/2026-08-10-debug-launcher-profile-selection.md). + ## Why Developers often keep a local development instance and one or more pull-request @@ -24,6 +26,9 @@ easy to use the wrong instance. contract. - A normal production/start launch with no explicit prefix keeps the plain `Kandev` title. +- A debug start (`make start-debug`) can enable pprof and debug logging without + selecting the development profile. It uses the explicit prefix `Debug` by + default, so its title is `Debug Kandev`. - The prefix is present both on the initial server-rendered page and after a client boot from `/api/v1/app-state`, using the title behavior delivered by PR #2459. @@ -37,6 +42,7 @@ The existing environment contract remains the public configuration surface: |---|---|---| | Development profile (`make dev`) | `Dev` by default | `Dev Kandev` | | PR preview launcher | `Preview` | `Preview Kandev` | +| Debug start (`make start-debug`) | `Debug` by default | `Debug Kandev` | | Normal start with no override | unset | `Kandev` | | Any launch with an explicit override | caller value | ` Kandev` | @@ -46,6 +52,9 @@ not changed from the Kandev UI. ## Failure modes - If the prefix is unset or blank, the title remains `Kandev`. +- `KANDEV_DEBUG_PPROF_ENABLED=true` enables legacy pprof behavior but does not + select the development profile or add the `Dev` prefix. The debug launcher + supplies the `Debug` prefix unless the caller supplies another prefix. - If the preview launcher cannot set the environment variable, the preview falls back to the existing plain-title behavior rather than failing startup. - If a supervised restart occurs, the launcher preserves the allowlisted prefix @@ -69,11 +78,14 @@ caller supplies the same environment or profile again. the backend starts, **THEN** the browser title is `Custom Kandev`. - **GIVEN** a normal start with no title-prefix environment variable, **WHEN** the user opens Kandev, **THEN** the browser title is `Kandev`. +- **GIVEN** a debug start with pprof and debug logging enabled but without the + development-profile selector, **WHEN** the user opens Kandev, **THEN** the + browser title is `Debug Kandev`. - **GIVEN** a preview or development backend with a configured prefix, **WHEN** its supervisor restarts the backend, **THEN** the browser title retains the same prefix. - **GIVEN** a phone-sized browser viewport, **WHEN** either environment is - opened, **THEN** the same title is shown; no viewport-specific interaction is + opened, **THEN** the same title is shown. No viewport-specific interaction is required. ## Out of scope diff --git a/scripts/check-make-shells b/scripts/check-make-shells index 53a9e3e406..e05e79deba 100755 --- a/scripts/check-make-shells +++ b/scripts/check-make-shells @@ -29,7 +29,7 @@ status=0 # expect