fix: separate debug and development launch profiles (#2487)

This commit is contained in:
Carlos Florêncio
2026-08-10 15:44:31 +01:00
committed by GitHub
parent 152e5a34c0
commit 4f2cf8f9df
20 changed files with 533 additions and 29 deletions
+14 -4
View File
@@ -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
+13 -1
View File
@@ -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)
}
@@ -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)
})
}
@@ -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,
@@ -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) {
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"
@@ -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)
@@ -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" });
+4 -3
View File
@@ -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_<SECTION>_<KEYUPPERCASE>`) 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
+7 -1
View File
@@ -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:
@@ -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.
+1
View File
@@ -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 |
@@ -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.
@@ -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.
@@ -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.
+9 -1
View File
@@ -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 `<home>/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 `<home>/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 `<home>/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
+12 -4
View File
@@ -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 `<prefix> 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 `<prefix> 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.
+13 -1
View File
@@ -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 | `<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
+24 -11
View File
@@ -29,7 +29,7 @@ status=0
# expect <label> <OS> <MSYSTEM> <target> <expected recipe tail>
expect() {
local label=$1 os=$2 msys=$3 target=$4 want=$5 marker=${6:-__backend} got
local label=$1 os=$2 msys=$3 target=$4 want=$5 marker=${6:-__backend} title_prefix=${7:-} got
# The OS=/MSYSTEM= overrides only steer which branch the Makefile picks;
# $(shell ...) still runs under this harness's POSIX shell, and it expands
# even under `make -n`. So neutralise the build-stamp probes rather than let
@@ -38,10 +38,17 @@ expect() {
# NULL_REDIR (rather than passing /dev/null) also dodges Git Bash rewriting
# a /dev/null argument to `nul` on its way to native make. None of this
# affects the run/dev/start-debug recipes under assertion.
got=$(make -C "$backend_dir" OS="$os" MSYSTEM="$msys" NULL_REDIR= BUILD_TIME=simulated -n "$target" 2>/dev/null \
| grep -F "$marker" \
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/[[:space:]]\{1,\}/ /g' \
|| true)
if [ -n "$title_prefix" ]; then
got=$(KANDEV_WEB_TITLE_PREFIX="$title_prefix" make -C "$backend_dir" OS="$os" MSYSTEM="$msys" NULL_REDIR= BUILD_TIME=simulated -n "$target" 2>/dev/null \
| grep -F "$marker" \
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/[[:space:]]\{1,\}/ /g' \
|| true)
else
got=$(env -u KANDEV_WEB_TITLE_PREFIX make -C "$backend_dir" OS="$os" MSYSTEM="$msys" NULL_REDIR= BUILD_TIME=simulated -n "$target" 2>/dev/null \
| grep -F "$marker" \
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/[[:space:]]\{1,\}/ /g' \
|| true)
fi
if [ "$got" = "$want" ]; then
printf 'ok %-16s %-12s %s\n' "$label" "$target" "$got"
else
@@ -53,18 +60,24 @@ expect() {
# Unix — no .exe, POSIX env-prefix + exec.
expect unix '' '' run './bin/kandev __backend'
expect unix '' '' dev 'KANDEV_DEBUG_PPROF_ENABLED=true exec ./bin/kandev __backend'
expect unix '' '' start-debug 'KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug exec ./bin/kandev __backend'
expect unix '' '' dev 'KANDEV_DEBUG_DEV_MODE=true exec ./bin/kandev __backend'
expect unix '' '' start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Debug' exec ./bin/kandev __backend"
expect unix '' '' start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Preview' exec ./bin/kandev __backend" __backend Preview
expect unix '' '' start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='QA Debug' exec ./bin/kandev __backend" __backend "QA Debug"
# Native Windows (cmd/PowerShell) — .exe, `set VAR=VAL&`, backslash path, no exec.
expect windows-native Windows_NT '' run '"bin\kandev.exe" __backend'
expect windows-native Windows_NT '' dev 'set KANDEV_DEBUG_PPROF_ENABLED=true& "bin\kandev.exe" __backend'
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& "bin\kandev.exe" __backend'
expect windows-native Windows_NT '' dev 'set KANDEV_DEBUG_DEV_MODE=true& "bin\kandev.exe" __backend'
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=Debug"& "bin\kandev.exe" __backend'
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=Preview"& "bin\kandev.exe" __backend' __backend Preview
expect windows-native Windows_NT '' start-debug 'set KANDEV_DEBUG_PPROF_ENABLED=true& set KANDEV_LOG_LEVEL=debug& set "KANDEV_WEB_TITLE_PREFIX=QA Debug"& "bin\kandev.exe" __backend' __backend "QA Debug"
# Git Bash/MSYS — .exe suffix but POSIX syntax (recipes run through sh).
expect git-bash Windows_NT MINGW64 run './bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 dev 'KANDEV_DEBUG_PPROF_ENABLED=true exec ./bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 start-debug 'KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug exec ./bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 dev 'KANDEV_DEBUG_DEV_MODE=true exec ./bin/kandev.exe __backend'
expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Debug' exec ./bin/kandev.exe __backend"
expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='Preview' exec ./bin/kandev.exe __backend" __backend Preview
expect git-bash Windows_NT MINGW64 start-debug "KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug KANDEV_WEB_TITLE_PREFIX='QA Debug' exec ./bin/kandev.exe __backend" __backend "QA Debug"
# deadcode — the analyzer is invoked through the same shell-specific env-prefix
# mechanism, but its command has no __backend marker, so match its module path.