Step 20 of v4.3 playground plan. Document-level keydown handler:
- J = next annotation (next sorted-by-line draft, wraps)
- K = prev annotation (wraps)
- ] = toggle sidebar visibility
- Escape = clear active anchor + sidebar list selection
Active annotation gets yellow-tint (Step 18 setActiveAnchor) and the
matching gutter-badge receives focus + scrollIntoView. Aria-live region
announces position + target: "Annotering 3 av 7: <target> — <snippet>".
Skips input/textarea/select/contenteditable so playground never steals
keystrokes from form fields. Modifier keys (Ctrl/Alt/Meta) pass through
to browser shortcuts. Wired in init() after dashboard nav.
Trace: SC2 (WCAG AA keyboard), SC6, research/04 Dim 2 + Insight 5 +
Recommendation keyboard-navigation.
Step 19 of v4.3 playground plan. Sidebar now default aria-hidden=true
(translateX collapses panel, leaves 40px FAB rail). FAB toggle has
data-action=toggle-sidebar for keyboard binding (] in Step 20).
New annotation-list section in sidebar panel:
- filter radiogroup: Alle (default), Åpne (unresolved), Resolved
- voyage-jumplist ordered list with numbered badges matching the
gutter-badge ordering (sorted by line ASC)
- aria-live jumplist count: "X av N" (filtered/total)
- click list-item -> setActiveAnchor + scrollIntoView + data-active
renderAnnotationList wires into mountRender so list refreshes on every
render. Filter state (voyageFilterState) persists across renders within
the session.
Trace: SC6, research/04 Dim 1 (hidden-by-default) + Insight 1 +
Recommendation sidebar/navigation.
Step 18 of v4.3 playground plan. Replaces v4.2 Gesture 2 pencil-icon
hover-reveal with numbered circular badges in the left gutter (one per
anchored paragraph; ordering matches sidebar jumplist). 2-3px accent stripe
extends right from the badge into the gutter. Yellow-tint highlight
(rgba 255, 235, 59, 0.25 — Google Docs pattern) applies to the anchored
paragraph when an annotation is active. Body text never reflowed or
recolored. Gesture 1 (text-select adder) and Gesture 3 (page-level note)
remain for new annotation creation.
CSS uses --color-scope-voyage token for badge background and stripe.
JS adds injectAnchorBadges() + setActiveAnchor() and rewires mountRender.
Trace: SC1 + SC6, research/04 Insight 3 + Recommendation marker-design.
Step 17 of v4.3 playground plan. Pure function relocateAnchorsToBlockBoundaries
(text, anchors) detects atomic markdown blocks (fenced code, tables, deeply
nested lists) and relocates anchor-comment insertion to the line BEFORE block
opening rather than inside the block. Pure markdown-text -> markdown-text
transform (no DOM, no markdown-it dependency).
Companion test tests/integration/annotation-block-boundary.test.mjs extracts
the function via balanced-brace scan and exercises it through Function() —
7 unit tests covering empty anchors, outside-block stays, fenced-code
relocation, table relocation, deeply-nested list relocation, mixed
inside/outside, and shape contract.
Trace: SC6, research/04 Dim 3 (Notion block-level fallback), plan-critic
major #6 (DOM-vs-no-DOM contradiction resolved via pure-function design).
Step 15 (v4.3 Sesjon 3 — Wave 3) — wires the dashboard fleet-tiles
to a drill-down view with breadcrumb update + back-to-dashboard
navigation + browser back/forward restoration via popstate.
renderArtifactDetail(artifactKey) renders the chosen artifact into
the #voyage-detail slot using renderPageShell + renderArtifact:
- brief / plan / review → markdown render
- progress → JSON pretty-print in <pre>
- research → list of all research-briefs
- missing entry → "Artifact mangler" placeholder
Click delegation on .fleet-tile[data-artifact] triggers detail render
+ pushDetailURL (?artifact=<key>); data-action=back-to-dashboard
returns to the dashboard view + pushDashboardURL. Topbar breadcrumb
gets a third segment for detail views.
URL-parameter deep-linking: at page-load, ?project=<basePath>
surfaces a guide-panel hint explaining that webkitdirectory requires
a user-gesture; the hint links to the same Last prosjektmappe button
that wireProjectLoader already exposes. popstate handler restores
the view-state on browser back/forward (no-op when no project loaded).
Test additions (4): renderArtifactDetail function, URLSearchParams
presence, data-action=back-to-dashboard attribute, popstate listener.
Step 14 (v4.3 Sesjon 3 — Wave 3) — adds renderDashboard pipeline that
turns a ProjectArtifacts struct (produced by loadProjectDirectory in
Step 13) into a fleet-grid of fleet-tiles, one per artifact-type
(brief / plan / review / research / progress).
Status vocabulary: complete, in-progress, blocked, missing, stale
Severity mapping: missing → critical, blocked → high, in-progress
+ stale → medium, complete → low. Severity drives DS color tokens
via [data-severity] attribute selectors.
When loadProjectDirectory completes, dashboard takes over the main
stage (paste-flow elements hidden); topbar updates with project
breadcrumb. Step 13's pipeline already calls renderDashboard via
graceful-fallback, so wiring is automatic.
Test additions (4): fleet-grid + fleet-tile presence, renderDashboard
function declaration, status vocabulary completeness.
New docs/three-tier-context.md (110 lines) documents Voyage's position as
Tier 1 in a three-tier ecosystem with upstream consumers (app-creator,
app-factory; both currently in private incubation). The brief identifies
upstream-consumed contracts (brief.md format, /trekplan CLI, handover
schemas), prescribes stability principles, and explicitly preserves
Voyage's runtime agnosticism — no imports, no detection, no special cases.
Awareness without coupling.
CLAUDE.md § Architecture: 2-line callout pointing to the brief, following
the existing "opt-in upstream architect plugin" precedent.
No Voyage behavior change. Documentation-only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v4.1.0 → v4.2.0. Two-file change per Step 14 manifest (package.json +
.claude-plugin/plugin.json). Description tagline expanded from
"brief, research, plan, execute, review, continue" to include "revise"
and "+ first marketplace playground".
Out-of-scope under Step 14 forbidden_paths (left at 4.1.0 intentionally):
- lib/exporters/otlp-format.mjs (VOYAGE_SCOPE_VERSION constant)
- hooks/scripts/otel-export.mjs (User-Agent header)
These constants are touched on the next bump where the constants directory
is in scope; keeping them stale for one release is acceptable since
otel/otlp telemetry is opt-in and the version field is informational.
Verification:
- node -e "import('./package.json',{with:{type:'json'}}).then(m=>console.log(m.default.version))" → 4.2.0
- jq -r .version .claude-plugin/plugin.json → 4.2.0
- npm test: 610 pass / 0 fail / 2 skipped (Docker)
- SC11 pipeline-self-eat gate: render-artifact.mjs renders own brief.md + plan.md to non-empty HTML
Pure module computing deterministic 16-char SHA-256 prefix for annotation set.
Canonicalization: sort by id, fixed field order (id|target_artifact|target_anchor|intent|comment|timestamp), \n-join, sha256, take first 16 hex.
Brief SC4 specifies sha256-prefix; research-05 said sha1 — brief wins per Hard Rule "Brief-driven".
6 tests pass: empty digest, order-independence, intent-sensitivity, format invariant, golden value, undefined-vs-empty equivalence.
3 new test files, 24 cases (8 per validator):
- baseline (no annotation fields) still valid
- revision: 0 / revision: 5 accepted
- source_annotations list-of-dict accepted
- annotation_digest string accepted
- revision_reason accepted
- all 4 fields together accepted
- unrecognized future field tolerated (forward-compat policy)
Pin against future strict-key refactors. No production code change — pure regression pin.
Found by simulert v4.1 smoke — doc/code-drift in v4.1 ship:
docs/observability.md claims "Cloud metadata endpoints (169.254.169.254)
are permanently blocked" but the validator allowed them when
VOYAGE_OTEL_ALLOW_PRIVATE=1. Cloud metadata services expose IAM
credentials and instance secrets — operator-trust extended to
RFC-1918 home-lab access does NOT extend here, because the
blast-radius (cloud-account compromise) is qualitatively different.
New HARD_BLOCKED_HOSTS set checked BEFORE the link-local opt-in path:
- 169.254.169.254 (AWS / GCP / Azure metadata)
- 100.100.100.200 (AliCloud metadata)
- metadata.google.internal
- metadata.azure.com
New error code ENDPOINT_HARD_BLOCKED. Existing test for
ENDPOINT_LINK_LOCAL_REJECTED on 169.254.169.254 updated to assert
the new code; 3 new tests verify the hard-block holds even with
VOYAGE_OTEL_ALLOW_PRIVATE=1, plus AliCloud + GCP-hostname coverage.
Tests: 487 → 490 pass + 2 skipped.
Step 21 of v4.1 — extend-in-place per Plan-critic Blocker 2 split:
commands-only assertions land here; CLAUDE.md / README.md pinning is
deferred to Step 22 (post-write).
Changes:
1. CLAUDE.md command coverage loop now spans all SIX pipeline commands
(added /trekcontinue — was 5 of 6 pre-v4.1 per HIGH risk-assessor).
2. New: every pipeline command-file (trekbrief/research/plan/execute/
review/continue.md) must document the --profile flag.
3. New: forbidden-alias check — no command-file may use the legacy
names model_per_phase / phase_to_model / profile_phase_models.
Canonical name is "phase_models" (locked in brief).
4. New: at least one command-file must mention "phase_models" by name
so the regression detects total removal of the canonical-name
reference.
Tests: 482 pass + 2 skipped (Docker not installed).
Step 20 of v4.1 — implements drift detection in plan-validator.mjs per
brief Assumptions block 7: "Mismatch (e.g. korrupt manuell endring)
emitterer MANIFEST_PROFILE_DRIFT-warning fra plan-validator i --strict-modus."
Logic (after validateAllManifests in validatePlanContent):
1. Strict-mode only — soft mode never emits drift warnings.
2. Plan frontmatter must declare 'profile: <name>' to establish baseline.
3. For each step manifest, if profile_used is set AND differs from plan
profile, emit warning (NOT error) with code MANIFEST_PROFILE_DRIFT
and location 'step N: profile_used = X, plan profile = Y'.
Forward-compat preserved: drift is a warning, plan remains valid:true.
Operators see the drift in --strict mode without parsing breaking.
New files:
tests/validators/plan-validator-profile-drift.test.mjs — 4 tests
tests/fixtures/plan-profile-drift.md — drift fixture
Tests verify:
1. drift detected in strict mode → MANIFEST_PROFILE_DRIFT in warnings
2. drift NOT detected in soft mode → strict gate honored
3. matching profile → no drift warning
4. no plan-level profile → drift detection silent (no baseline)
Tests: 479 pass + 2 skipped (Docker not installed).
Step 19 of v4.1 — extend-in-place per brief Preferences. Three new test
blocks asserting forward-compat:
1. Legacy fixtures (plan-run-A.md, plan-run-B.md) — without profile_used
in frontmatter — still parse cleanly after manifest-yaml.mjs added
OPTIONAL_STRING_KEYS.
2. New fixtures (profile-plan-run-{economy,premium}-*.md) — with
profile_used in frontmatter — parse cleanly with correct profile
value extracted.
3. Real v4.1 plan (.claude/projects/2026-05-08-voyage-v4.1-modellprofiler/plan.md)
validates strict, emits no PLAN_VERSION_MISMATCH warning.
Tests: 475 pass + 2 skipped (Docker not installed).
Step 17 of v4.1 — escalate-handler invoked. Live LLM-budget ($60-120 for
4 plan-runs á /trekplan --profile {economy,premium} on
examples/01-add-verbose-flag/brief.md) was not authorized for the
v4.1-execute-4b session.
Per Step 17 escalate-fallback (and NEXT-SESSION-PROMPT.local.md
fallback-strategy): document economy-Plan as parked, use balanced as
low-threshold profile, defer empirical calibration to v4.2.
Files:
tests/synthetic/profile-plan-run-economy-1.md — 30 steps, parked-synthetic
tests/synthetic/profile-plan-run-economy-2.md — 30 steps, parked-synthetic
tests/synthetic/profile-plan-run-premium-1.md — 40 steps, parked-synthetic
tests/synthetic/profile-plan-run-premium-2.md — 40 steps, parked-synthetic
tests/synthetic/profile-jaccard-calibration.md — threshold 0.55 pinned per
research/02 conservative starting value
Replacement procedure documented in calibration.md "How to replace"
section. Trigger conditions for empirical re-run:
1. Cross-tier smoke-test (Step 18) flips red on a real run
2. v4.2 LLM-budget approval
3. New profile tier added
Step 16 of v4.1 — first test in tests/integration/, establishes the
skip-on-missing-tool pattern voyage will reuse for environment-dependent
integration tests. Two tests:
1. compose config parses and contains expected services
2. compose config pins required image versions
Both skip cleanly when 'docker info' fails (no Docker installed). On a
machine with Docker, both tests run docker compose config and assert the
4 services + 3 version pins are present.
Tests: 468 pass + 2 skipped (Docker not installed in dev env).
Step 14 follow-up — VOYAGE_OTEL_ENDPOINT (not VOYAGE_OTLP_ENDPOINT) per
hooks/scripts/otel-export.mjs and lib/exporters/endpoint-validator.mjs.
Adds VOYAGE_OTEL_ALLOW_PRIVATE=1 for localhost since 127.0.0.1 is
loopback and rejected by default.