Compare commits

...

66 commits

Author SHA1 Message Date
dec8a13952 fix(okr): okf-index saner title/desc + escape klammer + skip innboks/dot (SC index-integritet)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_012HzGPGJ81k1BkUC6UvgX4Y
2026-06-30 14:01:19 +02:00
3b45be70be feat(okr): okf-check++ strictIngest (vokab+lenker) + skip innboks/dot (SC konformitet) [skip-docs]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_012HzGPGJ81k1BkUC6UvgX4Y
2026-06-30 13:55:34 +02:00
935c1d0ae9 feat(okr): konsept-skriver m/ type-til-nivaa-ruting + original-bevaring (SC original, riktig nivaa) [skip-docs]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HzGPGJ81k1BkUC6UvgX4Y
2026-06-30 13:33:38 +02:00
2b0c520efd feat(okr): delt bundle-root-relativ lenke-resolver + zero-dangling relasjoner (SC relasjoner) [skip-docs] 2026-06-30 13:01:19 +02:00
d011fb1922 feat(okr): deterministisk frontmatter-projeksjon + vokab-snap + mtime-timestamp (SC idempotens) [skip-docs] 2026-06-30 12:58:26 +02:00
136093da38 feat(okr): deterministisk heading-split for ingestion (SC idempotens) [skip-docs] 2026-06-30 12:54:57 +02:00
ca6c89e7be feat(okr): writeFrontmatter multi-linje tags-liste (additiv array-gren) [skip-docs] 2026-06-30 11:04:29 +02:00
a9d9c45192 feat(okr): lukket OKF type/tags-vokabular for ingestion-snap [skip-docs] 2026-06-30 11:00:16 +02:00
abfcc6b48e test(okr): innboks-ingestion fixtures (inbox + pre-converted) 2026-06-30 10:57:54 +02:00
f0ef6c1abc docs(claude-md): tighten State-Management prose (okr CLAUDE.md near-optimal)
CLAUDE.md loads every turn while working in this repo (measured 1,732
always-loaded tokens — the whole per-repo delta; no .claude/rules or .mcp.json).
Unlike the larger plugins, this file is already information-dense (tables + precise
design contracts), and the OKF Knowledge Layout content is under active development
(the "OKF second-brain spec" work), so it is left untouched.

The only safe, fact-preserving compression is the org-profile State-Management
paragraph (stable infra, not OKF-spec): same facts, tighter wording. 1,732→1,717
tok. The one remaining lever — the command→agent Architecture routing diagram
(~230 tok) — is a deliberate wiring reference and is kept; it can move to
/okr:help on request. Docs-only — no version bump, no catalog ref change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
2026-06-29 10:22:57 +02:00
75bfc9b47d docs(okr): ratify OKF second-brain spec v0.1 + adapt innboks-plan
Handoff catalog/docs/okf-second-brain/handoff-2026-06-29.md §3 (operator relay
fra linkedin-studio-sesjonen). okr ratifiserer den delte konvensjonen (spec.md
v0.1, katalog-eid single source of truth) etter ground-truth-verifisering:

- okf-check.mjs-semantikken bekreftet = referansekontrakt (spec §3/§7): kun
  type paakrevd (okf-check.mjs:50), anbefalte felt -> warnings (:19,:55),
  okf_version-ekko uten auto-fetch (:36-41,:85). 91/55 linjer, zero npm-deps.
- okr bruker resource (ikke source): alle emittere (okf-check RECOMMENDED,
  compose-org-profile:59, template:15, second-brain SKILL:56). Eneste source-
  treff er prosa, ikke felt.
- Ingen feltgap: profil/config-noekler baeres som extension keys (spec §5).

Adaptert: veivalg-doc + okf-note refererer naa spec-en (redefinerer ikke);
premiss-korreksjonene (§2) kanonisert i spec §9. Delings-scope for evt.
Stage-3-skill = okr + ms-ai-architect (ikke linkedin-studio). Ingen kode-/
versjonsendring (okr forblir 1.6.1); separat go for selve innboks-byggingen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HzGPGJ81k1BkUC6UvgX4Y
2026-06-29 09:46:57 +02:00
30545b982e fix(okr): credibility-sanering fra 360-re-evaluering + bump 1.6.1
Lukker de 5 credibility-kritiske funnene fra docs/evaluering-360-2026-06-26.md:
1. okr-quality-rubrics.md omskrevet til korrekt norsk (var ASCII-strippet, 0
   aa/oe/ae; lastes brukervendt av /okr:kvalitet + kvalitetssjekker +
   freshen-references). Regresjon i samme klasse som v1.3.0-showstopperen.
2. SKILL.md flaggskip-eksempel: strippet norsk rettet (per aar/hoyrisiko/
   spoerreundersoekelse/Gjennomfoere moeter).
3. okr-second-brain-search ~-glob: literal ~ ekspanderes ikke av Glob ->
   hjemme-roten ble aldri soekt. Naa eksplisitt absolutt hjemme-sti.
4. Uverifisert kb-search-sitat fjernet (motsagt av innboks-ingestion-funn);
   erstattet med funksjonell Glob/Read/Grep + discovery-moenster.
5. okr-oboard-guide.md KR1-regnefeil 52% -> 65% (per filens egen formel).

Bump alle flater 1.6.0 -> 1.6.1 (plugin.json, CLAUDE.md, README badge+hist,
begge SKILL.md, CHANGELOG). Suite 55/55 groenn. C+ -> A-; systemiske funn
(KB-kryssref, Sist-oppdatert-markoerer) bevisst utsatt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 21:31:42 +02:00
e2d5ddb85f docs(okr): 360-re-evaluering v1.6.0 (C+ -> A-) + skills/KB-scoring
Tiltak #2 (full re-evaluering) levert. 5 Opus-agenter: 2 KB-ref-scorere
(16 domene-filer mot freshen-rubrikken), 2 skill-evaluatorer, 1 360-delta-
verifikator mot alle v1.3.0-funn. Lead reviewer ground-truth-verifiserte
de baerende funnene mot fil.

Resultat: alle KRITISKE/HOEYE 360-funn (DDT, scoringsterskel, UTF-8,
find-replace, doc-motsigelser) bevist lukket; hele ms-ai-architect-gapet
levert (org-katalog, /help, rubrikker, 55 groenne tester, PDF, org-lesende
agenter). C+ -> A-.

Holder igjen A: EN regresjon i samme klasse som showstopperen --
okr-quality-rubrics.md er ASCII-strippet (0 aa/oe/ae) og lastes brukervendt
av /okr:kvalitet. + SKILL.md flaggskip-eksempel strippet; second-brain
~-glob soeker aldri hjemme-roten; kb-search-sitat uverifisert; oboard-guide
regnefeil 52->65%. Prioritert retteliste = kandidat 1.6.1.

Skills: okr-offentlig-sektor B+, okr-second-brain-search B (foerstegangs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 21:23:45 +02:00
6a595bf69e docs(okr): innboks-ingestion veivalg + avstemming mot convergence-brief
Operatør-låst posisjon (2026-06-26): innboks-ingestion er OBLIGATORISK for okr (ikke 'defer'), krever grundig planlegging, eget Voyage-løp. linkedin-studio er IKKE en mal (annen tilnærming) -> omstøter convergence-briefens 'rise to the reference design' for okr. Skiller verifiserte tekniske premisser (beholdes: ingen Google inbox->OKF-pipeline, mdcode ikke OKF, relasjoner=md-lenker, OKF v0.1 paa okf/SPEC.md) fra strategisk framing (avvises for okr). Lister gap som maa bygges + gjenbruk fra 1.6.0 + aapne /trekbrief-spoersmaal (zero-dep-konflikt for dok-konvertering flagget). STATE.md peker hit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 20:47:49 +02:00
7fdbf9bcbd chore(okr): bump 1.6.0 (OKF second brain) + CLAUDE.md/README/CHANGELOG docs
Bump 1.5.0 -> 1.6.0 across plugin.json, CLAUDE.md title, okr-offentlig-sektor SKILL.md, README badge. README version-history row + Under-the-Hood Skills table; CHANGELOG 1.6.0 entry. CLAUDE.md gains OKF Knowledge Layout section (two-root model, okf-index/okf-check/compose-org-profile, lib/frontmatter), second skill in Skills table, and two architecture lines.

Ref-count: kept '16 domain reference files' (correct -- freshen-references scores 16, excludes okr-quality-rubrics.md as the scoring instrument; 17 is the directory total). Tightened CLAUDE.md:20 wording to '16 domain' for clarity; did NOT apply the plan's 16->17 (would contradict the command). References dir untouched (SC9). Verify: node --test 55/55; all version surfaces 1.6.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 20:02:42 +02:00
80d96898bc docs(okr): innboks-ingestion funn-grunnlag (web-verifisert)
Self-bearing grunnlag for den koeede innboks-ingestion-oppgaven (start etter
OKF-fasen). Web-verifisert undersoekelse av GoogleCloudPlatform/knowledge-catalog:
ingen ferdig innboks->OKF-pipeline finnes (verken Google eller oss). Dokumenterer
hva som finnes (reference_agent/index-generator/Source-ABC/fileskb), OKF-relasjons-
modellen (markdown-lenker i body), gap (dok-konvertering + konsept-ekstraksjon +
generalisert relasjons-oppdagelse), gjenbrukbare byggeklosser, og kilder for
etterproevbarhet. STATE I-koe-peker oppdatert (local-only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 18:03:55 +02:00
77280ac319 feat(okr): tre-skrivere emitterer OKF-frontmatter + index-regenerering (SC2,SC3)
oppsett.md (eneste tre-skriver m/ Write+Bash): strategisk-kontekst-filer
faar OKF-frontmatter (type: Tildelingsbrev/Virksomhetsplan/Overordnede OKR),
retro -> type: Retrospektiv, ny-syklus status.md -> type: Status; regenererer
.claude/okr-indeksen via okf-index.mjs etter tre-skriv (2 punkter). skriv.md/
sporing.md har ikke Write -> dokumenterer OKF-frontmatter (type: OKR / Status)
i eksempel-output (malen brukeren lagrer). Lukker SC2-gapet: alle konsept-
filer paa disk baerer type:. Verify: okf-check fixture exit 0; suite 55/55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 17:24:20 +02:00
5219d50d4b feat(okr): onboarding skriver OKF-frontmatter paa org-profil (SC6)
compose-org-profile.mjs prepender top-level OKF-noekler (type:
Organisasjonsprofil/resource/timestamp) over den nestede org-profilen i
ETT frontmatter-blokk, pipes til write-org-profile.mjs (uendret). Kun de
tre trygge noeklene top-level; FORBID nye top-level navn/id/fase/domene/
sektor som ville skygge nestede verdier hooken leser. oppsett.md (begge
profil-skriv-sites) + template oppdatert. Test: compose->pip->readback +
inject-roundtrip (4), full suite 51->55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 17:19:14 +02:00
9701c81158 feat(okr): okf-index (ekte format)/okf-check + okf_version per rot (SC2,SC3,SC7) [skip-docs]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 15:12:48 +02:00
615542d064 fix(okr): analyse leser historikk direkte etter hook-splitt (SC5)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
2026-06-26 15:07:19 +02:00
ea2bd444ea feat(okr): splitt inject-hook ved 91/92 - kjerne + resolvert index-peker (SC5) [skip-docs] 2026-06-26 14:45:34 +02:00
bf0e463b4a feat(okr): retrieval-SKILL m/ OKF-mønster + fixtures (SC1,SC4) [skip-docs] 2026-06-26 14:39:46 +02:00
9b85c0b436 refactor(okr): begge hooks bruker delt frontmatter-modul (SC2) [skip-docs] 2026-06-26 14:21:58 +02:00
d9f2848d90 feat(okr): delt frontmatter-parse/skrive-modul + #-strip-fix (SC2) [skip-docs] 2026-06-26 14:19:20 +02:00
274d833e64 docs(okr): OKF second-brain retningsnotat (neste initiativ -> 1.6.0) 2026-06-26 10:48:48 +02:00
3ed59d05fb chore(okr): bump 1.5.0 (Fase 3) 2026-06-26 10:06:13 +02:00
88eaca81e0 feat(okr): /okr:help kommando-/agentoversikt + syklusarbeidsflyt (SC2) 2026-06-26 09:53:46 +02:00
4488a84e3a feat(okr): /okr:freshen-references + KB-selvevaluator-rubrikk (SC8) 2026-06-26 09:49:35 +02:00
a0e533a856 feat(okr): /okr:export PDF via weasyprint + scripts/export-pdf.py (SC5) 2026-06-26 09:46:39 +02:00
5890d09ac9 feat(okr): utvid metrics-library m/ verifiserte offentlig-metrikker (SC7) 2026-06-26 00:42:39 +02:00
65d4f289d5 feat(okr): ankret kvalitetsrubrikk-referansefil + wiring (SC3) 2026-06-26 00:38:15 +02:00
c7a0323a81 feat(okr): UserPromptSubmit emne-guard + test (SC6) [skip-docs] 2026-06-26 00:17:10 +02:00
b594eeb88c feat(okr): coaching-hook klokke-seam + node-test fixtures (SC4) [skip-docs] 2026-06-26 00:11:14 +02:00
60174e8f5c feat(okr): wire org-profil-skriving i oppsett + dokumenter hjem-sti (SC1) 2026-06-26 00:08:52 +02:00
63a642b29f feat(okr): atomisk org-profil-skrivehelper + test (SC1) [skip-docs] 2026-06-26 00:06:11 +02:00
e4f6c4d498 chore(okr): bump 1.4.0 (Fase 1+2) 2026-06-24 11:47:16 +02:00
d969cedf47 feat(okr): wire 6 referansefiler i kommando/agent + rett CLAUDE.md-tall (SC4) 2026-06-24 11:42:18 +02:00
97adff50de feat(okr): template-skjema-paritet med onboarding (SC5) [skip-docs] 2026-06-24 11:38:41 +02:00
9ab9ebe499 feat(okr): hook hybrid org-lese-fallback + node-test (SC6) [skip-docs] 2026-06-24 11:37:23 +02:00
e390a7a213 feat(okr): agent-modning — negative triggere + org-lesing + antipatterns (SC3) [skip-docs] 2026-06-24 11:34:30 +02:00
9eddec836b feat(okr): avdupliser gap/governance-tabell + disambiguering (SC2) 2026-06-24 11:28:23 +02:00
2cb8f71a47 feat(okr): kontekstbevissthet-blokk i moter (SC1) 2026-06-24 11:26:50 +02:00
ef3d51cde7 fix(okr): fjern loopende Stop-hook + bump 1.3.2
Stop-hooken var type `prompt`: en prompt-Stop-hook sendes til modellen
hver gang økten stopper, og enhver ikke-tom output tolkes som feedback →
agenten re-invokeres → stopper igjen → loop. «Skip entirely if no OKR
work»-grenen kunne aldri bli helt taus (modellen forklarer at den
skipper = ikke-tom output), så den looper hver økt uten OKR-arbeid.
Observert i praksis: 6+ runder før harness flagget «Stop hook error».

Funksjonen var dessuten svak: en blind prosa-nudge som ikke kan
verifisere om arbeid er lagret, og som peker på integrasjoner som knapt
er wiret (Oboard: ingen; Linear: valgfri MCP). Kontinuitet er allerede
dekket av PreCompact-hooken (bevarer arbeid-i-progresjon) + STATE.md.
Fjernet framfor å konvertere til command-hook (over-engineering uten
reell sync-sjekk). Revurderes hvis ekte ekstern-tracker-integrasjon
lander (Fase 3).

Versjonssync → 1.3.2: plugin.json, README-badge, SKILL.md, og rettet
stale CLAUDE.md-header (v1.3.0 → v1.3.2; Fase 0 bommet på denne).
Fjernet Stop-raden fra CLAUDE.md Hooks-tabell.
2026-06-24 10:26:29 +02:00
46ac021355 fix(okr): harmoniser DFO→DFØ (korrekt forkortelse) — løser /trekreview WARN
Fase 1-prosaen innførte den korrekte moderne forkortelsen DFØ i
dfo-okr-mapping.md, mens resten av fila (+ README, SKILL) brukte den
upresise legacy-formen DFO. Samme etat staves dermed to måter — en
konsistens-sprekk en DFØ/Riksrevisjon-leser (pluginets målgruppe) fanger.

Harmonisert hele pluginen mot DFØ (offisiell form, Direktoratet for
forvaltning og økonomistyring):
- dfo-okr-mapping.md: 20 tokens (14 ordgrense + 6 genitiv «DFOs»→«DFØs»)
- README.md: 2 tokens
- SKILL.md: 1 token

dfo.no-URLene er urørt (faktisk domene, ikke forkortelsen). Filnavnet
dfo-okr-mapping.md beholdes (ASCII-slug; rename ville brutt referanser).
Ingen versjonsbump (Fase 1-disiplin — filene forblir 1.3.1).

Løser review.md MAJOR-funn a1f3c9e2 (PLAN_EXECUTE_DRIFT,
verdict WARN → resolved).
2026-06-24 10:11:11 +02:00
43d3a3b01d feat(okr): reelle norske OKR-caser m/ proveniens (Digdir/Oslo/Ruter/Knowit), drop NRK 2026-06-23 23:06:41 +02:00
9a12c3b03e feat(okr): tillitsreform-seksjon (datert/hedget, Kuvaas vs Bjurstrøm) 2026-06-23 23:05:16 +02:00
0f5b2d1ca7 feat(okr): MRS-posisjonering — OKR lag oppå MRS (økonomiregelverket §4) 2026-06-23 23:03:41 +02:00
7b861bca2c feat(okr): målforskyvning-antipattern (Merton/Campbell, norske caser) 2026-06-23 23:02:46 +02:00
6adf290aad fix(okr): committed og aspirational scores skal ikke aggregeres til ett tall (Doerr) 2026-06-23 23:00:32 +02:00
0d4adb9cb3 fix(okr): dropp ukildet Locke 250%, fullfør Kleingeld-sitering (JAP 96(6)) 2026-06-23 22:59:48 +02:00
a4ae0cc45b fix(okr): Intel-faktakorreksjon — gjenvunnet 85% 16-bit innen 1986, ikke fra 0% 2026-06-23 22:59:15 +02:00
2403682a89 fix(okr): tertial-korreksjon — tre rapporteringsspor, sjekk tildelingsbrev 2026-06-23 22:58:23 +02:00
6f31a52b6a refactor(okr): rename plugin-egen Kvartalsreview til Syklusreview (6 sites) 2026-06-23 22:56:44 +02:00
7cd5e115dc chore(okr): bump versjon til 1.3.1 (Fase 0 troverdighetssanering) 2026-06-23 21:22:44 +02:00
5723c7957e fix(okr): rett CONTRIBUTING/GOVERNANCE-motsigelse, SECURITY-kanal, uciterte statistikker 2026-06-23 21:19:50 +02:00
a40da9b9c6 fix(okr): fjern død futureworks-lenke, normaliser rework-lenker til /en/ 2026-06-23 21:17:10 +02:00
821ee242a0 fix(okr): innfør én kanonisk scoringsterskel (0.7/0.6-0.7), bevar kanon-kilde 2026-06-23 21:14:42 +02:00
73b34c09fd fix(okr): merk DDT fiktiv, fjern fabrikkert Oboard-kilde og README reell-bruker-påstand 2026-06-23 21:11:16 +02:00
ca14bde4d0 fix(okr): rett find-replace-artefakter i framework/examples/skriv 2026-06-23 21:07:54 +02:00
2ae7d5a31b fix(okr): saner governance.md — find-replace, DDT, scoring, dokumenttittel 2026-06-23 21:04:03 +02:00
b1bf0db1d2 fix(okr): omskriv individual-vs-team-okr.md til korrekt norsk UTF-8 2026-06-23 20:57:56 +02:00
c32b7f6777 fix(okr): saner coaching-hook — læring-regex, UTF-8-strenger, kommandonavn 2026-06-23 20:55:22 +02:00
4231ccf513 fix(okr): saner oppsett.md — UTF-8, rm-rf-gardering, kommandonavn, DDT
Fase 0 Step 1/10 (circuit-breaker). Restaurerer norske tegn i prosa
(0→55å/67ø/5æ), garderer rm -rf med regex ^[TQ][1-4]-[0-9]{4}$ + non-empty,
retter /okr:moter→/okr:møter + oppstartsmate→oppstartsmøte, merker 2 DDT
som fiktiv eksempeletat. YAML-schema-nøkler bevart (Fase 2-scope).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSVEbrrahcSVoNG4WG3NrP
2026-06-23 20:43:52 +02:00
16ae8d3db3 docs: add 360 evaluation + reference-grade execution plan (okr)
360-degree evaluation (14-agent workflow: 6 readers, 4 researchers,
2 benchmarks, synthesis + adversarial plan-critic). Verdict C+:
strong architecture/methodology, credibility-critical content defects
(fictional agency presented as fact, find-replace artefacts, scoring
self-contradiction across 5+ files, UTF-8 corruption, dead links).

- docs/evaluering-360-2026-06-23.md: findings per dimension, scorecard,
  verification log, ms-ai-architect maturity gaps, plan-critic appendix.
- docs/plan-referansegrad-2026-06-23.md: critic-corrected, followable
  multi-session plan (Fase 0-3) with finding->phase traceability,
  version-per-phase, file-ownership, On-failure/circuit-breaker rules,
  and Voyage usage per phase.

Pilot review (2026-06-20) covered security/config; this covers domain,
content, language, UX, completeness. Implementation not started.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSVEbrrahcSVoNG4WG3NrP
2026-06-23 18:36:01 +02:00
cff9ac0447 docs: add full-depth plugin review (pilot, 2026-06-20)
Pilot review for a marketplace-wide review method. config-audit v5.4.0 (posture A,
plugin-health 0) + llm-security (A-, 0 critical/high/medium) + structure + version.
Verdict: strong, low-risk plugin. 2 Low nits (assert cycle-ID before rm in oppsett.md:457;
SECURITY.md:57 key-storage guidance). Read-only review; this file is the only artifact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 07:28:54 +02:00
ea2827ef1c chore(gitignore): add session/local-state baseline (polyrepo split) 2026-06-18 10:21:13 +02:00
99 changed files with 5344 additions and 403 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "okr", "name": "okr",
"version": "1.3.0", "version": "1.6.1",
"description": "Expert OKR guidance for Norwegian public sector. Write, review, cascade, track and govern OKR based on Google/Doerr methodology adapted for 4-month tertial cycles.", "description": "Expert OKR guidance for Norwegian public sector. Write, review, cascade, track and govern OKR based on Google/Doerr methodology adapted for 4-month tertial cycles.",
"author": { "author": {
"name": "Kjell Tore Guttormsen" "name": "Kjell Tore Guttormsen"

12
.gitignore vendored
View file

@ -23,3 +23,15 @@ Thumbs.db
# Temporary files # Temporary files
*.tmp *.tmp
*.bak *.bak
# --- session/local state (gitignored per ~/.claude polyrepo-konvensjon) ---
STATE.md
REMEMBER.md
ROADMAP.md
TODO.md
NEXT-SESSION-PROMPT*.local.md
*.local.md
*.local.json
*.local.sh
.DS_Store
.claude/

View file

@ -5,6 +5,73 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- **OKF second-brain spec v0.1 ratifisert (cross-plugin koordinering)** — den delte konvensjonen er kanonisert i `catalog/docs/okf-second-brain/spec.md` (katalog-eid, single source of truth). okr ratifiserer den per handoff §3/§4: `okf-check.mjs`-semantikken (kun `type` påkrevd; anbefalte felt → advarsler; `okf_version`-ekko) står som referansekontrakt (spec §7); okr bruker `resource` (ikke `source`); ingen feltgap (profil/config-nøkler bæres som extension keys, spec §5). Innboks-ingestion-planen (`docs/innboks-ingestion-veivalg-2026-06.md`) bygger nå mot spec-en; delings-scope for en eventuell Stage-3-skill = okr + ms-ai-architect (ikke linkedin-studio). Ingen kode- eller versjonsendring.
## [1.6.1] - 2026-06-26
### Fixed
- **`okr-quality-rubrics.md` omskrevet til korrekt norsk** — den ankrede kvalitetsrubrikken var fullstendig ASCII-strippet (0 å/ø/æ) til tross for at den lastes brukervendt av `/okr:kvalitet`, `kvalitetssjekker`-agenten og `/okr:freshen-references`. Samme tekstkvalitetsklasse som 360-en flagget KRITISK i v1.3.0 — en regresjon innført med selve rubrikk-fila.
- **`SKILL.md` flaggskip-eksempel** — strippet norsk i det første konkrete OKR-eksemplet (per år / høyrisiko / spørreundersøkelse / Gjennomføre møter).
- **`okr-second-brain-search` `~`-glob** — retrieval-instruksjonen brukte literal `~/.claude/okr/org/`, som Glob-verktøyet ikke ekspanderer → hjemme-roten (org-identitet) ble stilltiende aldri søkt, mot skillens eget «search both roots»-premiss. Nå eksplisitt absolutt hjemme-sti.
- **Uverifisert `kb-search`-sitat fjernet**`okr-second-brain-search` hevdet en OKF `kb-search`-triad som ikke finnes (motsagt av `docs/innboks-ingestion-funn-2026-06.md`); erstattet med funksjonell Glob/Read/Grep-beskrivelse + `discovery`-mønsteret.
- **Regnefeil i `okr-oboard-guide.md`** — KR1-progresjon viste 52 %; korrekt er 65 % per filens egen formel `(startcurrent)/(starttarget)`.
### Notes
- Patch utløst av `docs/evaluering-360-2026-06-26.md` (360-re-evaluering på v1.6.0): C+ → A. Systemiske funn (KB-kryssreferanser, «Sist oppdatert»-markører) er bevisst utsatt.
## [1.6.0] - 2026-06-26
### Added
- **`okr-second-brain-search`-skill** — on-demand retrieval over en OKF-kompatibel markdown-wiki (`.claude/okr/` prosjekt + `~/.claude/okr/org/` home) via native Glob/Read/Grep, i fri chat OG under `/okr:*`-kommandoer. Semantisk dekomponering med opptil 3 query-variasjoner; ingen for-injeksjon av alt innhold.
- **OKF-frontmatter på alle kontekstfiler** — org-profil (`type: Organisasjonsprofil`) og tre-skrivere (`type: Tildelingsbrev`/`Virksomhetsplan`/`Overordnede OKR`/`OKR`/`Retrospektiv`/`Status`) emitterer ekte OKF-schema (Title-Case `type` + `resource`/`title`/`description`/`tags`/`timestamp`).
- **`scripts/okf-index.mjs`** — genererer `index.md` per nivå (verbatim OKF-format `* [title](link) - description`) per bundle-rot.
- **`scripts/okf-check.mjs`** — validerer at hver konsept-fil bærer `type:`; rapporterer `okf_version` per rot.
- **`scripts/compose-org-profile.mjs`** — bygger nestet org-profil-YAML med top-level OKF-nøkler før pipe til `write-org-profile.mjs`.
- **`lib/frontmatter.mjs`** — delt frontmatter-parser/skriver; konsoliderer duplisert parse-logikk i begge hooks og fikser comment-leak på usiterte verdier.
### Changed
- **`inject-okr-context.mjs` splittet** — for-injiserer ikke lenger hele kontekstfil-enumerasjonen; emitterer kjerne-profil + én resolvert peker til `index.md` (< 512 B, uavhengig av filantall). Retrieval skjer on-demand via skill.
- **`/okr:analyse`** leser `historikk/` direkte (Glob/Read) etter hook-splitten.
- **Begge hooks** ruter frontmatter-parsing gjennom delt `lib/frontmatter.mjs`.
## [1.5.0] - 2026-06-26
### Added
- **`/okr:help`** — full oversikt over alle kommandoer (13), agenter (7) og anbefalt syklusarbeidsflyt.
- **`/okr:export`** — eksporter OKR-leveranser (kvalitetsvurdering, gap-matrise, statusrapport, retrospektiv) til print-klar PDF via `scripts/export-pdf.py` (weasyprint).
- **`/okr:freshen-references`** — KB-selvevaluator: scorer de 16 referansefilene mot en ankret dekning-/kvalitet-rubrikk og currency-poller offentlige kilder.
- **Ankret kvalitetsrubrikk** (`references/okr-quality-rubrics.md`, 10 dimensjoner × 5 forankrede nivåer) wiret inn i `/okr:kvalitet`, `kvalitetssjekker`-agenten og SKILL.md.
- **Atomisk org-profil-skriving**: `scripts/write-org-profile.mjs` skriver maskin-global org-identitet til `~/.claude/okr/org/profil.md` (temp + `renameSync`), med circuit-breaker til prosjekt-lokal `.claude/okr.local.md` ved hjem-skrivefeil. Fullfører hybrid org-kontekst (skrive-sti) startet i 1.4.0.
- **Node-test-dekning**: nye fixturer for org-profil-skriving, coaching-hookens fase-deteksjon og UserPromptSubmit emne-guard (17 tester totalt, grønn).
### Changed
- **Metrics-library** utvidet med verifiserte kryss-sektor offentlig-metrikker (EGDI m.fl., kildebelagt; udokumentert `eForvaltningsindeks` fjernet).
- **Coaching-hook** fikk en klokke-seam for testbar fase-deteksjon (early/mid/late).
- **UserPromptSubmit emne-guard**: injiserer kun OKR-kontekst på relevante prompts (tvil → injiser).
## [1.4.0] - 2026-06-24
### Added
- **Fase 1 — Domenedybde og kildeintegritet**: tertial-korreksjon (DFØ interim-/årsrapport-regime), OKR posisjonert som lag oppå mål- og resultatstyring (økonomireglementet §4), tillitsreform-seksjon, «målforskyvning» som norsk antipattern, reelle norske offentlig-sektor-referanser (Digdir/Oslo/Ruter/Knowit) med proveniens.
- **Fase 2 — UX-koherens og kommando-/agent-modning**: Kontekstbevissthet-blokk i `/okr:møter` (9/10 kommandoer; `oppsett` bevisst unntatt), `/okr:gap` vs `/okr:governance` disambiguert, negative triggere + direkte org-lesing i alle 7 agenter, 6 referansefiler wiret inn ved bruksstedet, template↔onboarding skjema-paritet.
- **Hybrid org-kontekst-lesing (lese-sti)**: `inject-okr-context.mjs` resolverer prosjekt-lokal `.claude/okr.local.md` → maskin-global `~/.claude/okr/org/profil.md` (mest-spesifikk-vinner; forward-compat, migrasjon kommer i Fase 3). `node:test`-fixtur for hooken.
### Changed
- Metodikk-presisjoner (Intel/Locke-fakta, committed+aspirational ikke snittet sammen), «Kvartalsreview» → «Syklusreview».
## [1.3.2] - 2026-06-24
### Fixed
- Fjernet loopende Stop-hook (type `prompt` → enhver output blokkerte stop → re-invoke-loop).
## [1.3.1] - 2026-06-23
### Fixed
- Fakta- og troverdighetssanering: scoringsterskel harmonisert (0.60.7, Google/Doerr), fiktiv eksempeletat merket, find-replace-artefakter rettet, UTF-8/ASCII-korreksjoner, døde lenker fjernet.
## [1.3.0] - 2026-04-08 ## [1.3.0] - 2026-04-08
### Added ### Added

View file

@ -1,4 +1,4 @@
# OKR Offentlig Sektor v1.3.0 # OKR Offentlig Sektor v1.6.1
Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapted for 4-month tertial cycles. Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapted for 4-month tertial cycles.
@ -16,6 +16,9 @@ Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapte
| `/okr:gap` | Automatic gap analysis between tildelingsbrev and current OKR | | `/okr:gap` | Automatic gap analysis between tildelingsbrev and current OKR |
| `/okr:analyse` | Cross-cycle analytics with Mermaid trend visualizations | | `/okr:analyse` | Cross-cycle analytics with Mermaid trend visualizations |
| `/okr:oppsett` | Configure plugin: onboarding interview (full/mvp), cycle archival, profile update. Args: `full\|mvp\|arkiver\|oppdater\|vis` | | `/okr:oppsett` | Configure plugin: onboarding interview (full/mvp), cycle archival, profile update. Args: `full\|mvp\|arkiver\|oppdater\|vis` |
| `/okr:export` | Export OKR deliverables (quality review, gap matrix, status report, retrospective) to print-ready PDF via weasyprint |
| `/okr:freshen-references` | KB self-evaluator: score the 16 domain reference files against an anchored rubric + currency-poll public sources |
| `/okr:help` | Full overview of all commands, agents, and recommended cycle workflow |
## Agents ## Agents
@ -36,23 +39,39 @@ Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapte
| SessionStart | command | Proactive coaching based on cycle position and OKR status | | SessionStart | command | Proactive coaching based on cycle position and OKR status |
| UserPromptSubmit | command | Inject org profile (expanded YAML), current cycle, `.claude/okr/` summary, and archived cycle count | | UserPromptSubmit | command | Inject org profile (expanded YAML), current cycle, `.claude/okr/` summary, and archived cycle count |
| PreCompact | prompt | Preserve OKR draft state during compaction | | PreCompact | prompt | Preserve OKR draft state during compaction |
| Stop | prompt | Remind user to save OKR work to tracking system |
## Skill ## Skills
| Component | Location | | Component | Location |
|-----------|----------| |-----------|----------|
| SKILL.md | `skills/okr-offentlig-sektor/SKILL.md` | | SKILL.md (okr-offentlig-sektor) | `skills/okr-offentlig-sektor/SKILL.md` |
| References (15) | `skills/okr-offentlig-sektor/references/` | | References (17) | `skills/okr-offentlig-sektor/references/` |
| SKILL.md (okr-second-brain-search) | `skills/okr-second-brain-search/SKILL.md` |
The second skill (`okr-second-brain-search`) does on-demand retrieval over the user's OKF-compatible wiki (native Glob/Read/Grep, no MCP) — see OKF Knowledge Layout below.
## State Management ## State Management
User configuration: `.claude/okr.local.md` in the project directory (not in plugin root). User configuration: `.claude/okr.local.md` in the project directory (not in plugin root).
Org profile (reinstall-surviving): `~/.claude/okr/org/profil.md` — machine-global org identity (`organisasjon:`/`program:`), written atomically (temp + `renameSync`) by `scripts/write-org-profile.mjs` during `/okr:oppsett`. Resolution is most-specific-wins: project-local `.claude/okr.local.md` overrides the home profile (the backwards-compatible fallback when no project config exists; read side `hooks/scripts/inject-okr-context.mjs`). Home-write failure → circuit-break to the gitignored project-local file. Only the org *profile* migrates to home; cycle/`historikk` data stays cwd-bound.
Template: `templates/okr.local.md.template` Template: `templates/okr.local.md.template`
Context tree: `.claude/okr/``strategisk-kontekst/`, `syklus/[id]/`, `historikk/`, `dokumenter/` Context tree: `.claude/okr/``strategisk-kontekst/`, `syklus/[id]/`, `historikk/`, `dokumenter/`
Onboarding state: `onboarding_status` field in okr.local.md (`partial` | `fullfort`) Onboarding state: `onboarding_status` field in okr.local.md (`partial` | `fullfort`)
Cycle archival: `/okr:oppsett arkiver` — moves `syklus/` to `historikk/`, generates `retrospektiv.md` Cycle archival: `/okr:oppsett arkiver` — moves `syklus/` to `historikk/`, generates `retrospektiv.md`
## OKF Knowledge Layout
Context files carry OKF-compatible frontmatter (Knowledge Catalog "Documents/kb Layout", aka "Metadata as Code" — not a formal "OKF v0.1" standard): a Title-Case `type` (`Organisasjonsprofil`, `Tildelingsbrev`, `Virksomhetsplan`, `Overordnede OKR`, `OKR`, `Retrospektiv`, `Status`) plus recommended `resource`/`title`/`description`/`tags`/`timestamp`. Only `type` is de-facto required. The flat frontmatter parser reads only single-line keys (`type`, profile/config fields); tree files may carry multi-line `tags` lists without breaking it.
**Two bundle roots** with distinct lifecycles: project `.claude/okr/` (cwd-bound cycle/work) and home `~/.claude/okr/org/` (reinstall-surviving org identity). Each root carries its own `index.md` (no frontmatter; `# H1` + `* [title](link) - description`) and an `okf_version` marker. `okf-index`/`okf-check` run per root; retrieval Globs both (project preferred, else home).
- `lib/frontmatter.mjs` — shared frontmatter parser/writer; both hooks route through it (strips trailing ` #…` only from unquoted values).
- `scripts/okf-index.mjs` — regenerate a root's per-level `index.md` (verbatim OKF index format).
- `scripts/okf-check.mjs` — validate each concept file carries `type:` (exit 1 + count otherwise); report `okf_version` per root.
- `scripts/compose-org-profile.mjs` — build the nested org-profile YAML with top-level OKF keys (`type`/`resource`/`timestamp`) before piping to `write-org-profile.mjs`.
Retrieval is on-demand via the `okr-second-brain-search` skill. The UserPromptSubmit hook no longer pre-injects the full context-file enumeration — it emits the core profile plus one resolved pointer to `index.md` (< 512 B, independent of file count).
## Language Policy ## Language Policy
- Commands, agents, user-facing text: Norwegian - Commands, agents, user-facing text: Norwegian
@ -73,5 +92,10 @@ Cycle archival: `/okr:oppsett arkiver` — moves `syklus/` to `historikk/`, gene
/okr:analyse ──→ trendanalytiker /okr:analyse ──→ trendanalytiker
/okr:oppsett ──→ (inline wizard: full/mvp/arkiver/oppdater/vis) /okr:oppsett ──→ (inline wizard: full/mvp/arkiver/oppdater/vis)
/okr:oppsett arkiver ──→ cycle archival + retrospektiv-generering /okr:oppsett arkiver ──→ cycle archival + retrospektiv-generering
/okr:export ──→ scripts/export-pdf.py (weasyprint, documented prerequisite)
/okr:freshen-references ──→ (inline KB-evaluator + currency-polling via WebSearch/Task)
/okr:help ──→ (inline command/agent/workflow overview)
SessionStart ──→ coaching-hook.mjs (proactive coaching) SessionStart ──→ coaching-hook.mjs (proactive coaching)
UserPromptSubmit ──→ inject-okr-context.mjs (core profile + resolved index.md pointer)
okr-second-brain-search (skill) ──→ Glob/Read/Grep over .claude/okr/ + ~/.claude/okr/org/ (on-demand retrieval)
``` ```

View file

@ -20,14 +20,13 @@ Takk for at du vil bidra til OKR-pluginen!
3. Explain why this would benefit other users 3. Explain why this would benefit other users
4. Consider public sector context and constraints 4. Consider public sector context and constraints
### Submitting Pull Requests ### Pull Requests — not accepted
1. Fork the repository This is a single-maintainer, fork-and-own reference plugin: **pull requests are
2. Create a feature branch (`git checkout -b feature/my-feature`) not accepted.** See [GOVERNANCE.md](GOVERNANCE.md) (section "Pull requests — no")
3. Make your changes for the rationale and the contribution model. To propose a change, open an issue
4. Test thoroughly with `/okr` commands (see above); to adapt the plugin for your organization, fork it and own the
5. Commit with clear messages (`feat:`, `fix:`, `docs:`) result — the recommended adoption pattern described in GOVERNANCE.md.
6. Push and open a PR
## Development Setup ## Development Setup

View file

@ -6,12 +6,12 @@
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)* *AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
![Version](https://img.shields.io/badge/version-1.3.0-blue) ![Version](https://img.shields.io/badge/version-1.6.1-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple) ![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Agents](https://img.shields.io/badge/agents-7-orange) ![Agents](https://img.shields.io/badge/agents-7-orange)
![Commands](https://img.shields.io/badge/commands-10-blue) ![Commands](https://img.shields.io/badge/commands-13-blue)
![Hooks](https://img.shields.io/badge/hooks-4-green) ![Hooks](https://img.shields.io/badge/hooks-4-green)
![References](https://img.shields.io/badge/references-16-yellow) ![References](https://img.shields.io/badge/references-17-yellow)
![License](https://img.shields.io/badge/license-MIT-lightgrey) ![License](https://img.shields.io/badge/license-MIT-lightgrey)
--- ---
@ -20,14 +20,14 @@
Every organization has a strategy. Few manage to turn it into goals that teams actually work toward. Every organization has a strategy. Few manage to turn it into goals that teams actually work toward.
OKR (Objectives and Key Results) is a proven framework for that translation — used by Google, Intel, and increasingly by Norwegian public sector organizations like NAV, FINN.no, and Direktoratet for digital tjenesteutvikling. But adopting OKR is hard. The methodology sounds simple ("write inspiring goals with measurable results") until you try it. Then you hit real questions: OKR (Objectives and Key Results) is a proven framework for that translation — used by Google, Intel, and increasingly by Norwegian public sector organizations like NAV and FINN.no. But adopting OKR is hard. The methodology sounds simple ("write inspiring goals with measurable results") until you try it. Then you hit real questions:
- *How do we connect our OKR to the goals in our tildelingsbrev?* - *How do we connect our OKR to the goals in our tildelingsbrev?*
- *What's a good Key Result vs. just an activity disguised as one?* - *What's a good Key Result vs. just an activity disguised as one?*
- *How do we handle it when political priorities shift mid-cycle?* - *How do we handle it when political priorities shift mid-cycle?*
- *Are we actually getting better at this, or just going through the motions?* - *Are we actually getting better at this, or just going through the motions?*
This plugin is an AI coach that helps with all of these. It's built specifically for Norwegian public sector — 4-month tertial cycles, DFO's "mål- og resultatstyring" framework, tillitsvalgt involvement, and the governance chain from Stortingsmelding to team OKR. This plugin is an AI coach that helps with all of these. It's built specifically for Norwegian public sector — 4-month tertial cycles, DFØ's "mål- og resultatstyring" framework, tillitsvalgt involvement, and the governance chain from Stortingsmelding to team OKR.
### What makes it different from a methodology guide? ### What makes it different from a methodology guide?
@ -103,7 +103,7 @@ You don't have to remember to check in. When you start a session, the plugin aut
### Meetings and Facilitation ### Meetings and Facilitation
``` ```
> /okr:moter > /okr:møter
``` ```
Generates tailored agendas for OKR workshops, check-ins, reviews, and 1:1 conversations using the CFR (Conversations, Feedback, Recognition) framework — adapted for public sector where bonuses aren't an option. Generates tailored agendas for OKR workshops, check-ins, reviews, and 1:1 conversations using the CFR (Conversations, Feedback, Recognition) framework — adapted for public sector where bonuses aren't an option.
@ -111,7 +111,7 @@ Generates tailored agendas for OKR workshops, check-ins, reviews, and 1:1 conver
### Adoption and Change Management ### Adoption and Change Management
``` ```
> /okr:innforing > /okr:innføring
``` ```
Planning to introduce OKR in your organization? Get a phased rollout plan with resistance-handling strategies, maturity assessment, and guidance on involving tillitsvalgte — because in Norwegian public sector, change management without union involvement doesn't work. Planning to introduce OKR in your organization? Get a phased rollout plan with resistance-handling strategies, maturity assessment, and guidance on involving tillitsvalgte — because in Norwegian public sector, change management without union involvement doesn't work.
@ -124,6 +124,23 @@ Planning to introduce OKR in your organization? Get a phased rollout plan with r
Translate tildelingsbrev requirements into OKR. Map the governance chain (Stortingsmelding → tildelingsbrev → etatsstrategi → OKR). Verify that your OKR documentation meets Riksrevisjon standards. Translate tildelingsbrev requirements into OKR. Map the governance chain (Stortingsmelding → tildelingsbrev → etatsstrategi → OKR). Verify that your OKR documentation meets Riksrevisjon standards.
### Export Deliverables
```
> /okr:export
```
Renders any OKR deliverable — quality review, gap matrix, status report, or retrospective — to a print-ready A4 PDF for management or Riksrevisjon. Tables get striping and score cells are color-coded (green/yellow/red). PDF generation is a documented prerequisite (`pip install markdown weasyprint`, `brew install pango`), not bundled — a missing dependency exits with a clear install hint, never a traceback.
### Help and Maintenance
```
> /okr:help
> /okr:freshen-references
```
`/okr:help` lists all 13 commands, 7 agents, and a recommended workflow keyed to where you are in the tertial cycle. `/okr:freshen-references` is a knowledge-base self-evaluator: it scores the 16 domain reference files against an anchored quality rubric and polls named public sources (UN EGDI, EU eGov Benchmark, OECD DGI, Digdir, WCAG, forvaltningsloven) to flag outdated `Sist oppdatert` markers.
--- ---
## Getting Started ## Getting Started
@ -163,7 +180,7 @@ Want the full experience? `/okr:oppsett full` runs a deeper interview (15-20 min
**During a cycle:** **During a cycle:**
``` ```
/okr:sporing # Update progress /okr:sporing # Update progress
/okr:moter # Prepare check-in meeting /okr:møter # Prepare check-in meeting
``` ```
**End of cycle:** **End of cycle:**
@ -183,7 +200,7 @@ Want the full experience? `/okr:oppsett full` runs a deeper interview (15-20 min
|-----------|---------------------| |-----------|---------------------|
| 4-month tertial cycles (not quarterly) | Built-in tertial support with correct week counting and phase detection | | 4-month tertial cycles (not quarterly) | Built-in tertial support with correct week counting and phase detection |
| Tildelingsbrev drives priorities | `/okr:governance` and `/okr:gap` translate and check coverage | | Tildelingsbrev drives priorities | `/okr:governance` and `/okr:gap` translate and check coverage |
| DFO uses different terminology | Built-in mapping between "mål- og resultatstyring" and OKR concepts | | DFØ uses different terminology | Built-in mapping between "mål- og resultatstyring" and OKR concepts |
| Riksrevisjonen requires documentation | Audit-ready format with baseline, target, data source per KR | | Riksrevisjonen requires documentation | Audit-ready format with baseline, target, data source per KR |
| No performance bonuses | CFR framework designed for recognition without financial incentives | | No performance bonuses | CFR framework designed for recognition without financial incentives |
| Tillitsvalgte must be involved | Guidance on drøftingsplikt, transparent KR, and union involvement | | Tillitsvalgte must be involved | Guidance on drøftingsplikt, transparent KR, and union involvement |
@ -235,9 +252,16 @@ The plugin understands this hierarchy and helps you maintain alignment at every
| PreCompact | Preserves OKR draft state if the conversation gets long | | PreCompact | Preserves OKR draft state if the conversation gets long |
| Stop | Reminds you to save work to your tracking system | | Stop | Reminds you to save work to your tracking system |
### Skills
| Skill | Role |
|-------|------|
| okr-offentlig-sektor | Core OKR methodology and Norwegian public-sector domain knowledge |
| okr-second-brain-search | On-demand retrieval from your personal OKF wiki (`.claude/okr/` + `~/.claude/okr/org/`) — in free chat and during `/okr:*` commands, without pre-injecting everything |
### Knowledge Base ### Knowledge Base
16 reference files covering OKR methodology, Norwegian public sector governance, antipatterns, meeting guides, metrics library, integration patterns, and more. The plugin reads only what's relevant to each interaction — never the whole library at once. 17 reference files covering OKR methodology, Norwegian public sector governance, antipatterns, meeting guides, metrics library, anchored quality rubrics, integration patterns, and more. The plugin reads only what's relevant to each interaction — never the whole library at once.
### Persistent Context ### Persistent Context
@ -266,6 +290,12 @@ The plugin understands this hierarchy and helps you maintain alignment at every
| Version | Date | Highlights | | Version | Date | Highlights |
|---------|------|------------| |---------|------|------------|
| **1.6.1** | 2026-06-26 | Patch: credibility-sanering etter 360-re-evaluering (C+ → A) — ASCII-strippet kvalitetsrubrikk omskrevet til korrekt norsk, `okr-second-brain-search` `~`-glob søker nå hjemme-roten, uverifisert kb-search-sitat fjernet, regnefeil i oboard-eksempel rettet |
| **1.6.0** | 2026-06-26 | OKF «second brain»: on-demand retrieval-skill (`okr-second-brain-search`) over OKF-wiki, OKF-frontmatter på kontekstfiler, `okf-index`/`okf-check`, delt frontmatter-modul, slankere inject-hook |
| **1.5.0** | 2026-06-26 | Referansegrad-løft (Fase 3): `/okr:help`, `/okr:export` (PDF), `/okr:freshen-references`, ankret kvalitetsrubrikk, atomisk org-profil-skriving |
| **1.4.0** | 2026-06-24 | Domenedybde + kildeintegritet (Fase 1), UX-/kommando-/agent-modning (Fase 2), hybrid org-kontekst-lesing |
| 1.3.2 | 2026-06-24 | Fix: loopende Stop-hook fjernet |
| 1.3.1 | 2026-06-23 | Fakta- og troverdighetssanering |
| **1.3.0** | 2026-04-08 | Gap analysis, cross-cycle analytics with Mermaid visualizations, proactive SessionStart coaching | | **1.3.0** | 2026-04-08 | Gap analysis, cross-cycle analytics with Mermaid visualizations, proactive SessionStart coaching |
| **1.1.0** | 2026-04-08 | Persistent context, deep onboarding, context-aware commands, cycle archival | | **1.1.0** | 2026-04-08 | Persistent context, deep onboarding, context-aware commands, cycle archival |
| **1.0.0** | 2026-04-08 | Architecture overhaul, self-contained commands, hooks, marketplace-ready | | **1.0.0** | 2026-04-08 | Architecture overhaul, self-contained commands, hooks, marketplace-ready |

View file

@ -11,7 +11,7 @@
If you discover a security vulnerability, please: If you discover a security vulnerability, please:
1. **Do not** open a public issue 1. **Do not** open a public issue
2. Email the maintainer directly or use GitHub's private vulnerability reporting 2. Email the maintainer directly, or report privately via the Forgejo repository (git.fromaitochitta.com/open/okr)
3. Include: 3. Include:
- Description of the vulnerability - Description of the vulnerability
- Steps to reproduce - Steps to reproduce

View file

@ -4,6 +4,8 @@ description: |
Bruk denne agenten når brukeren vil oppdatere OKR-status, beregne score, få prognose for Bruk denne agenten når brukeren vil oppdatere OKR-status, beregne score, få prognose for
måloppnåelse, eller generere statusrapport. måloppnåelse, eller generere statusrapport.
Ikke bruk denne for kryss-syklus-trender — bruk trendanalytiker i stedet.
<example> <example>
Context: Bruker har nye tall og vil oppdatere status Context: Bruker har nye tall og vil oppdatere status
user: "Vi har oppnådd 130 av 150 på KR1, hva er scoren?" user: "Vi har oppnådd 130 av 150 på KR1, hva er scoren?"
@ -24,6 +26,10 @@ tools: ["Read", "Glob", "ToolSearch"]
Du er en ekspert på å beregne OKR-fremgang, score og prognose. Du er en ekspert på å beregne OKR-fremgang, score og prognose.
## Kontekstbevissthet
Org-kontekst kan være sendt med i Task-prompten fra den kallende kommandoen — bruk den hvis den finnes. Hvis ikke, les prosjekt-lokal `.claude/okr.local.md` (relativt til brukerens prosjekt-cwd) direkte. Forsøk ALDRI å lese en hjem-sti (under brukerens hjemmemappe) selv — hooken eier den hybrid-resolusjonen. Finnes ingen org-kontekst: fortsett uten — ikke feil.
## Scoring-formel ## Scoring-formel
``` ```
@ -41,7 +47,7 @@ Score = (Nåværende - Baseline) / (Target - Baseline)
| Score | Betydning | Anbefaling | | Score | Betydning | Anbefaling |
|-------|-----------|------------| |-------|-----------|------------|
| 1.0 | 100% oppnådd | Feir! Var målet ambisiøst nok? | | 1.0 | 100% oppnådd | Feir! Var målet ambisiøst nok? |
| 0.7-0.9 | Suksess | Godt resultat for stretch-mål | | ≈0.7 | Sweet spot — 0.6-0.7 (Google/Doerr) | 0.7 = forventet for stretch-mål |
| 0.5-0.7 | Delvis | Analyser hva som hindret | | 0.5-0.7 | Delvis | Analyser hva som hindret |
| 0.3-0.5 | Under mål | Tiltak nødvendig | | 0.3-0.5 | Under mål | Tiltak nødvendig |
| < 0.3 | Kritisk | Vurder om KR er realistisk | | < 0.3 | Kritisk | Vurder om KR er realistisk |
@ -141,3 +147,4 @@ Hvis Linear er konfigurert, tilby å:
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-calculator.md` - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-calculator.md`
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-framework.md` - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-framework.md`
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-antipatterns.md` — les HELE filen ved fremdriftsvurdering (fange aktivitets-/sandbagging-/målforskyvnings-mønstre i statusen, ikke bare et utvalg)

View file

@ -4,6 +4,8 @@ description: |
Bruk denne agenten for automatisk gap-analyse mellom tildelingsbrev-krav og Bruk denne agenten for automatisk gap-analyse mellom tildelingsbrev-krav og
gjeldende OKR. Identifiserer udekte krav og OKR uten forankring. gjeldende OKR. Identifiserer udekte krav og OKR uten forankring.
Ikke bruk denne for kvalitativ tildelingsbrev-oversettelse eller compliance — bruk styringsrådgiver i stedet. Denne agenten leverer en kvantitativ dekningsmatrise.
<example> <example>
Context: Bruker har tildelingsbrev og OKR lagret i .claude/okr/ Context: Bruker har tildelingsbrev og OKR lagret i .claude/okr/
user: "Sjekk om OKR dekker tildelingsbrevet" user: "Sjekk om OKR dekker tildelingsbrevet"
@ -25,6 +27,10 @@ tools: ["Read", "Glob"]
Du er en ekspert på å analysere samsvaret mellom tildelingsbrev-krav og Du er en ekspert på å analysere samsvaret mellom tildelingsbrev-krav og
gjeldende OKR i norsk offentlig sektor. gjeldende OKR i norsk offentlig sektor.
## Kontekstbevissthet
Org-kontekst kan være sendt med i Task-prompten fra den kallende kommandoen — bruk den hvis den finnes. Hvis ikke, les prosjekt-lokal `.claude/okr.local.md` (relativt til brukerens prosjekt-cwd) direkte. Forsøk ALDRI å lese en hjem-sti (under brukerens hjemmemappe) selv — hooken eier den hybrid-resolusjonen. Finnes ingen org-kontekst: fortsett uten — ikke feil.
## Din oppgave ## Din oppgave
Analyser tildelingsbrev-krav mot gjeldende OKR og identifiser gap i begge Analyser tildelingsbrev-krav mot gjeldende OKR og identifiser gap i begge

View file

@ -4,6 +4,8 @@ description: |
Bruk denne agenten når brukeren trenger hjelp med å kaskadere OKR fra organisasjon til team, Bruk denne agenten når brukeren trenger hjelp med å kaskadere OKR fra organisasjon til team,
sikre alignment mellom nivåer, eller visualisere hvordan OKR henger sammen. sikre alignment mellom nivåer, eller visualisere hvordan OKR henger sammen.
Ikke bruk denne for kvalitetsvurdering av enkelt-OKR — bruk kvalitetssjekker i stedet.
<example> <example>
Context: Bruker vil kaskadere fra etat til team Context: Bruker vil kaskadere fra etat til team
user: "Hvordan kobler vi team-OKR til etatens mål?" user: "Hvordan kobler vi team-OKR til etatens mål?"
@ -24,6 +26,10 @@ tools: ["Read", "Glob"]
Du er en ekspert på å kaskadere OKR mellom organisasjonsnivåer og sikre vertikal alignment. Du er en ekspert på å kaskadere OKR mellom organisasjonsnivåer og sikre vertikal alignment.
## Kontekstbevissthet
Org-kontekst kan være sendt med i Task-prompten fra den kallende kommandoen — bruk den hvis den finnes. Hvis ikke, les prosjekt-lokal `.claude/okr.local.md` (relativt til brukerens prosjekt-cwd) direkte. Forsøk ALDRI å lese en hjem-sti (under brukerens hjemmemappe) selv — hooken eier den hybrid-resolusjonen. Finnes ingen org-kontekst: fortsett uten — ikke feil.
## Kaskaderingsprinsipp ## Kaskaderingsprinsipp
``` ```

View file

@ -4,6 +4,8 @@ description: |
Bruk denne agenten når brukeren presenterer OKR for vurdering, ber om feedback på OKR-kvalitet, Bruk denne agenten når brukeren presenterer OKR for vurdering, ber om feedback på OKR-kvalitet,
eller ønsker å forbedre eksisterende OKR. eller ønsker å forbedre eksisterende OKR.
Ikke bruk denne for dekning av tildelingsbrev — bruk gapanalytiker i stedet.
<example> <example>
Context: Bruker deler OKR for vurdering Context: Bruker deler OKR for vurdering
user: "Er dette gode OKR? Objective: Forbedre kundeservice. KR1: Gjennomføre 5 kurs" user: "Er dette gode OKR? Objective: Forbedre kundeservice. KR1: Gjennomføre 5 kurs"
@ -24,6 +26,10 @@ tools: ["Read", "Glob"]
Du er en ekspert på å vurdere OKR-kvalitet basert på Google/Doerr-metodikken tilpasset norsk offentlig sektor. Du er en ekspert på å vurdere OKR-kvalitet basert på Google/Doerr-metodikken tilpasset norsk offentlig sektor.
## Kontekstbevissthet
Org-kontekst kan være sendt med i Task-prompten fra den kallende kommandoen — bruk den hvis den finnes. Hvis ikke, les prosjekt-lokal `.claude/okr.local.md` (relativt til brukerens prosjekt-cwd) direkte. Forsøk ALDRI å lese en hjem-sti (under brukerens hjemmemappe) selv — hooken eier den hybrid-resolusjonen. Finnes ingen org-kontekst: fortsett uten — ikke feil.
## Din oppgave ## Din oppgave
Når du mottar OKR for vurdering: Når du mottar OKR for vurdering:
@ -110,5 +116,6 @@ Når du mottar OKR for vurdering:
Les disse filene for metodikk: Les disse filene for metodikk:
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/SKILL.md` - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/SKILL.md`
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md` — ankret scoringsguide (5 ankere per dimensjon; single sannhetskilde for scoring)
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-antipatterns.md` - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-antipatterns.md`
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-examples.md` - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-examples.md`

View file

@ -4,6 +4,8 @@ description: |
Bruk denne agenten når brukeren skal planlegge eller gjennomføre OKR-møter, workshops, Bruk denne agenten når brukeren skal planlegge eller gjennomføre OKR-møter, workshops,
eller 1:1-samtaler. eller 1:1-samtaler.
Ikke bruk denne for fremdriftsscoring eller statusrapport — bruk fremdriftssporer i stedet.
<example> <example>
Context: Bruker skal ha planleggingsworkshop Context: Bruker skal ha planleggingsworkshop
user: "Vi skal ha OKR-planleggingsworkshop neste uke for 8 personer" user: "Vi skal ha OKR-planleggingsworkshop neste uke for 8 personer"
@ -24,6 +26,10 @@ tools: ["Read", "Glob"]
Du er en ekspert på å planlegge og fasilitere OKR-relaterte møter i norsk offentlig sektor. Du er en ekspert på å planlegge og fasilitere OKR-relaterte møter i norsk offentlig sektor.
## Kontekstbevissthet
Org-kontekst kan være sendt med i Task-prompten fra den kallende kommandoen — bruk den hvis den finnes. Hvis ikke, les prosjekt-lokal `.claude/okr.local.md` (relativt til brukerens prosjekt-cwd) direkte. Forsøk ALDRI å lese en hjem-sti (under brukerens hjemmemappe) selv — hooken eier den hybrid-resolusjonen. Finnes ingen org-kontekst: fortsett uten — ikke feil.
## Møtetyper ## Møtetyper
### 1. Planleggingsworkshop ### 1. Planleggingsworkshop
@ -36,7 +42,7 @@ Du er en ekspert på å planlegge og fasilitere OKR-relaterte møter i norsk off
- **Varighet**: 15-30 min - **Varighet**: 15-30 min
- **Deltakere**: Team - **Deltakere**: Team
### 3. Kvartalsreview ### 3. Syklusreview
- **Når**: Ved syklusslutt - **Når**: Ved syklusslutt
- **Varighet**: 1-2 timer - **Varighet**: 1-2 timer
- **Deltakere**: Team + stakeholders - **Deltakere**: Team + stakeholders

View file

@ -4,6 +4,8 @@ description: |
Bruk denne agenten når brukeren har spørsmål om OKR og offentlig sektor-styring, tildelingsbrev, Bruk denne agenten når brukeren har spørsmål om OKR og offentlig sektor-styring, tildelingsbrev,
Riksrevisjon-krav, politisk styring, eller dokumentasjon. Riksrevisjon-krav, politisk styring, eller dokumentasjon.
Ikke bruk denne for kvantitativ gap-/dekningsanalyse av tildelingsbrev — bruk gapanalytiker i stedet. Denne agenten gir kvalitativ oversettelse og compliance-rådgivning.
<example> <example>
Context: Bruker har tildelingsbrev som skal bli OKR Context: Bruker har tildelingsbrev som skal bli OKR
user: "Hvordan kobler vi OKR til tildelingsbrevet?" user: "Hvordan kobler vi OKR til tildelingsbrevet?"
@ -24,6 +26,10 @@ tools: ["Read", "Glob"]
Du er en ekspert på OKR i kontekst av norsk offentlig sektor-styring. Du er en ekspert på OKR i kontekst av norsk offentlig sektor-styring.
## Kontekstbevissthet
Org-kontekst kan være sendt med i Task-prompten fra den kallende kommandoen — bruk den hvis den finnes. Hvis ikke, les prosjekt-lokal `.claude/okr.local.md` (relativt til brukerens prosjekt-cwd) direkte. Forsøk ALDRI å lese en hjem-sti (under brukerens hjemmemappe) selv — hooken eier den hybrid-resolusjonen. Finnes ingen org-kontekst: fortsett uten — ikke feil.
## Styringsrammeverk ## Styringsrammeverk
``` ```

View file

@ -4,6 +4,8 @@ description: |
Bruk denne agenten for å analysere OKR-trender på tvers av sykluser. Bruk denne agenten for å analysere OKR-trender på tvers av sykluser.
Leser arkiverte resultater og identifiserer mønstre, fremgang og risiko. Leser arkiverte resultater og identifiserer mønstre, fremgang og risiko.
Ikke bruk denne for én enkelt syklus' scoring — bruk fremdriftssporer i stedet.
<example> <example>
Context: Bruker har 3+ arkiverte sykluser Context: Bruker har 3+ arkiverte sykluser
user: "Vis OKR-trender over tid" user: "Vis OKR-trender over tid"
@ -25,6 +27,10 @@ tools: ["Read", "Glob"]
Du er en ekspert på å analysere OKR-trender over tid og identifisere Du er en ekspert på å analysere OKR-trender over tid og identifisere
mønstre i organisasjonens OKR-praksis. mønstre i organisasjonens OKR-praksis.
## Kontekstbevissthet
Org-kontekst kan være sendt med i Task-prompten fra den kallende kommandoen — bruk den hvis den finnes. Hvis ikke, les prosjekt-lokal `.claude/okr.local.md` (relativt til brukerens prosjekt-cwd) direkte. Forsøk ALDRI å lese en hjem-sti (under brukerens hjemmemappe) selv — hooken eier den hybrid-resolusjonen. Finnes ingen org-kontekst: fortsett uten — ikke feil.
## Din oppgave ## Din oppgave
Les arkiverte sykluser fra `.claude/okr/historikk/` og produser trendanalyse Les arkiverte sykluser fra `.claude/okr/historikk/` og produser trendanalyse
@ -143,7 +149,7 @@ Map score-bane til modenhetsnivåer:
| 0.5-0.7 | Skalering | | 0.5-0.7 | Skalering |
| > 0.7 | Moden | | > 0.7 | Moden |
Sammenlign med selvrapportert `modenhetsnivaa` fra okr.local.md. Sammenlign med selvrapportert `modenhetsnivaa` fra `.claude/okr.local.md`.
Hvis avvik: kommenter forsiktig ("Score-trenden tilsier [nivå], mens Hvis avvik: kommenter forsiktig ("Score-trenden tilsier [nivå], mens
organisasjonen rapporterer [nivå]. Vurder å oppdatere profilen."). organisasjonen rapporterer [nivå]. Vurder å oppdatere profilen.").

View file

@ -12,11 +12,12 @@ antipatterns og alignment-utvikling med Mermaid-visualiseringer.
## Kontekstbevissthet ## Kontekstbevissthet
OKR-kontekst injiseres automatisk via hook. Sjekk system-konteksten: Oppdag arkiverte sykluser direkte fra disk (hooken for-injiserer ikke lenger en
- Hvis arkiverte sykluser er listet (f.eks. "Arkiverte sykluser (3): T1-2025, T2-2025, T3-2025"): arkiv-liste — den emitterer kun et kjerne-sammendrag + en peker til wikien):
les filene i `.claude/okr/historikk/` direkte. - Glob `.claude/okr/historikk/*/` for å liste arkiverte sykluser. Hver undermappe
- Hvis ingen arkiverte sykluser finnes: vis hjelpsom melding (se edge cases). er én arkivert syklus (f.eks. `T1-2025/`, `T2-2025/`, `T3-2025/`).
- Sjekk også gjeldende syklus for sammenligning mot historikk. - Hvis ingen undermapper finnes: vis hjelpsom melding (se edge cases).
- Glob også `.claude/okr/syklus/` for gjeldende syklus til sammenligning mot historikk.
## Ruting basert på argument ## Ruting basert på argument

84
commands/export.md Normal file
View file

@ -0,0 +1,84 @@
---
name: okr:export
description: Eksporter OKR-dokumenter (kvalitetsvurdering, gap-matrise, statusrapport, retrospektiv) til print-klar PDF
allowed-tools: Read, Bash
argument-hint: "[dokumenttype eller filsti]"
---
# OKR Export - Eksporter OKR-leveranser til PDF
Generer en print-klar A4-PDF av et OKR-dokument for deling med ledelse,
Riksrevisjon eller styringsdialog. Konverterer Markdown til PDF via
`scripts/export-pdf.py` (weasyprint), med tabell-striping og fargekodede
score-celler (`.score-green` / `.score-yellow` / `.score-red`).
## Kontekstbevissthet
OKR-kontekst injiseres automatisk via hook. Sjekk system-konteksten FØR du spør:
- Hvis syklus og aktive OKR-filer er listet (f.eks. `.claude/okr/syklus/T1-2026/`):
tilby å eksportere dem direkte.
- Hvis `.claude/okr/dokumenter/` inneholder genererte rapporter: tilby disse.
- Spør kun om dokumenttype når konteksten ikke gjør valget åpenbart.
## Arbeidsflyt
1. **Velg dokumenttype** — hvilken leveranse skal eksporteres:
- **kvalitetsvurdering** — output fra `/okr:kvalitet`
- **gap-matrise** — output fra `/okr:gap`
- **statusrapport** — output fra `/okr:sporing`
- **retrospektiv**`retrospektiv.md` fra `/okr:oppsett arkiver`
- eller en vilkårlig Markdown-fil brukeren oppgir.
2. **Sørg for Markdown-kilde** — hvis dokumentet finnes som fil, bruk filstien.
Hvis det kun finnes i samtalen, skriv det til en `.md`-fil først (f.eks.
`.claude/okr/dokumenter/kvalitetsvurdering-T1-2026.md`).
3. **Kjør eksporten** via Bash:
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/export-pdf.py <input.md> <output.pdf>
```
Eksempel:
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/export-pdf.py \
.claude/okr/dokumenter/kvalitetsvurdering-T1-2026.md \
.claude/okr/dokumenter/kvalitetsvurdering-T1-2026.pdf
```
4. **Bekreft resultatet** — skriptet skriver `Skrev PDF: <sti>` til stderr ved
suksess. Rapporter filstien til brukeren.
**Tips for fargekoding:** I Markdown-tabellen kan score-celler tagges med
`attr_list`-syntaks slik at de blir fargelagt i PDF-en:
```markdown
| KR | Score |
|----|-------|
| KR1 | 0.9 {: .score-green} |
| KR2 | 0.5 {: .score-yellow} |
| KR3 | 0.2 {: .score-red} |
```
## Feilhåndtering
Eksporten har to dokumenterte forutsetninger (ikke bundlet med pluginet):
- **Manglende Python-pakker** (`markdown`, `weasyprint`) → skriptet avslutter med
exit 1 og hintet:
```
pip install markdown weasyprint
```
- **Manglende native bibliotek** (Pango/cairo/gdk-pixbuf) → feiler ved rendring
med exit 1 og hintet:
```
brew install pango
```
Begge feil gir en tydelig instruks (ikke en traceback). Når en forutsetning
mangler: rapporter instruksen til brukeren, ikke prøv å installere noe selv.
## Referanser
- `${CLAUDE_PLUGIN_ROOT}/scripts/export-pdf.py` — selve eksport-skriptet
- `${CLAUDE_PLUGIN_ROOT}/commands/kvalitet.md` — produserer kvalitetsvurdering
- `${CLAUDE_PLUGIN_ROOT}/commands/gap.md` — produserer gap-matrise
- weasyprint: https://doc.courtbouillon.org/weasyprint/stable/

View file

@ -0,0 +1,166 @@
---
name: okr:freshen-references
description: KB-selvevaluator og currency-polling — scorer de 16 domene-referansefilene mot en ankret kvalitetsrubrikk og oppdager utdaterte kilder
allowed-tools: Read, Glob, WebSearch, Task
argument-hint: "[referansefil å fokusere på, eller tom for full gjennomgang]"
---
# OKR Freshen References - KB-selvevaluator + currency-polling
Vedlikehold kunnskapsbasen (KB) bak OKR-pluginet. Kommandoen gjør to ting:
1. **KB-selvevaluator** — scorer hver av de 16 domene-referansefilene mot en
ankret kvalitetsrubrikk (dekning + kvalitet), slik at svake filer kan
prioriteres for forbedring.
2. **Currency-polling** — sjekker navngitte offentlige kilder for å oppdage om
`Sist oppdatert`-markører i KB-en har blitt utdaterte siden sist.
Dette speiler KB-selvevaluatoren i ms-ai-architect (ankret rubrikk, dekning- og
kvalitetsdimensjoner med fem ankere hver), tilpasset OKR-domenet for norsk
offentlig sektor.
## Kontekstbevissthet
OKR-kontekst injiseres automatisk via hook. Før du starter:
- Hvis brukeren oppgir en spesifikk referansefil som argument: scor kun den.
- Ellers: kjør full gjennomgang av alle 16 filer.
- Bruk `Glob``${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/*.md`
for å bekrefte at fil-listen under fortsatt stemmer før scoring.
## Arbeidsflyt
### Del A — KB-selvevaluator
**Filer som scores (16 domene-referansefiler):**
1. `cfr-framework.md`
2. `dfo-okr-mapping.md`
3. `individual-vs-team-okr.md`
4. `meeting-guides.md`
5. `metrics-library.md`
6. `okr-antipatterns.md`
7. `okr-arshjul.md`
8. `okr-calculator.md`
9. `okr-cheatsheet.md`
10. `okr-examples.md`
11. `okr-framework.md`
12. `okr-implementation.md`
13. `okr-integrations.md`
14. `okr-oboard-guide.md`
15. `okr-offentlig-governance.md`
16. `okr-sources.md`
**Eksplisitt ekskludert:** `okr-quality-rubrics.md` scores IKKE av denne
evaluatoren. Den er selv scoringsinstrumentet for `/okr:kvalitet`; en
KB-evaluator som scorer sin egen rubrikk blir sirkulær.
**Slik scorer du:** les hver fil, finn ankeret den faktisk treffer per
dimensjon, og gi 1-5. Rapporter en tabell (fil × dimensjon) + en prioritert
liste over filer som scorer ≤ 2 på en eller flere dimensjoner.
#### KB-kvalitetsrubrikk
**Dekning** — måler om filen dekker domenet sitt fullstendig.
##### Fullstendighet
Måler om filen dekker hele sitt erklærte domene uten åpenbare hull.
1. **Anker 1 (svakest)** — Fragmentarisk; dekker bare en liten del av domenet filen lover.
2. **Anker 2** — Vesentlige hull; flere sentrale undertemaer mangler.
3. **Anker 3** — Dekker kjernen, men med merkbare hull i randsoner.
4. **Anker 4** — Nær fullstendig; kun mindre kanttilfeller utelatt.
5. **Anker 5 (sterkest)** — Fullstendig dekning av domenet med kanttilfeller adressert.
##### Offentlig-sektor-tilpasning
Måler om innholdet er tilpasset norsk offentlig kontekst (ikke generisk privat-sektor-OKR).
1. **Anker 1 (svakest)** — Rent generisk; ingen tilpasning til offentlig sektor.
2. **Anker 2** — Overflatisk tilpasning; norske termer limt på generisk innhold.
3. **Anker 3** — Delvis tilpasset; noen offentlig-spesifikke eksempler.
4. **Anker 4** — Godt tilpasset; tertialsyklus, tildelingsbrev og etatskontekst gjennomgående.
5. **Anker 5 (sterkest)** — Fullt forankret i norsk offentlig praksis med konkrete, etterprøvbare eksempler.
##### Eksempeldekning
Måler om filen gir konkrete eksempler, ikke bare abstrakte prinsipper.
1. **Anker 1 (svakest)** — Ingen eksempler; ren teori.
2. **Anker 2** — Ett eller to tynne eksempler.
3. **Anker 3** — Noen eksempler, men ujevnt fordelt.
4. **Anker 4** — Gode eksempler på de fleste sentrale punkter.
5. **Anker 5 (sterkest)** — Rike, varierte gode-vs-dårlige eksempler gjennomgående.
##### Kryssreferanser
Måler om filen kobler til relevante søsken-referanser og kommandoer.
1. **Anker 1 (svakest)** — Isolert; ingen kobling til resten av KB-en.
2. **Anker 2** — Én løs henvisning.
3. **Anker 3** — Noen kryssreferanser, men ufullstendige.
4. **Anker 4** — Godt koblet til relaterte filer og kommandoer.
5. **Anker 5 (sterkest)** — Fullt integrert; peker presist til alle relevante naboer.
**Kvalitet** — måler hvor pålitelig og brukbart innholdet er.
##### Presisjon
Måler om påstandene er presise og entydige.
1. **Anker 1 (svakest)** — Vagt og flertydig; vanskelig å handle på.
2. **Anker 2** — Flere upresise eller løse formuleringer.
3. **Anker 3** — Stort sett presist, med noen vage partier.
4. **Anker 4** — Presist; kun ubetydelige uklarheter.
5. **Anker 5 (sterkest)** — Skarpt og entydig hele veien; ingen tolkningsrom.
##### Provenans
Måler om faktapåstander er kildebelagt (jf. verifiseringsplikt).
1. **Anker 1 (svakest)** — Ingen kilder; påstander hviler på antakelse.
2. **Anker 2** — Få kilder; det meste ubelagt.
3. **Anker 3** — Sentrale påstander belagt, men hull gjenstår.
4. **Anker 4** — Godt kildebelagt; uverifisert merket som «Ikke verifisert».
5. **Anker 5 (sterkest)** — Hver faktapåstand provenans-tagget til primærkilde.
##### Aktualitet
Måler om innholdet er oppdatert mot gjeldende rammeverk, lover og tall.
1. **Anker 1 (svakest)** — Utdatert; refererer opphevde lover eller døde rammeverk.
2. **Anker 2** — Flere foreldede referanser.
3. **Anker 3** — Stort sett aktuelt, men enkelte utdaterte tall.
4. **Anker 4** — Aktuelt; `Sist oppdatert`-markør innenfor siste år.
5. **Anker 5 (sterkest)** — Fullt aktuelt mot nyeste rammeverk og gjeldende rett.
##### Handlingsbarhet
Måler om leseren kan handle på innholdet uten ekstra oppslag.
1. **Anker 1 (svakest)** — Ren bakgrunn; gir ingen handlingsretning.
2. **Anker 2** — Antyder handling, men for abstrakt til å følge.
3. **Anker 3** — Handlingsbart for den erfarne, men krever tolkning.
4. **Anker 4** — Klar handlingsretning på de fleste punkter.
5. **Anker 5 (sterkest)** — Umiddelbart handlingsbart; konkrete steg og maler.
### Del B — Currency-polling
Volatile offentlige kilder endrer seg uavhengig av KB-en. Bruk en
`WebSearch`/Tavily-subagent (via `Task`) til å sjekke om `Sist oppdatert`-
markørene i KB-en (særlig i `metrics-library.md` og `okr-offentlig-governance.md`)
har blitt utdaterte. Poll disse navngitte kildene:
- **UN E-Government Survey (EGDI)** — ny utgave annethvert år; sjekk siste indeksår.
- **EU eGovernment Benchmark** — årlig rapport; sjekk gjeldende årgang.
- **OECD Digital Government Index (DGI)** — periodisk; sjekk siste publisering.
- **Digdir «Rikets digitale tilstand»** — årlig; sjekk siste utgave.
- **WCAG** — sjekk om gjeldende referansenivå (2.1 A+AA) fortsatt er det som
henvises i offentlig regelverk, og status for EAA-ikrafttredelse i Norge.
- **Forvaltningsloven** — sjekk om den nye forvaltningsloven (vedtatt 2025) har
trådt i kraft og erstattet 1967-loven KB-en refererer.
- **eFormidling** — sjekk om status fortsatt er «bør» (ikke påbudt) for statlige virksomheter.
Kildegrunnlaget for de volatile markørene er dokumentert i
`${CLAUDE_PLUGIN_ROOT}/.claude/projects/2026-06-24-fase3-referansegrad-loft/research/01-norsk-offentlig-metrikker.md`
(lokal research). Rapporter hvilke `Sist oppdatert`-markører som bør bumpes, og
hvilke faktapåstander som må re-verifiseres.
## Referanser
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/` — de 16 domenefilene som scores
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md` — OKR-scoringsrubrikk (ekskludert fra KB-scoring; ankerstilen gjenbrukt her)
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-sources.md` — primærkilde-register (provenans-stil)
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/metrics-library.md` — bærer volatile offentlig-metrikker (hovedmål for currency-polling)

View file

@ -10,6 +10,8 @@ argument-hint: "[tildelingsbrev-fil eller tomt for auto-deteksjon]"
Analyser automatisk om gjeldende OKR dekker kravene i tildelingsbrevet, og om Analyser automatisk om gjeldende OKR dekker kravene i tildelingsbrevet, og om
OKR har forankring i styrende dokumenter. OKR har forankring i styrende dokumenter.
> **Bruk /okr:gap når** du trenger en *kvantitativ dekningsmatrise* mellom tildelingsbrev og gjeldende OKR (hvilke krav er dekket, hvilke mangler). For *kvalitativ oversettelse* av tildelingsbrev til OKR + Riksrevisjon-compliance, bruk `/okr:governance`.
## Kontekstbevissthet ## Kontekstbevissthet
OKR-kontekst injiseres automatisk via hook. Sjekk system-konteksten FØR du spør brukeren: OKR-kontekst injiseres automatisk via hook. Sjekk system-konteksten FØR du spør brukeren:
@ -39,15 +41,12 @@ Auto-les fra persistent context:
Les referansemateriale: Les referansemateriale:
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-offentlig-governance.md` - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-offentlig-governance.md`
Ekstraher individuelle krav fra tildelingsbrevet. Kategoriser hvert krav: Ekstraher individuelle krav fra tildelingsbrevet. Kategoriser hvert krav etter
innholdsklassifiseringen i `/okr:governance` (Driftskrav → KPI-dashboard,
| Type innhold | OKR-egnet | Riktig håndtering | Resultatmål med tall → Key Result-kandidat, Strategiske satsinger →
|--------------|-----------|-------------------| Objective-kandidat, Rapporteringskrav → rapporteringsrutine, Budsjettføringer →
| Driftskrav ("Oppretthold X") | Lav | KPI-dashboard | økonomioppfølging, Særskilte oppdrag → case by case). Se `/okr:governance` for
| Resultatmål med tall | Høy | Key Result-kandidat | full tabell — denne kommandoen dupliserer den ikke.
| Strategiske satsinger | Høy | Objective-kandidat |
| Rapporteringskrav | Lav | Rapporteringsrutine |
| Særskilte oppdrag | Medium | Case by case |
### 3. OKR-mapping (tildelingsbrev → OKR) ### 3. OKR-mapping (tildelingsbrev → OKR)

View file

@ -9,6 +9,8 @@ argument-hint: "[tildelingsbrev, revisjonsrapport, eller spørsmål]"
Hjelp brukeren med å koble OKR til styringsmekanismer i norsk offentlig sektor. Hjelp brukeren med å koble OKR til styringsmekanismer i norsk offentlig sektor.
> **Bruk /okr:governance når** du trenger *kvalitativ oversettelse* av tildelingsbrev til OKR + Riksrevisjon-compliance og politisk styringskontekst. For en *kvantitativ dekningsmatrise* (hvilke tildelingsbrev-krav er dekket av gjeldende OKR), bruk `/okr:gap`.
## Kontekstbevissthet ## Kontekstbevissthet
OKR-kontekst injiseres automatisk via hook. Sjekk system-konteksten FØR du spør brukeren: OKR-kontekst injiseres automatisk via hook. Sjekk system-konteksten FØR du spør brukeren:
@ -88,7 +90,7 @@ Verifiser at OKR-arbeidet tåler ekstern revisjon:
## Eksempel: Tildelingsbrev → OKR ## Eksempel: Tildelingsbrev → OKR
**Fra tildelingsbrev:** **Fra tildelingsbrev** (DDT er en fiktiv eksempeletat):
> "Direktoratet for digital tjenesteutvikling skal bidra til å redusere antall drepte og hardt skadde i trafikken med 50% innen 2030, sammenlignet med 2020-nivå." > "Direktoratet for digital tjenesteutvikling skal bidra til å redusere antall drepte og hardt skadde i trafikken med 50% innen 2030, sammenlignet med 2020-nivå."
**Som OKR (årlig):** **Som OKR (årlig):**
@ -115,3 +117,5 @@ Politiske signaler kan endre seg midt i syklusen:
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-offentlig-governance.md` — full governance-veiledning - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-offentlig-governance.md` — full governance-veiledning
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-arshjul.md` — årshjul og budsjettprosess - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-arshjul.md` — årshjul og budsjettprosess
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/dfo-okr-mapping.md` — DFØ mål- og resultatstyring → OKR-mapping
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-sources.md` — kildebelegg og proveniens for governance-påstander

77
commands/help.md Normal file
View file

@ -0,0 +1,77 @@
---
name: okr:help
description: Full oversikt over alle OKR-kommandoer, agenter og anbefalt arbeidsflyt gjennom tertialsyklusen
allowed-tools: Read
argument-hint: "[tema å få hjelp med, eller tom for full oversikt]"
---
# OKR Help - Kommando- og agentoversikt
Gi brukeren en oversikt over hva pluginet kan, og hvilken kommando som passer
til hva. Hvis brukeren oppgir et tema som argument, foreslå de mest relevante
kommandoene for det temaet. Ellers vis full oversikt.
## Kommandoer (13)
| Kommando | Hva den gjør |
|----------|--------------|
| `/okr:skriv` | Skriv ny OKR med veiledet Objective- og KR-utforming |
| `/okr:kvalitet` | Kvalitetssjekk OKR mot ankret rubrikk og 19 antipatterns |
| `/okr:kaskade` | Kaskader OKR fra org til team, visualiser alignment |
| `/okr:sporing` | Spor fremdrift, beregn score (0.01.0), generer check-ins |
| `/okr:møter` | Planlegg OKR-workshops, check-ins, reviews og 1:1 (CFR) |
| `/okr:innføring` | Innføringsplan, motstandshåndtering, modenhetsvurdering |
| `/okr:governance` | Tildelingsbrev-oversettelse, Riksrevisjon-compliance |
| `/okr:gap` | Automatisk gap-analyse mellom tildelingsbrev og gjeldende OKR |
| `/okr:analyse` | Kryss-syklus-analyse med Mermaid-trendvisualisering |
| `/okr:oppsett` | Konfigurer plugin: onboarding (`full`/`mvp`), `arkiver`, `oppdater`, `vis` |
| `/okr:export` | Eksporter OKR-dokumenter til print-klar PDF (ledelse/Riksrevisjon) |
| `/okr:freshen-references` | KB-selvevaluator + currency-polling av offentlige kilder |
| `/okr:help` | Denne oversikten — kommandoer, agenter, anbefalt arbeidsflyt |
## Agenter (7)
Agentene aktiveres automatisk av kommandoene over; du kaller dem ikke direkte.
| Agent | Rolle |
|-------|-------|
| kvalitetssjekker | Score kvalitet, oppdage antipatterns, sjekke alignment |
| kaskadebygger | Bygge OKR-kaskader mellom organisasjonsnivåer |
| fremdriftssporer | Beregne score, lage prognoser, flagge risiko-KR |
| møtefasilitator | Generere møteagendaer og fasiliteringsmateriell |
| styringsrådgiver | Governance-analyse, tildelingsbrev-oversettelse, audit-compliance |
| gapanalytiker | Dekningsmatrise mellom strategidokumenter og OKR |
| trendanalytiker | Kryss-syklus mønsteranalyse med trendvisualisering |
## Anbefalt arbeidsflyt
OKR-arbeid følger tertialsyklusen (16 uker). Velg kommando etter hvor i syklusen
du er — coaching-hooken minner deg på dette ved sesjonsstart.
### Tidlig i syklus (uke 15) — sett retning
1. `/okr:oppsett full` (én gang) — la pluginet lære organisasjonen din
2. `/okr:governance` / `/okr:gap` — forankre i tildelingsbrev, finn dekningshull
3. `/okr:skriv` — skriv Objectives og Key Results
4. `/okr:kvalitet` — kvalitetssjekk og forbedre
5. `/okr:kaskade` — juster mot org-OKR
### Midt i syklus (uke 611) — hold tempo
6. `/okr:sporing` — oppdater fremdrift, fang risiko tidlig
7. `/okr:møter` — forbered check-in og 1:1 (CFR)
### Sent i syklus (uke 1216) — lukk og lær
8. `/okr:sporing` — endelig scoring
9. `/okr:export` — eksporter status/retrospektiv til PDF for ledelse/Riksrevisjon
10. `/okr:oppsett arkiver` — arkiver syklus, generer retrospektiv
11. `/okr:analyse` — se trender på tvers av sykluser
12. `/okr:skriv` — start neste syklus med lærdommene
### Løpende vedlikehold
- `/okr:help` — denne oversikten når du er usikker på hvilken kommando du trenger
- `/okr:freshen-references` — hold kunnskapsbasen aktuell (KB-scoring + kilde-polling)
## Referanser
- `${CLAUDE_PLUGIN_ROOT}/README.md` — fyldig introduksjon og bruksområder
- `${CLAUDE_PLUGIN_ROOT}/CLAUDE.md` — kommando-/agent-/hook-arkitektur
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/SKILL.md` — kunnskapsbase-indeks

View file

@ -98,3 +98,4 @@ Team KR: "100% av identifiserte strekninger remarked innen august"
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-framework.md` — kaskaderingsmetodikk - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-framework.md` — kaskaderingsmetodikk
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-offentlig-governance.md` — hierarki i offentlig sektor - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-offentlig-governance.md` — hierarki i offentlig sektor
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/individual-vs-team-okr.md` — individ- vs team-OKR ved kaskadering

View file

@ -114,5 +114,6 @@ OKR-kontekst injiseres automatisk via hook. Sjekk system-konteksten FØR du spø
## Referanser ## Referanser
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md` — ankret scoringsrubrikk (5 ankere per dimensjon); kanonisk kilde for scoringen over
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-antipatterns.md` — alle 19 antipatterns - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-antipatterns.md` — alle 19 antipatterns
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-examples.md` — gode vs dårlige eksempler - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-examples.md` — gode vs dårlige eksempler

View file

@ -9,13 +9,22 @@ argument-hint: "[møtetype eller kontekst]"
Hjelp brukeren med å planlegge og gjennomføre OKR-relaterte møter. Hjelp brukeren med å planlegge og gjennomføre OKR-relaterte møter.
## Kontekstbevissthet
OKR-kontekst injiseres automatisk via hook. Sjekk system-konteksten FØR du spør brukeren:
- Hvis organisasjon og syklus er kjent: hopp over de spørsmålene
- Hvis relevante filer er listet (f.eks. `.claude/okr/syklus/T1-2026/okr-teamet.md`):
les den filen direkte i stedet for å be brukeren lime inn innhold
- Hvis `.claude/okr/strategisk-kontekst/` inneholder relevante docs: les dem
- Hvis syklusfase er kjent (fra injisert kontekst), tilpass møtetype og timing
direkte uten å spørre «hvor i syklusen er dere».
## Arbeidsflyt ## Arbeidsflyt
1. **Identifiser møtetype** — spør med AskUserQuestion: 1. **Identifiser møtetype** — spør med AskUserQuestion:
- Planleggingsworkshop, check-in, review, eller 1:1? - Planleggingsworkshop, check-in, review, eller 1:1?
- Hvor mange deltakere? - Hvor mange deltakere?
- Fysisk eller digitalt? - Fysisk eller digitalt?
- Hvor i syklusen er dere?
2. **Les relevant referansemateriale**: 2. **Les relevant referansemateriale**:
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/meeting-guides.md` — alle møteguider - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/meeting-guides.md` — alle møteguider
@ -57,7 +66,7 @@ Hjelp brukeren med å planlegge og gjennomføre OKR-relaterte møter.
3. Beslutninger som trengs (5 min) 3. Beslutninger som trengs (5 min)
4. Fokus neste periode (3 min) 4. Fokus neste periode (3 min)
### 3. Kvartalsreview ### 3. Syklusreview
**Når**: Ved syklusslutt | **Varighet**: 1-2 timer | **Deltakere**: Team + stakeholders **Når**: Ved syklusslutt | **Varighet**: 1-2 timer | **Deltakere**: Team + stakeholders

View file

@ -5,18 +5,18 @@ allowed-tools: Read, Write, Bash, AskUserQuestion, ToolSearch, Glob
argument-hint: "[full|mvp|arkiver|oppdater|vis]" argument-hint: "[full|mvp|arkiver|oppdater|vis]"
--- ---
# OKR Oppsett - Onboarding, konfigurasjon og syklushandtering # OKR Oppsett - Onboarding, konfigurasjon og syklushåndtering
Interaktiv konfigurasjon av OKR-pluginen. Bygger en persistent organisasjonsprofil Interaktiv konfigurasjon av OKR-pluginen. Bygger en persistent organisasjonsprofil
som alle andre kommandoer bruker automatisk via hook-injeksjon. som alle andre kommandoer bruker automatisk via hook-injeksjon.
## Ruting basert pa argument ## Ruting basert på argument
| Argument | Handling | | Argument | Handling |
|----------|----------| |----------|----------|
| _(ingen)_ | Detekter eksisterende konfig, tilby meny | | _(ingen)_ | Detekter eksisterende konfig, tilby meny |
| `full` | Kjor 6-fase dyp onboarding (15-20 min) | | `full` | Kjør 6-fase dyp onboarding (15-20 min) |
| `mvp` | Kjor minimum viable onboarding (6 sporsmaal, ~5 min) | | `mvp` | Kjør minimum viable onboarding (6 spørsmål, ~5 min) |
| `arkiver` | Arkiver gjeldende syklus med retrospektiv | | `arkiver` | Arkiver gjeldende syklus med retrospektiv |
| `oppdater` | Oppdater spesifikke felt i eksisterende profil | | `oppdater` | Oppdater spesifikke felt i eksisterende profil |
| `vis` | Vis gjeldende konfigurasjon | | `vis` | Vis gjeldende konfigurasjon |
@ -31,15 +31,15 @@ Les `.claude/okr.local.md` (relativt til brukerens prosjekt-cwd).
Bruk AskUserQuestion: Bruk AskUserQuestion:
``` ```
Ser ut som du ikke har konfigurert OKR-pluginen enna. Ser ut som du ikke har konfigurert OKR-pluginen ennå.
Vil du kjore onboarding-intervjuet? Vil du kjøre onboarding-intervjuet?
1. Full onboarding (6 faser, 15-20 min) — anbefalt for beste resultat 1. Full onboarding (6 faser, 15-20 min) — anbefalt for beste resultat
2. MVP onboarding (6 sporsmaal, ~5 min) — rask start 2. MVP onboarding (6 spørsmål, ~5 min) — rask start
3. Avbryt 3. Avbryt
``` ```
Kjor `full` eller `mvp` basert pa valg. Kjør `full` eller `mvp` basert på valg.
**Hvis filen finnes OG `onboarding_status: partial`:** **Hvis filen finnes OG `onboarding_status: partial`:**
@ -47,8 +47,8 @@ Kjor `full` eller `mvp` basert pa valg.
Fant ufullstendig onboarding. Du kom til fase [N] forrige gang. Fant ufullstendig onboarding. Du kom til fase [N] forrige gang.
1. Fortsett fra fase [N] 1. Fortsett fra fase [N]
2. Start pa nytt (full) 2. Start på nytt (full)
3. Start pa nytt (mvp) 3. Start på nytt (mvp)
4. Avbryt 4. Avbryt
``` ```
@ -56,13 +56,13 @@ Hvis fortsett: les YAML, finn siste utfylte fase-seksjon, fortsett fra neste.
**Hvis filen finnes OG `onboarding_status: fullfort`:** **Hvis filen finnes OG `onboarding_status: fullfort`:**
Vis kort oppsummering av gjeldende profil (org, syklus, modenhet, verktoy). Vis kort oppsummering av gjeldende profil (org, syklus, modenhet, verktøy).
Bruk AskUserQuestion: Bruk AskUserQuestion:
``` ```
Konfigurasjon funnet for [organisasjon.navn]. Konfigurasjon funnet for [organisasjon.navn].
1. Oppdater konfigurasjon (endre spesifikke felt) 1. Oppdater konfigurasjon (endre spesifikke felt)
2. Kjor intervju pa nytt (full) 2. Kjør intervju på nytt (full)
3. Vis full konfigurasjon 3. Vis full konfigurasjon
4. Avslutt 4. Avslutt
``` ```
@ -71,15 +71,15 @@ Konfigurasjon funnet for [organisasjon.navn].
## Full onboarding — 6-fase dyp intervju ## Full onboarding — 6-fase dyp intervju
Skriv `onboarding_status: partial` til `.claude/okr.local.md` for hver fase Skriv `onboarding_status: partial` til `.claude/okr.local.md` for hver fase er
er fullfort. Oppdater til `fullfort` etter siste fase. fullført. Oppdater til `fullfort` etter siste fase.
### Fase 1 — Identitet (2 min) ### Fase 1 — Identitet (2 min)
Still alle sporsmaal med AskUserQuestion (grupper 2-3 sammen der naturlig): Still alle spørsmål med AskUserQuestion (grupper 2-3 sammen der naturlig):
1. **Organisasjonsnavn og kortform?** 1. **Organisasjonsnavn og kortform?**
Eksempel: "Direktoratet for digital tjenesteutvikling" / "DDT" Eksempel (fiktiv eksempeletat): «Direktoratet for digital tjenesteutvikling» / «DDT»
2. **Type organisasjon?** 2. **Type organisasjon?**
Alternativer: statlig etat | direktorat | kommune | fylkeskommune Alternativer: statlig etat | direktorat | kommune | fylkeskommune
@ -101,30 +101,30 @@ Still alle sporsmaal med AskUserQuestion (grupper 2-3 sammen der naturlig):
### Fase 2 — Modenhet (5 min) ### Fase 2 — Modenhet (5 min)
7. **OKR-historikk?** 7. **OKR-historikk?**
Alternativer: aldri provd | prover na (forste syklus) | erfaren (2+ sykluser) Alternativer: aldri prøvd | prøver nå (første syklus) | erfaren (2+ sykluser)
8. **Hvis erfaren: Antall gjennomforte sykluser?** 8. **Hvis erfaren: Antall gjennomførte sykluser?**
(Hopp over hvis "aldri provd" eller "prover na") (Hopp over hvis "aldri prøvd" eller "prøver nå")
9. **Hvem er OKR-sponsor (navn og rolle)?** 9. **Hvem er OKR-sponsor (navn og rolle)?**
Personen som gir mandat og synlighet. Eksempel: "Divisjonsdirektor Kari Holm" Personen som gir mandat og synlighet. Eksempel: "Divisjonsdirektør Kari Holm"
10. **Hvem er OKR-champion (navn og rolle)?** 10. **Hvem er OKR-champion (navn og rolle)?**
Personen som driver det operative arbeidet. Eksempel: "Seniorradgiver Per Lund" Personen som driver det operative arbeidet. Eksempel: "Seniorrådgiver Per Lund"
11. **Er OKR eksplisitt koblet fra lonnssamtale og medarbeidersamtale?** 11. **Er OKR eksplisitt koblet fra lønnssamtale og medarbeidersamtale?**
Alternativer: ja | nei | usikkert Alternativer: ja | nei | usikkert
**VIKTIG:** Hvis "nei" eller "usikkert" — sett `okr_frikoblet_fra_loenn: false` og **VIKTIG:** Hvis "nei" eller "usikkert" — sett `okr_frikoblet_fra_loenn: false` og
legg til advarsel i Markdown-body: legg til advarsel i Markdown-body:
> **ADVARSEL:** OKR koblet til lonn er et av de farligste antipatternene. > **ADVARSEL:** OKR koblet til lønn er et av de farligste antipatternene.
> Nar medarbeidere vet at OKR-score pavirker lonn, setter de trygge, lave mal > Når medarbeidere vet at OKR-score påvirker lønn, setter de trygge, lave mål
> i stedet for ambisiose stretch goals. Anbefaler sterkt a dekoblere for utrulling. > i stedet for ambisiøse stretch goals. Anbefaler sterkt å dekoble før utrulling.
12. **Hva er alignment-tilnaermingen?** 12. **Hva er alignment-tilnærmingen?**
Alternativer: top-down | bottom-up | bidireksjonell Alternativer: top-down | bottom-up | bidireksjonell
Forklar kort: "Bidireksjonell betyr at ledelsen setter retning og teamene foreslaar Forklar kort: "Bidireksjonell betyr at ledelsen setter retning og teamene foreslår
egne OKR som kobles opp. Dette er anbefalt tilnaerming." egne OKR som kobles opp. Dette er anbefalt tilnærming."
**Etter fase 2:** Oppdater `.claude/okr.local.md` med program-seksjonen. **Etter fase 2:** Oppdater `.claude/okr.local.md` med program-seksjonen.
@ -133,54 +133,82 @@ Still alle sporsmaal med AskUserQuestion (grupper 2-3 sammen der naturlig):
13. **Har dere et tildelingsbrev for 2026?** 13. **Har dere et tildelingsbrev for 2026?**
Alternativer: ja | nei Alternativer: ja | nei
Hvis ja: "Vil du lagre nokkelmal fra tildelingsbrevet? Lim inn de viktigste malene Hvis ja: "Vil du lagre nøkkelmål fra tildelingsbrevet? Lim inn de viktigste målene
(stikkpunkter, ikke hele brevet) sa kan pluginen bruke dem til alignment." (stikkpunkter, ikke hele brevet) så kan pluginen bruke dem til alignment."
Hvis brukeren limer inn tekst: skriv til Hvis brukeren limer inn tekst: skriv til
`.claude/okr/strategisk-kontekst/tildelingsbrev-2026.md` med YAML-frontmatter `.claude/okr/strategisk-kontekst/tildelingsbrev-2026.md` med **OKF-frontmatter**
og innlimt innhold. Opprett mappene med Write-tool. (second-brain-kompatibel: påkrevd `type:` i Title-Case + anbefalt `resource`/
`title`/`description`/`tags`-liste/`timestamp`) og innlimt innhold. Opprett mappene
med Write-tool:
```markdown
---
type: Tildelingsbrev
resource: "[lenke/arkiv-ID til kilden]"
title: "Tildelingsbrev 2026"
description: "Nøkkelmål fra tildelingsbrevet for alignment"
tags:
- tildelingsbrev
- styringssignaler
timestamp: "[ISO-8601, f.eks. 2026-01-15T09:00:00+00:00]"
---
[innlimt innhold]
```
14. **Har dere en virksomhetsplan eller strategiplan?** 14. **Har dere en virksomhetsplan eller strategiplan?**
Eventuelt lenke eller nokkelpunkter. Hvis innhold gis: skriv til Eventuelt lenke eller nøkkelpunkter. Hvis innhold gis: skriv til
`.claude/okr/strategisk-kontekst/virksomhetsplan.md`. `.claude/okr/strategisk-kontekst/virksomhetsplan.md` med OKF-frontmatter
(`type: Virksomhetsplan` + anbefalte felt som over).
15. **Har dere org-niva OKR for 2026?** 15. **Har dere org-nivå OKR for 2026?**
Alternativer: ja | nei | planlegges Alternativer: ja | nei | planlegges
Hvis ja: "Lim inn org-OKR (Objectives og Key Results) sa pluginen kan bruke dem Hvis ja: "Lim inn org-OKR (Objectives og Key Results) så pluginen kan bruke dem
til kaskadering og alignment-sjekk." til kaskadering og alignment-sjekk."
Hvis innhold gis: skriv til `.claude/okr/strategisk-kontekst/overordnede-okr.md`. Hvis innhold gis: skriv til `.claude/okr/strategisk-kontekst/overordnede-okr.md`
med OKF-frontmatter (`type: Overordnede OKR` + anbefalte felt som over).
**Etter fase 3:** Oppdater `.claude/okr.local.md` med eventuell strategisk kontekst-info. **Etter fase 3:** Oppdater `.claude/okr.local.md` med eventuell strategisk kontekst-info.
Hvis du skrev én eller flere strategisk-kontekst-filer, regenerér OKF-indeksen for
prosjekt-treet så `index.md` per nivå reflekterer de nye konsept-filene:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/okf-index.mjs .claude/okr
```
(Idempotent: bygger `index.md`-entries fra konsept-filenes `title`/`description`,
bevarer menneske-skrevne overskrifter og rotens `okf_version`.)
### Fase 4 — Struktur (3 min) ### Fase 4 — Struktur (3 min)
16. **Hvilke OKR-nivaer bruker dere?** 16. **Hvilke OKR-nivåer bruker dere?**
Alternativer (flervalg): etat | divisjon | avdeling | seksjon | team Alternativer (flervalg): etat | divisjon | avdeling | seksjon | team
17. **Din rolle i OKR-programmet?** 17. **Din rolle i OKR-programmet?**
Alternativer: leder | produkteier | seksjonsleder | koordinator | annet Alternativer: leder | produkteier | seksjonsleder | koordinator | annet
18. **Antall team som kjorer OKR denne syklusen (ca.)?** 18. **Antall team som kjører OKR denne syklusen (ca.)?**
Tall Tall
19. **Bruker dere bidireksjonell kaskadering?** 19. **Bruker dere bidireksjonell kaskadering?**
Forklaring: "Teamene foreslaar egne OKR som kobles til org-OKR, i stedet for at Forklaring: "Teamene foreslår egne OKR som kobles til org-OKR, i stedet for at
alt kommer ovenfra." alt kommer ovenfra."
Alternativer: ja | nei | planlegger det Alternativer: ja | nei | planlegger det
**Etter fase 4:** Oppdater gjeldende_syklus-seksjonen. **Etter fase 4:** Oppdater gjeldende_syklus-seksjonen.
### Fase 5 — Verktoy (2 min) ### Fase 5 — Verktøy (2 min)
20. **Oppgavestyringsverktoy?** 20. **Oppgavestyringsverktøy?**
Alternativer: Linear | Jira | Azure DevOps | Planner | ingen | annet Alternativer: Linear | Jira | Azure DevOps | Planner | ingen | annet
21. **OKR-sporingsverktoy?** 21. **OKR-sporingsverktøy?**
Alternativer: Oboard | Quantive | regneark | ingen | annet Alternativer: Oboard | Quantive | regneark | ingen | annet
22. **Moteverktoy?** 22. **Møteverktøy?**
Alternativer: Microsoft Teams | Zoom | annet Alternativer: Microsoft Teams | Zoom | annet
23. **Leveransemetodikk?** 23. **Leveransemetodikk?**
@ -189,27 +217,27 @@ Still alle sporsmaal med AskUserQuestion (grupper 2-3 sammen der naturlig):
24. **Vil du koble Linear til OKR-pluginen?** 24. **Vil du koble Linear til OKR-pluginen?**
Alternativer: ja | nei Alternativer: ja | nei
Hvis ja: Kjor Linear-oppsett: Hvis ja: Kjør Linear-oppsett:
1. Sjekk om Linear MCP er konfigurert — les `~/.claude/settings.json` 1. Sjekk om Linear MCP er konfigurert — les `~/.claude/settings.json`
2. Hvis ikke: veilede oppsett av MCP-server (referer til Linear MCP docs) 2. Hvis ikke: veilede oppsett av MCP-server (referer til Linear MCP docs)
3. Bruk ToolSearch for a finne Linear-tools, hent teams og projects 3. Bruk ToolSearch for å finne Linear-tools, hent teams og projects
4. La bruker velge team og project for OKR-tracking 4. La bruker velge team og project for OKR-tracking
**Etter fase 5:** Oppdater verktoy- og integrasjoner-seksjonen. **Etter fase 5:** Oppdater verktøy- og integrasjoner-seksjonen.
### Fase 6 — Kultur (3 min) ### Fase 6 — Kultur (3 min)
25. **Hva er check-in-rytmen (eller onsket rytme)?** 25. **Hva er check-in-rytmen (eller ønsket rytme)?**
Alternativer: ukentlig | annenhver uke | maanedlig Alternativer: ukentlig | annenhver uke | månedlig
Anbefaling: "Annenhver uke er et godt utgangspunkt. Ukentlig for team i utfoerelsesfase." Anbefaling: "Annenhver uke er et godt utgangspunkt. Ukentlig for team i utførelsesfase."
26. **Psykologisk trygghet for a sette stretch goals?** 26. **Psykologisk trygghet for å sette stretch goals?**
Alternativer: lav | middels | hoy Alternativer: lav | middels | høy
Forklaring: "Kan teammedlemmer trygt foreslaa ambisiose mal uten a frykte konsekvenser Forklaring: "Kan teammedlemmer trygt foreslå ambisiøse mål uten å frykte konsekvenser
om de ikke naar 100%?" om de ikke når 100%?"
27. **Kjente utfordringer eller antipatterns dere har sett?** 27. **Kjente utfordringer eller antipatterns dere har sett?**
Fritekst, ENTER for a hoppe over. Fritekst, ENTER for å hoppe over.
Eksempler: "set-and-forget", "aktivitetsfokus i KR", "for mange Objectives" Eksempler: "set-and-forget", "aktivitetsfokus i KR", "for mange Objectives"
28. **Er tillitsvalgte involvert i OKR-prosessen?** 28. **Er tillitsvalgte involvert i OKR-prosessen?**
@ -217,22 +245,46 @@ Still alle sporsmaal med AskUserQuestion (grupper 2-3 sammen der naturlig):
**Etter fase 6:** Oppdater kultur-seksjonen. Sett `onboarding_status: fullfort`. **Etter fase 6:** Oppdater kultur-seksjonen. Sett `onboarding_status: fullfort`.
### Fullforingssteg ### Fullføringssteg
Etter alle 6 faser: Etter alle 6 faser:
1. Skriv komplett YAML til `.claude/okr.local.md` (oppdater alle seksjoner) 1. Skriv komplett YAML til `.claude/okr.local.md` (oppdater alle seksjoner)
2. Sett `onboarding_status: fullfort` og `Sist oppdatert: [dato]` 2. Sett `onboarding_status: fullfort` og `Sist oppdatert: [dato]`
3. Vis oppsummering: 3. **Skriv reinstall-overlevende org-profil til hjem-stien.** Profilen
(`organisasjon:`- og `program:`-seksjonene — org-identiteten som skal overleve
reinstall og være tilgjengelig på tvers av prosjekter) skrives **uten `---`-fences**
til en temp-fil og sendes gjennom OKF-komponisten + skrivehelperen via Bash:
```bash
OUT=$(node ${CLAUDE_PLUGIN_ROOT}/scripts/compose-org-profile.mjs < /tmp/okr-profil.tmp \
| node ${CLAUDE_PLUGIN_ROOT}/scripts/write-org-profile.mjs)
echo "$OUT"
```
`compose-org-profile.mjs` prepender de tre top-level OKF-nøklene (`type:
Organisasjonsprofil`, `resource:`, `timestamp:`) over den nestede YAML-en og pakker
alt i **ett** frontmatter-blokk (second-brain-kompatibelt). Legg **aldri** til en ny
top-level `navn:`/`id:`/`fase:`/`domene:`/`sektor:` i profilen — de ville skygge de
nestede verdiene som hooken leser. Helperen skriver atomisk (temp + `renameSync`) til `~/.claude/okr/org/profil.md` og
rapporterer den **faktisk brukte stien** på stdout (`$OUT`); en eventuell
fallback-notice går til stderr. Rapporter til brukeren: «Org-profil skrevet til: $OUT».
Hvis `$OUT` peker på den prosjektlokale fallbacken (`.claude/okr.local.md` — skjer kun
ved skrivefeil mot hjem), vis i tillegg hele profil-YAML-en for manuell plassering
(jf. Feilhåndtering «Fil kan ikke skrives»), slik at brukeren kan legge
`~/.claude/okr/org/profil.md` på plass selv. Behold den prosjektlokale
`.claude/okr.local.md`-skrivingen (steg 1) for syklus- og strategisk-kontekst-data —
kun selve profilen migrerer til hjem.
4. Vis oppsummering:
``` ```
Onboarding fullfort for [organisasjon]! Onboarding fullført for [organisasjon]!
Profil: Profil:
- Organisasjon: [navn] ([type]) - Organisasjon: [navn] ([type])
- Modenhet: [niva] ([N] sykluser gjennomfort) - Modenhet: [nivå] ([N] sykluser gjennomført)
- Gjeldende syklus: [id] ([fase]) - Gjeldende syklus: [id] ([fase])
- Verktoy: [oppgavestyring], [okr_tracking] - Verktøy: [oppgavestyring], [okr_tracking]
- Check-in: [rytme] - Check-in: [rytme]
Lagrede kontekstfiler: Lagrede kontekstfiler:
@ -241,34 +293,34 @@ Lagrede kontekstfiler:
[- .claude/okr/strategisk-kontekst/overordnede-okr.md] [- .claude/okr/strategisk-kontekst/overordnede-okr.md]
Anbefalte neste steg: Anbefalte neste steg:
- /okr:skriv — skriv forste OKR for denne syklusen - /okr:skriv — skriv første OKR for denne syklusen
- /okr:kvalitet — sjekk eksisterende OKR - /okr:kvalitet — sjekk eksisterende OKR
- /okr:moter — planlegg OKR-workshop eller kick-off - /okr:møter — planlegg OKR-workshop eller kick-off
``` ```
--- ---
## MVP onboarding — 6 sporsmaal ## MVP onboarding — 6 spørsmål
For brukere som vil komme raskt i gang. Samle alle sporsmaal i en AskUserQuestion For brukere som vil komme raskt i gang. Samle alle spørsmål i en AskUserQuestion
eller maks 2 runder: eller maks 2 runder:
1. **Organisasjonsnavn og type?** 1. **Organisasjonsnavn og type?**
Eksempel: "Direktoratet for digital tjenesteutvikling, statlig etat" Eksempel (fiktiv eksempeletat): «Direktoratet for digital tjenesteutvikling, statlig etat»
2. **Fagdomene?** 2. **Fagdomene?**
Alternativer: transport | helse | justis | digitalisering | utdanning | annet Alternativer: transport | helse | justis | digitalisering | utdanning | annet
3. **OKR-modenhet?** 3. **OKR-modenhet?**
Alternativer: aldri provd | prover na (forste syklus) | erfaren (2+ sykluser) Alternativer: aldri prøvd | prøver nå (første syklus) | erfaren (2+ sykluser)
4. **Gjeldende syklusfase?** 4. **Gjeldende syklusfase?**
Alternativer: planlegging | utfoerelse | review | mellom sykluser Alternativer: planlegging | utførelse | review | mellom sykluser
5. **Primaere verktoy?** 5. **Primære verktøy?**
Fritekst. Eksempel: "Linear og Microsoft Teams" Fritekst. Eksempel: "Linear og Microsoft Teams"
6. **Er OKR frikoblet fra lonnssamtale?** 6. **Er OKR frikoblet fra lønnssamtale?**
Alternativer: ja | nei | usikkert Alternativer: ja | nei | usikkert
**Etter svar:** Skriv YAML med disse 6 feltene pluss fornuftige defaults for alle andre: **Etter svar:** Skriv YAML med disse 6 feltene pluss fornuftige defaults for alle andre:
@ -285,7 +337,7 @@ organisasjon:
domene: "[svar 2]" domene: "[svar 2]"
geografi: "nasjonal" geografi: "nasjonal"
program: program:
modenhetsnivaa: "[utledet fra svar 3: aldri provd→ikke-startet, prover na→utforsker, erfaren→pilot]" modenhetsnivaa: "[utledet fra svar 3: aldri prøvd→ikke-startet, prøver nå→utforsker, erfaren→pilot]"
sykluser_gjennomfort: 0 sykluser_gjennomfort: 0
sponsor: "" sponsor: ""
champion: "" champion: ""
@ -317,7 +369,23 @@ preferanser:
--- ---
``` ```
Vis kort oppsummering og foreslaa `/okr:oppsett full` for a fylle ut resten senere. **Skriv også reinstall-overlevende org-profil til hjem-stien.** Skriv profil-YAML-en
(`organisasjon:` + `program:`, **uten `---`-fences**) til en temp-fil og kjør OKF-komponisten
+ skrivehelperen via Bash:
```bash
OUT=$(node ${CLAUDE_PLUGIN_ROOT}/scripts/compose-org-profile.mjs < /tmp/okr-profil.tmp \
| node ${CLAUDE_PLUGIN_ROOT}/scripts/write-org-profile.mjs)
echo "$OUT"
```
`compose-org-profile.mjs` prepender top-level OKF-nøkler (`type: Organisasjonsprofil`,
`resource:`, `timestamp:`) over den nestede profilen i ett frontmatter-blokk. Rapporter
«Org-profil skrevet til: $OUT». Hvis `$OUT` er den prosjektlokale fallbacken, vis YAML-en
for manuell plassering (jf. Feilhåndtering). Syklus-/kontekstdata forblir prosjektlokalt
i `.claude/okr.local.md`.
Vis kort oppsummering og foreslå `/okr:oppsett full` for å fylle ut resten senere.
--- ---
@ -328,7 +396,7 @@ Trigger: `/okr:oppsett oppdater` eller valg 1 fra ingen-argument-menyen.
1. Les `.claude/okr.local.md` 1. Les `.claude/okr.local.md`
2. Vis gjeldende verdier gruppert per seksjon 2. Vis gjeldende verdier gruppert per seksjon
3. Bruk AskUserQuestion: "Hvilken seksjon vil du oppdatere?" 3. Bruk AskUserQuestion: "Hvilken seksjon vil du oppdatere?"
Alternativer: Organisasjon | Program | Syklus | Verktoy | Kultur | Integrasjoner | Preferanser Alternativer: Organisasjon | Program | Syklus | Verktøy | Kultur | Integrasjoner | Preferanser
4. Vis feltene i valgt seksjon med gjeldende verdier 4. Vis feltene i valgt seksjon med gjeldende verdier
5. La bruker endre ett eller flere felt 5. La bruker endre ett eller flere felt
6. Skriv oppdatert YAML tilbake 6. Skriv oppdatert YAML tilbake
@ -354,10 +422,10 @@ Les `.claude/okr.local.md` og vis innholdet formatert:
### Program ### Program
- Modenhet: [modenhetsnivaa] - Modenhet: [modenhetsnivaa]
- Sykluser gjennomfort: [tall] - Sykluser gjennomført: [tall]
- Sponsor: [sponsor] - Sponsor: [sponsor]
- Champion: [champion] - Champion: [champion]
- OKR frikoblet fra lonn: [ja/nei] - OKR frikoblet fra lønn: [ja/nei]
### Gjeldende syklus ### Gjeldende syklus
- ID: [id] - ID: [id]
@ -365,7 +433,7 @@ Les `.claude/okr.local.md` og vis innholdet formatert:
- Fase: [fase] - Fase: [fase]
- Antall team: [tall] - Antall team: [tall]
### Verktoy ### Verktøy
[liste] [liste]
### Kultur ### Kultur
@ -391,7 +459,7 @@ Sjekk at `.claude/okr/syklus/[id]/` finnes. Hvis ikke:
``` ```
Ingen aktiv syklusmappe funnet for [id]. Ingen aktiv syklusmappe funnet for [id].
Opprett `.claude/okr/syklus/[id]/` med OKR-filer for a bruke arkivering. Opprett `.claude/okr/syklus/[id]/` med OKR-filer for å bruke arkivering.
``` ```
### Arkiveringsflyt ### Arkiveringsflyt
@ -400,16 +468,17 @@ Opprett `.claude/okr/syklus/[id]/` med OKR-filer for a bruke arkivering.
og `status.md` hvis den finnes. og `status.md` hvis den finnes.
2. **Samle endelige scorer** — hvis `status.md` har endelige scorer, bruk dem. 2. **Samle endelige scorer** — hvis `status.md` har endelige scorer, bruk dem.
Ellers spor med AskUserQuestion: Ellers spør med AskUserQuestion:
``` ```
Hva ble de endelige scorene for [syklus]? Hva ble de endelige scorene for [syklus]?
Oppgi score per KR (0.0-1.0) eller ENTER for a bruke siste kjente status. Oppgi score per KR (0.0-1.0) eller ENTER for å bruke siste kjente status.
``` ```
3. **Generer retrospektiv** — skriv til `.claude/okr/syklus/[id]/retrospektiv.md`: 3. **Generer retrospektiv** — skriv til `.claude/okr/syklus/[id]/retrospektiv.md`:
```markdown ```markdown
--- ---
type: Retrospektiv
syklus: [id] syklus: [id]
periode: [periode fra config] periode: [periode fra config]
arkivert: [dato] arkivert: [dato]
@ -424,74 +493,99 @@ Opprett `.claude/okr/syklus/[id]/` med OKR-filer for a bruke arkivering.
## Hva gikk bra ## Hva gikk bra
[Basert pa KR som scoret 0.7+] [Basert på KR som scoret 0.7+]
## Hva var utfordrende ## Hva var utfordrende
[Basert pa KR som scoret <0.5, noter monstere] [Basert på KR som scoret <0.5, noter mønstre]
## Laering til neste syklus ## Læring til neste syklus
[Identifiser 2-3 konkrete forbedringer] [Identifiser 2-3 konkrete forbedringer]
## Antipatterns observert ## Antipatterns observert
[Sjekk mot antipatternene fra referansemateriale — [Sjekk mot antipatternene fra referansemateriale —
nevn kun de som er relevante basert pa scoremansteret. nevn kun de som er relevante basert på scoremønsteret.
Les ${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-antipatterns.md] Les ${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-antipatterns.md]
``` ```
4. **Arkiver til historikk** — opprett `.claude/okr/historikk/[id]/` med Write-tool 4. **Arkiver til historikk** — opprett `.claude/okr/historikk/[id]/` med Write-tool
og kopier alle filer fra syklusmappen dit (les innhold, skriv til ny sti). og kopier alle filer fra syklusmappen dit (les innhold, skriv til ny sti).
Opprett ogsa `.claude/okr/historikk/[id]-resultater.md` med sammendrag: Opprett også `.claude/okr/historikk/[id]-resultater.md` med sammendrag:
endelige scorer, topp-laeringer, og lenke til full retrospektiv. endelige scorer, topp-læringer, og lenke til full retrospektiv.
5. **Bekreft sletting** — bruk AskUserQuestion: 5. **Bekreft sletting** — bruk AskUserQuestion:
``` ```
Slett syklusmappen `.claude/okr/syklus/[id]/`? Slett syklusmappen `.claude/okr/syklus/[id]/`?
Den er na arkivert i `historikk/[id]/`. Den er nå arkivert i `historikk/[id]/`.
Alternativer: ja | nei (behold begge) Alternativer: ja | nei (behold begge)
``` ```
Hvis ja og Bash-tool er tilgjengelig: `rm -rf .claude/okr/syklus/[id]/` Hvis ja og Bash-tool er tilgjengelig: **valider FØRST at `[id]` ikke er tom OG
Hvis Bash ikke tilgjengelig: vis kommando brukeren kan kjore manuelt: matcher mønsteret `^[TQ][1-4]-[0-9]{4}$`** (f.eks. `T1-2026`). Kun hvis
`rm -rf .claude/okr/syklus/[id]/` valideringen passerer, kjør: `rm -rf .claude/okr/syklus/[id]/`. Ved tom eller
malformert `[id]`: AVVIS slettingen og be om en gyldig syklus-id — kjør ALDRI
`rm -rf` med tom verdi (det ville blitt `rm -rf .claude/okr/syklus//` og slettet
hele syklus-treet).
Hvis Bash ikke er tilgjengelig: vis kommandoen brukeren kan kjøre manuelt (samme
validering gjelder): `rm -rf .claude/okr/syklus/[id]/`
6. **Oppdater profil** — i `.claude/okr.local.md`: 6. **Oppdater profil** — i `.claude/okr.local.md`:
- Inkrementer `sykluser_gjennomfort` med 1 - Inkrementer `sykluser_gjennomfort` med 1
- Oppdater `gjeldende_syklus.id` til neste syklus: - Oppdater `gjeldende_syklus.id` til neste syklus:
- Tertial: T1→T2→T3→T1 (med aarsoppdatering) - Tertial: T1→T2→T3→T1 (med årsoppdatering)
- Kvartal: Q1→Q2→Q3→Q4→Q1 (med aarsoppdatering) - Kvartal: Q1→Q2→Q3→Q4→Q1 (med årsoppdatering)
- Sett `gjeldende_syklus.fase` til `planlegging` - Sett `gjeldende_syklus.fase` til `planlegging`
- Oppdater `gjeldende_syklus.periode` til neste periodestreng - Oppdater `gjeldende_syklus.periode` til neste periodestreng
7. **Opprett ny syklusmappe** — skriv `.claude/okr/syklus/[ny-id]/status.md` med 7. **Opprett ny syklusmappe** — skriv `.claude/okr/syklus/[ny-id]/status.md` med
tom mal (tabellstruktur med KR-kolonner, ingen data enna). OKF-frontmatter + tom mal (tabellstruktur med KR-kolonner, ingen data ennå):
```markdown
---
type: Status
syklus: [ny-id]
title: "Status [ny-id]"
description: "Fremdrift og scoring for syklus [ny-id]"
timestamp: "[ISO-8601]"
---
# OKR Status [ny-id]
[tom tabellstruktur med KR-kolonner]
```
Regenerér deretter OKF-indeksen for prosjekt-treet:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/okf-index.mjs .claude/okr
```
8. **Bekreft** — vis oppsummering: 8. **Bekreft** — vis oppsummering:
``` ```
Syklus [id] er arkivert i `.claude/okr/historikk/[id]/`. Syklus [id] er arkivert i `.claude/okr/historikk/[id]/`.
Gjeldende syklus er na [ny-id] (planleggingsfase). Gjeldende syklus er nå [ny-id] (planleggingsfase).
Anbefalte neste steg: Anbefalte neste steg:
- /okr:moter — planlegg oppstartsmate for [ny-id] - /okr:møter — planlegg oppstartsmøte for [ny-id]
- /okr:skriv — skriv nye OKR for [ny-id] - /okr:skriv — skriv nye OKR for [ny-id]
- /okr:kaskade — kaskader org-OKR til team-niva - /okr:kaskade — kaskader org-OKR til team-nivå
``` ```
--- ---
## Feilhandtering ## Feilhåndtering
- **Fil kan ikke skrives:** Vis YAML som kan kopieres manuelt - **Fil kan ikke skrives:** Vis YAML som kan kopieres manuelt
- **Bruker avbryter midt i intervju:** `onboarding_status: partial` allerede lagret — neste kjoring tilbyr a fortsette - **Bruker avbryter midt i intervju:** `onboarding_status: partial` allerede lagret — neste kjøring tilbyr å fortsette
- **Linear API feiler:** Gi tydelig feilmelding, fortsett uten Linear-integrasjon - **Linear API feiler:** Gi tydelig feilmelding, fortsett uten Linear-integrasjon
- **Mappeopprettelse feiler:** Vis `mkdir -p .claude/okr/strategisk-kontekst/` kommando brukeren kan kjore manuelt - **Mappeopprettelse feiler:** Vis `mkdir -p .claude/okr/strategisk-kontekst/` kommando brukeren kan kjøre manuelt
## Syklusberegning ## Syklusberegning
Beregn gjeldende syklus-ID basert pa dagens dato: Beregn gjeldende syklus-ID basert på dagens dato:
- **Tertial:** Jan-Apr = T1, Mai-Aug = T2, Sep-Des = T3 - **Tertial:** Jan-Apr = T1, Mai-Aug = T2, Sep-Des = T3
- **Kvartal:** Jan-Mar = Q1, Apr-Jun = Q2, Jul-Sep = Q3, Okt-Des = Q4 - **Kvartal:** Jan-Mar = Q1, Apr-Jun = Q2, Jul-Sep = Q3, Okt-Des = Q4
- Format: `T[1-3]-YYYY` eller `Q[1-4]-YYYY` - Format: `T[1-3]-YYYY` eller `Q[1-4]-YYYY`

View file

@ -58,7 +58,7 @@ Et godt Objective er:
| Dårlig | Bedre | Problem | | Dårlig | Bedre | Problem |
|--------|-------|---------| |--------|-------|---------|
| "Forbedre kundeservice" | "Levere raskere support for saksbehandlingsøknader" | Vag | | "Forbedre kundeservice" | "Levere raskere support for søknadsbehandling" | Vag |
| "Implementere nytt system" | "Gi saksbehandlere sanntids innsikt" | Aktivitet | | "Implementere nytt system" | "Gi saksbehandlere sanntids innsikt" | Aktivitet |
| "Digitalisere etaten" | "Gjøre tjenestefornyelse friksjonsfri" | For bredt | | "Digitalisere etaten" | "Gjøre tjenestefornyelse friksjonsfri" | For bredt |
| "Øke kundetilfredshet til 4.5" | "Bli den mest responsive serviceenheten i etaten" | Har tall (= KR) | | "Øke kundetilfredshet til 4.5" | "Bli den mest responsive serviceenheten i etaten" | Har tall (= KR) |
@ -131,7 +131,22 @@ Hjelp med å balansere committed vs aspirational mål.
## Eksempel på komplett output ## Eksempel på komplett output
``` Når en OKR lagres som konsept-fil i second-brain-treet
(`.claude/okr/syklus/[id]/okr-[team].md`), bær den **OKF-frontmatter** (påkrevd
`type: OKR` + anbefalt `resource`/`title`/`description`/`tags`-liste/`timestamp`):
```markdown
---
type: OKR
resource: ".claude/okr/syklus/T2-2026/okr-digital.md"
title: "OKR Digital avdeling T2-2026"
description: "Gjøre tjenestefornyelse friksjonsfri"
tags:
- digital
- tjenestefornyelse
timestamp: "[ISO-8601]"
---
## OKR for Digital avdeling — T2-2026 ## OKR for Digital avdeling — T2-2026
**Objective**: Gjøre tjenestefornyelse til en friksjonsfri opplevelse **Objective**: Gjøre tjenestefornyelse til en friksjonsfri opplevelse
@ -152,3 +167,4 @@ Hjelp med å balansere committed vs aspirational mål.
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-framework.md` — full metodikk - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-framework.md` — full metodikk
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-examples.md` — eksempler - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-examples.md` — eksempler
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-cheatsheet.md` — hurtigreferanse for OKR-formulering

View file

@ -30,7 +30,7 @@ disse filene direkte. Brukeren trenger ikke lime inn OKR-tekst.
| Score | Betydning | Typisk for | | Score | Betydning | Typisk for |
|-------|-----------|------------| |-------|-----------|------------|
| 1.0 | 100% oppnådd | Committed mål | | 1.0 | 100% oppnådd | Committed mål |
| 0.7-0.8 | Suksess | Aspirational mål | | ≈0.7 | Sweet spot — 0.7 = forventet for aspirational; 0.6-0.7 = sweet spot (Google/Doerr) | Aspirational mål |
| 0.5-0.7 | Delvis suksess | Trenger analyse | | 0.5-0.7 | Delvis suksess | Trenger analyse |
| < 0.5 | Ikke oppnådd | Trenger intervensjon | | < 0.5 | Ikke oppnådd | Trenger intervensjon |
@ -69,7 +69,19 @@ Generer en strukturert check-in:
## Eksempel på output ## Eksempel på output
``` Når statusrapporten lagres som `.claude/okr/syklus/[id]/status.md` i second-brain-treet,
bær filen **OKF-frontmatter** (påkrevd `type: Status` + anbefalt `title`/`description`/
`timestamp`); rapportinnholdet under følger frontmatteren:
```markdown
---
type: Status
syklus: T1-2026
title: "Status T1-2026"
description: "Fremdrift og scoring uke 8 av 16"
timestamp: "[ISO-8601]"
---
## OKR Status - Uke 8 av 16 ## OKR Status - Uke 8 av 16
### Objective: Forbedre trafikksikkerhet i skolesoner ### Objective: Forbedre trafikksikkerhet i skolesoner
@ -102,3 +114,5 @@ Hvis Linear er konfigurert (sjekk med ToolSearch):
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-calculator.md` — beregningsformler - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-calculator.md` — beregningsformler
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-framework.md` — scoring-metodikk - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-framework.md` — scoring-metodikk
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/metrics-library.md` — metrikkbibliotek for offentlige KR
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-integrations.md` — Linear-/verktøyintegrasjon for statussporing

View file

@ -0,0 +1,246 @@
# 360-evaluering: OKR Offentlig Sektor-plugin (v1.3.0)
**Dato:** 2026-06-23
**Evaluator:** Lead reviewer (syntese av 6 fil-lesere, 4 web-/Gemini-researchere, 2 benchmark-agenter)
**Mål:** Vurdere hva som skal til for å gjøre dette til DET refererte OKR-pluginet for norsk offentlig sektor.
**Avgrensning:** Denne 360-en dekker **domenekorrekthet, innhold, språk, UX og fullstendighet**. Sikkerhet/config/struktur/versjon ble dekket av piloten (`docs/review-2026-06-20.md`) og gjentas ikke her.
---
## Sammendrag
Pluginet er **arkitektonisk sterkt og metodisk substansielt**, med ekte norsk forvaltningstilpasning som ikke er kosmetisk (DFØ-begrepsbro, Hovedavtalen/tillitsvalgte, tildelingsbrev-kategorisering, Riksrevisjon-etterprøvbarhet). Hooks, agent-arkitektur og state-modell er på høyde med — og på coaching-siden over — referanse-pluginet `ms-ai-architect`.
Men det er **ikke referansegrad i dag, og kan ikke trygt deles eksternt**, fordi *innholdslaget* har troverdighetskritiske feil som en norsk fagperson vil oppdage ved første gjennomlesning:
1. **Fiktiv etat presentert som fakta.** «Direktoratet for digital tjenesteutvikling (DDT)» finnes ikke. Den brukes som løpende eksempel-etat, og i kildefilen presenteres «Oboard (DDTs valg)» som en faktisk kilde — fabrikkert proveniens.
2. **Find-replace-vrøvl** i de mest kopierte eksemplene: doblede ord («saksbehandlingbehandling»), semantisk umulige fraser («fornye saksbehandling»), feil sammensetninger («saksbehandlingtjenester»).
3. **Scoringsterskel som motsier plugins egne kilder** på tvers av minst 5 filer (0.6-0.7 / 0.7-0.8 / 0.6-0.8 / 0.7-0.9 / 70-80 %).
4. **Total UTF-8-korrupsjon** i to filer (0 å-tegn) + coaching-hookens brukervendte strenger.
5. **Døde lenker og dokument-selvmotsigelser** (CONTRIBUTING inviterer PR-er som GOVERNANCE avviser; README oppgir DDT som reell bruker).
Fundamentet er godt nok til at **én fokusert saneringsrunde løfter pluginet raskt**. Men inntil fakta-laget er ryddet, er pilotens «lav risiko, høy kvalitet»-verdikt riktig på sikkerhet/struktur og **misvisende på innhold**.
**Samlet karakter: C+** (sterkt fundament trukket ned av troverdighetskritiske innholdsfeil).
---
## Metode
- **Pilotens dekning (ikke gjentatt):** sikkerhet (A-), config (A), struktur, versjonskonsistens. Pilotens ene svakhet: «Feature Coverage D 40». To nits: OKR-01 (ugardert `rm -rf`), OKR-02 (Linear-nøkler i okr.local.md).
- **Denne 360-ens dekning:** domenekorrekthet/metodikk, norsk forvaltningstilpasning, innholds-/språkkvalitet, kommando-/agent-/hook-kvalitet, dokumentasjon/markedsplass-klarhet, modenhet vs `ms-ai-architect`, plugin-craft, og referansegrad-gap.
- **Kildehåndtering:** Researcher-verdiktene er brukt som fasit for faktapåstander. Alle kritiske og høye funn er **verifisert direkte mot filene** (se Verifiseringslogg). Der lesere var uenige eller upresise, er filsjekken avgjørende.
- **Korreksjon av lesere:** Lesere oppga «15 referansefiler» og en CHANGELOG uten 1.2.0-forklaring. Filsjekk viser **16 referansefiler** (`okr-oboard-guide.md` ble oversett) og at CHANGELOG **faktisk** noterer «Version bumped from 1.1.0 to 1.3.0». CLAUDE.md sier derimot feilaktig «References (15)».
---
## Scorecard
| Dimensjon | Karakter | Kort begrunnelse |
|-----------|----------|------------------|
| Domenekorrekthet/metodikk | C+ | Kjernemekanikk korrekt; scoringsterskel selvmotsigende på tvers av ≥5 filer og motsier egen bibliografi. |
| Norsk forvaltningstilpasning | C | Ekte differensierende lag, men fiktiv DDT, tertial overdrevet som DFØ-standard, tillitsreformen fraværende. |
| Innholds- og språkkvalitet | D | Total UTF-8-korrupsjon i 2 filer + hook; ≥8 find-replace-artefakter i kopierte eksempler; uciterte statistikker. |
| Kommandoer | B | Komplette og selvstendige; møter.md mangler Kontekstbevissthet, gap/governance overlapper, «Kvartalsreview» feil. |
| Agenter | B | Ren ansvarsdeling; mangler negative triggere, leser ikke org-kontekst når Task-spawnet, 0.7-0.9-tabell uten kilde. |
| Hooks/state/arkitektur | B+ | Robuste hooks og sammenhengende state; læring-regex feiler på «Læring», rm -rf ugardert, ikke reinstall-overlevende. |
| Dokumentasjon/markedsplass-klar | C | Fullt hygiene-sett, men README-feil (DDT, ASCII-kommandonavn), CONTRIBUTING↔GOVERNANCE-motsigelse, SECURITY peker GitHub. |
| Modenhet vs ms-ai-architect | C+ | Paritet/overlegen på coaching; mangler reinstall-overlevende org-katalog, /help, PDF-eksport, rubrikk-filer, fixtures. |
| Plugin-craft | B | Tynn SKILL.md, riktig eksternalisering; SKILL-versjon 1.0.0 vs 1.3.0, 6 usiterte filer, ingen emne-guard på injeksjon. |
---
## Funn per dimensjon
### A. Domenekorrekthet og metodikk
**[KRITISK] Scoringsterskel selvmotsigende og i strid med egne kilder** — `SKILL.md:47`, `okr-framework.md:104/262/268`, `okr-offentlig-governance.md:73`, `commands/sporing.md:33`, `agents/fremdriftssporer-agent.md:44`, `okr-cheatsheet.md:48`.
Pluginet bruker fem forskjellige stretch-suksessterskler: 0.6-0.7 (sources, korrekt), 0.7-0.8 (SKILL/framework/sporing), 0.6-0.8 (framework L268), 0.7-0.9 (cheatsheet L48 + fremdriftssporer L44), 70-80 % (governance L73). Kanon (Doerr/Google re:Work, verifisert live) er **0.7 forventet score, 0.6-0.7 sweet spot**. 0.7-0.9 og 70-80 % har ingen kildebasis og forteller brukeren at 0.9 på et stretch-mål bare er «suksess» heller enn eksepsjonelt.
*Anbefaling:* Innfør ÉN kanonisk formulering overalt: «0.7 = forventet for aspirational; 0.6-0.7 = sweet spot (Google/Doerr)». Hvis offentlig-sektor-tilpasning ønsker høyere gulv, merk det eksplisitt som bevisst avvik.
**[INFO/MEDIUM] Faktarammings-feil i sekundære caser** — `okr-sources.md:125/131`.
Intel «fra 0 % til 85 %» er misvisende (85 % var *gjenvunnet* andel av 16-bit-markedet, ikke fra null), og «2000 design wins» var *målet*, ikke resultatet (~2300-2500 oppnådd). Locke & Latham «250 %» bør rammes som «vanskeligste vs enkleste mål», ikke «vs do-your-best».
*Anbefaling:* Rett begge; legg til Kleingeld-journal (Journal of Applied Psychology 96(6):1289-1304).
**[MEDIUM] Aggregering snitter committed og aspirational sammen** — `okr-framework.md` aggregeringslogikk.
Doerr advarer eksplisitt mot dette. Skår de to typene separat.
### B. Norsk forvaltningstilpasning
**[KRITISK] Fiktiv etat presentert som fakta** — `okr-sources.md:299`, `README.md:23`, gjennom hele `okr-offentlig-governance.md` og `okr-examples.md`.
«Direktoratet for digital tjenesteutvikling (DDT)» finnes ikke (verifisert mot regjeringen.no/Digdir). I kildefilen står «Oboard (Direktoratet for digital tjenesteutviklings valg)» — fabrikkert kilde-proveniens i en evidensfil. README oppgir DDT som reell OKR-bruker ved siden av NAV og FINN.no. DDT er dessuten internt usammenhengende: et «digital tjenesteutvikling»-direktorat eier riksvei-vedlikehold og trafikkdrepte-mål (Statens vegvesens domene), med tildelingsbrev fra Samferdselsdepartementet.
*Anbefaling:* Enten konsekvent merk DDT «(fiktiv eksempeletat)» med disclaimer ved første bruk, eller splitt i reelle etater (Statens vegvesen for vei/trafikk; Digdir for digitale tjenester). Fjern den fabrikkerte Oboard-attribusjonen og DDT fra README-listen over reelle brukere.
**[MEDIUM] Tertial overdrevet som DFØ-standard** — `dfo-okr-mapping.md:45`, `okr-framework.md:205`.
Tertialrapportering eksisterer i 2026, men det formelle regimet har **2 interim-punkter** (per 30. april / 31. august) + årsrapport, ikke 3 likeverdige sykluser, og DFØ vektlegger nå halvårs-/årsrapportmøte. 4-måneders OKR-syklus er et forsvarlig *praktisk* valg, men må ikke fremstilles som identisk med rapporteringsregimet.
*Anbefaling:* Omramme: «mange virksomheter bruker tertialrapportering; 4-måneders syklus aligner med — er ikke identisk med — det formelle (2-punkts) regimet. Sjekk eget tildelingsbrev.»
**[MEDIUM] Feil dokumenttittel** — `okr-offentlig-governance.md:395`.
«Digitaliseringsstrategi for offentlig sektor 2024-2030» finnes ikke under det navnet. Korrekt: **«Fremtidens digitale Norge — Nasjonal digitaliseringsstrategi 2024-2030»** (lansert 26.09.2024). «...for offentlig sektor» tilhører den forrige 2019-2025-strategien.
**[REFERANSEGRAD-GAP] Tillitsreformen fraværende.** Den sittende regjeringens tillitsreform (tillitsbasert styring, redusert detaljmålstyring) er den mest sentrale styringsspenningen for OKR i staten nå — og er helt utelatt. «Målforskyvning» mangler som førsteklasses norsk antipattern.
### C. Innholds- og språkkvalitet
**[KRITISK/HØY] Total UTF-8-korrupsjon** — `individual-vs-team-okr.md` (0 å-tegn, 1 ø), `commands/oppsett.md` (0 å-tegn).
Alle norske spesialtegn er strippet: «i trad med radene», «a eie outcomes», «Nar individuell», «lonn», «sporsmal», «verktoy», «Kjor», «sporsmaal», «foreslaar», «ambisiose», «nokkelmal». Begge filene er i praksis upubliserbare for en norsk leser. Coaching-hookens **brukervendte strenger** har samme feil (`coaching-hook.mjs:115/124/125`: «fokus pa», «naermer», «/okr:moter») — synlig i hver sesjonsstart.
*Anbefaling:* Full regenerering med korrekt UTF-8 (tryggere enn find-replace pga. omfang).
**[HØY] Find-replace-artefakter i kopierte eksempler** — verifisert liste:
- `okr-offentlig-governance.md:346, :422` — «saksbehandlingbehandling» (doblet ord) → «saksbehandling»
- `okr-offentlig-governance.md:319, :413` + `okr-framework.md:63` — «saksbehandlingtjenester» → «saksbehandlingstjenester»
- `okr-offentlig-governance.md:71` + `okr-framework.md:101` — «saksbehandlingklager» → «saksbehandlingsklager»
- `okr-offentlig-governance.md:340` — «fornye saksbehandling» (semantisk umulig; klue L342 «mottatt kort» tyder på opprinnelig førerkort/pass) → koherent domene
- `okr-offentlig-governance.md:326` — «saksbehandling-søknader» → «søknader»
- `okr-examples.md:60, :64` — «vevedlikehold» → «veivedlikehold»
- `okr-examples.md:25`, `okr-framework.md:245`, `commands/skriv.md:61` — «saksbehandlingsøknader» (tautologi)
*Anbefaling:* Bestem ett koherent domene (sannsynlig opprinnelse: førerkort/kjøretøy hos Statens vegvesen) og rett systematisk. Bevis 0 gjenstående med grep.
**[MEDIUM] Uciterte statistikker presentert som fakta** — `okr-implementation.md:8-10`, `okr-antipatterns.md:3`.
«39 % mer sannsynlige», «43 % flere mål», «30-45 % flere objectives», «60 % mislykkes» — ingen kilde; ligner vendor-markedsføring. I en Riksrevisjon-/DFØ-kontekst er dette en tillitsbelastning.
*Anbefaling:* Merk «industri-rapportert (ikke fagfellevurdert)» eller erstatt med Locke & Latham-effektstørrelser som allerede er i okr-sources.md.
### D. Kommandoer
**[MEDIUM] «Kvartalsreview» i 4-måneders plugin** — `commands/møter.md:60`, `agents/møtefasilitator-agent.md:39`, `okr-implementation.md:48/415` («kvartalsvise»). En norsk fagperson vil anta Q1-Q4 og ikke kjenne igjen sin egen tertialsyklus. → «Syklusreview»/«syklusvise».
**[LAV] møter.md mangler Kontekstbevissthet-blokk** — alene med oppsett blant de 10. De 8 andre har den (verifisert).
**[MEDIUM] gap vs governance overlapper uten disambiguering** — identisk innholdstabell duplisert; begge leser samme tildelingsbrev. Legg til «Bruk /okr:gap når... (kvantitativ dekningsmatrise)» vs «Bruk /okr:governance når... (kvalitativ oversettelse + compliance)».
### E. Agenter
**[MEDIUM] Ingen negative triggere** — særlig `styringsrådgiver` vs `gapanalytiker` (begge fyrer på «tildelingsbrev»).
**[MEDIUM] Agenter leser ikke org-kontekst fra fil** — de er blinde når de Task-spawnes (arver ikke UserPromptSubmit-injeksjonen). Legg til eksplisitt fil-lesing som i ms-ai-architect.
**[LAV] fremdriftssporer bruker 0.7-0.9-tabell** uten kilde (del av scoring-saneringen).
### F. Hooks, state og arkitektur
**[HØY] Læring-regex feiler på korrekt norsk** — `coaching-hook.mjs:100` matcher kun `[Ll]aering`. Når en bruker skriver korrekt «Læring», feiler den stille og longitudinell læring forsvinner. → `/## (?:Laering|Læring) til neste syklus/`.
**[MEDIUM] OKR-01 bekreftet** — `commands/oppsett.md:457/459`: `rm -rf .claude/okr/syklus/[id]/` uten validering av at `[id]` matcher `^[TQ][1-4]-[0-9]{4}$`. Tom verdi → `rm -rf .claude/okr/syklus//`.
**[REFERANSEGRAD-GAP] Ikke reinstall-overlevende** — onboarding skriver til prosjektlokal `okr.local.md`; org-identitet/syklushistorikk tapes ved reinstall/nytt prosjekt.
### G. Dokumentasjon og markedsplass-klarhet
**[HØY] CONTRIBUTING ↔ GOVERNANCE-motsigelse** — `CONTRIBUTING.md:23-30` har full «Submitting Pull Requests»-seksjon; `GOVERNANCE.md:9` avviser PR-er eksplisitt. → Erstatt med kryssreferanse.
**[HØY] SECURITY peker til GitHub** på en Forgejo-hostet repo (`open/okr.git`) — feil disclosure-kanal.
**[MEDIUM] README ASCII-kommandonavn** — `/okr:moter`, `/okr:innforing` (L106/L114/L166) matcher ikke filnavnene `møter.md`/`innføring.md`; copy-paste feiler. → bruk korrekte tegn.
**[MEDIUM] CLAUDE.md «References (15)»** — faktisk 16 filer.
### H. Plugin-craft
**[MUST] SKILL.md-versjon 1.0.0 vs plugin.json 1.3.0** — bump + sync.
**[MEDIUM] Template-skjema ≠ onboarding-YAML** — manuell bootstrap mangler felter hooken averterer.
**[SHOULD] 6 usiterte referansefiler** (dfo-okr-mapping, individual-vs-team-okr, metrics-library, okr-cheatsheet, okr-integrations, okr-sources) — dødvekt; wire inn eller avskriv.
**[SHOULD] UserPromptSubmit uten emne-guard** — injiserer OKR-kontekst på hver prompt.
---
## Verifiseringslogg
Nøkkelpåstander sjekket direkte mot filer og researcher-verdikter (oppfyller verifiseringsplikten):
| Påstand | Verdikt | Korrekt verdi / funn | Kilde |
|---------|---------|----------------------|-------|
| DDT er en reell norsk etat | **Avkreftet** | Finnes ikke; reell digital-etat er Digdir, reell vei-etat er Statens vegvesen | regjeringen.no; `okr-sources.md:299`, `README.md:23` (filsjekk) |
| «Oboard (DDTs valg)» er en faktisk kilde | **Avkreftet** | Fabrikkert proveniens; Oboard er reelt verktøy men ikke noen navngitt etats verifiserte valg | oboard.io; `okr-sources.md:299` |
| Stretch-suksess = 0.7-0.8 | **Avkreftet** | Kanon: 0.7 forventet / 0.6-0.7 sweet spot (Doerr/Google re:Work, live) | rework.withgoogle.com/en/guides/set-goals-with-okrs |
| Scoringsterskel er konsistent i pluginet | **Avkreftet** | 5 ulike verdier funnet: 0.6-0.7, 0.7-0.8, 0.6-0.8, 0.7-0.9, 70-80 % | filsjekk (6 filer) |
| Find-replace-artefakter eksisterer | **Bekreftet** | «saksbehandlingbehandling», «fornye saksbehandling», «saksbehandlingtjenester», «vevedlikehold» m.fl. | grep over references/ + commands/ |
| individual-vs-team-okr.md mangler norske tegn | **Bekreftet** | 0 å-tegn, 1 ø-tegn i hele filen | `grep -o å \| wc -l` |
| oppsett.md mangler norske tegn | **Bekreftet** | 0 å-tegn; «lonn», «sporsmaal», «foreslaar» m.fl. | filsjekk |
| coaching-hook har encoding-feil i brukerstrenger | **Bekreftet** | L115/124/125: «fokus pa», «naermer», «/okr:moter» | `coaching-hook.mjs` |
| Læring-regex feiler på «Læring» | **Bekreftet** | L100 matcher kun ASCII `[Ll]aering` | `coaching-hook.mjs:100` |
| Statistikkene 39/43/30-45/60 % er uciterte | **Bekreftet** | Ingen kilde; ligner vendor-data | `okr-implementation.md:8-10`, `okr-antipatterns.md:3` |
| futureworks.no-artikkel er død | **Bekreftet (web)** | HTTP 404; futureworks.no er nå SaaS, ikke artikkel | `okr-sources.md:230` |
| Kleingeld d=0.80/-1.75/1.20 | **Bekreftet** | Eksakt korrekt; JAP 96(6):1289-1304, 2011 | pubmed.ncbi.nlm.nih.gov/21744940 |
| Locke & Latham «250 % vs do-your-best» | **Delvis** | Effektstørrelser (d≈0.52-0.82) korrekte; 250 % = vanskeligste vs *enkleste* mål | Locke & Latham 2002, Am. Psychologist |
| Intel «fra 0 % til 85 %» / «2000 design wins resultat» | **Delvis** | 85 % = *gjenvunnet* 16-bit-andel; 2000 var mål, ~2300-2500 oppnådd | whatmatters.com/operation-crush |
| Tertialrapportering er current 2026 | **Bekreftet** | Eksisterer; men formelt 2 interim-punkter + årsrapport, DFØ vektlegger halvårs-/årsrapportmøte | dfo.no/...etatsstyring/rapportering |
| «Digitaliseringsstrategi for offentlig sektor 2024-2030» | **Delvis (feil tittel)** | Korrekt: «Fremtidens digitale Norge — Nasjonal digitaliseringsstrategi 2024-2030» | regjeringen.no |
| NTP 2025-2036 sitert korrekt | **Bekreftet** | Meld. St. 14 (2023-2024) | regjeringen.no |
| Antall referansefiler | **Lesere upresise** | **16** filer (ikke 15); `okr-oboard-guide.md` oversett | `ls references/ \| wc -l` |
| CHANGELOG forklarer 1.2.0-hopp | **Lesere feil** | CHANGELOG **noterer** det; CLAUDE.md sier feil «(15)» | `CHANGELOG.md:19`, `CLAUDE.md:46` |
| CONTRIBUTING inviterer PR-er GOVERNANCE avviser | **Bekreftet** | CONTRIBUTING L23-30 vs GOVERNANCE L9 | filsjekk |
| Gemini-triangulering levert | **Uverifiserbar** | Gemini-bridge nede (deprecated SDK); triangulering MANGLER | researcher-rapport |
---
## Modenhet vs ms-ai-architect
**På paritet eller overlegen:** proaktiv coaching-hook med syklusposisjon og at-risk-KR-deteksjon (overgår ms-ai-architect), rik UserPromptSubmit-injeksjon, 7 spesialiserte agenter, dyp onboarding-wizard med resume, syklus-arkivering med retrospektiv-generering, 16-fils kuratert kunnskapsbase, fullt community-hygiene-sett.
**Gap som skiller fra referanse-plugin (prioritert):**
- **MUST:** reinstall-overlevende `~/.claude/okr/org/` (i dag prosjektlokal okr.local.md — tapes ved reinstall); `/okr:help`-kommando (10 kommandoer + 7 agenter uten oppdagbarhetshjelp).
- **SHOULD:** ankrede scoringsrubrikker per dimensjon (egen KB-fil, 5 ankere/dimensjon) for determinisme i governance-kontekst; test-fixtures + smoke-test for hooks (fanger skjema-drift); `/okr:export` PDF for deling med ledelse/tillitsvalgte/Riksrevisjon; agenter som leser org-kontekst direkte (Task-spawn-blindhet); SessionStart-hook som signaliserer manglende onboarding.
- **NICE:** KB-ferskhetssjekk + `/okr:freshen-references`; interaktiv playground for innkjøps-/champion-demo.
---
## Veikart til referansegrad (flersesjons, Voyage-drevet)
> **Den følgbare, kritikk-korrigerte utførelsesplanen ligger i `docs/plan-referansegrad-2026-06-23.md`.** Veikartet under er syntese-versjonen; planen korrigerer det på flere punkter (DDT = 61 forekomster i 14 filer, ikke 2; UTF-8 = omskriving ikke encoding-fiks; full scoring-målliste med KEEP-merking av kanon-kilden; versjon-per-fase; fil-eierskap Fase 1↔2; OKR-01 flyttet til Fase 0). Se Vedlegg F for plan-criticens funn.
**Fase 0 — Fakta- og troverdighetssanering (1-2 sesjoner). Avhenger av: ingenting.**
Credibility-critical. Alt annet bygger på et troverdig fundament.
Leveranser: DDT-beslutning (merk fiktiv ELLER splitt i reelle etater) + fjern fabrikkert Oboard-kilde og DDT fra README; rett alle find-replace-artefakter; ÉN kanonisk scoringsterskel overalt; full UTF-8-regenerering av de to filene + hook-strenger + læring-regex; rett døde/feil lenker og dokumenttittel; rydd CONTRIBUTING↔GOVERNANCE, SECURITY, README-kommandonavn, CLAUDE.md-count, uciterte statistikker; bump SKILL-versjon + katalog-ref.
*Voyage:* `/trekbrief` rammer saneringen + forankrer verifiseringsplikten → `/trekplan` med eksplisitt Verifisering-seksjon (grep beviser 0 artefakter, 0 strippede å, én scoringsverdi) → `/trekexecute``/trekreview`.
**Fase 1 — Domenedybde og kildeintegritet (2-3 sesjoner). Avhenger av: Fase 0.**
Tertial-korreksjon + rasjonale-dokument; posisjoner OKR som lag oppå MRS + tillitsreform-seksjon + målforskyvning-antipattern; metodikk-presisjoner (Doerr-aggregering, Intel, Locke, Kleingeld-journal); «Kvartalsreview»→«Syklusreview»; reelle norske caser (Digdir produktorganisering, NRK/Ruter, Knowit) med vendor-merking.
*Voyage:* `/trekresearch` med Tavily/WebFetch triangulerer DFØ-currency, tillitsreform og reelle caser mot primærkilder → `/trekplan``/trekexecute``/trekreview`.
**Fase 2 — UX-koherens og kommando-/agent-modning (1-2 sesjoner). Avhenger av: Fase 0.**
Kontekstbevissthet i møter.md; gap/governance-disambiguering + avdupliser tabell; negative triggere + Task-spawn-fil-lesing i agenter; wire inn / avskriv 6 usiterte filer; rm -rf-validering (OKR-01) + template-sync.
*Voyage:* `/trekbrief``/trekplan` med Verifisering (grep bekrefter Kontekstbevissthet i alle 10; manuell rm-validering med tom id) → `/trekexecute`.
**Fase 3 — Referansegrad-løft mot ms-ai-architect-paritet (2-3 sesjoner). Avhenger av: Fase 1 + Fase 2.**
Reinstall-overlevende org-katalog; `/okr:help`; ankrede rubrikker + hook-fixtures/smoke-test; `/okr:export` PDF; bredere offentlig-metrikkbibliotek (eForvaltningsindeks, WCAG, klagefrist, eFormidling) + etat/kommune-differensiering; vurder `/okr:freshen-references`.
*Voyage:* flersesjons `/trekplan` med `.session-state`, `/trekexecute` + `/trekcontinue` mellom sesjoner, `/trekresearch` for metrikkbiblioteket, avslutt med `/trekreview` mot brief-kriteriene.
---
## Vedlegg
### A. Konsolidert «find-and-replace»-retteliste (Fase 0)
| Fil:linje | Feil | Rett til |
|-----------|------|----------|
| okr-offentlig-governance.md:346, :422 | saksbehandlingbehandling | saksbehandling |
| okr-offentlig-governance.md:319, :413; okr-framework.md:63 | saksbehandlingtjenester | saksbehandlingstjenester |
| okr-offentlig-governance.md:71; okr-framework.md:101 | saksbehandlingklager | saksbehandlingsklager |
| okr-offentlig-governance.md:77 | saksbehandlingsaker | saksbehandlingssaker |
| okr-offentlig-governance.md:340 | fornye saksbehandling | koherent domene (f.eks. fornye førerkort) |
| okr-offentlig-governance.md:326 | saksbehandling-søknader | digitale søknader |
| okr-examples.md:60, :64 | vevedlikehold | veivedlikehold |
| okr-examples.md:25; okr-framework.md:245; skriv.md:61 | saksbehandlingsøknader | domenenøytralt/koherent |
### B. Scoring-retteliste (Fase 0)
Mål-formulering overalt: **«0.7 = forventet for aspirational; 0.6-0.7 = sweet spot (Google/Doerr)»**.
Filer: SKILL.md:47/107, okr-framework.md:104/262/268, okr-offentlig-governance.md:73, sporing.md:33, fremdriftssporer-agent.md:44, okr-cheatsheet.md:48. Fjern 0.7-0.9 og 70-80 % helt.
### C. Encoding-retteliste (Fase 0)
Full UTF-8-regenerering: `individual-vs-team-okr.md`, `commands/oppsett.md`. Streng-retting: `coaching-hook.mjs:100/115/124/125`.
### D. Pilot-nits (status)
- OKR-01 (rm -rf ugardert): **bekreftet**, planlagt i Fase 2.
- OKR-02 (Linear-nøkler i okr.local.md): utenfor denne 360-ens scope (sikkerhet), men SECURITY.md bør samtidig rettes for GitHub→Forgejo-feilen i Fase 0.
### E. Manglende triangulering
Gemini Deep Research-bridgen var nede (deprecated Interactions API SDK). Independent triangulering ble ikke levert; faktapåstander her er triangulert via direkte filsjekk + web-researchere i stedet. Operatør bør oppgradere gemini-mcp-SDK før neste runde.
### F. Veikart-kritikk (adversariell plan-critic)
Veikartet over (syntese-versjon) ble stress-testet av en plan-critic. Materielle korrigeringer — alle innarbeidet i `docs/plan-referansegrad-2026-06-23.md`:
| # | Kritikk | Korrigering i planen |
|---|---------|----------------------|
| 1 | DDT-saneringen scopet til 2 sites; reelt **61 forekomster i 14 filer**. «Splitt i reelle etater» er en flersesjons innholds-omskriving, ikke find-replace. | Beslutningsport før Fase 0: (a) merk fiktiv [mekanisk, default] vs (b) egen dedikert Fase 0b. Avgjøres i `/trekbrief`. |
| 2 | Scoring-lista ufullstendig og delvis feilrettet: `okr-sources.md:44/115` ER kanon 0.6-0.7 (skal **bevares**, ikke slettes); `analyse.md:138-139` + `styringsrådgiver:90` manglet. | Re-derivér med grep; KEEP-merk kanon-kilden; avklar at analyse.md trolig er tverr-syklus modenhet (annen akse, utenfor scope). |
| 3 | «Full UTF-8-regenerering» underspesifisert: filene har 0 å **og** 0 ø/æ — ingen ren kilde. Det er manuell omskriving, og `å>0`-grep beviser ikke ø/æ. | Omformulert til manuell norsk omskriving + spellcheck-verifisering (`aspell -l nb`/vokabular-diff). Circuit-breaker. |
| 4 | Versjon-bump kun nevnt i Fase 0; Fase 1-3 sender også brukervendte endringer (polyrepo-regel = ny tag hver gang). | Versjon-per-fase: 1.3.1 / 1.4.0 / 1.5.0; tag + katalog-`ref` som avsluttende steg i HVER fase. |
| 5 | Fase 1 og Fase 2 deler `møter.md` + `møtefasilitator-agent.md` → kan ikke kjøre parallelt. | Serialisér Fase 1 før Fase 2; eksplisitt fil-eierskap. |
| 6 | Fase 3 flytter org-kontekst-sti **etter** at Fase 2 wirer agenter til gammel sti → dobbeltarbeid. | Sti-beslutning **før** Fase 2; agenter peker på endelig sti én gang; migrasjon m/ fallback i Fase 3. |
| 7 | OKR-01 rm -rf (data-tap-risiko) parkert i Fase 2. | Flyttet til Fase 0 som circuit-breaker (høyest risiko først). |
| 8 | ASCII-kommandonavn også i `oppsett.md:246/478`, ikke bare README/hook. | Eget grep-mål `'/okr:moter\|/okr:innforing\|oppstartsmate'` over alle filer. |
| 9 | Ingen On-failure/Checkpoint/circuit-breaker på destruktive steg. | Krav i §1/§7: hvert `/trekplan`-steg bærer begge; destruktive = halt-on-failure. |
| 10 | CLAUDE.md «(15)→(16)» blir stale hvis Fase 2 avskriver filer. | Tall-fiks utsatt til etter Fase 2s avskriv-beslutning. |

View file

@ -0,0 +1,155 @@
# 360-re-evaluering: OKR Offentlig Sektor-plugin (v1.6.0)
**Dato:** 2026-06-26
**Evaluator:** Lead reviewer (syntese av 5 Opus-agenter: 2 KB-ref-scorere, 2 skill-evaluatorer, 1 360-delta-verifikator)
**Mål:** Verifisere at den forrige 360-en (v1.3.0, karakter C+) er innfridd, score begge skills + alle 16 KB-referansefiler mot ankret rubrikk på dagens innhold, og dekke det som aldri har vært evaluert.
**Avgrensning:** Bygger på `docs/evaluering-360-2026-06-23.md` (v1.3.0) + `docs/plan-referansegrad-2026-06-23.md`. Sikkerhet/config/struktur ble dekket av piloten (`docs/review-2026-06-20.md`) og gjentas ikke. Alle bærende funn er verifisert direkte mot fil (file:line) — se Verifiseringslogg.
---
## Sammendrag
Pluginet er løftet **C+ → A**. Alle KRITISKE og HØYE funn fra v1.3.0-360-en er bevist lukket i fil (DDT-fabrikasjon, scoringsterskel-selvmotsigelse, UTF-8-korrupsjon, find-replace-vrøvl, døde lenker, dokument-motsigelser), og hele referansegrad-gapet mot referanse-pluginen `ms-ai-architect` er levert (reinstall-overlevende org-katalog, `/okr:help`, ankrede rubrikker, hook-fixtures/55 grønne tester, PDF-eksport, org-lesende agenter). Pluginet er ikke lenger «utrygt å dele eksternt».
Det som holder igjen det siste steget til **A** er **én regresjon i nøyaktig samme klasse som den opprinnelige showstopperen**: den nye Fase 3-fila `okr-quality-rubrics.md` (den ankrede kvalitetsrubrikken) er **fullstendig ASCII-strippet** (0 å/ø/æ) — og den lastes aktivt av `/okr:kvalitet`, `kvalitetssjekker`-agenten, `/okr:freshen-references` og `SKILL.md`, så feilen forplanter seg til brukervendt kvalitetsscoring. Samme tekstkvalitetssykdom (strippet norsk) finnes i flaggskip-eksempelet i `SKILL.md` — det første konkrete eksemplet en bruker ser.
To skills ble evaluert. Hovedskillen `okr-offentlig-sektor`: **B+** (strukturelt moden, alle credibility-funn lukket, holdt under A av den strippede rubrikken + SKILL-eksempelet). Den nye `okr-second-brain-search` (førstegangs-evaluering, fantes ikke i juni-23-360-en): **B** — OKF-lagbeskrivelsen er verifisert korrekt i hver detalj, men har én funksjonell defekt (`~`-glob søker aldri hjemme-roten) og ett verifiseringsplikt-brudd (uverifisert `kb-search`-sitat motsagt av teamets egen funn-doc).
De 16 KB-referansefilene scorer i snitt ~3.6/5. Sterkest: `okr-offentlig-governance.md` og `metrics-library.md` (begge 37/40 — provenans- og aktualitets-forbilledlige). Svakest: `okr-oboard-guide.md` (22/40, + regnefeil i kanonisk eksempel) og `individual-vs-team-okr.md` (22/40, usitert). Systemisk svakhet på tvers: manglende interne **kryssreferanser** (5 av 16 filer isolert) og manglende **«Sist oppdatert»-markører** (kapper Aktualitet-scoren uavhengig av innhold).
**Samlet karakter: A** (referansegrad innen rekkevidde; én fokusert saneringsrunde lukker gapet til A).
---
## Del 1 — Delta vs 360-en (v1.3.0 → v1.6.0)
Hvert 360-funn er åpnet mot dagens fil. Alle KRITISK/HØY/MEDIUM er **FIKSET** med følgende unntak:
| 360-funn | Status | Bevis |
|----------|--------|-------|
| Semantisk inkoherens: digital-etat «DDT» eier vei/trafikk-KR | **DELVIS (by design)** | Ikke splittet i reelle etater; løst via plan-default — konsekvent «(fiktiv eksempeletat)»-merking. Inkoherensen består som *erklært fiksjon*, ikke faktapåstand. `okr-framework.md:59`, `okr-integrations.md:392`, `okr-offentlig-governance.md` gjennomgående. |
| Ankrede rubrikker (Fase 3 SHOULD) | **DELVIS** | `okr-quality-rubrics.md` finnes (5 ankere/dim) MEN er ASCII-strippet — se Del 4 #1. |
| Uciterte statistikker (39/43/30-45 %) | **DELVIS** | Nå hedget «Industri-rapportert (ikke fagfellevurdert)» (`okr-implementation.md:7-10`, `okr-antipatterns.md:3`), men fortsatt uten *navngitt* kilde. |
Alt øvrig — scoringsterskel (én kanon `0.7 forventet / 0.6-0.7 sweet spot`), Intel/Locke/Kleingeld-rettelser, committed-vs-aspirational-aggregering, DDT ut av README, tertial-presisjon, dokumenttittel, tillitsreform-seksjon, målforskyvning-antipattern, UTF-8 i `individual-vs-team-okr.md`/`oppsett.md`/`coaching-hook.mjs`, alle find-replace-artefakter (0 gjenstående), Læring-regex, rm-rf-garde (OKR-01), reinstall-overlevende org-katalog, CONTRIBUTING↔GOVERNANCE, SECURITY→Forgejo, ASCII-kommandonavn, CLAUDE.md-count, SKILL-versjonssynk, 6 usiterte filer wiret inn, emne-guard, `/okr:help`, PDF-eksport, hook-fixtures (55/55 grønn), negative agent-triggere, org-lesende agenter — **bekreftet FIKSET**.
### Oppdatert scorecard (9 dimensjoner)
| Dimensjon | v1.3.0 | v1.6.0 | Begrunnelse |
|-----------|--------|--------|-------------|
| Domenekorrekthet/metodikk | C+ | **A** | Én kanonisk scoringsterskel; Intel/Locke/Kleingeld rettet; aggregering korrigert. |
| Norsk forvaltningstilpasning | C | **A** | DDT erklært fiktiv; tertial-regime presist; tillitsreform + målforskyvning inne. Trekk: digital-etat-eier-vei består som erklært fiksjon. |
| Innholds- og språkkvalitet | D | **B** | De 2 navngitte filene + hook fullt rettet; 0 find-replace. Holdt nede av ny ASCII-strippet `okr-quality-rubrics.md`. |
| Kommandoer | B | **A** | Kontekstbevissthet komplett; gap/governance disambiguert; Syklusreview; `/okr:help`. |
| Agenter | B | **A** | Negative triggere + org-kontekst-lesing i alle 7. |
| Hooks/state/arkitektur | B+ | **A** | Læring-regex + rm-rf-garde + reinstall-overlevende org + emne-guard + 55 grønne tester. |
| Dokumentasjon/markedsplass | C | **A** | DDT ut av README; ASCII-kommandonavn ut; CONTRIBUTING/SECURITY/count rettet. |
| Modenhet vs ms-ai-architect | C+ | **A** | Alle MUST/SHOULD-gap levert. Rubrikk-kvalitet svekket av encoding. |
| Plugin-craft | B | **A** | SKILL-versjon synket; 6 filer wiret inn; emne-guard. SKILL.md bevisst lean. |
---
## Del 2 — Skill-evalueringer
### `okr-offentlig-sektor` — **B+**
| # | Dimensjon | Score | Bevis |
|---|-----------|-------|-------|
| 1 | Description/triggering | 4 | Intent-tett (`SKILL.md:3-4`); trekk: ingen negative triggere, overlapp med søsterskill på «tildelingsbrev». |
| 2 | Progressive disclosure | 4 | 155 linjer, lean; 17 ref eksternalisert. Trekk: flat ref-katalog vs ms-ai nestede. |
| 3 | Struktur & navigerbarhet | 5 | Nummererte Core Tasks 1-10, grupperte Resources. |
| 4 | Instruksjonskvalitet | 4 | Konkret mal + scoringsskala; cascade/track noe høynivå. |
| 5 | Dekning/fullstendighet | 5 | Skriv/review/track/cascade/møter/CFR/governance + 17 ref. |
| 6 | Korrekthet/integritet | 4 | Alle 17 ref-filer eksisterer; scoring-kanon konsistent. Trekk: stale telling «19 mistakes» (`SKILL.md:134`) vs 20; strippet norsk i eksempel. |
| 7 | ms-ai-architect-paritet | 4 | Alle 360-gap lukket. Trekk: ms-ai har negativ-scoping + nestet disclosure. |
| 8 | Vedlikeholdbarhet | 3 | Tester/fixtures finnes, men dekker hooks/scripts, ikke skill-*innhold* (innholdsdrift fanges ikke); 4/17 currency-markører. |
**≤2-flagg:** `okr-quality-rubrics.md` ASCII-strippet (kvalitet ≤2, brukervendt via `/okr:kvalitet`); SKILL.md flaggskip-eksempel (`:32-34, :41`) strippet norsk.
### `okr-second-brain-search`**B** (førstegangs-evaluering)
| # | Dimensjon | Score | Bevis |
|---|-----------|-------|-------|
| 1 | Description/triggering | 4 | Possessiv-ankret («våre mål»), eksplisitt carve-out av metodikk (`SKILL.md:30-31`). Trekk: `tildelingsbrev`-overlapp m/ companion; selvmotsigelse om trigger-plassering (`:132-133`). |
| 2 | Progressive disclosure | 4 | Én selvbærende fil, rimelig for retrieval-skill. |
| 3 | Struktur & navigerbarhet | 4 | Rene seksjoner; test-digresjon midt i prosedyren (`:110-112`). |
| 4 | Instruksjonskvalitet | 3 | Rank-tie-break presis, MEN `~`-glob-defekt (se Flagg). |
| 5 | Dekning/fullstendighet | 4 | Begge røtter, index-pekere, type-verdier, edge-cases. Hull: ingen fallback for nivå uten `index.md`. |
| 6 | Korrekthet/integritet | 4 | OKF-lag verifisert eksakt mot impl. (felt-rekkefølge, `okf_version`, index-format). Brudd: uverifisert `kb-search`-sitat. |
| 7 | OKF-arkitektur-konsistens | 5 | Fullt i synk m/ to-bundle-modellen. |
| 8 | Vedlikeholdbarhet | 4 | 2 fixtures + grundig regresjonstest (`okf-retrieval.test.mjs`). |
**Flagg 1 (funksjonell defekt):** `SKILL.md:88-89` instruerer `Glob ... ~/.claude/okr/org/**/*.md`. Glob-verktøyet ekspanderer ikke shell-`~` → hjemme-roten (org-identitet) blir stilltiende usøkt, mot kjernepåstanden «always search both» (`:36`). Hook-en gjør det riktig via `homedir()` (`inject-okr-context.mjs:106`); skillen overfører ikke samme presisjon.
**Flagg 2 (verifiseringsplikt-brudd):** `SKILL.md:84` hevder OKF har en `kb-search`-triad (`list_contents`/`read_file`/`search_content`). Teamets egen funn-doc motsier dette: `docs/innboks-ingestion-funn-2026-06.md:78-80` — «kb-search/SKILL.md ble IKKE funnet … Skillene heter `fileset-source` og `knowledge_catalog_discovery_agent`». Funn-doc-en (2026-06-26) kom etter SKILL-en; korrigeringen nådde aldri tilbake.
---
## Del 3 — KB-referansefiler (16 domene-filer)
Rubrikk: 4 dekning- + 4 kvalitetsdimensjoner (`commands/freshen-references.md`), 5 ankere hver, 1-5. UTF-8 bekreftet ren i alle 16 (v1.3.0-korrupsjonen i `individual-vs-team-okr.md` er fikset).
| Rang | Fil | Sum/40 | ≤2-flagg |
|------|-----|--------|----------|
| 1 | `okr-offentlig-governance.md` | 37 | — |
| 1 | `metrics-library.md` | 37 | — |
| 3 | `okr-sources.md` | 34 | Kryssref |
| 4 | `dfo-okr-mapping.md` | 32 | — |
| 4 | `okr-framework.md` | 32 | — |
| 6 | `okr-antipatterns.md` | 31 | — |
| 6 | `okr-integrations.md` | 31 | — |
| 8 | `cfr-framework.md` | 31 | Kryssref (2) |
| 9 | `okr-implementation.md` | 29 | Kryssref |
| 10 | `okr-examples.md` | 28 | Kryssref (1) |
| 11 | `okr-arshjul.md` | 28 | Provenans |
| 12 | `okr-cheatsheet.md` | 26 | Off.-tilpasning, Kryssref |
| 13 | `okr-calculator.md` | 26 | Kryssref (1), Provenans |
| 14 | `meeting-guides.md` | 25 | Kryssref (1), Provenans |
| 15 | `individual-vs-team-okr.md` | 22 | Off.-tilpasning, Eksempel, **Provenans (1)** |
| 15 | `okr-oboard-guide.md` | 22 | Off.-tilpasning, Kryssref (1), Provenans |
### Systemiske mønstre (på tvers)
- **Kryssreferanser (størst):** 5 filer er reelt isolert fra KB-en (`okr-examples.md`=1, `okr-oboard-guide.md`=1, `meeting-guides.md`=1, `okr-calculator.md`=1, `cfr-framework.md`=2). How-to/mal-filer slutter på «Tips» uten Ressurser-seksjon. `okr-offentlig-governance.md`/`okr-integrations.md` viser standarden.
- **«Sist oppdatert»-markører mangler** på de fleste filer → kapper Aktualitet uavhengig av innhold. Billig løft.
- **Provenans-gradient:** `metrics-library.md`/`okr-sources.md`/`dfo-okr-mapping.md` er forbilledlige (per-påstand provenanstagging, DOI, «Ikke verifisert»-tagger). How-to-filene mangler kilde-seksjon helt.
- **DDT-inkoherens** forplanter seg til flere ref-filer (se Del 1).
---
## Del 4 — Gjenstående funn (prioritert — kandidat for 1.6.1-sanering)
1. **[HØY — ny regresjon] `okr-quality-rubrics.md` fullstendig ASCII-strippet.** 0 å/ø/æ: «nivaabeskrivelse», «paa tvers», «maalbart», «ambisioest», «maaloppnaaelse», «aa baere». Eksakt samme sykdom 360-en kalte KRITISK — i en ny Fase 3-fil som lastes brukervendt av `/okr:kvalitet`, `kvalitetssjekker`, `freshen-references`, `SKILL.md`. *Fix:* manuell norsk omskriving + spellcheck (samme oppskrift som Fase 0).
2. **[MEDIUM] `SKILL.md` flaggskip-eksempel strippet norsk** (`:32-34`, `:41`): «per ar», «hoyrisiko», «far», «sporreundersokelse», «Gjennomfore 5 moter». Første konkrete eksempel en bruker ser; undergraver «norsk forvaltning»-troverdighet.
3. **[MEDIUM — funksjonell] `okr-second-brain-search` `~`-glob** (`SKILL.md:88-89`): instruér eksplisitt absolutt hjemme-sti før Glob, ellers søkes aldri org-roten.
4. **[MEDIUM — integritet] `okr-second-brain-search` `kb-search`-sitat** (`SKILL.md:84`): rett til verifisert OKF-virkelighet (`fileset-source` / `discovery`-mønster) per `innboks-ingestion-funn-2026-06.md`.
5. **[MEDIUM] `okr-oboard-guide.md:95` regnefeil:** KR1 32 ulykker (45→25) = 65 %, ikke «Progress: 52 %» (mot filens egen formel `:47`). + plassholderlenker `:124-125`. Svakest fil — vurder om vendor-spesifikk fil hører i KB.
6. **[LAV-MEDIUM, systemisk] Kryssreferanser:** legg «Relaterte filer»-seksjon i de 5 isolerte filene.
7. **[LAV, systemisk] «Sist oppdatert»-markører** på ref-filer som mangler dem.
8. **[LAV] `individual-vs-team-okr.md`:** kilde Spotify-2013-påstand (`:5`), norsk off.-vinkling, ikke-privat-sektor-eksempler.
9. **[LAV] `okr-implementation.md` statistikker:** navngi kilde bak 39/43/30-45 % eller dropp.
10. **[TRIVIELT] Språk-nits:** `meeting-guides.md:82` «schuld»→«skyld», `:215` «Næste»→«Neste»; `dfo-okr-mapping.md:10` «resultmål»→«resultatmål»; `okr-arshjul.md:97` uverifisert «15. oktober»-budsjettdato; `SKILL.md:134` «19»→«20 mistakes».
---
## Verifiseringslogg
Bærende funn sjekket direkte mot fil (oppfyller verifiseringsplikten):
| Påstand | Verdikt | Bevis |
|---------|---------|-------|
| `okr-quality-rubrics.md` er ASCII-strippet | **Bekreftet** | `grep -o '[åøæ]' = 0`; «paa tvers» `:3`, «ambisioest» `:43`, «aa baere» `:5` |
| `SKILL.md`-eksempel strippet norsk | **Bekreftet** | `:32` «per ar», `:33` «hoyrisiko … far», `:34` «sporreundersokelse», `:41` «Gjennomfore … moter» |
| `okr-second-brain-search` bruker literal `~` i Glob | **Bekreftet** | `SKILL.md:89` `~/.claude/okr/org/**/*.md` |
| `kb-search`-triaden finnes i OKF | **Avkreftet** | `innboks-ingestion-funn-2026-06.md:78-80` — heter `fileset-source`/`knowledge_catalog_discovery_agent` |
| `okr-oboard-guide.md` Progress-% | **Feil i fil** | `:95` «52 %»; korrekt (4532)/(4525)=65 % per formel `:47` |
| DDT fjernet fra README | **Bekreftet** | `grep DDT README.md = 0`; README:23 lister kun NAV/FINN.no |
| Scoringsterskel konsistent | **Bekreftet** | `0.7-0.9`/`70-80%`/`0.6-0.8` = 0 treff; kanon `0.7 / 0.6-0.7` på tvers |
| UTF-8 i `individual-vs-team-okr.md` | **Bekreftet fikset** | 13 å / 6 ø / 1 æ, 0 strippede ord |
| Find-replace-artefakter | **Bekreftet 0** | hele vedlegg-A-mønsteret = 0 treff |
| Testsuite | **Bekreftet** | `node --test tests/*.test.mjs` → 55 passerer, 0 feiler |
| Reinstall-overlevende org-katalog | **Bekreftet** | `~/.claude/okr/org/profil.md` via `write-org-profile.mjs`, lest av `inject-okr-context.mjs`, testet |
### Metode
5 Opus-agenter (xhigh): Ref-scorer A (filer 1-8) + B (9-16) mot freshen-rubrikken; skill-evaluator × 2 (én per skill); 360-delta-verifikator mot alle v1.3.0-funn. Lead reviewer syntetiserte + ground-truth-verifiserte de bærende funnene. Gemini-triangulering ikke kjørt (bridge nede — deprecated SDK, jf. memory `gemini-mcp-sdk-outage`).

View file

@ -0,0 +1,109 @@
# Innboks-ingestion — funn-grunnlag (2026-06-26)
> Self-bearing grunnlag for den **køede** innboks-ingestion-oppgaven (start **etter**
> OKF-fasen er stengt; egen minor-bump). Speiler rollen `okf-second-brain-note-2026-06.md`
> hadde for OKF-fasen. Alle påstander om Google-repoet er **verifisert mot faktisk lest
> innhold** (kilder i §9) — ikke antakelser.
## 1. Oppgaven
Bruker slipper vilkårlige dokumenter (PDF, Word, tekst, e-post) i en **innboks-mappe**.
okr-pluginen skal oppdage dem, konvertere til markdown, og legge dem inn **OKF-kompatibelt**
i second-brain-treet — med korrekt frontmatter (`type/resource/title/description/tags/timestamp`),
`index.md` per nivå, **og relasjoner/kryss-lenker** mellom konsepter. Original beholdes,
markdown-peker plasseres. Søsken-pluginen `ms-ai-architect` har samme oppgave (delt mønster).
## 2. Hovedkonklusjon
**Det finnes ingen ferdig innboks→OKF-ingestion-løsning med relasjonshåndtering — verken i
`GoogleCloudPlatform/knowledge-catalog` eller i okr-pluginen.** Google gir oss spec +
gjenbrukbare byggeklosser (index-generator, relasjons-mønster, kilde-utvidelsespunkt), men
**ikke** en pipeline fra «ustrukturert innboks» til «ferdig OKF-bundle». Dette blir et reelt
byggeløp.
Rammeavklaring (verifisert): «knowledge-catalog» = Google Cloud **Knowledge Catalog (tidl.
Dataplex)** — metadata for *dataassets*. «OKF» (`okf/`) er et eget, vendor-nøytralt
markdown-format. Beslektede, men distinkte spor i repoet.
## 3. Hva Google-repoet faktisk har
| Komponent | Sti | Hva det er | Innboks-ingestion? |
|---|---|---|---|
| `reference_agent` | `okf/src/reference_agent/` | OKF-**produsent** (CLI: `enrich`/`visualize`). Auto-genererer `index.md` + auto-vever kryss-lenker | **Delvis** — kun fra **BigQuery + web-crawl**, ingen «ingest folder»-kommando |
| `bundle/index.py` (`regenerate_indexes`) | `okf/src/reference_agent/bundle/` | Kildeagnostisk `index.md`-generator per nivå (LLM-synt. mappebeskrivelser) | Gjenbrukbart |
| `sources/base.py` (`Source`-ABC) | `okf/src/reference_agent/sources/` | Utvidelsespunkt for kilder; eneste impl. er `bq` (`_SOURCES = ("bq",)`) | Utvidelsespunkt — ingen fil-/dokumentkilde finnes |
| `fileskb` / `md-fileset` | `samples/enrichment/src/tools/fileskb/`, `toolbox/enrichment/src/tools/md/` | **Read-only** MCP over en markdown-mappe (`fileset-source`-skill) | Nei — retrieval, antar allerede markdown |
| `enrichment` | `samples/enrichment/`, `toolbox/enrichment/` | Beriker **katalog-metadata** for et BQ-datasett, publiserer til Dataplex | Nei — ikke OKF-output |
| `discovery` | `samples/discovery/SKILL.md` | `knowledge_catalog_discovery_agent` — søk/ranking | Nei — ren retrieval |
| `mdcode` | `toolbox/mdcode/` | Spec + bi-direksjonell sync (`kcmd` pull/push) mot Dataplex | Nei — sync, ikke ingestion |
| `okf/SPEC.md` | `okf/` | OKF v0.1 spec (markdown + frontmatter, `index.md`, `log.md`, kryss-lenker, citations) | Spec |
## 4. Relasjons-modellen i OKF (verifisert, `okf/SPEC.md` §5, §8)
- Relasjoner = **vanlige markdown-lenker i body**, IKKE et frontmatter-felt. Anbefalt
bundle-relativ form (`/tables/customers.md`). En lenke er en *uttypet, rettet* relasjon;
*hvilken* relasjon (join/parent/depends-on) bæres av prosaen rundt lenken.
- Citations (§8) = lenker til eksterne kilder. Vieweren beregner «Cited by»-backlinks som
reverserte kanter.
- **Auto-etablering finnes** i `reference_agent` (`prompts/reference_instruction.md` +
`web_ingestion_instruction.md`: mynter `references/joins/<a>__<b>.md`, `references/metrics/<slug>.md`,
legger til `# Joins`/`# Metrics`-seksjoner) — men logikken er **skreddersydd for BQ-joins/metrikker
+ web-dokumentasjon**, ikke et vilkårlig dokumentkorpus.
- Kontrast: `mdcode` har et eget `EntryLink`-konsept i YAML for *katalog*-relasjoner — separat fra OKF.
## 5. Gap — hva vi må bygge selv
1. **Dokument-konvertering** (PDF/Word/e-post/tekst → markdown). Finnes ikke i repoet. De
eneste fil-verktøyene (`fileskb`/`md-fileset`) antar markdown og er read-only.
2. **Konsept-ekstraksjon + frontmatter-tilordning** for vilkårlige dokumenter (splitte til
«konsepter», sette `type/resource/title/description/tags/timestamp`). reference_agent gjør
dette kun for BQ-tabeller.
3. **Generalisert relasjons-oppdagelse** — dagens relasjons-prompt er BQ/web-spesifikk og må
generaliseres til vilkårlige dokumenter.
4. **Innboks-orkestrering** — oppdage nye filer, beholde original + markdown-peker, idempotent
re-kjøring, plassering i riktig nivå av treet.
## 6. Gjenbrukbart (lener oss på, bygger ikke fra null)
- **Index-generering:** Vi har allerede `scripts/okf-index.mjs` (Step 6, OKF-fasen) som speiler
Googles `regenerate_indexes` — kildeagnostisk, idempotent. Innboks-pipelinen kan kalle den
etter skriv (samme mønster som `oppsett.md`-tre-skriverne i Step 8).
- **Frontmatter:** `lib/frontmatter.mjs` (parse/skrive) + `scripts/okf-check.mjs` (validering)
fra OKF-fasen dekker skrive- og verifiserings-siden.
- **Kilde-mønster:** Googles `Source`-ABC viser et rent adapter-mønster — en «innboks-kilde»
som lister konsepter fra en mappe kunne mate eksisterende skrive-/index-/kryss-lenke-maskineri.
## 7. Forbehold / ikke verifisert (verifiseringsplikt)
- **`kb-search/SKILL.md` ble IKKE funnet** i repoet, til tross for at våre egne planer
(`plan.md:26`, `brief.md:168`) refererer den. Skillene som finnes heter `fileset-source`
(retrieval) og `knowledge_catalog_discovery_agent` (søk). Mulig repoet har endret seg, eller
referansen var unøyaktig. **Ikke kritisk for OKF-fasen** (vår retrieval-SKILL er ferdig og
testet), men korrigerer en navne-antakelse for innboks-løpet.
- Ikke fullt lest: `okf/src/reference_agent/{agent.py, runner.py, tools/*.py}` og
`toolbox/enrichment/src/agent/*`. Kilde-/ingestion-konklusjonen er likevel entydig fra
`cli.py` (`_SOURCES = ("bq",)`), `sources/`-innholdet, og enrichment-README/`enrich.py`.
## 8. Sekvens / scope
- **Start: etter OKF-fasen er stengt** (Session 5 release → 1.6.0). Scope-guard: innboks bygger
på OKF-leveransen og er bevisst utsatt — **ikke start før operatør sier fra**.
- Eget Voyage-løp (brief → plan → execute), egen **minor-bump** (1.7.0-kandidat).
- Delt mønster med `ms-ai-architect` — vurder felles abstraksjon før dobbel-implementasjon.
## 9. Kilder (faktisk lest, etterprøvbart)
GitHub API-mappelistinger: repo-rot, `okf/`, `samples/`, `toolbox/`,
`okf/src/reference_agent/{prompts,tools,sources,bundle}`, `samples/enrichment/src/{enrichment,tools}`,
`toolbox/enrichment/src/{agent,tools}`, `toolbox/mdcode/{docs,demo}`.
Rå filer (`raw.githubusercontent.com/GoogleCloudPlatform/knowledge-catalog/main/`):
`README.md`, `samples/README.md`, `okf/README.md`, `okf/SPEC.md`,
`okf/src/reference_agent/cli.py`, `.../sources/base.py`, `.../prompts/reference_instruction.md`,
`.../prompts/web_ingestion_instruction.md`, `.../bundle/synthesizer.py`, `.../bundle/index.py`,
`samples/enrichment/README.md`, `samples/enrichment/src/enrichment/enrich.py`,
`samples/enrichment/src/tools/fileskb/README.md`, `toolbox/enrichment/README.md`,
`samples/discovery/SKILL.md`, `toolbox/mdcode/docs/spec.md`, `toolbox/mdcode/docs/concept.md`,
`toolbox/mdcode/README.md`.
Undersøkelse utført 2026-06-26 (Opus-subagent, web-verifisert).

View file

@ -0,0 +1,106 @@
# Innboks-ingestion — veivalg og posisjon (2026-06-26)
> **Operatør-låst retning for okr-pluginens innboks-ingestion.** Avstemmer okr-STATE mot den
> cross-cutting convergence-briefen (`linkedin-studio/docs/okf-convergence-brief.md`). Self-bearing —
> leses ved oppstart av ingestion-løpet. **Teknisk grunnlag (web-verifisert):**
> `docs/innboks-ingestion-funn-2026-06.md`. Status-of-play: `STATE.md`.
> **RATIFISERT 2026-06-29 (handoff `catalog/docs/okf-second-brain/handoff-2026-06-29.md` §3).** Den
> delte konvensjonen er nå kanonisert i `catalog/docs/okf-second-brain/spec.md` v0.1 (single source of
> truth, katalog-eid). Dette dokumentet **refererer** spec-en — det redefinerer den ikke. Innboks-løpet
> bygger mot spec §3 (minimal kontrakt = gulv, ikke tak) + §5 (extension keys). okr-bekreftelser (§4):
> `okf-check.mjs`-semantikken står som referansekontrakt (spec §7); okr bruker `resource` (ikke
> `source`); ingen feltgap (profil/config-nøkler = extension keys). Premiss-korreksjonene i §2 under er
> nå kanonisert i spec §9.
## 1. Låste beslutninger (operatør, 2026-06-26)
1. **Innboks-ingestion SKAL bygges for okr.** Ikke valgfritt, ikke «defer». Krever **grundig
planlegging** — eget Voyage-løp (`/trekbrief``/trekplan` → execute), egen minor-bump
(1.7.0-kandidat). Start når operatør sier fra (scope-guard).
2. **linkedin-studio er IKKE en mal for okr.** Deres second-brain har en *annen tilnærming*
(provenans-vektet læring, episodisk/semantisk split, evidens-terskel-promotering) som vi
**ikke kopierer**. okr bygger sin egen sti.
3. **Delt cross-repo OKF-skill: ikke nå.** Stage 3 (betinget) i convergence-briefen — utsatt.
okr-ingestion er okr-eid og ikke gated på en delt skill.
## 2. Avstemming mot convergence-briefen (hva gjelder for okr)
Convergence-briefen er linkedin-studio-sentrert og cross-cutting. For **okr** skiller vi
verifiserte tekniske fakta (beholdes) fra strategisk framing (avvises):
**BEHOLD — verifiserte tekniske premisser (gjelder uansett strategi):**
- Google `knowledge-catalog` har **ingen** innboks→OKF-pipeline (funn §2, brief §2). Vi får spec +
byggeklosser, ikke en ferdig løype.
- `mdcode` er **ikke** et OKF-verktøy — det er Dataplex git-sync med et *annet* frontmatter-schema
(`id`/`resource.name`/`createTime`/`links`). **Ikke** planlegg `kcmd` til å emittere/synke OKF.
- Googles `reference_agent` ER en OKF-produsent, men leser **BigQuery + seed-URLer**, ikke en
dokumentmappe, og er Gemini/GCP-bundet. Gjenbrukbare (GCP-frie) deler: SPEC, emit/serialize/
validate-kjernen, `index.md`-syntese.
- **Dokument-klassifisering/-konvertering av vilkårlige filer: Google gir INGENTING** — 100 % bygg
selv.
- OKF-relasjoner = **vanlige markdown-lenker i body** (ikke frontmatter-felt); konsumenter MÅ
tolerere brutte lenker (funn §4, brief §5).
- **OKF v0.1-spec finnes**`okf/SPEC.md` (distinkt fra `mdcode`s «Metadata as Code» for Dataplex).
Kun `type` påkrevd; `index.md` reservert (ingen frontmatter); `okf_version` i rot-`index.md`.
*(NB: okr 1.6.0 pinnet bevisst til «Documents/kb Layout» og kalte det IKKE en formell standard —
ingestion-løpet bør re-verifisere `okf/SPEC.md` ved brief-tid og avgjøre hvor tett vi konformer.)*
**AVVIS for okr — strategisk framing som IKKE styrer okr-ingestion:**
- «Defer auto-classify/convert; build only on demonstrated need» → **avvist** (beslutning 1: skal bygges).
- «linkedin-studio is the reference design; siblings rise to it» → **avvist for okr** (beslutning 2).
- «Inbox forblir en manuell drop-zone uten auto-klassifiserer» → det var linkedin-studios stance;
okr-målet er det motsatte: **auto-oppdage + konvertere + tilordne OKF**.
## 3. Hva som faktisk skal bygges (gap, fra funn-grunnlaget §5)
1. **Dokument-konvertering** — PDF/Word/e-post/tekst → markdown. Finnes ikke i Google-repoet
(`fileskb`/`md-fileset` antar markdown og er read-only).
2. **Konsept-ekstraksjon + frontmatter-tilordning** — splitte vilkårlige dokumenter til «konsepter»,
sette `type`/`resource`/`title`/`description`/`tags`/`timestamp`.
3. **Generalisert relasjons-oppdagelse** — Googles relasjons-prompt er BQ-joins/web-spesifikk; må
generaliseres til vilkårlig dokumentkorpus (relasjoner som markdown-lenker i body).
4. **Innboks-orkestrering** — oppdage nye filer, **beholde original + plassere markdown-peker**,
idempotent re-kjøring, plassering i riktig nivå av treet, regenerere berørt `index.md`.
## 4. Gjenbrukbart fra OKF-fasen (1.6.0 — bygger ikke fra null)
- `scripts/okf-index.mjs` — kildeagnostisk, idempotent `index.md`-regen (speiler Googles
`regenerate_indexes`). Innboks-pipelinen kaller den etter skriv (som tre-skriverne i `oppsett.md`).
- `lib/frontmatter.mjs` (parse/skrive) + `scripts/okf-check.mjs` (validering) dekker skrive- og
verifiserings-siden.
- Googles `Source`-ABC viser et rent adapter-mønster — en «innboks-kilde» som lister konsepter fra en
mappe kan mate eksisterende skrive-/index-/kryss-lenke-maskineri.
## 5. Åpne spørsmål til `/trekbrief` (avgjøres da, ikke nå)
- **Konverterings-motor:** okr er i dag **zero-dependency Node ESM** (kun `node:`-builtins). PDF/Word→md
krever realistisk enten et eksternt verktøy (pandoc/libreoffice/markitdown) eller en avhengighet —
**bryter zero-dep-invarianten**. Dette er et reelt arkitekturvalg (dokumentert prerequisite à la
`export-pdf.py`/weasyprint? egen avhengighet? hvilke formater i v1?).
- **Konsept-ekstraksjon:** LLM-drevet (Claude i kommando) vs. heuristisk splitting — og hvor deterministisk.
- **Relasjons-oppdagelse:** hvor generalisert; hvordan holde testbart.
- **Innboks-plassering:** prosjekt-`.claude/okr/innboks/`? home? begge røtter? Original-bevaring + peker-layout.
- **Mål-format:** full OKF v0.1 (`okf/SPEC.md`) vs. dagens lettere «OKF-kompatible form» okr emitterer.
- **Idempotens + sikkerhet:** re-kjøring uten dubletter; ikke ødelegge brukerens originaler.
## 6. Forhold til søsken-pluginer
- **Delings-scope (operatør-låst 2026-06-29):** en eventuell delt Stage-3-skill deles mellom
**okr + ms-ai-architect****IKKE linkedin-studio**. linkedin-studio har sin egen mekanisme
(rikere ikke-OKF-brain, reference-design — «not levelled down to bare OKF», brief §1) og leveres
ikke ned til en delt skill. Låsen endrer kun *hvem* som deler, ikke *når*: Stage 3 er fortsatt
betinget på Stage-2-måling og utsatt.
- `ms-ai-architect` har samme oppgave (designet, ikke bygget). **Vurder felles abstraksjon før
dobbel-implementasjon** — men felles ≠ kopiere linkedin-studio. Eventuell deling skjer på okr-egne
premisser.
- En delt OKF-**spec** (convergence-briefens Stage 1, katalog-nivå) er grei interop og ikke i konflikt
med dette løpet; okr-ingestion er ikke blokkert på den.
## 7. Kilder
- **Delt konvensjon (single source of truth):** `catalog/docs/okf-second-brain/spec.md` v0.1 + koordinerings-`log.md` + `handoff-2026-06-29.md` (samme mappe, katalog-repo).
- `docs/innboks-ingestion-funn-2026-06.md` — web-verifisert Google-repo-analyse (kilder i §9 der).
- `linkedin-studio/docs/okf-convergence-brief.md` — cross-cutting convergence (avstemt over; nå foldet inn i spec-en).
- OKF v0.1: `github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md`.
- Leveranse vi bygger på: okr 1.6.0 (`scripts/okf-*`, `lib/frontmatter.mjs`, skill `okr-second-brain-search`).

View file

@ -0,0 +1,57 @@
# Retningsnotat — Google OKF for brukerens «second brain» (LLM-wiki) i okr-pluginen
_Notert 2026-06-26. **Fremtidig initiativ — IKKE implementer før plan er laget og godkjent.** Dette notatet er grunnlagsmateriale for neste sesjons `/trekbrief``/trekplan`. Bygger på (a) verifisert lesning av OKF SPEC v0.1 og (b) det ferdige referansedesignet i søsken-pluginen `ms-ai-architect` (`docs/okf-second-brain-brief-2026-06.md`, operatør-bekreftet 2026-06-26). State-of-play i `STATE.md`._
> **Oppdatert 2026-06-29:** konvensjonen er nå kanonisert i `catalog/docs/okf-second-brain/spec.md` (v0.1, single source of truth, katalog-eid). Denne noten er **design-historikk/grunnlag** — referer spec-en for den normative kontrakten, ikke denne.
## Hva oppgaven er (operatør 2026-06-26)
Adopter **Google Open Knowledge Format (OKF)** som formatet brukeren lagrer sin egen kontekst i — en bruker-eid «LLM-wiki» / «second brain» **utenfor** pluginen. Brukeren legger inn så mye org-/strategisk kontekst som ønskes; **deler av den injiseres/hentes smart** når okr-pluginen brukes — både i fri **chat** (plugin lastet) og når `/okr:*`-kommandoer kjøres. Speiler arbeidet som nå designes i `ms-ai-architect`.
## Scope-grense (ufravikelig — speiler architects operatør-bekreftede grense)
- **OKF gjelder KUN brukerens egen kontekst/data** — okr-pluginens «second brain»: i dag org-profilen `~/.claude/okr/org/profil.md` (maskin-global, skrevet av `scripts/write-org-profile.mjs` i Fase 3) + kontekst-treet `.claude/okr/` (`strategisk-kontekst/`, `syklus/[id]/`, `historikk/`, `dokumenter/`) + prosjekt-lokal `.claude/okr.local.md`.
- **OKF gjelder IKKE pluginens 17 domene-referansefiler** (`skills/okr-offentlig-sektor/references/*`). De forblir Claude Code **skill-references** (native, Anthropic-anbefalt progressiv-disclosure-mekanisme). Avgjørende skille (architects begrunnelse, overføres): ikke «er det en LLM-wiki» (begge er det), men **«finnes det allerede en native, anbefalt mekanisme?»** — for skill-refs JA (skills + references + grep), for second brain NEI (bor i dag i ad-hoc `org/`-/`.claude/okr/`-filer uten retrieval-mekanisme under chat). OKF fyller et reelt tomrom kun for second brain.
## Lastemodell-skiftet (kjernen i hvorfor dette er ikke-trivielt)
- **I dag (Fase 3):** `hooks/scripts/inject-okr-context.mjs` (UserPromptSubmit) **for-injiserer** org-profil + syklus-sammendrag når prompten er OKR-relevant (topic-guard regex). Hybrid resolusjon: prosjekt-lokal `.claude/okr.local.md` → hjem `~/.claude/okr/org/profil.md` (mest-spesifikk-vinner).
- **Mål (OKF):** kontekst hentes **smart/selektivt on-demand** — fordi en rik second-brain-wiki er for stor til å for-injisere i sin helhet, og fordi brukeren oftest **chatter** med pluginen lastet uten å kjøre kommandoer. Trenger en **retrieval-skill** (list → search → read) aktiv i fri chat, + en **vedlikeholds-mekanisme** som holder wikien oppdatert når brukeren tilfører kontekst OG når OKF-standarden bumpes.
- For-injeksjons-hooken og on-demand-retrieval er komplementære, ikke konkurrerende: hooken kan beholde et lite «alltid-relevant» kjerne-sammendrag; retrieval-skillen dekker dybden.
## OKF v0.1 — kjernekontrakt (verifisert mot SPEC 2026-06-26)
- Kilde: https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md (v0.1, 12. juni 2026 — «starting point, not finished standard»).
- **Bundle** = katalogtre av markdown-filer, **ett konsept per fil**. **Concept ID** = filsti uten `.md` (f.eks. `strategi/tillitsreform`).
- **Frontmatter:** påkrevd `type` (fri streng, f.eks. «Strategidokument», «Tildelingsbrev», «Org-enhet»). Anbefalt: `title`, `description`, `resource` (kanonisk kilde-URI), `tags`, `timestamp`. Konsumenter MÅ tolerere/bevare ukjente felt og ukjente `type`-verdier.
- **Reserverte filnavn:** `index.md` (katalog-enumerasjon, ingen frontmatter, bærer progressiv disclosure) og `log.md` (endringslogg).
- **Kryss-lenking:** markdown-lenker (bundle-relative `/...` eller relative); relasjonstype utledes av prosa. Konsumenter må **tolerere brutte lenker**.
- **Agent-konsum:** «parseable by agents without bespoke SDKs» — traverser katalogen, parse frontmatter for ruting/filtrering, følg lenker. I Claude Code dekker **Grep/Glob/Read** allerede list/search/read — egen MCP-server er trolig unødvendig; den bærende mekanismen er en SKILL-instruks («søk wikien først, åpne kun relevant»).
## Hvor det plugger inn i okr (forankringspunkter — verifiser i `/trekbrief`)
- **Lese-sømmen finnes:** `inject-okr-context.mjs` + den hybride org-sti-resolusjonen (Fase 3) er allerede on-disk. OKF-retrieval bygger på/ved siden av denne.
- **Skrive-sømmen finnes:** `scripts/write-org-profile.mjs` (atomisk temp+rename) skriver org-profilen i dag — naturlig sted å la onboarding skrive **OKF-konform** frontmatter (`org/*.md` mangler i praksis bare `type:` for å være nær-OKF).
- **Onboarding:** `/okr:oppsett full/mvp` samler org-konteksten — kandidat for å produsere OKF-bundle-struktur i stedet for (eller i tillegg til) dagens flate profil.
## Hva som må bygges (fremtidige faser — ikke nå; planlegges i `/trekplan`)
1. **Frontmatter-/struktur-konvensjon** for okr-second-brain: minimal OKF-konform (`type` påkrevd + `resource`/`timestamp`), `index.md` per nivå. Migrer/utvid dagens `~/.claude/okr/org/` + `.claude/okr/`-tre.
2. **Retrieval-skill** (f.eks. «okr-second-brain-search»): list → search → read over wikien, **aktiv i fri chat** (ikke bare kommandoer). Avgjør: ren SKILL + native Grep/Glob/Read vs. dedikert MCP-server.
3. **Vedlikeholds-mekanisme:** hvordan wikien oppdateres når brukeren tilfører kontekst (onboarding/oppsett skriver OKF-konform), + rutine når OKF-standarden bumpes (`okf_version` i rot-`index.md`).
## Åpne valg (avklares i `/trekbrief` / måling)
- **Retrieval-mekanisme:** SKILL + native Grep/Glob/Read vs. dedikert fileskb-MCP-server. Ekte tvil → kandidat for «implementer begge, mål hvilken gir best kontekst-treff» (operatørs faktabasert-prinsipp). I Claude Code lener det mot ren SKILL (Grep/Glob/Read dekker OKFs list/search/read).
- **Grad av OKF-formalisme:** full v0.1-konformitet vs. «OKF-kompatibel form» (frontmatter + `index.md` uten resten). Lén mot det letteste som gir smart retrieval.
- **For-injeksjon vs. on-demand:** hva hooken beholder som «alltid-på kjerne» vs. hva retrieval-skillen henter ved behov.
- **Oppdaterings-kadens mot standarden:** hvordan fange OKF-versjonsbump uten manuell polling.
## Suksesskriterium (per operatør, overført fra architect-sporet)
Det viktigste er **ikke teknologien/OKF-konformitet i seg selv**, men at second brain blir **så bra som mulig for brukeren** (henter pluginen riktig personlig/org-kontekst i chat og kommandoer?) og at **vedlikeholds-/oppdateringsmekanismene fungerer veldig bra**. Mål mot brukerverdi + vedlikeholds-pålitelighet.
## Voyage-vurdering (operatør ba om dette eksplisitt)
**Anbefaling: JA — bruk Voyage,** med ett forbehold om research-fasen.
- **For:** Hele okr-referansegrad-løftet (Fase 13, nettopp fullført) kjørte ende-til-ende på Voyage (`/trekbrief``/trekresearch``/trekplan``/trekexecute`) over 4+ sesjoner. OKF-integrasjon treffer Voyages kjerneformål: fler-sesjons, krever grundig adversarial-revidert plan, krever deterministisk kontekst mellom sesjoner (STATE-bootstrap), og egner seg for session-dekomponering. `ms-ai-architect` kjører samme mønster (brief → plan → faser).
- **Forbehold (ikke bare bekreftelse):** Research-delen (lese OKF `samples/` + `toolbox/`) er **allerede gjort og digerert** i architect-briefens «Økosystem-digest» (kanoniske filer markert: `samples/enrichment/.../kb-search/SKILL.md` = retrieval-mønsteret; `okf/bundles/ga4/...` = frontmatter-mal; `okf/src/reference_agent/prompts/*` = vedlikeholds-mal). → Neste sesjons `/trekresearch` kan være **lett eller hoppes over** ved å arve den digesten; ikke betal for full research på nytt.
- **Hvis scope viser seg lite** (f.eks. kun `type:`-frontmatter + én retrieval-SKILL): vurder i `/trekbrief` om full Voyage-maskineri er overkill — men architect-briefen viser at dette genuint er fler-delt (konvensjon + retrieval + vedlikehold + mål-MCP-vs-native + standard-tracking), så Voyage passer.
- **Anbefalt pipeline neste sesjon:** `/trekbrief` (avgrens scope + de åpne valgene over) → arve architect-digest (lett/ingen `/trekresearch`) → `/trekplan` (fler-sesjons med `.session-state`, proaktiv kutting før compaction) → `/trekexecute --session N` foreground. Ny fase = minor-bump **1.5.0 → 1.6.0**.
## Referanser (les ved planlegging)
- **OKF SPEC v0.1:** https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md
- **Samples / toolbox:** https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/samples · `.../tree/main/toolbox`
- **Architects referansedesign (fyldigere, les FØRST):** `../ms-ai-architect/docs/okf-second-brain-brief-2026-06.md` (+ `docs/ref-kb-direction-note-2026-06.md` for skill-ref-vs-second-brain-skillet). Architect-memory: `okf-scope-second-brain-only`.
- **okr-sømmer:** `hooks/scripts/inject-okr-context.mjs`, `scripts/write-org-profile.mjs`, `commands/oppsett.md`, CLAUDE.md «State Management».

View file

@ -0,0 +1,156 @@
# Utførelsesplan: OKR-plugin → referansegrad (norsk offentlig sektor)
**Dato:** 2026-06-23
**Grunnlag:** `docs/evaluering-360-2026-06-23.md` (360-evaluering, 14 agenter) + adversariell plan-critic-runde.
**Status i dag:** Samlet karakter **C+**. Sterkt fundament (arkitektur, hooks, metodikk-kjerne, ekte forvaltningstilpasning), trukket ned av **troverdighetskritiske innholdsfeil**.
**Mål:** Lukke alle funn i fire faser slik at pluginet trygt kan deles og fungere som referanse. Hver fase er et eget Voyage-løp over én eller flere sesjoner.
> Denne planen er **kritikk-korrigert**: plan-criticen avdekket at synteserunden underestimerte flere leveranser (DDT = 61 forekomster i 14 filer, ikke 2; UTF-8 = total ASCII-stripping som krever omskriving, ikke encoding-fiks; scoring-linjelista var ufullstendig og delvis feilrettet; versjon-per-fase manglet; Fase 1/2 deler filer). Alle disse er innarbeidet under.
---
## 1. Slik følger vi planen (driftsmodell)
1. **Én fase = ett Voyage-løp.** `/trekbrief` (ramme + verifiseringskriterier) → `/trekplan` (fil-for-fil steg) → `/trekexecute``/trekreview` (mot brief-kriteriene). Flersesjons faser bruker `.session-state` + `/trekcontinue`.
2. **Hvert `/trekplan`-steg MÅ bære `On-failure:` og `Checkpoint:` (git commit).** Destruktive steg merkes **circuit-breaker**: ved feil → stopp hele fasen, ikke fortsett.
3. **Verifiseringsplikt.** Hver fase avsluttes med konkrete bevis (grep-/spellcheck-kommandoer som beviser at funnet er borte og at ingen nye er introdusert). Ingen fase er «ferdig» uten grønne bevis.
4. **Versjon per fase.** Hver fase som endrer brukervendt innhold/funksjon bumper versjon + lager release-tag + oppdaterer katalog-`ref` (`catalog/scripts/release-plugin.mjs <plugin> <versjon>`), innenfor push-vinduet. Se §6.
5. **STATE.md oppdateres ved hver sesjonslutt** (local-only, gitignored — offentlig remote).
6. **Scope-guard.** Ingen fase utvider seg utover sine leveranser uten ny godkjenning.
---
## 2. Beslutningsport FØR Fase 0 (avgjøres i `/trekbrief`)
**DDT-beslutning.** «Direktoratet for digital tjenesteutvikling (DDT)» er fiktiv og brukes som løpende eksempel-etat. Verifisert omfang: **61 forekomster i 14 filer** (28 i `okr-offentlig-governance.md`, 7 i `okr-integrations.md`, 4 hver i `okr-examples.md`/`okr-framework.md`, flere i `oppsett.md` m.fl.). To veier:
| Valg | Hva | Innsats | Konsekvens |
|------|-----|---------|------------|
| **(a) Merk fiktiv** *(anbefalt for rask troverdighet)* | Konsekvent «(fiktiv eksempeletat)» + disclaimer ved første bruk; behold alle eksempler | Mekanisk, **én grep-verifisert pass** over alle 61 | Rask, lav risiko. Eksemplene forblir koherente. |
| **(b) Splitt i reelle etater** | Statens vegvesen (vei/trafikk) + Digdir (digitale tjenester) | **Egen dedikert fase (2-3 sesjoner)** mellom Fase 0 og Fase 1 | Omskriver tildelingsbrev-, kaskade- og metrikk-eksempler. Fase 1 avhenger av hvilken etat som «eier» hvilket eksempel. |
**Default i denne planen:** (a). Velges (b), settes en egen «Fase 0b — Etats-resplitt» inn, og Fase 1 omdefineres til å bygge på de nye etatene. **Avgjør i brief, ikke i utførelse.** Uansett valg: fjern den fabrikkerte kilde-attribusjonen «Oboard (Direktoratet for digital tjenesteutviklings valg)» (`okr-sources.md:299`) og DDT fra README-listen over *reelle* brukere (`README.md:23`).
---
## 3. Fase 0 — Fakta- og troverdighetssanering (credibility-critical)
**Avhenger av:** ingenting. **Estimat:** 2-3 sesjoner (oppjustert fra 1-2 etter kritikk: UTF-8 = omskriving av 2 hele filer; DDT = 61 sites). **Versjon ved slutt:** **1.3.1 (patch)**.
**Hvorfor først:** alt annet bygger på at teksten er til å stole på.
**Leveranser (hver med On-failure + Checkpoint):**
1. **[CIRCUIT-BREAKER, flyttet hit fra Fase 2] OKR-01 rm -rf-validering.** Høyeste data-tap-risiko. Valider at `[id]` matcher `^[TQ][1-4]-[0-9]{4}$` og er ikke-tom **før** `rm -rf .claude/okr/syklus/[id]/` i `oppsett.md:457/459`. Manuell test: tom + malformert id skal avvises. On-failure: halt hele fasen.
2. **DDT** per beslutningsporten (default: merk fiktiv). Grep-fasit: `grep -rni 'DDT\|Direktoratet for digital tjenesteutvikling' .` → forvent 61 → 0 umerkede. Fjern fabrikkert Oboard-attribusjon + DDT fra README reell-bruker-liste.
3. **Find-replace-artefakter.** Re-derivér full målliste (ikke stol på synteselista alene): `grep -rni 'saksbehandlingbehandling\|saksbehandlingtjenester\|saksbehandlingklager\|saksbehandling-søknad\|saksbehandlingsøknad\|vevedlikehold\|fornye saksbehandling' .`. Rett til koherent domene. **Regresjonssjekk:** blanket-erstatninger må ikke mangle korrekte sammensetninger — bevis 0 GAMLE *og* 0 NYE artefakter.
4. **ÉN kanonisk scoringsterskel.** Re-derivér: `grep -rn '0\.[567]-0\.[789]\|70-80%\|0\.7 =' .`. Innfør overalt: **«0.7 = forventet for aspirational; 0.6-0.7 = sweet spot (Google/Doerr)»**. **BEVAR `okr-sources.md:44` og `:115`** (de ER kanon 0.6-0.7 — ikke slett). Fjern 0.7-0.9 (`okr-cheatsheet.md:48`, `fremdriftssporer-agent.md:44`) og 70-80% (`governance.md:73`). Inkluder `styringsrådgiver-agent.md:90`. **Avklar:** `analyse.md:138-139` (0.5-0.7/>0.7) er trolig *tverr-syklus modenhet* ≠ enkelt-OKR stretch-score → sannsynlig utenfor scope; bekreft før endring. Verifisering: nøyaktig én stretch-definisjon, kanonisk kilde ikke rørt.
5. **[CIRCUIT-BREAKER] UTF-8: manuell norsk omskriving** av `individual-vs-team-okr.md` og `commands/oppsett.md` (begge 0 å + 0 ø/æ — total ASCII-stripping, ingen ren kilde å «regenerere» fra). Dette er korrekturlesing/omskriving, ikke encoding-fiks. Checkpoint (git) før start. **Verifisering:** spellcheck (`aspell -l nb` eller kuratert vokabular-diff mot øvrige referansefiler) — *ikke* bare `å > 0` (en fil kan ha riktig å men feil ø/æ).
6. **ASCII-kommandonavn.** `grep -rn '/okr:moter\|/okr:innforing\|oppstartsmate' .` → inkluderer `README.md` (L106/114/166), `oppsett.md` (L246/478), `coaching-hook.mjs` (L125). Rett til `/okr:møter`, `/okr:innføring`.
7. **coaching-hook-strenger + læring-regex.** `coaching-hook.mjs:115/124/125` (brukervendte strenger, synlig hver sesjonsstart) + L100 regex → `/## (?:Laering|Læring)/`.
8. **Døde/feil lenker + dokumenttittel.** `futureworks.no` (404) fjern/erstatt; `rework.withgoogle.com` normaliser til `/en/`-form + Wayback-fallback; rett tittel til **«Fremtidens digitale Norge — Nasjonal digitaliseringsstrategi 2024-2030»** (`governance.md:395`).
9. **Dokument-motsigelser.** CONTRIBUTING PR-seksjon → kryssreferanse til GOVERNANCE; SECURITY GitHub → Forgejo/e-post; uciterte statistikker (39/43/30-45/60%) merk «industri-rapportert (ikke fagfellevurdert)» eller erstatt med Locke & Latham-effektstørrelser.
10. **CLAUDE.md «References (15)»****utsett til etter Fase 2** (Fase 2 kan avskrive filer og endre tallet). Sett her bare hvis Fase 2-beslutningen allerede er tatt; ellers er dette en Fase 2-leveranse.
11. **Versjon:** SKILL.md `version` → 1.3.1, sync plugin.json, release-tag + katalog-`ref` (release-plugin.mjs), push-vindu.
**Voyage:** `/trekbrief` (ramme sanering + DDT-beslutning + verifiseringsplikt) → `/trekplan` (fil-for-fil, hvert steg med On-failure/Checkpoint, circuit-breakers på #1/#5) → `/trekexecute``/trekreview` (bekreft 0 artefakter, 0 strippede tegn, én scoringsverdi, kanonisk kilde intakt).
---
## 4. Fase 1 — Domenedybde og kildeintegritet
**Avhenger av:** Fase 0. **Serialisér FØR Fase 2** (deler `møter.md` + `møtefasilitator-agent.md` — kan ikke kjøre parallelt). **Estimat:** 2-3 sesjoner. **Versjon ved slutt:** **1.4.0** (sammen med Fase 2).
**Leveranser:**
1. **Tertial-korreksjon.** Presiser: formelt regime har **2 interim-punkter** (30. april / 31. august) + årsrapport; DFØ vektlegger nå halvårs-/årsrapportmøte. 4-mnd OKR-syklus = pragmatisk valg som *aligner med* — ikke er identisk med — rapporteringsregimet. «Sjekk eget tildelingsbrev.» (`dfo-okr-mapping.md:45`, `okr-framework.md:205`).
2. **OKR posisjonert som lag oppå mål- og resultatstyring** (MRS, økonomireglementet §4) — ikke erstatning. Ny seksjon om **tillitsreformen** (sentral styringsspenning, i dag fraværende). **«Målforskyvning»** lagt til som førsteklasses norsk antipattern.
3. **Metodikk-presisjoner.** Doerrs advarsel mot å snitte committed+aspirational sammen (inn i aggregeringslogikk); Intel rettet (85% = *gjenvunnet* 16-bit-andel, ikke «fra 0%»; ~2300-2500 design wins oppnådd vs 2000 mål); Locke & Latham «250%» → «vanskeligste vs enkleste mål»; legg til Kleingeld-journalref (JAP 96(6):1289-1304).
4. **«Kvartalsreview» → «Syklusreview»** (`møter.md:60`, `møtefasilitator-agent.md:39`); «kvartalsvise» → «syklusvise» (`okr-implementation.md:48/415`).
5. **Reelle norske offentlig-sektor-referanser** (Digdir produktorganisering, NRK/Ruter som faktisk offentlige, Knowit «fire fallgruver») med tydelig vendor-/praktiker- vs uavhengig-merking.
**Voyage:** `/trekresearch` (Tavily/WebFetch triangulerer DFØ-currency, tillitsreform, reelle caser mot primærkilder; confidence-rating) → `/trekplan``/trekexecute``/trekreview`.
---
## 5. Fase 2 — UX-koherens og kommando-/agent-modning
**Avhenger av:** Fase 0 (+ org-kontekst-sti-beslutning, se under). **Serialisér ETTER Fase 1** (delt fil-eierskap). **Estimat:** 1-2 sesjoner. **Versjon ved slutt:** **1.4.0**.
**Forhåndsbeslutning (kritisk avhengighet):** Bestem **kanonisk org-kontekst-sti FØR denne fasen**`~/.claude/okr/org/` (reinstall-overlevende, Fase 3-mål) vs prosjektlokal. Agentene wires til å lese fra **endelig** sti her, slik at Fase 3 ikke må gjøre om arbeidet.
**Leveranser:**
1. **Kontekstbevissthet-blokk** i `møter.md` (syklusfase-deteksjon + OKR-antall) — på linje med de 8 andre.
2. **gap vs governance disambiguert.** «Bruk /okr:gap når … (kvantitativ dekningsmatrise)» vs «Bruk /okr:governance når … (kvalitativ oversettelse + compliance)». Avdupliser den identiske innholdstabellen.
3. **Negative triggere** per agent (særlig `styringsrådgiver` vs `gapanalytiker`, begge på «tildelingsbrev»); `fremdriftssporer` instruert til å lese full antipatterns-fil; **alle Task-spawnbare agenter leser org-kontekst direkte fra fil** (endelig sti).
4. **6 usiterte referansefiler** (`dfo-okr-mapping`, `individual-vs-team-okr`, `metrics-library`, `okr-cheatsheet`, `okr-integrations`, `okr-sources`) — wire inn i relevant kommando/agent eller eksplisitt avskriv. **Deretter** rett CLAUDE.md referanse-tall til post-avskriv-tallet.
5. **Template-YAML ↔ onboarding-skjema-sync** (`okr.local.md.template`); paritetstest manuell bootstrap vs hook-forventet skjema.
**Filer eid av Fase 2 (ikke Fase 1):** `møter.md` (Kontekstbevissthet), agentfilene (negative triggere). Fase 1s `møter.md`-rename (Kvartalsreview) gjøres FØR Fase 2 starter.
**Voyage:** `/trekbrief` (avgrens UX-scope + org-sti-beslutning) → `/trekplan` (Verifisering: grep bekrefter Kontekstbevissthet i alle 10 kommandoer; manuell rm-validering allerede gjort i Fase 0) → `/trekexecute`.
---
## 6. Fase 3 — Referansegrad-løft mot ms-ai-architect-paritet
**Avhenger av:** Fase 1 + Fase 2. **Estimat:** 2-3 sesjoner (flersesjons). **Versjon ved slutt:** **1.5.0** (nye kommandoer = minor).
**Leveranser (prioritert etter adopsjonsverdi):**
1. **[MUST] Reinstall-overlevende `~/.claude/okr/org/`.** Implementer flytting + **bakoverkompatibel fallback** (les ny sti, fall tilbake til prosjektlokal). Oppdater CLAUDE.md «State Management» + `okr.local.md.template` i samme endring. On-failure: agenter faller tilbake til hook-injeksjon, ikke error. (Sti-beslutningen ble tatt i Fase 2; her implementeres migrasjonen.)
2. **[MUST] `/okr:help`** — alle 10 kommandoer + 7 agenter + anbefalt tidlig/midt/sen-syklus-arbeidsflyt.
3. **[SHOULD] Ankrede scoringsrubrikker** (`okr-quality-rubrics.md`, 5 ankere/dimensjon) som `kvalitetssjekker` laster → determinisme i governance-kontekst.
4. **[SHOULD] Test-fixtures + `node --test` smoke-test** for de to hookene (fanger skjema-drift).
5. **[SHOULD] `/okr:export` (PDF)** for kvalitetsvurderinger, gap-matriser, statusrapporter, retrospektiver — deling med ledelse/tillitsvalgte/Riksrevisjon.
6. **[SHOULD] UserPromptSubmit emne-guard** — injiser ikke OKR-kontekst på hver prompt uten relevans.
7. **[SHOULD] Bredere offentlig-metrikkbibliotek** (eForvaltningsindeks, WCAG/universell utforming, klagebehandlingsfrist, eFormidling) + minst én reell etat/kommune-differensiering (statlig vs kommunalt styringsregime).
8. **[NICE] `/okr:freshen-references`** — WebSearch/Tavily-subagenter mot dfo.no/regjeringen.no/riksrevisjonen.no for KB-currency.
**Voyage:** flersesjons `/trekplan` med `.session-state`; `/trekexecute` + `/trekcontinue` mellom sesjoner; `/trekresearch` for metrikkbiblioteket; avslutt med `/trekreview` mot brief-kriteriene for å bekrefte referansegrad.
---
## 7. Tverrgående krav
| Krav | Detalj |
|------|--------|
| **Versjon per fase** | Fase 0 → 1.3.1 (patch) · Fase 1+2 → 1.4.0 (minor) · Fase 3 → 1.5.0 (minor, nye kommandoer). Tag + katalog-`ref` (release-plugin.mjs) som **avsluttende leveranse i HVER fase**, i push-vinduet. |
| **Rollback** | Hvis en publisert tag senere må korrigeres fordi en senere fase endret kanonisk innhold tag-en averterte: dokumentér og lag patch-tag. |
| **Fil-eierskap** | `møter.md` + `møtefasilitator-agent.md` deles av Fase 1 (rename) og Fase 2 (Kontekstbevissthet/triggere) → **serialisér Fase 1 før Fase 2**. Ingen fil redigeres av to parallelle løp. |
| **On-failure/Checkpoint** | Hvert `/trekplan`-steg bærer begge. Destruktive steg (UTF-8-omskriving, DDT, rm -rf) = circuit-breaker (halt hele fasen ved feil). |
| **Gemini-triangulering** | Gemini-bridge var nede (deprecated Interactions API SDK). Oppgradér `gemini-mcp`-SDK før neste runde som trenger uavhengig triangulering. |
| **STATE.md** | Local-only (offentlig remote). Oppdatér ved hver sesjonslutt. |
---
## 8. Sporbarhet: funn → fase
| # | Funn (alvorlighet) | Fase |
|---|--------------------|------|
| 1 | Scoringsterskel selvmotsigende ≥5 filer (KRITISK) | Fase 0 |
| 2 | Fiktiv etat DDT presentert som fakta + fabrikkert Oboard-kilde (KRITISK) | Beslutningsport + Fase 0 (el. Fase 0b) |
| 3 | Total UTF-8-korrupsjon i 2 filer + hook (KRITISK/HØY) | Fase 0 |
| 4 | Find-replace-artefakter i kopierte eksempler (HØY) | Fase 0 |
| 5 | Læring-regex feiler på «Læring» (HØY) | Fase 0 |
| 6 | CONTRIBUTING↔GOVERNANCE-motsigelse (HØY) | Fase 0 |
| 7 | SECURITY peker GitHub på Forgejo-repo (HØY) | Fase 0 |
| 8 | OKR-01 rm -rf ugardert (MEDIUM, men høyest data-tap-risiko) | Fase 0 (flyttet fra Fase 2) |
| 9 | ASCII-kommandonavn i README/oppsett/hook (MEDIUM) | Fase 0 |
| 10 | Døde lenker + feil dokumenttittel (MEDIUM) | Fase 0 |
| 11 | Uciterte statistikker som fakta (MEDIUM) | Fase 0 |
| 12 | Intel/Locke faktarammings-feil (INFO/MEDIUM) | Fase 1 |
| 13 | Committed+aspirational snittes sammen (MEDIUM) | Fase 1 |
| 14 | Tertial overdrevet som DFØ-standard (MEDIUM) | Fase 1 |
| 15 | Tillitsreform + målforskyvning fraværende (REFERANSEGRAD-GAP) | Fase 1 |
| 16 | «Kvartalsreview» i 4-mnd-plugin (MEDIUM) | Fase 1 |
| 17 | møter.md mangler Kontekstbevissthet (LAV) | Fase 2 |
| 18 | gap vs governance overlapp (MEDIUM) | Fase 2 |
| 19 | Agenter mangler negative triggere (MEDIUM) | Fase 2 |
| 20 | Agenter blinde ved Task-spawn (MEDIUM) | Fase 2 |
| 21 | 6 usiterte referansefiler (SHOULD) + CLAUDE.md-tall | Fase 2 |
| 22 | Template ≠ onboarding-YAML (MEDIUM) | Fase 2 |
| 23 | Ikke reinstall-overlevende org-kontekst (REFERANSEGRAD-GAP/MUST) | Fase 2 (beslutning) + Fase 3 (impl.) |
| 24 | Mangler /okr:help, rubrikker, fixtures, PDF-eksport (SHOULD) | Fase 3 |
| 25 | UserPromptSubmit uten emne-guard (SHOULD) | Fase 3 |
| 26 | Tynt offentlig-metrikkbibliotek (SHOULD) | Fase 3 |
| 27 | SKILL-versjon 1.0.0 vs 1.3.0 (MUST) | Fase 0 (del av versjonsbump) |
**Vurdert og forkastet:** Share-session-as-artifacts (claude.ai-hostet, Team/Enterprise-gated, dataresidens/Schrems II for offentlig sektor) — operatørbeslutning 2026-06-23, tas ikke inn.

50
docs/review-2026-06-20.md Normal file
View file

@ -0,0 +1,50 @@
# Plugin review — okr (2026-06-20)
> Full-depth review, run as the **pilot** for a marketplace-wide review method (validated here
> before scaling to the other 9 plugins). Tooling: config-audit **v5.4.0** scanners (run from
> source) + llm-security posture assessor + structure/version checks. Read-only; this file is the
> only artifact written.
## Verdict
**okr is a strong, low-risk plugin.** No critical/high/medium security findings, config posture A,
version-consistent. Two Low documentation/hardening nits are the only actionable items; neither
blocks anything.
## Results by dimension
| Dimension | Tool | Result |
|-----------|------|--------|
| Config posture | config-audit `posture` | **A** overall — CLAUDE.md A, Settings/Hooks/Rules/MCP/Conflicts/Plugin-Hygiene A 100, Token-Efficiency A 90. Only weak area: **Feature Coverage D 40** (an opportunity, not a defect). |
| Plugin health | config-audit `plugin-health` | **0 findings.** (An earlier run reported 10× "command missing `model`"; that was an over-report in the scanner — `model` is an optional command field. Fixed in config-audit `a5cfc33` during this pilot.) |
| Security posture | llm-security posture assessor | **A** (risk ~6/100, ALLOW/Install). 0 critical/high/medium. 9 OWASP-LLM categories mostly A; B on deny-first/secrets/path/destructive only because explicit `settings.json`/guard-hooks are absent — not defects for a read-mostly content plugin. |
| Structure / hygiene | manual | README 280L, CHANGELOG ✓, CLAUDE.md 77L, LICENSE ✓. All 7 agents carry `name`+`description`+`model`; all 10 commands carry `name`+`description`+`allowed-tools` (no `model` — which is fine, it's optional). |
| Version consistency | catalog version-gate | **OK**`plugin.json` 1.3.0 == README badge == catalog `ref` v1.3.0… see note below. |
## Actionable findings
| ID | Severity | Location | Finding |
|----|----------|----------|---------|
| OKR-01 | Low | `commands/oppsett.md:457` | `rm -rf .claude/okr/syklus/[id]/` is human-gated (`AskUserQuestion`, line 450) and path-scoped, but a malformed/empty `[id]` could collapse the glob. **Recommend:** assert `[id]` is non-empty and matches the cycle-ID regex (`T[1-3]-YYYY`) before the `rm`. |
| OKR-02 | Low | `SECURITY.md:57` | Says Linear API keys are "stored locally in `okr.local.md`", while the template/best-practice text recommends env vars. Mitigated (`*.local.md` is gitignored), but the guidance could lead a user to paste a token. **Recommend:** steer keys to `.mcp.json`/env only. |
| — | Info | `agents/fremdriftssporer-agent.md`, `commands/oppsett.md:193`, `hooks/scripts/inject-okr-context.mjs` | Linear MCP write-back (user's own credentials), a read-only `~/.claude/settings.json` detection, and a bounded (≤50 files) hook context-injection — all traced to evidence and **cleared** (not risks). |
## Note — version drift (separate from this review)
`plugin.json` is 1.3.0 and the catalog `ref` is v1.3.0 → consistent. (Earlier in the marketplace
sweep, okr showed as version-OK in `catalog/scripts/check-versions.mjs`; the 2 WARN plugins were
linkedin-studio and ms-ai-architect, not okr.)
## What the pilot validated (for the other 9 plugins)
1. **The method works from config-audit source (v5.4.0)** — no dependency on the installed plugin
version. The 4 dimensions ran cleanly against a sibling repo, read-only.
2. **It surfaced a real tooling bug, not just plugin findings** — config-audit's plugin-health
required optional CC frontmatter fields (`model`/`tools`/`name`/`allowed-tools`). Fixed before
scaling, so the other 9 reviews get signal, not 1029 spurious medium findings each.
3. **The security pass clears false-positive-prone patterns** (MCP tool calls, a gated `rm -rf`, a
global-settings read, documentation URLs) by tracing each to evidence rather than flagging on
pattern alone — confirmed on this near-clean baseline.
4. **Expect heavier exercise on write/Bash-heavy plugins** (e.g. ms-ai-architect, voyage,
linkedin-studio) — okr's 7 agents are Read/Glob-only, so destructive-command and path-guard
categories were lightly tested here.

View file

@ -31,16 +31,6 @@
} }
] ]
} }
],
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "If OKR work occurred this session (new OKR written, scored, cascaded, or reviewed), generate a brief 2-3 bullet reminder of what was accomplished and what should be saved to the tracking system (Oboard/Linear). Skip this entirely if no OKR work occurred."
}
]
}
] ]
} }
} }

View file

@ -7,6 +7,7 @@
import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { parseFrontmatter } from '../../lib/frontmatter.mjs';
const cwd = process.cwd(); const cwd = process.cwd();
const configPath = join(cwd, '.claude', 'okr.local.md'); const configPath = join(cwd, '.claude', 'okr.local.md');
@ -17,14 +18,8 @@ if (!existsSync(configPath)) {
try { try {
const content = readFileSync(configPath, 'utf8'); const content = readFileSync(configPath, 'utf8');
const match = content.match(/^---\n([\s\S]*?)\n---/); const { raw, get } = parseFrontmatter(content);
if (!match) process.exit(0); if (raw === null) process.exit(0);
const fm = match[1];
const get = (key) => {
const m = fm.match(new RegExp(`${key}:\\s*["']?([^"'\\n]+)["']?`));
return m ? m[1].trim() : null;
};
const cycleId = get('id'); const cycleId = get('id');
const fase = get('fase'); const fase = get('fase');
@ -50,7 +45,9 @@ try {
totalWeeks = 13; totalWeeks = 13;
} }
const now = new Date(); // Testable clock seam: OKR_NOW (ISO date) overrides the wall clock so the
// phase branches (early/mid/late) can be asserted deterministically.
const now = process.env.OKR_NOW ? new Date(process.env.OKR_NOW) : new Date();
const cycleStart = new Date(cycleYear, startMonth, 1); const cycleStart = new Date(cycleYear, startMonth, 1);
const cycleEnd = new Date(cycleYear, endMonth + 1, 0); // last day of end month const cycleEnd = new Date(cycleYear, endMonth + 1, 0); // last day of end month
@ -97,7 +94,7 @@ try {
const retroPath = join(histDir, dirs[0], 'retrospektiv.md'); const retroPath = join(histDir, dirs[0], 'retrospektiv.md');
if (existsSync(retroPath)) { if (existsSync(retroPath)) {
const retro = readFileSync(retroPath, 'utf8'); const retro = readFileSync(retroPath, 'utf8');
const learningMatch = retro.match(/## [Ll]aering til neste syklus\n\n([\s\S]*?)(?:\n##|$)/); const learningMatch = retro.match(/## (?:Laering|Læring) til neste syklus\n\n([\s\S]*?)(?:\n##|$)/);
if (learningMatch) { if (learningMatch) {
const lines = learningMatch[1].trim().split('\n').filter(l => l.trim()); const lines = learningMatch[1].trim().split('\n').filter(l => l.trim());
if (lines.length > 0) lastLearning = lines[0].replace(/^[-*]\s*/, ''); if (lines.length > 0) lastLearning = lines[0].replace(/^[-*]\s*/, '');
@ -112,7 +109,7 @@ try {
parts.push(`OKR coaching: Uke ${currentWeek} av ${totalWeeks} i ${cycleId}.`); parts.push(`OKR coaching: Uke ${currentWeek} av ${totalWeeks} i ${cycleId}.`);
if (phase === 'early') { if (phase === 'early') {
parts.push('Tidlig i syklusen — fokus pa alignment og kvalitetssikring av OKR.'); parts.push('Tidlig i syklusen — fokus på alignment og kvalitetssikring av OKR.');
parts.push('Anbefalt: /okr:gap (dekning mot tildelingsbrev), /okr:kvalitet (kvalitetssjekk).'); parts.push('Anbefalt: /okr:gap (dekning mot tildelingsbrev), /okr:kvalitet (kvalitetssjekk).');
} else if (phase === 'mid') { } else if (phase === 'mid') {
parts.push('Midtveis i syklusen — tid for fremdriftssjekk.'); parts.push('Midtveis i syklusen — tid for fremdriftssjekk.');
@ -121,8 +118,8 @@ try {
parts.push(`OBS: ${atRiskCount} KR er merket som i fare/blokkert i siste status.`); parts.push(`OBS: ${atRiskCount} KR er merket som i fare/blokkert i siste status.`);
} }
} else if (phase === 'late') { } else if (phase === 'late') {
parts.push('Syklusen naermer seg slutt — fokus pa sluttspurt og forberedelse.'); parts.push('Syklusen nærmer seg slutt — fokus på sluttspurt og forberedelse.');
parts.push('Anbefalt: /okr:sporing (endelig scoring), /okr:moter (review-mote).'); parts.push('Anbefalt: /okr:sporing (endelig scoring), /okr:møter (review-møte).');
if (currentWeek >= totalWeeks - 2) { if (currentWeek >= totalWeeks - 2) {
parts.push('Mindre enn 2 uker igjen. Vurder /okr:oppsett arkiver for retrospektiv.'); parts.push('Mindre enn 2 uker igjen. Vurder /okr:oppsett arkiver for retrospektiv.');
} }
@ -135,7 +132,7 @@ try {
} }
if (lastLearning) { if (lastLearning) {
parts.push(`Laering fra forrige syklus: "${lastLearning}"`); parts.push(`Læring fra forrige syklus: "${lastLearning}"`);
} }
const msg = parts.join(' '); const msg = parts.join(' ');

View file

@ -2,35 +2,72 @@
// inject-okr-context.mjs // inject-okr-context.mjs
// Event: UserPromptSubmit // Event: UserPromptSubmit
// Purpose: Inject OKR organization context from .claude/okr.local.md and .claude/okr/ tree. // Purpose: Inject the core OKR organization context from .claude/okr.local.md
// Zero npm dependencies. Target execution: <50ms. // (or the home org profile) plus ONE pointer to the second-brain index.md.
// Cycle/history/context retrieval is on-demand via the okr-second-brain-search
// skill — no longer pre-injected here. Zero npm dependencies. Target: <50ms.
import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { homedir } from 'node:os';
import { parseFrontmatter } from '../../lib/frontmatter.mjs';
const cwd = process.cwd(); const cwd = process.cwd();
const configPath = join(cwd, '.claude', 'okr.local.md'); const projectConfigPath = join(cwd, '.claude', 'okr.local.md');
const homeConfigPath = join(homedir(), '.claude', 'okr', 'org', 'profil.md');
if (!existsSync(configPath)) { // Topic guard (UserPromptSubmit): only inject OKR context when the user's
// prompt is plausibly OKR-related. The stdin read is non-blocking here -- under
// execFileSync (Claude Code's hook call and the tests) stdin is a closed pipe
// so EOF is immediate; the isTTY guard avoids blocking in an interactive shell.
// Default to PASS (inject) on any doubt -- empty/unparseable stdin, missing
// prompt field, or TTY -- so we never drop a legitimate OKR prompt. Only an
// explicit non-matching prompt is suppressed. No network.
let rawPrompt = '';
try {
if (!process.stdin.isTTY) rawPrompt = readFileSync(0, 'utf8');
} catch {
rawPrompt = '';
}
if (rawPrompt) {
let promptText = null;
try {
promptText = JSON.parse(rawPrompt).prompt;
} catch {
promptText = null;
}
if (typeof promptText === 'string' && promptText.length > 0) {
const okrPattern = /\bokr\b|objective|key result|n[oø]kkelresultat|noekkelresultat|\bkr\b|\bm[aå]l\b|\bmaal\b|tildelingsbrev|kaskade|syklus|tertial|kvartal/i;
if (!okrPattern.test(promptText)) {
process.exit(0);
}
}
}
// Hybrid org-profile resolution (most-specific-wins): project-local overrides
// the machine-global home profile. The home path is the Fase 3 migration target;
// this read is forward-compatible and stays inert until that file exists.
// NOTE: only the org PROFILE resolves to home. Cycle/work data stays cwd-bound
// and is no longer pre-injected -- it is retrieved on-demand via the
// okr-second-brain-search skill.
const configPath = existsSync(projectConfigPath)
? projectConfigPath
: (existsSync(homeConfigPath) ? homeConfigPath : null);
if (!configPath) {
process.exit(0); process.exit(0);
} }
try { try {
const content = readFileSync(configPath, 'utf8'); const content = readFileSync(configPath, 'utf8');
const match = content.match(/^---\n([\s\S]*?)\n---/); const { raw, get } = parseFrontmatter(content);
if (!match) process.exit(0); if (raw === null) process.exit(0);
const fm = match[1];
const get = (key) => {
const m = fm.match(new RegExp(`${key}:\\s*["']?([^"'\\n]+)["']?`));
return m ? m[1].trim() : null;
};
// Core fields (backwards-compatible with old 4-field format) // Core fields (backwards-compatible with old 4-field format)
const org = get('navn') || get('name'); const org = get('navn') || get('name');
const syklus = get('gjeldende') || get('id') || get('current_cycle'); const syklus = get('gjeldende') || get('id') || get('current_cycle');
const sektor = get('sektor') || get('sector') || get('domene'); const sektor = get('sektor') || get('sector') || get('domene');
const linear = fm.includes('aktivert: true') || fm.includes('enabled: true'); const linear = raw.includes('aktivert: true') || raw.includes('enabled: true');
// New v1.1 fields (silently skipped if absent) // New v1.1 fields (silently skipped if absent)
const modenhet = get('modenhetsnivaa'); const modenhet = get('modenhetsnivaa');
@ -50,105 +87,23 @@ try {
if (trygghet) parts.push(`Psykologisk trygghet: ${trygghet}`); if (trygghet) parts.push(`Psykologisk trygghet: ${trygghet}`);
if (linear) parts.push('Linear: aktivert'); if (linear) parts.push('Linear: aktivert');
// Scan .claude/okr/ directory tree (cap at 50 files) // Base context line from the core profile fields only. The directory-tree
const okrDir = join(cwd, '.claude', 'okr'); // enumeration that used to live here (the :92-191 MOVE block) is removed:
const dirParts = []; // retrieval is now on-demand via the okr-second-brain-search skill, not
let totalFiles = 0; // pre-injected. The payload is therefore independent of file count. (SC5)
if (existsSync(okrDir)) {
try {
const topEntries = readdirSync(okrDir, { withFileTypes: true });
for (const entry of topEntries) {
if (!entry.isDirectory()) continue;
if (totalFiles >= 50) break;
try {
const subEntries = readdirSync(join(okrDir, entry.name), { withFileTypes: true });
const mdFiles = [];
const subDirs = [];
for (const sub of subEntries) {
if (totalFiles >= 50) break;
if (sub.isFile() && sub.name.endsWith('.md')) {
mdFiles.push(sub.name);
totalFiles++;
} else if (sub.isDirectory()) {
subDirs.push(sub.name);
}
}
// Enumerate nested subdirectories (e.g. syklus/T1-2026/)
for (const sd of subDirs) {
if (totalFiles >= 50) break;
try {
const nested = readdirSync(join(okrDir, entry.name, sd), { withFileTypes: true });
const nestedMd = [];
for (const n of nested) {
if (totalFiles >= 50) break;
if (n.isFile() && n.name.endsWith('.md')) {
nestedMd.push(n.name);
totalFiles++;
}
}
if (nestedMd.length > 0) {
dirParts.push(`${entry.name}/${sd}/ (${nestedMd.length} fil${nestedMd.length > 1 ? 'er' : ''}: ${nestedMd.join(', ')})`);
}
} catch { /* skip unreadable nested dirs */ }
}
if (mdFiles.length > 0) {
dirParts.push(`${entry.name}/ (${mdFiles.length} fil${mdFiles.length > 1 ? 'er' : ''}: ${mdFiles.join(', ')})`);
}
} catch { /* skip unreadable dirs */ }
}
} catch { /* .claude/okr/ scan failed — continue without */ }
}
// Scan historikk/ for archived cycle count
const histDir = join(okrDir, 'historikk');
const archivedCycles = [];
if (existsSync(histDir)) {
try {
const histEntries = readdirSync(histDir, { withFileTypes: true });
for (const entry of histEntries) {
if (entry.isDirectory()) {
archivedCycles.push(entry.name);
}
}
} catch { /* skip */ }
}
// List active cycle files
const cycleId = syklus;
const cycleParts = [];
if (cycleId) {
const cyclePath = join(okrDir, 'syklus', cycleId);
if (existsSync(cyclePath)) {
try {
const cycleEntries = readdirSync(cyclePath, { withFileTypes: true });
for (const e of cycleEntries) {
if (e.isFile() && e.name.endsWith('.md')) {
cycleParts.push(e.name);
}
}
} catch { /* skip */ }
}
}
// Build rich systemMessage
let msg = `OKR-kontekst (fra .claude/okr.local.md): ${parts.join(', ')}.`; let msg = `OKR-kontekst (fra .claude/okr.local.md): ${parts.join(', ')}.`;
if (dirParts.length > 0) { // Resolve ONE pointer to the second-brain index.md, reusing most-specific-wins
msg += `\nTilgjengelige kontekstfiler: ${dirParts.join('; ')}.`; // (project bundle preferred, else home bundle). Project root is cwd-bound
} // (.claude/okr/); the home bundle root is ~/.claude/okr/org/ (two-root model).
const projectIndex = join(cwd, '.claude', 'okr', 'index.md');
const homeIndex = join(homedir(), '.claude', 'okr', 'org', 'index.md');
const indexPath = existsSync(projectIndex)
? projectIndex
: (existsSync(homeIndex) ? homeIndex : null);
if (cycleParts.length > 0) { if (indexPath) {
msg += `\nAktive OKR-filer i syklus ${cycleId}: ${cycleParts.join(', ')}.`; msg += `\nPersonlig OKF-kontekst finnes - sok wikien (skill: okr-second-brain-search) eller se ${indexPath}.`;
}
if (archivedCycles.length > 0) {
msg += `\nArkiverte sykluser (${archivedCycles.length}): ${archivedCycles.sort().join(', ')}.`;
msg += ' Bruk /okr:analyse for trendanalyse.';
}
if (dirParts.length > 0 || cycleParts.length > 0) {
msg += '\nBruk disse filene automatisk nar relevant — ikke be brukeren om a lime inn innhold som allerede finnes.';
} }
process.stdout.write(JSON.stringify({ systemMessage: msg })); process.stdout.write(JSON.stringify({ systemMessage: msg }));

67
lib/frontmatter.mjs Normal file
View file

@ -0,0 +1,67 @@
// frontmatter.mjs
// Delt frontmatter-parse/skrive for okr-pluginen. Zero npm dependencies.
//
// Konsoliderer den dupliserte flate `get()`-parseren fra inject-okr-context.mjs
// (:59-66) og coaching-hook.mjs (:20-27) til EN modul, med to korreksjoner mot
// den gamle atferden:
// 1. Linjeanker (`^\s*key:`, multiline) — key matcher kun ved linjestart
// (modulo innrykk), aldri som substring midt i en annen key/verdi. Bevarer
// first-match og nestet (innrykket) oppslag (load-bearing: inject:69).
// 2. Trailing " #kommentar" strippes KUN fra USITERTE verdier. Siterte verdier
// beholder en intern '#' ("A #B" -> A #B). Retter comment-leak-bugen der
// `okr_frikoblet_fra_loenn: true # ...` lakk kommentaren inn i verdien.
//
// Tolererer fler-linje OKF-list-verdier (f.eks. `tags:`) uten krasj: get() paa
// en list-key returnerer null (rest-av-linja er tom); list-elementer paa
// foelgende linjer konsumeres aldri (ingen konsument leser tre-filenes `tags`).
const FM_RE = /^---\n([\s\S]*?)\n---/;
export function parseFrontmatter(content) {
const match = String(content).match(FM_RE);
const raw = match ? match[1] : null;
const get = (key) => {
if (raw === null) return null;
const m = raw.match(new RegExp(`^\\s*${key}:\\s*(.*)$`, 'm'));
if (!m) return null;
let v = m[1].trim();
if (v === '') return null;
const q = v[0];
if (q === '"' || q === "'") {
const end = v.indexOf(q, 1);
if (end !== -1) return v.slice(1, end); // intern '#' bevart
v = v.slice(1); // uavsluttet quote: fall tilbake til resten
} else {
v = v.replace(/\s+#.*$/, '').trim(); // usitert: strip trailing kommentar
}
return v === '' ? null : v;
};
return { raw, get };
}
// Siter naar verdien inneholder '#'/':' eller har kant-whitespace, slik at
// round-trip via parseFrontmatter bevarer den eksakt (jf. siter-#-regelen).
const quoteIfNeeded = (s) =>
/[#:]/.test(s) || /^\s|\s$/.test(s) || /^["']/.test(s) ? JSON.stringify(s) : s;
export function writeFrontmatter(fields) {
const lines = ['---'];
for (const [key, value] of Object.entries(fields)) {
// Additiv array-gren (Step 3): emit OKF multi-linje list-verdi (f.eks.
// `tags`) som `key:\n - item`. Lese-siden er uendret -- parseFrontmatter.
// get() returnerer fortsatt null for list-keys. Skalar-grenen under er
// bevart bit-for-bit (kun loftet ut til quoteIfNeeded, samme regel).
if (Array.isArray(value)) {
lines.push(`${key}:`);
for (const item of value) {
lines.push(` - ${quoteIfNeeded(String(item))}`);
}
continue;
}
lines.push(`${key}: ${quoteIfNeeded(String(value))}`);
}
lines.push('---');
return lines.join('\n') + '\n';
}

View file

@ -0,0 +1,75 @@
// innboks-frontmatter.mjs
// Step 5: deterministisk frontmatter-projeksjon for et konsept (fra splitConcepts).
// Regel-basert (INGEN LLM): type/tags utledes fra title+sti og snappes mot det
// lukkede vokabularet (okf-vocab); resource = original relativ sti (kanonisk navn);
// description = foerste ikke-tomme avsnitt (whitespace kollapset, trunkert);
// timestamp = ISO-8601 av original-fil-mtime (IKKE veggklokke -> idempotens by
// construction per maskin); kilde:innboks provenans-markoer (extension key for
// retrieval-rangering). Beregner ogsaa konseptets mal-sti
// (concept.destRel = routeLevel(type)/slug.md) FOER relasjons-steget (Step 6),
// jf. Pass-2 ordering-fiks (Revisions #21). Serialiserer via writeFrontmatter
// (additiv array-gren fra Step 3). Zero npm dependencies.
import { writeFrontmatter } from './frontmatter.mjs';
import { snapType, snapTags, routeLevel, TYPE_VOCAB, TAGS_VOCAB } from './okf-vocab.mjs';
const DESCRIPTION_MAX = 240;
// Vokab sortert lengst-foerst saa "Overordnede OKR" matcher foer "OKR".
const TYPE_BY_LENGTH = [...TYPE_VOCAB].sort((a, b) => b.length - a.length);
// Regel-utledning: foerste vokab-term som forekommer i title+sti -> kanonisk type.
function deriveType(title, sourcePath) {
const hay = `${title} ${sourcePath}`.toLowerCase();
for (const t of TYPE_BY_LENGTH) {
if (hay.includes(t.toLowerCase())) return snapType(t);
}
return snapType('');
}
// Tags: vokab-termer som forekommer i title (vokab-rekkefolge bevart), saa snappet.
function deriveTags(title) {
const hay = String(title).toLowerCase();
return snapTags(TAGS_VOCAB.filter((t) => hay.includes(t.toLowerCase())));
}
// Foerste ikke-tomme avsnitt, whitespace kollapset, trunkert deterministisk.
function deriveDescription(body) {
const para = String(body)
.split(/\n\s*\n/)
.map((p) => p.replace(/\s+/g, ' ').trim())
.find((p) => p !== '');
if (!para) return '';
return para.length > DESCRIPTION_MAX ? `${para.slice(0, DESCRIPTION_MAX).trimEnd()}...` : para;
}
// concept (fra splitConcepts) -> beriket konsept med OKF-frontmatter + destRel.
export function projectFrontmatter(concept, { sourcePath, sourceMtime } = {}) {
const resource = String(sourcePath ?? '');
const type = deriveType(concept.title, resource);
const description = deriveDescription(concept.body);
const tags = deriveTags(concept.title);
const timestamp = new Date(sourceMtime).toISOString();
const destRel = `${routeLevel(type)}/${concept.slug}.md`;
// Kanonisk noekkel-rekkefolge; description/tags utelates naar tomme.
const fields = { type, resource, title: concept.title };
if (description) fields.description = description;
if (tags.length > 0) fields.tags = tags;
fields.timestamp = timestamp;
fields.kilde = 'innboks';
const frontmatter = writeFrontmatter(fields);
return {
...concept,
type,
resource,
description,
tags,
timestamp,
kilde: 'innboks',
destRel,
frontmatter,
};
}

49
lib/innboks-relations.mjs Normal file
View file

@ -0,0 +1,49 @@
// innboks-relations.mjs
// Step 6: deterministisk relasjons-resolusjon innen KJOERINGENS konsept-sett
// (lukket lenke-vokabular). For hvert konsept oppdages referanser til ANDRE
// emitterte konsepter (exact-title substring, case-insensitivt) og emitteres som
// leading-'/' bundle-root-lenker bygd paa malet-konseptets destRel (satt i Step 5,
// kjent FOER skriv -- Pass-2 ordering-fiks). Asserterer zero-dangling mot de
// emitterte destRel-stiene og at hver generert lenke er trygg (delt okf-links-
// allow-list). v1-narrowing: kun innen-kjoering-settet -- lenking til pre-
// eksisterende tre-konsepter er v1.1. Zero npm dependencies.
import { isSafeBundleLink } from './okf-links.mjs';
const REL_HEADING = '## Relaterte dokumenter';
// concepts (beriket av projectFrontmatter, m/ destRel) -> samme konsepter m/
// relations[] + body utvidet med en deterministisk relasjons-seksjon.
export function resolveRelations(concepts) {
const emitted = new Set(concepts.map((c) => c.destRel));
return concepts.map((concept) => {
const haystack = `${concept.title}\n${concept.body}`.toLowerCase();
const found = new Map(); // target -> { title, target, destRel }
for (const other of concepts) {
if (other === concept) continue;
if (!other.title || !other.destRel) continue;
if (!haystack.includes(other.title.toLowerCase())) continue;
const target = `/${other.destRel}`;
if (!isSafeBundleLink(target)) {
throw new Error(`innboks-relations: utrygg generert lenke ${target}`);
}
if (!emitted.has(other.destRel)) {
throw new Error(`innboks-relations: dangling relasjon ${other.destRel}`);
}
found.set(target, { title: other.title, target, destRel: other.destRel });
}
const relations = [...found.values()].sort((a, b) => a.target.localeCompare(b.target));
let body = concept.body;
if (relations.length > 0) {
const links = relations.map((r) => `- [${r.title}](${r.target})`).join('\n');
body = `${concept.body}\n\n${REL_HEADING}\n\n${links}\n`;
}
return { ...concept, relations, body };
});
}

96
lib/innboks-split.mjs Normal file
View file

@ -0,0 +1,96 @@
// innboks-split.mjs
// Step 4: deterministisk heading-split av (allerede konvertert) markdown til
// konsepter. REN funksjon -- fil-settet er en funksjon av input alene (INGEN
// LLM, ingen veggklokke), saa run1 og run2 gir byte-identisk resultat (SC
// idempotens by construction). Split paa #/## (MarkdownHeaderTextSplitter-
// moenster): hver #/##-heading starter et nytt konsept; ### og dypere forblir
// body-innhold. Dokument uten brukbare overskrifter -> fallback "ett dokument =
// ett konsept". Innhold foer foerste heading bevares som ledende konsept (ingen
// datatap). Zero npm dependencies.
// Kebab-slug: lowercase, strip diakritika defensivt, ikke-alfanum -> '-', trim.
// Never-empty fallback 'konsept' (deterministisk; aldri tomt filnavn).
function slugify(text) {
const s = String(text)
.toLowerCase()
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return s || 'konsept';
}
// #/## heading-linje -> { level, title }. ### og dypere matcher IKKE: '#{1,2}'
// kan ikke etterfoelges av '\s+' naar tredje tegn er '#', saa de forblir body.
const HEADING_RE = /^(#{1,2})\s+(.+?)\s*$/;
// markdown -> Concept[]; opts.sourceSlug identifiserer kilde-dokumentet (brukt
// som tittel/slug for flat-fallback + preamble). Concept-form:
// { sourceSlug, title, slug, level, body }
export function splitConcepts(markdown, { sourceSlug } = {}) {
const src =
typeof sourceSlug === 'string' && sourceSlug.trim() !== '' ? sourceSlug : 'konsept';
const normalized = String(markdown).replace(/\r\n/g, '\n');
const lines = normalized.split('\n');
// Segmenter: ett per #/##-heading. Linjer foer foerste heading -> preamble.
const segments = [];
const preambleLines = [];
let current = null;
for (const line of lines) {
const m = line.match(HEADING_RE);
if (m) {
current = { title: m[2].trim(), level: m[1].length, lines: [] };
segments.push(current);
} else if (current) {
current.lines.push(line);
} else {
preambleLines.push(line);
}
}
const concepts = [];
const seen = new Map();
const uniqueSlug = (base) => {
const n = seen.get(base) || 0;
seen.set(base, n + 1);
return n === 0 ? base : `${base}-${n + 1}`;
};
// Reelt innhold foer foerste heading -> ledende konsept (titulert av kilde).
if (preambleLines.join('').trim() !== '') {
concepts.push({
sourceSlug: src,
title: src,
slug: uniqueSlug(slugify(src)),
level: 1,
body: preambleLines.join('\n').trim(),
});
}
// Flat dokument (ingen #/##): hele dokumentet = ett konsept.
if (segments.length === 0) {
if (concepts.length === 0) {
concepts.push({
sourceSlug: src,
title: src,
slug: uniqueSlug(slugify(src)),
level: 1,
body: normalized.trim(),
});
}
return concepts;
}
for (const seg of segments) {
concepts.push({
sourceSlug: src,
title: seg.title,
slug: uniqueSlug(slugify(seg.title)),
level: seg.level,
body: seg.lines.join('\n').trim(),
});
}
return concepts;
}

122
lib/innboks-write.mjs Normal file
View file

@ -0,0 +1,122 @@
// innboks-write.mjs
// Step 7: persister berikede konsepter (splitConcepts -> projectFrontmatter ->
// resolveRelations) inn i prosjekt-bundlen, ikke-destruktivt og idempotent.
//
// (1) Hvert konsept skrives til sin FORHAANDSBEREGNEDE concept.destRel (rutet
// via routeLevel i Step 5 -- skriveren ruter ikke selv). Skrevet fil =
// concept.frontmatter (ferdig OKF-blokk) + concept.body (baerer allerede
// relasjons-seksjonen) verbatim -- INGEN re-serialisering. Trailing newline
// garanteres (POSIX-ren, deterministisk -> byte-idempotent re-kjoering).
// (2) Atomisk skriv: temp i SAMME katalog + renameSync over maalet (en krasj
// midt i skriv kan aldri etterlate en halv-skrevet fil).
// (3) Original-bevaring (SC3, ikke-destruktiv): skriveren roerer ALDRI
// drop-zone-originalene -- den asserterer kun at de finnes (saa pekeren
// aldri dangler). Per original skrives EN markdown-peker i konseptets nivaa
// som lenker bundle-root-relativt til originalen i .claude/okr/innboks/.
// (4) Path-confinement (innboks-dok = fiendtlig, RAG-poisoning): hver maal-sti
// maa resolvere UNDER bundle-rota (avvis '..'-escape / absolutt-override),
// og bundle-rota selv maa ikke vaere den home-kanoniske org-profilen
// (~/.claude/okr/org) -- ingestion skriver kun i prosjekt-bundlen.
//
// Reuses: atomisk-skriv-moenster (scripts/write-org-profile.mjs:34-40);
// writeFrontmatter (lib/frontmatter.mjs) for peker-frontmatter. Zero npm deps.
import { writeFileSync, mkdirSync, renameSync, existsSync } from 'node:fs';
import path from 'node:path';
import { homedir } from 'node:os';
import { writeFrontmatter } from './frontmatter.mjs';
// Den home-kanoniske org-profil-rota. Ingestion skal ALDRI skrive hit (den eies
// av write-org-profile.mjs); prosjekt-bundlen er .claude/okr under cwd.
const HOME_ORG = path.join(homedir(), '.claude', 'okr', 'org');
// Atomisk: temp-fil i samme katalog, deretter renameSync over maalet (atomisk
// paa samme filsystem). Speiler write-org-profile.mjs:34-40.
function writeAtomic(target, data) {
const dir = path.dirname(target);
mkdirSync(dir, { recursive: true });
const tmp = path.join(dir, `${path.basename(target)}.${process.pid}.tmp`);
writeFileSync(tmp, data);
renameSync(tmp, target);
}
// Resolver en bundle-relativ sti og asserter at den blir UNDER bundle-rota.
// Avviser '..'-escape og absolutt-override (path.resolve lar en absolutt rel
// vinne -- containment-sjekken fanger det).
function resolveUnderBundle(resolvedBundle, rel) {
const resolved = path.resolve(resolvedBundle, rel);
if (resolved !== resolvedBundle && !resolved.startsWith(resolvedBundle + path.sep)) {
throw new Error(`innboks-write: maal-sti utenfor bundle-rot avvist: ${rel}`);
}
return resolved;
}
// Skrevet fil = frontmatter + body verbatim, med garantert trailing newline.
function fileContent(concept) {
const out = `${concept.frontmatter}${concept.body}`;
return out.endsWith('\n') ? out : `${out}\n`;
}
// Peker-fil: minimal gyldig OKF-fil (type i lukket vokab) som lenker til den
// bevarte originalen. resource = bundle-relativ original-sti (uten leading '/').
function pointerContent(original, link) {
const basename = path.basename(original.path);
const frontmatter = writeFrontmatter({
type: 'Notat',
resource: link.replace(/^\/+/, ''),
title: `Kilde: ${basename}`,
kilde: 'innboks',
});
const body = `Peker til bevart original i drop-zonen (ikke-destruktiv ingestion).\n\n- [${basename}](${link})\n`;
return `${frontmatter}${body}`;
}
// concepts (berikede, m/ destRel + frontmatter + body) + { bundleRoot, originals }
// -> skriver konsept-filer + peker-filer atomisk under bundle-rota.
// originals: [{ sourceSlug, path }] -- path = originalens plassering i drop-zonen.
// Returnerer { concepts: [skrevne konsept-stier], pointers: [skrevne peker-stier] }
// (absolutte stier; pipelinen (Step 10) bruker dette til discard-on-fail rollback).
export function writeConcepts(concepts, { bundleRoot, originals = [] } = {}) {
if (!bundleRoot) throw new Error('innboks-write: bundleRoot kreves');
const resolvedBundle = path.resolve(bundleRoot);
// Confinement: avvis skriv til home-org-rota -- ingestion eier kun prosjekt-bundlen.
if (resolvedBundle === HOME_ORG || resolvedBundle.startsWith(HOME_ORG + path.sep)) {
throw new Error(`innboks-write: skriv til home-org-rot avvist: ${bundleRoot}`);
}
const writtenConcepts = [];
for (const concept of concepts) {
if (!concept.destRel) {
throw new Error(`innboks-write: konsept mangler destRel: ${concept.slug ?? '?'}`);
}
const target = resolveUnderBundle(resolvedBundle, concept.destRel);
writeAtomic(target, fileContent(concept));
writtenConcepts.push(target);
}
const writtenPointers = [];
for (const original of originals) {
const resolvedOriginal = path.resolve(original.path);
if (!existsSync(resolvedOriginal)) {
throw new Error(`innboks-write: original mangler (peker ville dangle): ${original.path}`);
}
// Original maa ligge under bundle-rota (drop-zonen .claude/okr/innboks/).
const relToBundle = path.relative(resolvedBundle, resolvedOriginal);
if (relToBundle === '' || relToBundle.startsWith('..') || path.isAbsolute(relToBundle)) {
throw new Error(`innboks-write: original utenfor bundle-rot: ${original.path}`);
}
const link = `/${relToBundle.split(path.sep).join('/')}`;
// Nivaa = nivaaet til foerste konsept fra samme kilde; default dokumenter/.
const sibling = concepts.find((c) => c.sourceSlug === original.sourceSlug && c.destRel);
const level = sibling ? path.dirname(sibling.destRel) : 'dokumenter';
const pointerRel = path.join(level, `${original.sourceSlug}.kilde.md`);
const pointerTarget = resolveUnderBundle(resolvedBundle, pointerRel);
writeAtomic(pointerTarget, pointerContent(original, link));
writtenPointers.push(pointerTarget);
}
return { concepts: writtenConcepts, pointers: writtenPointers };
}

39
lib/okf-links.mjs Normal file
View file

@ -0,0 +1,39 @@
// okf-links.mjs
// Delt bundle-root-relativ lenke-resolver (Step 6), brukt av BAADE relasjons-
// emitteringen (innboks-relations) og okf-check++ --strict-ingest (Step 8).
// Konvensjon: en trygg bundle-lenke er ROOT-RELATIV med leading '/' (resolveres
// mot bundle-rota, jf. SC-regex \]\(/.*\.md\)). Avviser '../'-escape, absolutt
// Windows-sti (backslash), og scheme-lenker (file://, http(s)://). Den ENESTE
// lenke-allow-listen -- delt slik at emit og validering aldri divergerer.
// Zero npm dependencies.
import path from 'node:path';
// Scheme-prefiks (file://, http://, https://, ...) -- avvises uansett.
const SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
// '..'-segment hvor som helst i stien (escape ut av bundle-rota).
const PARENT_SEGMENT_RE = /(^|\/)\.\.(\/|$)/;
// Sann hvis target er en trygg, bundle-root-relativ .md-lenke (leading '/').
export function isSafeBundleLink(target) {
if (typeof target !== 'string' || target === '') return false;
if (target.includes('\0')) return false; // null-byte
if (target.includes('\\')) return false; // backslash (Windows-sti)
if (SCHEME_RE.test(target)) return false; // file://, http(s)://, ...
if (!target.startsWith('/')) return false; // maa vaere bundle-root-relativ
if (PARENT_SEGMENT_RE.test(target)) return false; // ingen '..'-escape
if (!target.endsWith('.md')) return false; // bundle-konseptlenker er .md
return true;
}
// Resolverer en trygg bundle-lenke til absolutt sti UNDER bundleRoot, ellers null.
// Belt-and-suspenders: isSafeBundleLink avviser '..' allerede, men confinement
// re-sjekkes paa den resolverte stien.
export function resolveBundleLink(target, bundleRoot) {
if (!isSafeBundleLink(target)) return null;
const rel = target.replace(/^\/+/, '');
const root = path.resolve(bundleRoot);
const resolved = path.resolve(root, rel);
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
return resolved;
}

73
lib/okf-vocab.mjs Normal file
View file

@ -0,0 +1,73 @@
// okf-vocab.mjs
// Lukket kontrollert vokabular for innboks-ingestion (type + tags) og
// type->nivaa-ruting. Zero npm dependencies.
//
// Net-new (premiss #1): handheves KUN paa ingestion-skrivestien (snapType/
// snapTags) og av okf-check++ --strict-ingest. Lese-siden (okr-second-brain-
// search, okf-check default) behandler fortsatt ukjente typer som gyldige --
// denne modulen er den ENESTE strenge porten, aktiv kun ved ingestion.
// Title-Case kanoniske OKF-typer (jf. catalog-spec Documents/kb Layout).
export const TYPE_VOCAB = [
'Organisasjonsprofil',
'Tildelingsbrev',
'Virksomhetsplan',
'Overordnede OKR',
'OKR',
'Retrospektiv',
'Status',
'Notat',
'Dokument',
];
// Safe default naar raw type ikke matcher vokabularet exact (premiss #1).
const DEFAULT_TYPE = 'Dokument';
// Lukket start-sett av Title-Case tags for ingestion-snap.
export const TAGS_VOCAB = [
'Strategi',
'Tildelingsbrev',
'Virksomhetsplan',
'OKR',
'Styring',
'Governance',
'Retrospektiv',
'Status',
'Maal',
'Risiko',
];
// type -> bundle-nivaa (katalog under bundle-rot). Organisasjonsprofil rutes
// til dokumenter/ (IKKE strategisk-kontekst/) for aa ikke konkurrere med den
// home-kanoniske ~/.claude/okr/org/profil.md (Revisions #25).
const LEVEL_BY_TYPE = {
Tildelingsbrev: 'strategisk-kontekst',
Virksomhetsplan: 'strategisk-kontekst',
'Overordnede OKR': 'strategisk-kontekst',
Retrospektiv: 'historikk',
OKR: 'dokumenter',
Status: 'dokumenter',
Notat: 'dokumenter',
Dokument: 'dokumenter',
Organisasjonsprofil: 'dokumenter',
};
const DEFAULT_LEVEL = 'dokumenter';
// Exact-match -> kanonisk type; ellers safe default Dokument.
export function snapType(raw) {
return TYPE_VOCAB.includes(raw) ? raw : DEFAULT_TYPE;
}
// Behold kun tags i det lukkede vokabularet (dropp ukjente); bevar rekkefolge.
export function snapTags(rawList) {
if (!Array.isArray(rawList)) return [];
return rawList.filter((t) => TAGS_VOCAB.includes(t));
}
// type -> bundle-nivaa; ukjent/ikke-vokab -> default dokumenter/. Plassert her
// i Session 1 slik at relasjons-steget (Step 6) kan resolvere konseptets
// mal-sti FOR skriv (Pass-2 ordering-fiks, Revisions #21).
export function routeLevel(type) {
return LEVEL_BY_TYPE[type] ?? DEFAULT_LEVEL;
}

View file

@ -0,0 +1,65 @@
#!/usr/bin/env node
// compose-org-profile.mjs
// Purpose: wrap the NESTED org-profile YAML body (organisasjon:/program: ...,
// read from stdin) in an OKF-compatible frontmatter document, prepending the
// three top-level OKF keys the second-brain "Documents/kb" layout expects on a
// concept file: type / resource / timestamp. Emits the full document to stdout,
// meant to be piped into write-org-profile.mjs (which writes it atomically to
// the reinstall-surviving home path ~/.claude/okr/org/profil.md).
//
// Why a dedicated composer (not lib/frontmatter.mjs writeFrontmatter): the org
// profile is NESTED (organisasjon: { navn, type }, program: { ... }). The flat
// writeFrontmatter would flatten/destroy that structure. We instead keep the
// caller's nested YAML verbatim and only PREPEND OKF keys above it.
//
// Why ONLY type/resource/timestamp top-level: the home profile is read by the
// flat parser (lib/frontmatter.mjs) in inject-okr-context.mjs via
// get('navn')/get('id')/get('domene')/... A NEW top-level key colliding with
// any of those (navn/name/id/fase/domene/sektor) would be matched FIRST and
// shadow the nested value, breaking org/cycle resolution (the Critical risk).
// type/resource/timestamp are read by NO consumer, so they are safe to prepend.
// The nested organisasjon.type ("offentlig") is untouched and still unread.
//
// Single-block invariant: the OKF keys and the nested body MUST live in ONE
// frontmatter block. A stray intermediate '---' would truncate the block the
// flat parser reads (dropping organisasjon: -> get('navn') === null -> no
// injection), so any fences the caller already put around the body are stripped
// before re-wrapping.
//
// Testable clock seam: OKR_NOW (ISO-8601) overrides the wall clock so the
// emitted timestamp is deterministic under test.
//
// Zero npm dependencies (node: builtins only). ASCII-clean identifiers.
import { readFileSync } from 'node:fs';
// OKF resource pointer: where the canonical org profile lives. A bare string
// pointer (OKF: "URL/path to the source"); no consumer reads it.
const RESOURCE = '~/.claude/okr/org/profil.md';
let body = '';
try {
body = readFileSync(0, 'utf8');
} catch {
body = '';
}
// Strip a leading/trailing frontmatter fence if the caller already wrapped the
// body, so the result is exactly one frontmatter block (see single-block note).
body = body.replace(/^\s*---\s*\r?\n/, '');
body = body.replace(/\r?\n---\s*\r?\n?\s*$/, '\n');
body = body.replace(/\s+$/, '');
const timestamp = process.env.OKR_NOW || new Date().toISOString();
const lines = [
'---',
'type: Organisasjonsprofil',
`resource: ${RESOURCE}`,
`timestamp: '${timestamp}'`,
];
if (body) lines.push(body);
lines.push('---', '');
process.stdout.write(lines.join('\n'));

108
scripts/export-pdf.py Normal file
View file

@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Render an OKR Markdown document to a print-ready A4 PDF.
Usage:
python3 export-pdf.py <input.md> <output.pdf>
Dependencies (documented prerequisite, not bundled):
pip install markdown weasyprint
# native libs on macOS: brew install pango
The script degrades gracefully: a missing Python package exits 1 with a pip
hint; a missing native library (Pango/cairo/gdk-pixbuf) surfaces at render
time as an OSError and exits 1 with a brew hint. Neither path leaks a traceback.
"""
import sys
PIP_HINT = (
"Mangler Python-avhengigheter for PDF-eksport.\n"
"Installer dem (dokumentert forutsetning, ikke bundlet):\n"
" pip install markdown weasyprint\n"
)
PANGO_HINT = (
"Klarte ikke aa rendre PDF: native bibliotek mangler "
"(Pango/cairo/gdk-pixbuf).\n"
"Installer dem (macOS):\n"
" brew install pango\n"
"Se: https://doc.courtbouillon.org/weasyprint/stable/first_steps.html\n"
)
try:
import markdown
import weasyprint
except ImportError:
sys.stderr.write(PIP_HINT)
sys.exit(1)
# A4 page geometry + table striping + traffic-light score classes.
# attr_list lets the Markdown author tag cells: `{: .score-green}` etc.
PAGE_CSS = """
@page {
size: A4;
margin: 20mm 18mm;
@bottom-right { content: counter(page) " / " counter(pages); font-size: 9pt; color: #666; }
}
body {
font-family: "Helvetica Neue", Arial, sans-serif;
font-size: 10.5pt;
line-height: 1.45;
color: #1a1a1a;
}
h1 { font-size: 20pt; border-bottom: 2px solid #2a4d69; padding-bottom: 4px; }
h2 { font-size: 15pt; color: #2a4d69; margin-top: 1.4em; }
h3 { font-size: 12.5pt; color: #4a6d89; }
table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: 9.5pt; }
th, td { border: 1px solid #ccc; padding: 5px 8px; text-align: left; vertical-align: top; }
th { background: #2a4d69; color: #fff; font-weight: 600; }
tbody tr:nth-child(even) { background: #f3f6f9; }
code { font-family: "SF Mono", Menlo, monospace; font-size: 9pt; background: #f0f0f0; padding: 1px 4px; border-radius: 3px; }
pre { background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; padding: 10px; overflow-x: auto; }
pre code { background: none; padding: 0; }
.score-green { background: #d6efd6 !important; color: #1a5c1a; font-weight: 600; }
.score-yellow { background: #fbf3cf !important; color: #7a5c00; font-weight: 600; }
.score-red { background: #f6d6d6 !important; color: #8c1a1a; font-weight: 600; }
"""
def build_html(markdown_text):
"""Convert Markdown to a full standalone HTML document with embedded CSS."""
body = markdown.markdown(
markdown_text,
extensions=["tables", "fenced_code", "codehilite", "toc", "attr_list"],
)
return (
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<style>" + PAGE_CSS + "</style></head><body>" + body + "</body></html>"
)
def main():
if len(sys.argv) != 3:
sys.stderr.write("Bruk: python3 export-pdf.py <input.md> <output.pdf>\n")
sys.exit(2)
in_path, out_path = sys.argv[1], sys.argv[2]
try:
with open(in_path, "r", encoding="utf-8") as handle:
markdown_text = handle.read()
except OSError as err:
sys.stderr.write("Kunne ikke lese inputfil '%s': %s\n" % (in_path, err))
sys.exit(1)
html_doc = build_html(markdown_text)
try:
# base_url anchors relative asset paths to the input file's directory.
weasyprint.HTML(string=html_doc, base_url=in_path).write_pdf(out_path)
except OSError:
sys.stderr.write(PANGO_HINT)
sys.exit(1)
sys.stderr.write("Skrev PDF: %s\n" % out_path)
if __name__ == "__main__":
main()

145
scripts/okf-check.mjs Normal file
View file

@ -0,0 +1,145 @@
#!/usr/bin/env node
// okf-check.mjs
// Validerer en OKF-bundle-rot. Kontrakt (de-facto OKF: kun `type` paakrevd):
// - Hver konsept-fil (.md unntatt index.md) MAA ha `type:` i frontmatter.
// - >= 1 fil uten type -> exit 1 + teller + navngir filene.
// - 0 filer uten type -> exit 0.
// Anbefalte felt (resource/title/description/timestamp) rapporteres som ADVARSEL,
// ikke feil. Rotens `okf_version` ekkoes for menneskelig sammenligning mot
// gjeldende standard (ingen auto-fetch — hooks/scripts er no-network; SC7 myket).
//
// Kjoeres PER ROT (prosjekt `.claude/okr/` + home `~/.claude/okr/org/`).
// Zero npm dependencies (node:-builtins).
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseFrontmatter } from '../lib/frontmatter.mjs';
import { TYPE_VOCAB } from '../lib/okf-vocab.mjs';
import { resolveBundleLink } from '../lib/okf-links.mjs';
const RECOMMENDED = ['resource', 'title', 'description', 'timestamp'];
// Markdown-lenker [tekst](maal) -- brukt av --strict-ingest til aa validere at
// hvert lenke-maal er en trygg, on-disk bundle-root-relativ .md (anti-RAG-poison).
const MD_LINK_RE = /\[[^\]]*\]\(([^)]+)\)/g;
// En title som selv baerer en markdown-lenke er en injeksjons-vektor -> avvises.
const TITLE_LINK_RE = /\[[^\]]*\]\([^)]*\)/;
// Drop-zone (raa innboks-filer) + skjulte kataloger (.cache osv.) skannes ALDRI:
// raa, ukonverterte og potensielt fiendtlige filer er ikke konsepter.
function isWalkableDir(name) {
return !name.startsWith('.') && name !== 'innboks';
}
// Alle konsept-filer (.md unntatt index.md) under root, rekursivt.
function walkConcepts(root) {
const out = [];
const walk = (dir) => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) {
if (isWalkableDir(e.name)) walk(p);
} else if (e.isFile() && e.name.endsWith('.md') && e.name !== 'index.md') {
out.push(p);
}
}
};
walk(root);
return out;
}
// Les rotens okf_version (markdown-tekst i index.md, ikke frontmatter). null hvis fravaerende.
function rootOkfVersion(root) {
const idx = join(root, 'index.md');
if (!existsSync(idx)) return null;
const m = readFileSync(idx, 'utf8').match(/^okf_version:\s*(.+)$/m);
return m ? m[1].trim() : null;
}
// strictIngest (default AV): paa skrivestien handheves det lukkede vokabularet
// + lenke-allow-listen. Lese-siden (default) forblir tolerant (exit 0/1 kun paa
// manglende type) -- denne porten er aktiv KUN ved ingestion (--strict-ingest).
export function checkBundle(root, { strictIngest = false } = {}) {
const concepts = walkConcepts(root);
const missingType = [];
const warnings = [];
const strictErrors = [];
for (const f of concepts) {
const raw = readFileSync(f, 'utf8');
const { get } = parseFrontmatter(raw);
const rel = relative(root, f);
const type = get('type');
if (!type) {
missingType.push(rel);
continue;
}
if (strictIngest) {
// 1. type maa vaere i det lukkede ingestion-vokabularet.
if (!TYPE_VOCAB.includes(type)) {
strictErrors.push(`${rel}: type «${type}» utenfor ingestion-vokabular`);
}
// 2. en title som baerer en markdown-lenke er en injeksjons-vektor.
const title = get('title');
if (title && TITLE_LINK_RE.test(title)) {
strictErrors.push(`${rel}: lenke-baerende title «${title}»`);
}
// 3. hver markdown-lenke maa resolvere til en trygg, on-disk bundle-fil.
for (const m of raw.matchAll(MD_LINK_RE)) {
const target = m[1];
const resolved = resolveBundleLink(target, root);
if (!resolved) strictErrors.push(`${rel}: utrygg lenke ${target}`);
else if (!existsSync(resolved)) strictErrors.push(`${rel}: dangling lenke ${target}`);
}
}
for (const field of RECOMMENDED) {
if (!get(field)) warnings.push(`${rel}: mangler anbefalt felt «${field}»`);
}
}
return {
scanned: concepts.length,
missingType,
warnings,
strictErrors,
strictIngest,
okfVersion: rootOkfVersion(root),
};
}
// --- CLI ---
const isMain = process.argv[1]
&& fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
const args = process.argv.slice(2);
const strictIngest = args.includes('--strict-ingest');
const root = args.find((a) => !a.startsWith('--'));
if (!root) {
process.stderr.write('Bruk: node okf-check.mjs <bundle-rot> [--strict-ingest]\n');
process.exit(2);
}
if (!existsSync(root)) {
process.stderr.write(`Bundle-rot finnes ikke: ${root}\n`);
process.exit(2);
}
const r = checkBundle(root, { strictIngest });
const out = [];
out.push(`OKF-sjekk: ${root}${strictIngest ? ' (strict-ingest)' : ''}`);
out.push(` Konsept-filer skannet: ${r.scanned}`);
out.push(` ${r.missingType.length} filer uten type:`);
for (const f of r.missingType) out.push(` - ${f}`);
if (strictIngest) {
out.push(` ${r.strictErrors.length} strict-ingest-feil:`);
for (const e of r.strictErrors) out.push(` x ${e}`);
}
out.push(` okf_version: ${r.okfVersion || 'MANGLER (rot-index uten okf_version)'}`);
out.push(` Advarsler (anbefalte felt): ${r.warnings.length}`);
for (const w of r.warnings) out.push(` ! ${w}`);
const failed = r.missingType.length > 0 || (strictIngest && r.strictErrors.length > 0);
let verdict;
if (!failed) verdict = 'OK: gyldig OKF-bundle';
else if (r.missingType.length > 0) verdict = `FEIL: ${r.missingType.length} fil(er) mangler type:`;
else verdict = `FEIL: ${r.strictErrors.length} strict-ingest-feil`;
out.push(verdict);
process.stdout.write(`${out.join('\n')}\n`);
process.exit(failed ? 1 : 0);
}

146
scripts/okf-index.mjs Normal file
View file

@ -0,0 +1,146 @@
#!/usr/bin/env node
// okf-index.mjs
// Genererer OKF-kompatible `index.md` per nivaa i en bundle-rot (prosjekt
// `.claude/okr/` eller home `~/.claude/okr/org/`). Verbatim OKF-«Documents/kb
// Layout»-index-form:
// # Overskrift
//
// okf_version: <ver> (KUN rot-index)
//
// * [title](relativ.md) - description
// Ingen frontmatter paa index.md (OKF-reservert). Konsept-filers `title`/
// `description` leses via lib/frontmatter.mjs. Underkataloger faar en peker til
// sin egen index.md. Kjoeres PER ROT (de to bundlene har ulik livssyklus).
//
// Idempotens / vedlikehold (NFR): en eksisterende index.md sin `# overskrift`,
// rotens `okf_version`-verdi, og menneske-skrevne beskrivelser for underkatalog-
// pekere bevares; konsept-entries regenereres alltid fra frontmatter (autoritativ
// kilde). Skriving er atomisk (temp + renameSync), jf. write-org-profile.mjs.
//
// Zero npm dependencies (node:-builtins).
import { readdirSync, readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs';
import { join, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseFrontmatter } from '../lib/frontmatter.mjs';
export const OKF_VERSION = 'kb-layout-2026-06';
// Drop-zone (raa innboks-filer) + skjulte kataloger (.cache osv.) er ikke nivaaer:
// de skal verken faa egen index.md eller listes som underkatalog-peker. Maa
// filtreres i BEGGE enumererings-steder (subdir-listing + rekursjon).
function isWalkableDir(name) {
return !name.startsWith('.') && name !== 'innboks';
}
// "strategisk-kontekst" -> "Strategisk kontekst"
function titleFromName(name) {
const spaced = name.replace(/[-_]+/g, ' ').trim();
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
}
// Parse en eksisterende index.md for bevaring: overskrift, okf_version, og
// beskrivelser pr. lenke (for underkatalog-pekere). Kaster aldri.
function parseExistingIndex(path) {
const result = { heading: null, okfVersion: null, descByLink: {} };
if (!existsSync(path)) return result;
for (const line of readFileSync(path, 'utf8').split('\n')) {
if (result.heading === null && line.startsWith('# ')) {
result.heading = line.slice(2).trim();
}
const ver = line.match(/^okf_version:\s*(.+)$/);
if (ver) result.okfVersion = ver[1].trim();
const entry = line.match(/^\*\s*\[([^\]]*)\]\(([^)]+)\)(?:\s*-\s*(.*))?$/);
if (entry) result.descByLink[entry[2]] = { title: entry[1], desc: (entry[3] || '').trim() };
}
return result;
}
// Saner en frontmatter-avledet tittel/beskrivelse for trygg, idempotent emit:
// noytraliser markdown-lenker (RAG-injeksjon), strip kontrolltegn + strooe ]/)
// som ville korrumpert round-trip-parsen (parseExistingIndex), kollaps whitespace,
// og cap lengden. Idempotent: sanitizeEntry(sanitizeEntry(x)) === sanitizeEntry(x).
function sanitizeEntry(s) {
if (!s) return '';
return String(s)
.replace(/[\x00-\x1f\x7f]/g, ' ') // kontrolltegn -> mellomrom
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // noytraliser markdown-lenker (behold tekst)
.replace(/[\])]/g, '') // strip strooe ] ) som brekker round-trip
.replace(/\s+/g, ' ')
.trim()
.slice(0, 200);
}
// Bygg en enkelt entry-linje paa OKF-form. Tittel/beskrivelse saneres (link
// forblir uroert -- kontrollert filnavn). Tom beskrivelse -> dropp ` - d`.
function entryLine(title, link, desc) {
const t = sanitizeEntry(title);
const d = sanitizeEntry(desc);
return d ? `* [${t}](${link}) - ${d}` : `* [${t}](${link})`;
}
// Generer og skriv index.md for EN katalog (ikke rekursivt). isRoot styrer okf_version.
function writeIndexFor(dir, isRoot, okfVersion) {
const existing = parseExistingIndex(join(dir, 'index.md'));
const dirents = readdirSync(dir, { withFileTypes: true });
const subdirs = dirents
.filter((e) => e.isDirectory() && isWalkableDir(e.name))
.map((e) => e.name)
.sort();
const concepts = dirents
.filter((e) => e.isFile() && e.name.endsWith('.md') && e.name !== 'index.md')
.map((e) => e.name)
.sort();
const heading = existing.heading
|| (isRoot ? 'OKF second brain' : titleFromName(basename(dir)));
const lines = [`# ${heading}`, ''];
if (isRoot) {
lines.push(`okf_version: ${existing.okfVersion || okfVersion}`, '');
}
for (const sd of subdirs) {
const link = `${sd}/index.md`;
const prev = existing.descByLink[link];
lines.push(entryLine(prev?.title || titleFromName(sd), link, prev?.desc || 'Underkatalog.'));
}
for (const c of concepts) {
const { get } = parseFrontmatter(readFileSync(join(dir, c), 'utf8'));
const title = get('title') || titleFromName(basename(c, '.md'));
lines.push(entryLine(title, c, get('description') || ''));
}
const content = `${lines.join('\n')}\n`;
const tmp = join(dir, `.index.md.${process.pid}.tmp`);
writeFileSync(tmp, content);
renameSync(tmp, join(dir, 'index.md'));
}
// Generer index.md for rot + alle underkataloger, rekursivt.
export function generateIndexes(root, opts = {}) {
const okfVersion = opts.okfVersion || OKF_VERSION;
const walk = (dir, isRoot) => {
writeIndexFor(dir, isRoot, okfVersion);
for (const e of readdirSync(dir, { withFileTypes: true })) {
if (e.isDirectory() && isWalkableDir(e.name)) walk(join(dir, e.name), false);
}
};
if (!existsSync(root)) throw new Error(`Bundle-rot finnes ikke: ${root}`);
walk(root, true);
}
// --- CLI ---
const isMain = process.argv[1]
&& fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
const root = process.argv[2];
if (!root) {
process.stderr.write('Bruk: node okf-index.mjs <bundle-rot> [--okf-version <ver>]\n');
process.exit(2);
}
const vi = process.argv.indexOf('--okf-version');
const okfVersion = vi !== -1 ? process.argv[vi + 1] : undefined;
generateIndexes(root, { okfVersion });
process.stdout.write(`OKF-index generert for ${root}\n`);
}

View file

@ -0,0 +1,65 @@
#!/usr/bin/env node
// write-org-profile.mjs
// Purpose: atomically write the OKR org profile to the reinstall-surviving
// home path (~/.claude/okr/org/profil.md). On ANY write failure
// (EACCES/EROFS/ENOSPC/pathguard-block) it circuit-breaks to the
// project-local, gitignored .claude/okr.local.md so internal org state never
// reaches a public mirror. Never exits non-zero -- the calling command must
// not be blocked. Reads the full profile content from stdin (fd 0).
// Zero npm dependencies (node: builtins only). ASCII-clean identifiers.
//
// Mirrors the canonical home path defined in
// hooks/scripts/inject-okr-context.mjs:14 (the most-specific-wins read side).
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { homedir } from 'node:os';
// Read the entire profile content from stdin. Under execFileSync (both Claude
// Code's command call and the tests) the child's stdin is a closed pipe, so
// EOF arrives immediately -- no hang.
let content = '';
try {
content = readFileSync(0, 'utf8');
} catch {
content = '';
}
const homeTarget = join(homedir(), '.claude', 'okr', 'org', 'profil.md');
// Atomic write: temp file in the SAME directory, then renameSync over the
// target. rename is atomic on the same filesystem -- a crash mid-write can
// never leave a half-written profil.md.
function writeAtomic(target, data) {
const dir = dirname(target);
mkdirSync(dir, { recursive: true });
const tmp = join(dir, `profil.md.${process.pid}.tmp`);
writeFileSync(tmp, data);
renameSync(tmp, target);
}
try {
writeAtomic(homeTarget, content);
process.stdout.write(homeTarget);
process.exit(0);
} catch (err) {
// Circuit-breaker: the home write failed. Fall back to the project-local,
// gitignored config so org state stays off any public mirror. The cycle /
// historikk tree remains cwd-bound regardless; only the profile migrates.
const fallback = join(process.cwd(), '.claude', 'okr.local.md');
try {
writeAtomic(fallback, content);
process.stderr.write(
`notice: kunne ikke skrive hjem-profil (${err.code || err.message}); ` +
`falt tilbake til prosjektlokal ${fallback}\n`,
);
process.stdout.write(fallback);
} catch (err2) {
process.stderr.write(
`notice: kunne ikke skrive verken hjem-profil eller prosjektlokal fallback ` +
`(${err2.code || err2.message})\n`,
);
}
process.exit(0);
}

View file

@ -2,7 +2,7 @@
name: okr-offentlig-sektor name: okr-offentlig-sektor
description: >- description: >-
OKR (Objectives and Key Results) for Norwegian public sector: writing OKR, reviewing OKR quality, cascading OKR from strategy to team, tracking progress, running OKR meetings, translating tildelingsbrev to OKR. Also CFR, OKR antipatterns, scoring, Oboard. Triggers on: "OKR", "skriv OKR", "vurder OKR", "OKR-scoring", "kaskadere OKR", "tildelingsbrev til OKR", "OKR for offentlig sektor". OKR (Objectives and Key Results) for Norwegian public sector: writing OKR, reviewing OKR quality, cascading OKR from strategy to team, tracking progress, running OKR meetings, translating tildelingsbrev to OKR. Also CFR, OKR antipatterns, scoring, Oboard. Triggers on: "OKR", "skriv OKR", "vurder OKR", "OKR-scoring", "kaskadere OKR", "tildelingsbrev til OKR", "OKR for offentlig sektor".
version: "1.0.0" version: "1.6.1"
--- ---
# OKR Skill for Offentlig Sektor (Norge) # OKR Skill for Offentlig Sektor (Norge)
@ -29,22 +29,22 @@ Objective: [Verb] + [clear outcome/improvement]
**Example**: **Example**:
``` ```
Objective: Forbedre trafikksikkerhet i skolesoner Objective: Forbedre trafikksikkerhet i skolesoner
KR1: Redusere ulykker i skolesoner med 25% (fra 40 til 30 per ar) KR1: Redusere ulykker i skolesoner med 25% (fra 40 til 30 per år)
KR2: 100% av hoyrisiko-skolesoner far nye fartshumper innen august KR2: 100% av høyrisiko-skolesoner får nye fartshumper innen august
KR3: 90% av foreldre vurderer skolesoner som trygge (via sporreundersokelse) KR3: 90% av foreldre vurderer skolesoner som trygge (via spørreundersøkelse)
``` ```
### 2. Review OKR Quality ### 2. Review OKR Quality
When users present existing OKR, evaluate against these criteria and provide concrete rewrites: When users present existing OKR, evaluate against these criteria and provide concrete rewrites:
- **Good**: Outcome-focused, measurable, ambitious but achievable, clear strategy link - **Good**: Outcome-focused, measurable, ambitious but achievable, clear strategy link
- **Common errors**: Activity-oriented ("Gjennomfore 5 moter"), vague ("Forbedre kundeservice"), sandbagging, not measurable, no link to higher goals - **Common errors**: Activity-oriented ("Gjennomføre 5 møter"), vague ("Forbedre kundeservice"), sandbagging, not measurable, no link to higher goals
### 3. Track Progress ### 3. Track Progress
To help update OKR status: To help update OKR status:
- Collect current numbers for each KR - Collect current numbers for each KR
- Calculate progression (0.0-1.0 scale): 0.7-0.8 = success for stretch goals, <0.5 = needs intervention - Calculate progression (0.0-1.0 scale): 0.7 = expected for aspirational, 0.6-0.7 = sweet spot (Google/Doerr), <0.5 = needs intervention
- Assess status: on track / at risk / blocked - Assess status: on track / at risk / blocked
- Suggest corrective actions and generate update text for meetings/reports - Suggest corrective actions and generate update text for meetings/reports
@ -129,6 +129,7 @@ All reference material is in `references/`:
### Methodology ### Methodology
- `okr-framework.md` — Core methodology, scoring, cycle management - `okr-framework.md` — Core methodology, scoring, cycle management
- `okr-quality-rubrics.md` — Anchored quality rubric (5 anchors per dimension) for `/okr:kvalitet`
- `okr-examples.md` — Good and bad examples from public sector - `okr-examples.md` — Good and bad examples from public sector
- `okr-antipatterns.md` — 19 common OKR mistakes - `okr-antipatterns.md` — 19 common OKR mistakes
- `okr-sources.md` — Bibliographic evidence base - `okr-sources.md` — Bibliographic evidence base
@ -142,7 +143,7 @@ All reference material is in `references/`:
### Governance ### Governance
- `okr-offentlig-governance.md` — Tildelingsbrev, political steering, audit readiness - `okr-offentlig-governance.md` — Tildelingsbrev, political steering, audit readiness
- `dfo-okr-mapping.md` — DFO terminology to OKR terminology bridge - `dfo-okr-mapping.md` — DFØ terminology to OKR terminology bridge
- `okr-implementation.md` — Rollout methodology and change management - `okr-implementation.md` — Rollout methodology and change management
- `okr-integrations.md` — OKR + Scrum/Kanban/SAFe and tool integration - `okr-integrations.md` — OKR + Scrum/Kanban/SAFe and tool integration
- `individual-vs-team-okr.md` — Team vs individual OKR rationale and alternatives - `individual-vs-team-okr.md` — Team vs individual OKR rationale and alternatives

View file

@ -1,17 +1,17 @@
# DFO-terminologi og OKR: Begrepsbro # DFØ-terminologi og OKR: Begrepsbro
Direktoratet for forvaltning og økonomistyring (DFO) bruker sitt eget rammeverk Direktoratet for forvaltning og økonomistyring (DFØ) bruker sitt eget rammeverk
for mål- og resultatstyring i staten. Denne filen mapper DFO-begreper til for mål- og resultatstyring i staten. Denne filen mapper DFØ-begreper til
OKR-begreper for å bygge bro mellom det offisielle styringsspråket og OKR-begreper for å bygge bro mellom det offisielle styringsspråket og
OKR-metodikken. OKR-metodikken.
## Hvorfor denne broen trengs ## Hvorfor denne broen trengs
Norsk offentlig sektor snakker "DFO-språk" — resultmål, styringsparametere, Norsk offentlig sektor snakker "DFØ-språk" — resultmål, styringsparametere,
resultatindikatorer. OKR-verden snakker om Objectives, Key Results, stretch goals. resultatindikatorer. OKR-verden snakker om Objectives, Key Results, stretch goals.
Denne terminologiske kløften skaper motstand og forvirring ved OKR-innføring. Denne terminologiske kløften skaper motstand og forvirring ved OKR-innføring.
Ved å vise at OKR-konseptene har direkte paralleller i DFO-rammeverket, senker Ved å vise at OKR-konseptene har direkte paralleller i DFØ-rammeverket, senker
vi terskelen og viser at OKR ikke er noe fremmed — det er en operasjonalisering vi terskelen og viser at OKR ikke er noe fremmed — det er en operasjonalisering
av prinsipper staten allerede anerkjenner. av prinsipper staten allerede anerkjenner.
@ -19,16 +19,16 @@ av prinsipper staten allerede anerkjenner.
## Begrepsmapping ## Begrepsmapping
| DFO-begrep | OKR-begrep | Forklaring | | DFØ-begrep | OKR-begrep | Forklaring |
|------------|------------|------------| |------------|------------|------------|
| **Hovedmål** | **Strategisk Objective (årlig)** | Overordnet retning for virksomheten. DFOs hovedmål tilsvarer organisasjonens årlige Objectives. | | **Hovedmål** | **Strategisk Objective (årlig)** | Overordnet retning for virksomheten. DFØs hovedmål tilsvarer organisasjonens årlige Objectives. |
| **Resultatmål** | **Objective (syklus)** | Konkret mål som skal oppnås. I OKR formuleres dette mer inspirerende og outcome-orientert. | | **Resultatmål** | **Objective (syklus)** | Konkret mål som skal oppnås. I OKR formuleres dette mer inspirerende og outcome-orientert. |
| **Styringsparameter** | **Key Result** | Målbar indikator som viser om målet nås. OKR-KR er mer ambisiøse (stretch) enn DFOs styringsparametere. | | **Styringsparameter** | **Key Result** | Målbar indikator som viser om målet nås. OKR-KR er mer ambisiøse (stretch) enn DFØs styringsparametere. |
| **Resultatindikator** | **Key Result (metrikk-type)** | Kvantifiserbar måling. I OKR skiller vi mellom leading og lagging indikatorer. | | **Resultatindikator** | **Key Result (metrikk-type)** | Kvantifiserbar måling. I OKR skiller vi mellom leading og lagging indikatorer. |
| **Virksomhetsplan** | **OKR-sett (organisasjonsnivå)** | Samlet oversikt over mål og tiltak. OKR erstatter ikke virksomhetsplanen men operasjonaliserer den. | | **Virksomhetsplan** | **OKR-sett (organisasjonsnivå)** | Samlet oversikt over mål og tiltak. OKR erstatter ikke virksomhetsplanen men operasjonaliserer den. |
| **Tildelingsbrev** | **Input til strategiske Objectives** | DFOs tildelingsbrev setter rammer og krav. OKR oversetter disse til inspirerende, målbare mål. | | **Tildelingsbrev** | **Input til strategiske Objectives** | DFØs tildelingsbrev setter rammer og krav. OKR oversetter disse til inspirerende, målbare mål. |
| **Etatsstyring** | **OKR alignment/kaskade** | Prosessen der departement styrer etat. OKR gjør dette mer transparent og gjensidig. | | **Etatsstyring** | **OKR alignment/kaskade** | Prosessen der departement styrer etat. OKR gjør dette mer transparent og gjensidig. |
| **Mål- og resultatstyring** | **OKR som metodikk** | DFOs overordnede styringsprinsipp. OKR er en konkret implementering av dette prinsippet. | | **Mål- og resultatstyring** | **OKR som metodikk** | DFØs overordnede styringsprinsipp. OKR er en konkret implementering av dette prinsippet. |
| **Rapportering til dep.** | **OKR scoring og review** | Periodisk resultatrapportering. OKR bruker 0.0-1.0 scoring med hyppigere kadense. | | **Rapportering til dep.** | **OKR scoring og review** | Periodisk resultatrapportering. OKR bruker 0.0-1.0 scoring med hyppigere kadense. |
| **Samfunnseffekt** | **Aspirational Objective** | Langsiktig effekt for samfunnet. I OKR formulert som ambisiøst stretch-mål. | | **Samfunnseffekt** | **Aspirational Objective** | Langsiktig effekt for samfunnet. I OKR formulert som ambisiøst stretch-mål. |
| **Brukereffekt** | **Team Objective (outcome)** | Effekt for brukerne av tjenestene. OKR fokuserer på outcome fremfor output. | | **Brukereffekt** | **Team Objective (outcome)** | Effekt for brukerne av tjenestene. OKR fokuserer på outcome fremfor output. |
@ -38,21 +38,23 @@ av prinsipper staten allerede anerkjenner.
## Viktige forskjeller ## Viktige forskjeller
| Dimensjon | DFO mål- og resultatstyring | OKR | | Dimensjon | DFØ mål- og resultatstyring | OKR |
|-----------|----------------------------|-----| |-----------|----------------------------|-----|
| **Retning** | Primært top-down (departement → etat) | Hybrid: top-down retning + bottom-up forslag | | **Retning** | Primært top-down (departement → etat) | Hybrid: top-down retning + bottom-up forslag |
| **Ambisjonsnivå** | Realistisk (100% = forventet) | Ambisiøst (70% = suksess for stretch) | | **Ambisjonsnivå** | Realistisk (100% = forventet) | Ambisiøst (70% = suksess for stretch) |
| **Kadense** | Årlig med tertialrapportering | 4-måneders sykluser med månedlig check-in | | **Kadense** | Månedlig til statsregnskapet; tertial-/halvårsrapportering der tildelingsbrevet krever det; årlig årsrapport | 4-måneders sykluser med månedlig check-in |
| **Formål** | Styring og kontroll | Læring og fokus | | **Formål** | Styring og kontroll | Læring og fokus |
| **Kobling til lønn** | Indirekte via medarbeidersamtale | Eksplisitt frakoblet | | **Kobling til lønn** | Indirekte via medarbeidersamtale | Eksplisitt frakoblet |
| **Transparens** | Hierarkisk (opp til departement) | Åpen (alle ser alles OKR) | | **Transparens** | Hierarkisk (opp til departement) | Åpen (alle ser alles OKR) |
| **Justering** | Formell prosess | Fleksibel mid-cycle justering | | **Justering** | Formell prosess | Fleksibel mid-cycle justering |
> **Om rapporteringskadens (presisering):** Det finnes ikke ett fast «tertialregime» i staten. Rapportering til statsregnskapet er **månedlig**. Interim-rapportering til eget departement (ofte med status per **30. april og 31. august**) settes i **tildelingsbrevet** der den brukes — det er ikke et generelt regelverkspåbud, og mange virksomheter bruker halvårs- eller særskilt rapportering i stedet. **Årsrapport** er obligatorisk for alle (innsending innen 15. mars, publisering innen 1. mai). En 4-måneders OKR-syklus *aligner med* en tertialkadens **der virksomheten faktisk bruker det**, men er ikke en alignment mot et fast lovbestemt regime. **Sjekk eget tildelingsbrev** for de reelle rapporteringspunktene. (Kilde: DFØ, [Veileder i etatsstyring Rapportering](https://www.dfo.no/rapportering); økonomiregelverket.)
--- ---
## Praktisk bruk: Slik snakker du om OKR i DFO-kontekst ## Praktisk bruk: Slik snakker du om OKR i DFØ-kontekst
### Overfor ledere med DFO-bakgrunn ### Overfor ledere med DFØ-bakgrunn
Ikke si: "Vi skal innføre OKR i stedet for mål- og resultatstyring." Ikke si: "Vi skal innføre OKR i stedet for mål- og resultatstyring."
@ -60,7 +62,7 @@ Si heller: "Vi skal bruke OKR som en operasjonaliseringsmetodikk for vår
mål- og resultatstyring. Det gir oss kortere feedback-loops og bedre fokus mål- og resultatstyring. Det gir oss kortere feedback-loops og bedre fokus
innenfor rammene av tildelingsbrevet." innenfor rammene av tildelingsbrevet."
### Overfor DFO/departement ### Overfor DFØ/departement
Ikke si: "Vi har erstattet styringsparametere med Key Results." Ikke si: "Vi har erstattet styringsparametere med Key Results."
@ -79,26 +81,34 @@ og teamdrevet. Vi bruker kortere sykluser og setter mål sammen."
## OKR som komplement, ikke erstatning ## OKR som komplement, ikke erstatning
OKR erstatter IKKE DFOs mål- og resultatstyring. OKR er et verktøy for å OKR erstatter IKKE DFØs mål- og resultatstyring. OKR er et verktøy for å
**operasjonalisere** de strategiske målene som allerede finnes i **operasjonalisere** de strategiske målene som allerede finnes i
tildelingsbrev og virksomhetsplan. tildelingsbrev og virksomhetsplan.
``` ```
Tildelingsbrev (DFO-rammeverk) Tildelingsbrev (DFØ-rammeverk)
└── Virksomhetsplan (DFO-rammeverk) └── Virksomhetsplan (DFØ-rammeverk)
└── OKR (operasjonalisering) └── OKR (operasjonalisering)
├── Strategiske OKR (årlige, fra virksomhetsplan) ├── Strategiske OKR (årlige, fra virksomhetsplan)
└── Taktiske OKR (syklusbaserte, fra team) └── Taktiske OKR (syklusbaserte, fra team)
``` ```
**Rapportering oppover** (til departement) følger DFOs format. **Rapportering oppover** (til departement) følger DFØs format.
**Styring nedover** (til team) bruker OKR-metodikk. **Styring nedover** (til team) bruker OKR-metodikk.
### MRS-forankring og hvorfor OKR passer som lag oppå
Mål- og resultatstyring er statens grunnleggende styringsprinsipp, nedfelt i **økonomiregelverket § 4 «Grunnleggende styringsprinsipper»** (Reglement for økonomistyring i staten). OKR er ikke et alternativt regime, men et *lag oppå* MRS for utviklings- og endringsarbeid, mens MRS ivaretar drift, kontroll og rapportering til departementet.
Den konseptuelle broen: DFØ beskriver selv MRS som å «konsentrere seg om *hva* … og overlate *hvordan* til nivåene under» — og som «til forveksling likt … tillitsbasert styring». Det er samme *hva/hvordan*-logikk OKR bygger på. **Merk:** dette er pluginets egen syntese av at OKR og MRS er konseptuelt forenlige — DFØ omtaler ikke OKR i sine veiledere, og OKR er *ikke* offisielt anbefalt som styringsverktøy i staten.
**Spenning å være obs på:** OKRs ambisiøse «safe-to-fail»-logikk (0.7 = bra for aspirational) står i en reell spenning mot MRS' ansvarliggjørings- og rapporteringslogikk. Kobles ambisiøse OKR ukritisk til styringsdialogens måloppnåelseskrav, risikerer man sandbagging (forsiktige mål). Hold forpliktende leveranser (committed/MRS) og ambisiøse strekkmål (aspirational/OKR) tydelig atskilt.
--- ---
## Ressurser ## Ressurser
- DFO: [Mål- og resultatstyring](https://dfo.no/fagomrader/styring-i-staten/mal-og-resultatstyring) - DFØ: [Mål- og resultatstyring](https://dfo.no/fagomrader/styring-i-staten/mal-og-resultatstyring)
- DFO: [Etatsstyring](https://dfo.no/fagomrader/styring-i-staten/etatsstyring) - DFØ: [Etatsstyring](https://dfo.no/fagomrader/styring-i-staten/etatsstyring)
- `okr-offentlig-governance.md` — Tildelingsbrev-analyse og Riksrevisjon-compliance - `okr-offentlig-governance.md` — Tildelingsbrev-analyse og Riksrevisjon-compliance
- `okr-framework.md` — OKR-metodikk i detalj - `okr-framework.md` — OKR-metodikk i detalj

View file

@ -2,26 +2,26 @@
## Anbefaling: Team-OKR som default ## Anbefaling: Team-OKR som default
Team-OKR anbefales fremfor individuell OKR for de fleste roller. Dette er i trad med radene fra John Doerr, Google, og ledende OKR-praktikere. Spotify sluttet med individuell OKR i 2013 av gode grunner. Team-OKR anbefales fremfor individuell OKR for de fleste roller. Dette er i tråd med rådene fra John Doerr, Google, og ledende OKR-praktikere. Spotify sluttet med individuell OKR i 2013 av gode grunner.
## Hvorfor team-OKR fungerer bedre ## Hvorfor team-OKR fungerer bedre
| Problem med individuell OKR | Hvorfor team-OKR er bedre | | Problem med individuell OKR | Hvorfor team-OKR er bedre |
|---------------------------|--------------------------| |---------------------------|--------------------------|
| Skaper konkurranse fremfor samarbeid | Team-OKR fremmer kollektivt ansvar | | Skaper konkurranse fremfor samarbeid | Team-OKR fremmer kollektivt ansvar |
| Vanskelig a eie outcomes alene | De fleste resultater krever samarbeid | | Vanskelig å eie outcomes alene | De fleste resultater krever samarbeid |
| Blir lett aktivitetslister | Team-OKR holder outcome-fokus | | Blir lett aktivitetslister | Team-OKR holder outcome-fokus |
| Overhead ved kaskadejustering | Enklere a justere ved endringer | | Overhead ved kaskadejustering | Enklere å justere ved endringer |
| Risiko for kobling til evaluering | Lettere a holde adskilt fra HR | | Risiko for kobling til evaluering | Lettere å holde adskilt fra HR |
## Nar individuell OKR KAN fungere ## Når individuell OKR KAN fungere
Kun i spesifikke operasjonelle roller hvor: Kun i spesifikke operasjonelle roller hvor:
- Personen har **full kontroll** over outcome - Personen har **full kontroll** over outcome
- Suksess avhenger **ikke** av samarbeid med andre - Suksess avhenger **ikke** av samarbeid med andre
- Eksempler: Selger med eget territorium, kundesuksess-ansvarlig med definert portefolje - Eksempler: Selger med eget territorium, kundesuksess-ansvarlig med definert portefølje
Selv i disse tilfellene: **aldri koble til lonn, bonus eller evaluering**. Selv i disse tilfellene: **aldri koble til lønn, bonus eller evaluering**.
## Alternativer for personlig utvikling ## Alternativer for personlig utvikling
@ -29,8 +29,8 @@ I stedet for individuell OKR, bruk:
| Behov | Alternativ | Beskrivelse | | Behov | Alternativ | Beskrivelse |
|-------|------------|-------------| |-------|------------|-------------|
| Personlig utvikling | **Personal Development Plan (PDP)** | Karrieremal, laring, kompetanse | | Personlig utvikling | **Personal Development Plan (PDP)** | Karrieremål, læring, kompetanse |
| Operasjonell accountability | **KPIer med targets** | Lopende metriker for rollen | | Operasjonell accountability | **KPIer med targets** | Løpende metriker for rollen |
| 1:1-struktur | **CFR** (Conversations, Feedback, Recognition) | Se `cfr-framework.md` | | 1:1-struktur | **CFR** (Conversations, Feedback, Recognition) | Se `cfr-framework.md` |
| Bidragsvurdering | **Team-OKR + bidragsvurdering** | Vurder bidrag til team-OKR | | Bidragsvurdering | **Team-OKR + bidragsvurdering** | Vurder bidrag til team-OKR |
@ -38,15 +38,15 @@ I stedet for individuell OKR, bruk:
- **1:1 OKR Conversation Guide** (via `/okr:møter`): Strukturerte samtaler om bidrag til team-OKR - **1:1 OKR Conversation Guide** (via `/okr:møter`): Strukturerte samtaler om bidrag til team-OKR
- **CFR-rammeverk**: Maler for samtaler, feedback og anerkjennelse - **CFR-rammeverk**: Maler for samtaler, feedback og anerkjennelse
- **Veiledning**: Hvis organisasjonen insisterer pa individuell OKR, hjelper vi med sikker implementering - **Veiledning**: Hvis organisasjonen insisterer på individuell OKR, hjelper vi med sikker implementering
## Vanlige sporsmal ## Vanlige spørsmål
**"Men vi har alltid satt individuelle mal?"** **"Men vi har alltid satt individuelle mål?"**
OKR er ikke det samme som individuelle mal. OKR er strategisk alignment-verktoy for organisasjonen. Individuelle mal kan fortsatt eksistere via PDP eller KPIer. OKR er ikke det samme som individuelle mål. OKR er strategisk alignment-verktøy for organisasjonen. Individuelle mål kan fortsatt eksistere via PDP eller KPIer.
**"Hvordan vurderer vi folk uten individuelle OKR?"** **"Hvordan vurderer vi folk uten individuelle OKR?"**
Via bidragsvurdering til team-OKR, 360-feedback, KPIer for rollen, og kvalitative vurderinger fra ledere og kollegaer. Via bidragsvurdering til team-OKR, 360-feedback, KPIer for rollen, og kvalitative vurderinger fra ledere og kollegaer.
**"Ledelsen vil ha individuelle OKR"** **"Ledelsen vil ha individuelle OKR"**
Del denne veiledningen og forskningen bak. foresla en hybrid hvor ledere har team-OKR, ikke personlige OKR. Del denne veiledningen og forskningen bak. Foreslå en hybrid hvor ledere har team-OKR, ikke personlige OKR.

View file

@ -83,7 +83,7 @@ For hvert Key Result:
--- ---
## 3. Kvartals Review-møte (End-of-Cycle) ## 3. Syklusreview-møte (End-of-Cycle)
**Formål**: Vurdere OKR-resultater, score, feire suksesser **Formål**: Vurdere OKR-resultater, score, feire suksesser
**Varighet**: 1-1.5 timer **Varighet**: 1-1.5 timer

View file

@ -222,3 +222,82 @@ Ikke alle mål kan være tall. Eksempler på kvalitative KR:
**Vær realistisk om datainnsamling** ikke velg KR du ikke kan måle pålitelig **Vær realistisk om datainnsamling** ikke velg KR du ikke kan måle pålitelig
**Balansér mellom input, output, og outcome** men prioriter outcome (f.eks. "færre ulykker" > "antall fartshumper bygget") **Balansér mellom input, output, og outcome** men prioriter outcome (f.eks. "færre ulykker" > "antall fartshumper bygget")
**Unngå vanity metrics** tall som ser imponerende ut men ikke driver reell verdi (f.eks. "antall møter holdt") **Unngå vanity metrics** tall som ser imponerende ut men ikke driver reell verdi (f.eks. "antall møter holdt")
---
## Offentlig digitalisering og tjenestekvalitet (kryss-sektor)
> Denne seksjonen gjelder **alle forvaltningsområder** (ikke bare transport/vei). Hver faktapåstand bærer en datert, lenket kilde; tall som ikke er primærkilde-verifisert er eksplisitt merket **«Ikke verifisert»** (verifiseringsplikt — dette biblioteket deles med ledelse/Riksrevisjon). Alt regelverk under er i bevegelse 20252026; se **Review-kadens** nederst og bruk `/okr:freshen-references` for currency-polling.
### KR-styrkemerking
Hver metrikk under er tagget med ett av tre nivåer, slik at man velger med åpne øyne:
- **sterk-KR** — outcome-nær, ekstern/uavhengig datakilde, vanskelig å gamifisere.
- **brukbar** — gyldig KR, men krever forsiktig baseline/target eller har en metodisk forutsetning.
- **vanity-felle** — ser imponerende ut, men driver lite reell verdi (ofte selv-deklarert eller binær). Unngå som primær-KR.
### Statlig vs kommunalt anvendelses-skille (les først)
Styringsregimet avgjør **hvordan** en metrikk kan settes — ikke bare hvilken:
- **Statlig etat:** mål- og resultatstyring (MRS), hjemlet i økonomireglementet §4. Instrument: **tildelingsbrev** (årlig styringsbrev dept→etat). Implikasjon: mål/metrikker **kan pålegges top-down** som styringsparametere og kaskaderes inn i Objectives. Kilde: [DFØ krav til MRS i staten](https://www.dfo.no/fagomrader/styring-i-staten/mal-og-resultatstyring/hvilke-krav-gjelder-mal-og-resultatstyring-i-staten) (offisiell).
- **Kommune:** kommuneloven §§2-1/2-2 (LOV-2018-06-22-83) — selvstyre, hver kommune er eget rettssubjekt, **ingen tildelingsbrev**. Implikasjon: metrikker **kan ikke pålegges** kommuner top-down; de er **lokalt eid**. Nasjonale organer kan anbefale/benchmarke/finansiere/sette lovpålagte minstekrav — ikke styre via brev. Kilde: [Regjeringen veileder om statlig styring av kommuner](https://www.regjeringen.no/no/dokumenter/veileder-om-statlig-styring-av-kommuner-og-fylkeskommuner/id2791598) (offisiell/juridisk).
- **Nyanse for governance/Riksrevisjon:** ren to-boks-modell er upresis. Bruk minst **tre nivåer** (+ fylkeskommune; Oslo er begge) **+ org-form-lag** (forvaltningsorgan, statsforetak (SF), helseforetak/RHF, interkommunale selskap (IKS), kommunale foretak (KF)) — compliance-plikt avhenger av org-form, ikke grov dikotomi. Kilde: [SNL offentlig sektor](https://snl.no/offentlig_sektor); [DFØ/Difi-rapport 2018:8 organisasjonsformer](https://www.dfo.no/sites/default/files/fagomr%C3%A5der/Rapporter/Rapporter-Difi/difi-rapport_2018-8_organisasjonsformer_i_offentlig_sektor._en_kartlegging.pdf) (2018; org-form-tall kan være utdaterte). *Provenans: offisiell + community.*
### Digitaliseringsmodenhet
Det finnes **ikke ett samlet, nasjonalt indekstall** for offentlig digitalisering bredt — bruk de tre eksternt eide internasjonale indeksene + Digdirs egne indikatorsett. (Difi er oppløst og innlemmet i Digdir; mål med gammelt Difi-navn er institusjonelt utdatert.)
- **UN EGDI (E-Government Development Index):** Norge = **0,9602** («Very High»), UN E-Government Survey 2024; oppdateres biennalt. Global rang ~15 er **Ikke verifisert** (tvetydig i sekundærkilde — bekreft mot primær UN DESA-rapport før publisering). Kilde: [UN E-Government Survey 2024](https://www.unescap.org/sites/default/d8files/event-documents/UN%20DESA%20E-gov%20Survey%202024%20Insights_for%20ESCAP%20event_24092024-ak.pdf). **brukbar** (ekstern indeks; måler land, ikke ett team).
- **EU eGov Benchmark (eGovernment Benchmark):** Norge score **82** (EU27-snitt 76), Capgemini for Kommisjonen 2024; under Danmark/Finland/Estland. Standalone EU DESI er avviklet siden 2023 (foldet inn i «State of the Digital Decade»). Kilde: [eGovernment Benchmark 2024 (Capgemini)](https://www.capgemini.com/wp-content/uploads/2024/07/eGovernment-Report-2024.pdf) (vendor/EU). **brukbar**.
- **OECD DGI (Digital Government Index):** Norge **5. av 34** OECD-land (2025-runde), best i Norden/Baltikum. Kilde: [Regjeringen ny måling av digitalisering (OECD DGI 5/34)](https://www.regjeringen.no/no/aktuelt/ny-maling-av-digitalisering-i-offentlig-sektor-norge-i-toppen/id3149478/) (offisiell). **brukbar**.
- **Digdir «Rikets digitale tilstand»** (nærmeste nasjonale scorecard): **57 indikatorer** under 6 mål (skår 1,003,00). Nasjonal digitaliseringsstrategi 20242030 «Fremtidens digitale Norge» definerer **23 nøkkelindikatorer** (14 med konkrete 2030-måltall); nullpunktmåling 2024, første statusrapportering høsten 2025. Kilde: [Digdir Rikets digitale tilstand (2024)](https://www.digdir.no/rikets-digitale-tilstand/samlet-vurdering-av-maloppnaelse-i-rikets-digitale-tilstand-2024/5859); [Fremtidens digitale Norge (strategi-PDF)](https://www.regjeringen.no/contentassets/c499c3b6c93740bd989c43d886f65924/no/pdfs/nasjonal-digitaliseringsstrategi_ny.pdf) (offisiell). **brukbar** (org kan tracke egne av de 57/23-indikatorene).
- Nasjonal indeks for digital inkludering (Digdir + Nkom, lansert 31. okt 2025) — avgrenset til digital inkludering, ikke e-forvaltning bredt; dimensjoner/metodikk **Ikke verifisert**.
### Universell utforming (WCAG)
- **WCAG 2.1 nivå A + AA** (IKKE 2.0) er lovpålagt for offentlig sektor: må møte **48 av 78** suksesskriterier (privat: 35 fra WCAG 2.0). Hjemmel: forskrift om universell utforming av IKT **FOR-2013-06-21-732 §4b**, via EN 301 549 (WAD-direktivet 2016/2102). Kilde: [Lovdata forskrift uu IKT](https://lovdata.no/dokument/SF/forskrift/2013-06-21-732); [uutilsynet EUs webdirektiv (WAD)](https://www.uutilsynet.no/webdirektivet-wad/eus-webdirektiv-wad/265); [NAV uu hva gjelder (WCAG 2.1)](https://navikt.github.io/uu/hva-gjelder/) (offisiell/juridisk).
- **Tilgjengelighetserklæring publisert + korrekt** (maskinlesbar, obligatorisk) — **sterk-KR** (binær, kryss-sjekkes av uutilsynets statusmåling fra 20242025). Kilde: [uutilsynet status tilgjengelegheitserklæringar](https://www.uutilsynet.no/innsikt-og-analyse/status-tilgjengelegheitserklaeringar/1800) (tilsyn).
- **uutilsynet statusmåling-score** (forenklet, automatisert kontroll av ~250500 nettsteder/år) — **brukbar** (ekstern, men ikke formelt tilsyn med pålegg).
- **«WCAG-compliant» selv-deklarert** uten uutilsynet-kryss-sjekk — **vanity-felle** (~78 % oppgir selv «delvis»). Kilde: [uutilsynet status](https://www.uutilsynet.no/innsikt-og-analyse/status-tilgjengelegheitserklaeringar/1800).
- **EAA (European Accessibility Act, 2019/882): IKKE i kraft i Norge** (gjelder i EU fra 28. juni 2025, ikke innlemmet i EØS-avtalen ennå; EFTA-tidspunkt uklart). Virksomheter som selger inn i EU-markedet må likevel etterleve EAA for markedstilgang. WCAG 2.2 (okt 2023) er praktiker-anbefalt, men **ikke norsk lov**. Kilde: [Lov & Data 2025-03 EAA-status Norden](https://lod.lovdata.no/asset/issue/2025/03/Lovogdata-2025-03.pdf) (juridisk; medium).
### Saksbehandling og klage
- Det finnes **ingen enkelt statutær «klagebehandlingsfrist»** (resolusjonsfrist) — en vanlig forveksling.
- **§11a (saksbehandlingstid)** = «uten ugrunnet opphold»; **foreløpig svar** kreves hvis et enkeltvedtak ikke kan besvares innen **1 måned**. Foreløpig svar er ikke et vedtak og kan ikke påklages. Kilde: [Jusinfo saksbehandlingstid/§11a](https://jusinfo.no/forvaltningsrett/saksbehandlingstid-og-forelopig-svar/saksbehandlingstid/) (community; medium).
- **Klagefrist (å fremme klage), §29:** generelt **3 uker** fra underretning; **varierer per sektor (NAV: 6 uker)**. For vedtak som gir andre en rett: maks 3 måneder. Kilde: [NAV klagerettigheter](https://www.nav.no/klagerettigheter) (offisiell).
- **Median saksbehandlingstid + % saker innen lovpålagt frist****sterk-KR** (publiseres jevnlig av f.eks. UDI/Skatteetaten/NAV; ekstern, outcome-nær). Kilde: [Skatteetaten saksbehandlingstid](https://www.skatteetaten.no/en/contact/case-processing-time/) (offisiell).
- 🔴 **Currency-flagg (pin loven):** operativ lov nå (juni 2026) = **forvaltningsloven 1967 (LOV-1967-02-10)**. **Ny forvaltningslov LOV-2025-06-20-81** ble vedtatt 20. juni 2025, men er **«Ikke i kraft»** (Kongen bestemmer ikrafttredelse; ingen dato satt). Ekvivalenter i ny lov: §17 (saksbehandlingstid), §63 (3-ukers klagefrist). Sitater **må pinnes til riktig lov + paragraf og re-verifiseres før hver release**. Kilde: [Lovdata forvaltningsloven 1967](https://lovdata.no/lov/1967-02-10); [Lovdata ny forvaltningslov 2025 (ikke i kraft)](https://lovdata.no/dokument/NL/lov/2025-06-20-81) (juridisk).
- Sektorspesifikke frister (byggesak/plan- og bygningsloven, helselovgivning) finnes utenfor forvaltningsloven — **Ikke verifisert**, sjekkes per sektorlov.
### Sikker meldingsutveksling
- **SvarUt/Fiks (KS):** andel forsendelser **printet til papir falt 48 % (2017) → 23 % (2023)**; brukt av alle kommuner/fylkeskommuner. % digital vs printet — **sterk-KR** (KS eier data; ren effektivitet). Kilde: [KS Status Kommune 2024](https://www.ks.no/globalassets/24095-KS-Status-kommune-2024-WEB.pdf) (KS).
- **eFormidling:** status **anbefalt («bør»), ikke påbudt** — Digitaliseringsrundskrivet **D-2/25** «...bør eFormidling benyttes», og binder **kun statlige organer** (ikke kommuner; utvidelse vurderes, ikke gjort). SLA-mål **99,90 % oppetid** (månedlig). Kilde: [Digitaliseringsrundskrivet D-2/25](https://www.regjeringen.no/no/dokumenter/digitaliseringsrundskrivet/id3103320); [Digdir docs eFormidling](https://docs.digdir.no/docs/eFormidling/Introduksjon/) (offisiell). **brukbar** (statlig anvendelse; «bør», ikke compliance-krav).
- **Mandatory («skal») til kontrast** (ikke eFormidling): ID-porten, Altinn, Meldingsboksen/DPV, Kontakt- og reservasjonsregisteret (eForvaltningsforskriften §8), eInnsyn (statlige organer, offentleglova §6).
- Integrasjonspunkt 4.0-migrasjon pågår ~20252026; **eksakt frist Ikke verifisert** (agent-diskrepans 31. mai 2025 vs 2026 — verifiser mot Digdirs stegvis-guide). Kilde: [eFormidling integrasjonspunkt 4.0-guide](https://samarbeid.digdir.no/eformidling/eformidling-stegvis-guide-overgang-til-integrasjonspunkt-40/3573).
- eFormidling adopsjonstall — **Ikke verifisert** (kun interaktive grafer; Digdir servicedesk).
- **% konfidensiell korrespondanse via sikre kanaler** (egen-kontrollert routing) — **brukbar** (reframe fra rå SLA-oppetid, som org ikke eier).
### Sikkerhet og personvern (bonus-domene)
- **NSM grunnprinsipper for IKT-sikkerhet v2.1 (juni 2024):** 4 kategorier, 21 prinsipper, **118 tiltak** i 3 prioritetsgrupper (~15/~20/~83). Implementeringsgrad (implementert/118, eller /15 i PG1) — **brukbar** (NB: NSM publiserer ikke fast modenhetsstige 15; implementeringsgrad er org-konstruert, ikke sertifisert score). Kilde: [NSM grunnprinsipper IKT-sikkerhet v2.1](https://nsm.no/aktuelt/ny-versjon-av-nsms-grunnprinsipper-for-ikt-sikkerhet-klar) (offisiell).
- **GDPR Art. 33 — 72-timersregelen:** brudd meldes Datatilsynet «uten ugrunnet opphold og senest 72 timer». **% brudd meldt innen 72t** (eller median deteksjon→melding-tid) — **sterk-KR**. Kilde: [GDPR Art. 33 (Lovdata)](https://lovdata.no/dokument/NL/lov/2018-06-15-38/gdpr/ARTIKKEL_33) (juridisk).
- **Internkontroll (eForvaltningsforskriften §15 / ISO 27001):** kontroll-dekning %, % planlagte risikovurderinger eller ledelsens gjennomgang gjennomført — **brukbar** (dekomponert fra binært krav). Kilde: [eForvaltningsforskriften §15 (Lovdata)](https://lovdata.no/dokument/SF/forskrift/2004-06-25-988); [Digdir internkontroll/ISO 27001](https://www.digdir.no/standarder/internkontroll-styringssystem-ledelsessystem-informasjonssikkerhet/1490) (offisiell).
- Generisk «§15-compliant» (binær) eller rå Digdir-SLA-oppetid (ikke eid av konsumerende organ) — **vanity-felle** (reframe til egen-kontrollert metrikk).
### Vanity-feller spesifikt for offentlig digitalisering
Disse ser imponerende ut, men Riksrevisjonen/tilsyn har dokumentert svak reell effekt — bruk dem aldri som primær-KR:
- **«WCAG-compliant»** selv-deklarert uten uutilsynet-kryss-sjekk (se over).
- **Gevinstrealisering** selv-rapportert — sektoren feiler ~15 % (synkende, Riksrevisjon-bekreftet).
- **«Sammenhengende tjenester»** uten brukereffekt-måling — Riksrevisjonen 2025: lite reell brukereffekt. Kilde: [Riksrevisjonen sammenhengende digitale tjenester (2025)](https://riksrevisjonen.no/kommende-rapporter/sammenhengende-digitale-tjenester) (audit).
### Review-kadens
Alt regelverk i denne seksjonen er et **bevegelig mål 20252026** (ny forvaltningslov fases inn, EAA pending, eFormidling 4.0-migrasjon, årlige indeksrunder, ny DI-indeks). Biblioteket kan derfor ikke være statisk. Før hver deling/release: re-verifiser kilde + dato per metrikk. `/okr:freshen-references` bør prioritere de volatile primærkildene: Lovdata (forvaltningslov-status), uutilsynet (WCAG/EAA), Digdir (eFormidling/indeks). Full provenans og confidence-vurdering: `research/01-norsk-offentlig-metrikker.md` (trekresearch 2026-06-24, confidence 0,85).
*Sist oppdatert: Juni 2026*

View file

@ -1,6 +1,6 @@
# OKR Antipatterns: Vanlige feil når organisasjoner innfører OKR # OKR Antipatterns: Vanlige feil når organisasjoner innfører OKR
Forskning viser at omtrent 60% av organisasjoner mislykkes med OKR-implementering eller forlater systemet innen tre år. Problemet er sjelden rammeverket selv, men hvordan det implementeres og vedlikeholdes. Industri-rapporterte tall (ikke fagfellevurdert) anslår at omtrent 60% av organisasjoner mislykkes med OKR-implementering eller forlater systemet innen tre år. Problemet er sjelden rammeverket selv, men hvordan det implementeres og vedlikeholdes.
Denne guiden dekker de vanligste feilmønstrene organisert i fem kategorier. Denne guiden dekker de vanligste feilmønstrene organisert i fem kategorier.
@ -199,6 +199,30 @@ Denne guiden dekker de vanligste feilmønstrene organisert i fem kategorier.
--- ---
### 2.5 Målforskyvning (goal displacement)
**Hva det er**: Det som er lett å måle fortrenger det som faktisk betyr noe — måltallet på papiret blir viktigere enn den egentlige effekten. Dette er ikke en OKR-spesifikk svakhet, men en generell risiko ved all kvantifisert målstyring (Merton 1957, «goal displacement»; Campbells lov 1979) som OKR *arver*, og som er forhøyet i offentlig sektor fordi OKR ofte legges oppå en eksisterende mål- og resultatstyrings-/tellekant-kultur. Skill dette fra 2.3 Goalpost Moving: der justeres selve målet underveis; her forskyves oppmerksomheten fra effekt til det målbare.
**Symptomer**:
- «Pynter på tall» — resultater dresses opp for å nå måltallet
- Sandbagging: team setter forsiktige strekkmål når scoren kobles til konsekvenser
- Gaming av Key Results — målet nås på papiret uten reell verdiskaping («teaching to the test»)
- Aktivitetstelling fortrenger effektmåling; man måler det som er lett å telle
**Konsekvenser**:
- Goodharts lov: Når målet blir target, slutter det å være et godt mål
- Tap av faglig skjønn og «feilfrykt» — ansatte optimaliserer for indikatoren, ikke oppdraget
- Dokumenterte norske eksempler: politiet (Wathne 2015/2018 fant at en andel ansatte «pynter på tall», og at fokus forskjøv seg mot det økonomisk målbare framfor faget); tellekant-systemet i UH-sektoren (publiseringskvantitet over kvalitet, «salami-slicing»)
**Løsning**:
- **Skill committed fra aspirational OKR** — aspirational med 0.7-forventning og ingen konsekvens demper sandbagging
- Aldri koble OKR-score til lønn eller individuell evaluering
- Bygg guardrail-metrikker; par leading- med lagging-indikatorer
- **Mål effekt, ikke aktivitet** (jf. skillet mellom mål- og *aktivitets*styring) — spør alltid «hvilken effekt ønsker vi?»
- Behold rom for faglig skjønn; behandle indikatoren som et hjelpemiddel, ikke fasit
---
## 3. Kulturfeil ## 3. Kulturfeil
### 3.1 OKR koblet til bonus/evaluering ### 3.1 OKR koblet til bonus/evaluering
@ -493,6 +517,7 @@ Denne guiden dekker de vanligste feilmønstrene organisert i fem kategorier.
- [ ] Har vi kryss-funksjonell alignment? - [ ] Har vi kryss-funksjonell alignment?
- [ ] Er KR outcomes, ikke aktiviteter? - [ ] Er KR outcomes, ikke aktiviteter?
- [ ] Er targets ambitious men oppnåelige? - [ ] Er targets ambitious men oppnåelige?
- [ ] Måler vi effekt, ikke bare det som er lett å telle? (målforskyvning)
### Underveis i syklus ### Underveis i syklus
- [ ] Gjennomfører vi ukentlige check-ins? - [ ] Gjennomfører vi ukentlige check-ins?
@ -516,6 +541,9 @@ Denne guiden dekker de vanligste feilmønstrene organisert i fem kategorier.
- Itamar Gilad - 5 måter organisasjoner misbruker OKR - Itamar Gilad - 5 måter organisasjoner misbruker OKR
- Quantive - Tidlige tegn på OKR-fiasko - Quantive - Tidlige tegn på OKR-fiasko
- Microsoft Viva Goals Maturity Model (Viva Goals avviklet 2024, men modenhetsmodellen er fortsatt relevant som konsept) - Microsoft Viva Goals Maturity Model (Viva Goals avviklet 2024, men modenhetsmodellen er fortsatt relevant som konsept)
- Merton (1957), *Social Theory and Social Structure* — «goal displacement» (målforskyvning)
- Campbell (1979), «Assessing the Impact of Planned Social Change» — Campbells lov
- Wathne (2015/2018), *Målstyring i politiet* — norsk empiri på målforskyvning
**Relaterte filer i denne skill**: **Relaterte filer i denne skill**:
- `okr-framework.md` - Metodikk i detalj - `okr-framework.md` - Metodikk i detalj

View file

@ -45,7 +45,7 @@ Key Results = Hvordan vi måler suksess (2-5 målbare outcomes)
| Score | Betydning | For Committed OKR | For Aspirational OKR | | Score | Betydning | For Committed OKR | For Aspirational OKR |
|-------|-----------|-------------------|----------------------| |-------|-----------|-------------------|----------------------|
| 1.0 | 100% nådd | Forventet | Exceptionelt | | 1.0 | 100% nådd | Forventet | Exceptionelt |
| 0.7-0.9 | God progresjon | Under forventning | Suksess! | | ≈0.7 | Sweet spot (0.6-0.7, Google/Doerr) | Under forventning | Forventet — 0.7 = mål for aspirational! |
| 0.5-0.6 | Delvis nådd | Trenger forbedring | Verdifullt forsøk | | 0.5-0.6 | Delvis nådd | Trenger forbedring | Verdifullt forsøk |
| <0.5 | Ikke nådd | Problem | Re-evaluer mål | | <0.5 | Ikke nådd | Problem | Re-evaluer mål |

View file

@ -2,7 +2,7 @@
## Fremragende eksempler ## Fremragende eksempler
### Eksempel 1: Trafikksikkerhet (Direktoratet for digital tjenesteutvikling) ### Eksempel 1: Trafikksikkerhet (Direktoratet for digital tjenesteutvikling — fiktiv eksempeletat)
**Objective**: Redusere alvorlige ulykker på E6 gjennom Trøndelag **Objective**: Redusere alvorlige ulykker på E6 gjennom Trøndelag
@ -22,7 +22,7 @@
### Eksempel 2: Digitale tjenester ### Eksempel 2: Digitale tjenester
**Objective**: Levere digital-først opplevelse for saksbehandlingsøknader **Objective**: Levere digital-først opplevelse for søknadsbehandling
**Key Results**: **Key Results**:
1. 90% av saksbehandlingssøknader sendes inn digitalt (opp fra 65%) 1. 90% av saksbehandlingssøknader sendes inn digitalt (opp fra 65%)
@ -57,11 +57,11 @@
### Dårlig eksempel 1: Vag og aktivitetsorientert ### Dårlig eksempel 1: Vag og aktivitetsorientert
**Objective**: Forbedre vevedlikehold **Objective**: Forbedre veivedlikehold
**Key Results**: **Key Results**:
1. Gjennomføre 10 vedlikeholdsprosjekter 1. Gjennomføre 10 vedlikeholdsprosjekter
2. Holde månedlige møter om vevedlikehold 2. Holde månedlige møter om veivedlikehold
3. Lage årsrapport om vedlikehold 3. Lage årsrapport om vedlikehold
**Hva er galt**: **Hva er galt**:

View file

@ -56,11 +56,11 @@ I offentlig sektor er det viktig å skille mellom to nivåer av OKR:
Strategiske OKR etablerer den overordnede retningen og kommuniserer hva organisasjonen skal oppnå i løpet av året. De er ambisiøse, inspirerende og koblet til virksomhetsstrategi og tildelingsbrev. Strategiske OKR etablerer den overordnede retningen og kommuniserer hva organisasjonen skal oppnå i løpet av året. De er ambisiøse, inspirerende og koblet til virksomhetsstrategi og tildelingsbrev.
**Eksempel - Direktoratet for digital tjenesteutvikling strategisk OKR:** **Eksempel - Direktoratet for digital tjenesteutvikling strategisk OKR (fiktiv eksempeletat):**
- **Objective**: Etablere DDT som ledende på datadrevet trafikksikkerhet - **Objective**: Etablere DDT som ledende på datadrevet trafikksikkerhet
- **KR1**: Redusere antall alvorlige ulykker på riksveinettet med 10% - **KR1**: Redusere antall alvorlige ulykker på riksveinettet med 10%
- **KR2**: Implementere prediktiv vedlikeholdsmodell på 70% av vegstrekningene - **KR2**: Implementere prediktiv vedlikeholdsmodell på 70% av vegstrekningene
- **KR3**: Oppnå 90% digital selvbetjening på saksbehandlingtjenester - **KR3**: Oppnå 90% digital selvbetjening på saksbehandlingstjenester
### Taktiske OKR (syklusbaserte) ### Taktiske OKR (syklusbaserte)
@ -98,10 +98,10 @@ Etatens KR → Teamets Objective
**Committed (forpliktet):** **Committed (forpliktet):**
- Forventer 100% måloppnåelse - Forventer 100% måloppnåelse
- Typisk: Regulatoriske krav, tildelingsbrev-mål, lovpålagte oppgaver - Typisk: Regulatoriske krav, tildelingsbrev-mål, lovpålagte oppgaver
- Eksempel: "100% av saksbehandlingklager behandlet innen 6 uker" - Eksempel: "100% av saksbehandlingsklager behandlet innen 6 uker"
**Aspirational (ambisiøst):** **Aspirational (ambisiøst):**
- 70-80% måloppnåelse er suksess - 0.7 = forventet for aspirational; 0.6-0.7 = sweet spot (Google/Doerr)
- Typisk: Innovasjon, forbedring, nye initiativ - Typisk: Innovasjon, forbedring, nye initiativ
- Eksempel: "Lansere AI-basert chatbot for saksbehandlingspørsmål" - Eksempel: "Lansere AI-basert chatbot for saksbehandlingspørsmål"
@ -202,7 +202,7 @@ OKR i offentlig sektor må koordineres med statens budsjettprosess for å være
**Fase 3: Løpende ressursstyring** **Fase 3: Løpende ressursstyring**
- Link OKR til ressursallokering i budsjett - Link OKR til ressursallokering i budsjett
- Bruk OKR-prioritering ved omfordeling - Bruk OKR-prioritering ved omfordeling
- Rapporter OKR-progress i tertialrapporter - Rapporter OKR-progress via virksomhetens faktiske rapporteringspunkter (tertial-/halvårs-/årsrapport der tildelingsbrevet krever det — sjekk eget tildelingsbrev; statsregnskapet rapporteres månedlig)
### OKR og tildelingsbrev ### OKR og tildelingsbrev
@ -242,7 +242,7 @@ OKR kan brukes aktivt i ressursdiskusjoner:
- ✅ Fokusert på outcome, ikke aktivitet - ✅ Fokusert på outcome, ikke aktivitet
**Dårlig**: "Forbedre kundeservice" **Dårlig**: "Forbedre kundeservice"
**Bedre**: "Levere raskere og mer responsiv kundeservice for saksbehandlingsøknader" **Bedre**: "Levere raskere og mer responsiv kundeservice for søknadsbehandling"
### Gode Key Results ### Gode Key Results
- ✅ Målbar (tall, prosent, eller klare milepæler) - ✅ Målbar (tall, prosent, eller klare milepæler)
@ -259,13 +259,13 @@ OKR kan brukes aktivt i ressursdiskusjoner:
**Skala**: 0.0 til 1.0 (eller 0% til 100%) **Skala**: 0.0 til 1.0 (eller 0% til 100%)
- **1.0** = Target nådd 100% - **1.0** = Target nådd 100%
- **0.7-0.8** = God score for ambisiøse (stretch) OKR - **≈0.7** = forventet for ambisiøse (stretch) OKR; 0.6-0.7 = sweet spot (Google/Doerr)
- **0.5-0.6** = Under forventning, men verdifullt forsøk - **0.5-0.6** = Under forventning, men verdifullt forsøk
- **<0.5** = Trenger grundig analyse: feil ambisjonsnivå, eller eksterne blokkere? - **<0.5** = Trenger grundig analyse: feil ambisjonsnivå, eller eksterne blokkere?
### Typer OKR ### Typer OKR
- **Committed**: Må nås (typisk 0.9-1.0 forventet). Eksempel: Regulatoriske krav. - **Committed**: Må nås (typisk 0.9-1.0 forventet). Eksempel: Regulatoriske krav.
- **Aspirational (Stretch)**: Ambisiøse mål (0.6-0.8 er suksess). Eksempel: Innovasjon, store forbedringer. - **Aspirational (Stretch)**: Ambisiøse mål (0.7 = forventet; 0.6-0.7 = sweet spot, Google/Doerr). Eksempel: Innovasjon, store forbedringer.
**Viktig**: Scorer brukes til læring, IKKE personlig evaluering eller bonus. **Viktig**: Scorer brukes til læring, IKKE personlig evaluering eller bonus.
@ -353,6 +353,10 @@ Vektet score = (0.7×2 + 0.5×1 + 0.8×1) / (2+1+1) = 2.7 / 4 = 0.675
**Anbefaling:** Start med uvektet gjennomsnitt. Introduser vekting kun hvis det gir tydelig verdi. **Anbefaling:** Start med uvektet gjennomsnitt. Introduser vekting kun hvis det gir tydelig verdi.
#### Aggreger ikke committed og aspirational sammen
Committed og aspirational OKR-scorer måles mot **ulike standarder** og bør ikke blandes til ett aggregat: committed forventes å nå **1.0** (enten grønn eller rød — et avvik krever forklaring), mens aspirational forventes å lande rundt **0.7** med høy varians og vurderes på **snittet på tvers av alle aspirational-OKR**, ikke på én enkelt. Å aggregere de to typene til ett tall blander to forskjellige målestokker og gjør resultatet tvetydig — et committed-snitt på 0.85 er en bekymring, mens et aspirational-snitt på 0.85 er svært bra. Rapportér derfor de to typene hver for seg. (Dette følger som logisk implikasjon av Doerr/Googles to-måls-struktur — ikke et ordrett Doerr-sitat; se committed-vs-aspirational-tabellen i `okr-cheatsheet.md`.)
### Confidence levels (konfidensnivåer) ### Confidence levels (konfidensnivåer)
Mens *score* måler faktisk oppnåelse, måler *confidence* sannsynligheten for å nå målet. Confidence oppdateres underveis i syklusen, score beregnes ved slutt. Mens *score* måler faktisk oppnåelse, måler *confidence* sannsynligheten for å nå målet. Confidence oppdateres underveis i syklusen, score beregnes ved slutt.

View file

@ -4,10 +4,10 @@
## Hvorfor innføring feiler ## Hvorfor innføring feiler
**Statistikk viser**: **Industri-rapportert (ikke fagfellevurdert)** — tall fra OKR-leverandører og bransjeundersøkelser, ikke fagfellevurdert forskning:
- Organisasjoner med OKR er 39% mer sannsynlige å nå sine mål - Organisasjoner med OKR er 39% mer sannsynlige å nå sine mål *(industri-rapportert)*
- Team som gjennomfører ukentlige check-ins oppnår 43% flere mål - Team som gjennomfører ukentlige check-ins oppnår 43% flere mål *(industri-rapportert)*
- Team som gjennomfører strukturerte retrospektiver fullfører 30-45% flere objectives - Team som gjennomfører strukturerte retrospektiver fullfører 30-45% flere objectives *(industri-rapportert)*
Men disse gevinstene krever systematisk innføring og kulturendring. Men disse gevinstene krever systematisk innføring og kulturendring.
@ -45,7 +45,7 @@ Men disse gevinstene krever systematisk innføring og kulturendring.
1. Ledelsen setter 3-4 **retningsgivende objectives** (basert på tildelingsbrev/strategiplan) 1. Ledelsen setter 3-4 **retningsgivende objectives** (basert på tildelingsbrev/strategiplan)
2. Avdelinger og team utarbeider **egne OKR** som støtter disse 2. Avdelinger og team utarbeider **egne OKR** som støtter disse
3. **Alignment-workshops** sikrer koordinering 3. **Alignment-workshops** sikrer koordinering
4. **Kvartalsvise check-ins** mellom nivåene 4. **Syklusvise check-ins** mellom nivåene
### 3. Big Bang (sjelden anbefalt) ### 3. Big Bang (sjelden anbefalt)
@ -412,7 +412,7 @@ Stretch-mål krever psykologisk trygghet. Ansatte må:
- Egne OKR som modellerer ønsket adferd - Egne OKR som modellerer ønsket adferd
- Allokerer ressurser (tid, opplæring, verktøy) - Allokerer ressurser (tid, opplæring, verktøy)
- Kobler OKR til organisasjonens strategi - Kobler OKR til organisasjonens strategi
- Deltar i kvartalsvise reviews - Deltar i syklusvise reviews
**Tidsbruk**: 2-4 timer per måned **Tidsbruk**: 2-4 timer per måned
@ -503,7 +503,7 @@ Stretch-mål krever psykologisk trygghet. Ansatte må:
## Ressurser ## Ressurser
- Doerr, John: *Measure What Matters* (2018) - Grunnbok om OKR - Doerr, John: *Measure What Matters* (2018) - Grunnbok om OKR
- [re:Work with Google - OKR Guide](https://rework.withgoogle.com/guides/set-goals-with-okrs/) - Googles offisielle OKR-ressurs - [re:Work with Google - OKR Guide](https://rework.withgoogle.com/en/guides/set-goals-with-okrs/) - Googles offisielle OKR-ressurs
- [OKR Institute](https://okrinstitute.org/) - Forskning og best practices - [OKR Institute](https://okrinstitute.org/) - Forskning og best practices
- [What Matters](https://www.whatmatters.com/) - John Doerrs ressursside - [What Matters](https://www.whatmatters.com/) - John Doerrs ressursside
- [Code for America OKR Case Study](https://www.whatmatters.com/articles/code-for-america-okrs-local-government) - Offentlig sektor-eksempel - [Code for America OKR Case Study](https://www.whatmatters.com/articles/code-for-america-okrs-local-government) - Offentlig sektor-eksempel

View file

@ -30,7 +30,7 @@ OKR og Scrum utfyller hverandre:
### Timing og synkronisering ### Timing og synkronisering
``` ```
OKR-syklus (4 måneder i DDT) OKR-syklus (4 måneder i DDT — fiktiv eksempeletat)
├── Sprint 1 (2 uker) ├── Sprint 1 (2 uker)
├── Sprint 2 (2 uker) ├── Sprint 2 (2 uker)
├── Sprint 3 (2 uker) ├── Sprint 3 (2 uker)
@ -229,7 +229,7 @@ Kanban-tavle med OKR-swimlanes:
**Utfordring**: Kanban er kontinuerlig, OKR er syklusbasert. **Utfordring**: Kanban er kontinuerlig, OKR er syklusbasert.
**Løsning**: Behold kvartalsvise OKR, men la arbeidet flyte kontinuerlig: **Løsning**: Behold syklusvise OKR, men la arbeidet flyte kontinuerlig:
1. **Sett OKR ved syklusstart** definerer retning 1. **Sett OKR ved syklusstart** definerer retning
2. **Replenishment-møter** prioriter alltid arbeid som støtter KR 2. **Replenishment-møter** prioriter alltid arbeid som støtter KR
@ -389,7 +389,7 @@ PROSESSFLYT:
### Praktisk eksempel ### Praktisk eksempel
**Kontekst**: Storprosjekt med 5 team (ART) i Direktoratet for digital tjenesteutvikling **Kontekst**: Storprosjekt med 5 team (ART) i Direktoratet for digital tjenesteutvikling (fiktiv eksempeletat)
``` ```
ORGANISASJONS-OKR (Årlig): ORGANISASJONS-OKR (Årlig):

View file

@ -92,7 +92,7 @@ Key Results:
├─ KR1: Reduser ulykker med personskade ├─ KR1: Reduser ulykker med personskade
│ ├─ Start: 45 ulykker/år │ ├─ Start: 45 ulykker/år
│ ├─ Target: 25 ulykker/år │ ├─ Target: 25 ulykker/år
│ ├─ Current: 32 ulykker (etter 2 mnd) [Progress: 52%] │ ├─ Current: 32 ulykker (etter 2 mnd) [Progress: 65%]
│ ├─ Confidence: 🟡 At Risk │ ├─ Confidence: 🟡 At Risk
│ └─ Owner: Lena Hansen │ └─ Owner: Lena Hansen

View file

@ -2,6 +2,8 @@
Norsk offentlig sektor har unike styringsmekanismer som OKR må forholde seg til. Denne guiden forklarer hvordan du kobler OKR til tildelingsbrev, politisk styring, hierarkisk alignment og revisjonskrav. Norsk offentlig sektor har unike styringsmekanismer som OKR må forholde seg til. Denne guiden forklarer hvordan du kobler OKR til tildelingsbrev, politisk styring, hierarkisk alignment og revisjonskrav.
> **Merk fiktiv eksempeletat:** «DDT» (Direktoratet for digital tjenesteutvikling) er en *fiktiv eksempeletat* konstruert utelukkende for illustrasjon i denne guiden. DDT er **ikke** en virkelig norsk etat, og alle tildelingsbrev, mål og tall knyttet til DDT nedenfor er oppdiktede eksempler.
## Innholdsfortegnelse ## Innholdsfortegnelse
1. [Tildelingsbrev og OKR](#tildelingsbrev-og-okr) 1. [Tildelingsbrev og OKR](#tildelingsbrev-og-okr)
@ -10,7 +12,8 @@ Norsk offentlig sektor har unike styringsmekanismer som OKR må forholde seg til
4. [Revisjons- og kontrollperspektiv](#revisjons--og-kontrollperspektiv) 4. [Revisjons- og kontrollperspektiv](#revisjons--og-kontrollperspektiv)
5. [Spesielle hensyn](#spesielle-hensyn) 5. [Spesielle hensyn](#spesielle-hensyn)
6. [Konkrete eksempler](#konkrete-eksempler) 6. [Konkrete eksempler](#konkrete-eksempler)
7. [Ressurser](#ressurser) 7. [Tillitsreformen og styringsspenningen](#tillitsreformen-og-styringsspenningen)
8. [Ressurser](#ressurser)
--- ---
@ -43,7 +46,7 @@ Finn de 2-4 viktigste resultatmålene som krever forbedring/endring (ikke bare d
Tildelingsbrev er ofte teknisk formulert. OKR skal inspirere. Tildelingsbrev er ofte teknisk formulert. OKR skal inspirere.
``` ```
Tildelingsbrev (SD til DDT): Tildelingsbrev (SD til DDT, fiktiv eksempeletat):
"Redusere vedlikeholdsetterslep på riksveinettet" "Redusere vedlikeholdsetterslep på riksveinettet"
→ OKR-formulering: → OKR-formulering:
@ -68,13 +71,13 @@ I offentlig sektor må du være tydelig på hva som er "must-have" vs "stretch":
- Tildelingsbrev-krav med eksplisitte måltall - Tildelingsbrev-krav med eksplisitte måltall
- Lovpålagte frister (f.eks. klagebehandling) - Lovpålagte frister (f.eks. klagebehandling)
- Politisk bindende løfter - Politisk bindende løfter
- *Eksempel*: "100% av saksbehandlingklager behandlet innen 6 uker" (lovkrav) - *Eksempel*: "100% av saksbehandlingsklager behandlet innen 6 uker" (lovkrav)
**Aspirational OKR** (70-80% er suksess): **Aspirational OKR** — 0.7 = forventet for aspirational; 0.6-0.7 = sweet spot (Google/Doerr):
- Ambisiøse forbedringer utover minimumskrav - Ambisiøse forbedringer utover minimumskrav
- Nye initiativer og innovasjon - Nye initiativer og innovasjon
- Kvalitetsforbedringer - Kvalitetsforbedringer
- *Eksempel*: "Lansere AI-basert førstelinjesupport for saksbehandlingsaker" - *Eksempel*: "Lansere AI-basert førstelinjesupport for saksbehandlingssaker"
**Anbefalt balanse i offentlig sektor**: 60% committed, 40% aspirational **Anbefalt balanse i offentlig sektor**: 60% committed, 40% aspirational
@ -293,7 +296,7 @@ Mange mål i offentlig sektor krysser etatsgrenser:
``` ```
Sektorovergripende mål: Redusere klimagassutslipp fra transport med 55% innen 2030 Sektorovergripende mål: Redusere klimagassutslipp fra transport med 55% innen 2030
DDT-bidrag (egen OKR): DDT-bidrag (egen OKR, fiktiv eksempeletat):
Objective: Akselerere overgangen til utslippsfri transport Objective: Akselerere overgangen til utslippsfri transport
KR1: 80% av nye fergekontrakter krever null-utslipp KR1: 80% av nye fergekontrakter krever null-utslipp
KR2: Lade-infrastruktur på 100% av rasteplasser på riksvei KR2: Lade-infrastruktur på 100% av rasteplasser på riksvei
@ -314,16 +317,16 @@ Når OKR krever samarbeid på tvers:
3. **Etabler koordineringspunkt**: Hvem snakker med hvem? 3. **Etabler koordineringspunkt**: Hvem snakker med hvem?
4. **Enes om måling**: Bruk samme baseline og målemetode 4. **Enes om måling**: Bruk samme baseline og målemetode
**Eksempel - Digitaliseringsdirektoratet + DDT**: **Eksempel - Digitaliseringsdirektoratet + DDT (fiktiv eksempeletat)**:
``` ```
Felles Objective: Gjøre saksbehandlingtjenester heldigitale Felles Objective: Gjøre saksbehandlingstjenester heldigitale
Digdir-bidrag (KR): Digdir-bidrag (KR):
- Nasjonal ID-løsning støtter saksbehandling innen Q2 - Nasjonal ID-løsning støtter saksbehandling innen Q2
- Felleskomponenter tilgjengelig for DDT - Felleskomponenter tilgjengelig for DDT
DDT-bidrag (KR): DDT-bidrag (KR):
- 100% av saksbehandling-søknader digitale innen Q4 - 100% av søknader digitale innen Q4
- Integrasjon med nasjonal ID-løsning implementert - Integrasjon med nasjonal ID-løsning implementert
``` ```
@ -337,13 +340,13 @@ Offentlig sektor eksisterer for å levere verdi til innbyggere og næringsliv. O
``` ```
Innbyggerperspektiv: Innbyggerperspektiv:
Objective: Gjøre det enklere å fornye saksbehandling Objective: Gjøre det enklere å fornye førerkort
KR1: 90% av fornyelser fullføres digitalt uten oppmøte KR1: 90% av fornyelser fullføres digitalt uten oppmøte
KR2: Gjennomsnittstid fra søknad til mottatt kort: 5 virkedager KR2: Gjennomsnittstid fra søknad til mottatt kort: 5 virkedager
KR3: Brukertilfredshet på fornyelsestjeneste: 4.5/5 KR3: Brukertilfredshet på fornyelsestjeneste: 4.5/5
Internt perspektiv (som muliggjør det over): Internt perspektiv (som muliggjør det over):
Objective: Automatisere saksbehandlingbehandling Objective: Automatisere saksbehandling
KR1: 80% av søknader behandles automatisk KR1: 80% av søknader behandles automatisk
KR2: Manuell saksbehandlingstid ned fra 20 til 5 min KR2: Manuell saksbehandlingstid ned fra 20 til 5 min
KR3: 0 feil i automatisk vedtak (kvalitetskontroll) KR3: 0 feil i automatisk vedtak (kvalitetskontroll)
@ -353,13 +356,13 @@ Objective: Automatisere saksbehandlingbehandling
## Konkrete eksempler ## Konkrete eksempler
### Eksempel 1: Fra NTP-mål til team-OKR i Direktoratet for digital tjenesteutvikling ### Eksempel 1: Fra NTP-mål til team-OKR i Direktoratet for digital tjenesteutvikling (fiktiv eksempeletat)
**Nasjonal transportplan 2025-2036** (Stortingsmelding): **Nasjonal transportplan 2025-2036** (Stortingsmelding):
> "Enklere reisehverdag og økt konkurranseevne for næringslivet" > "Enklere reisehverdag og økt konkurranseevne for næringslivet"
> "Nullvisjon for drepte og hardt skadde i trafikken" > "Nullvisjon for drepte og hardt skadde i trafikken"
**Tildelingsbrev fra departementet til DDT**: **Tildelingsbrev fra departementet til DDT (fiktiv eksempeletat)**:
> "DDT skal redusere antall drepte og hardt skadde på riksveinettet med minst 10% fra 2024-nivå" > "DDT skal redusere antall drepte og hardt skadde på riksveinettet med minst 10% fra 2024-nivå"
> "Vedlikeholdsetterslepet skal reduseres med 5%" > "Vedlikeholdsetterslepet skal reduseres med 5%"
@ -392,11 +395,11 @@ KR3: Alle 12 værstasjoner på E6 Øst operativ hele vinteren
### Eksempel 2: Fra Digitaliseringsstrategi til OKR ### Eksempel 2: Fra Digitaliseringsstrategi til OKR
**Digitaliseringsstrategi for offentlig sektor 2024-2030** (Regjeringen): **Fremtidens digitale Norge — Nasjonal digitaliseringsstrategi 2024-2030** (Regjeringen):
> "Offentlige tjenester skal være digitalt tilgjengelige for alle" > "Offentlige tjenester skal være digitalt tilgjengelige for alle"
> "Data skal deles og gjenbrukes på tvers av sektorer" > "Data skal deles og gjenbrukes på tvers av sektorer"
**Tildelingsbrev til Direktoratet for digital tjenesteutvikling**: **Tildelingsbrev til Direktoratet for digital tjenesteutvikling (fiktiv eksempeletat)**:
> "DDT skal øke digital selvbetjeningsgrad på publikumstjenester til minst 85%" > "DDT skal øke digital selvbetjeningsgrad på publikumstjenester til minst 85%"
**DDT strategisk OKR (årlig)**: **DDT strategisk OKR (årlig)**:
@ -410,7 +413,7 @@ KR3: 70% av henvendelser løst via selvbetjening (ned fra 85% telefon/oppmøte)
**Divisjon Trafikant OKR (syklus 2)**: **Divisjon Trafikant OKR (syklus 2)**:
``` ```
Objective: Gjøre saksbehandlingtjenester friksjonsfrie for brukeren Objective: Gjøre saksbehandlingstjenester friksjonsfrie for brukeren
KR1: Lansere ny digital fornyelsestjeneste med 95% oppetid KR1: Lansere ny digital fornyelsestjeneste med 95% oppetid
KR2: 80% av fornyelser gjennomført uten manuell saksbehandling KR2: 80% av fornyelser gjennomført uten manuell saksbehandling
@ -419,7 +422,7 @@ KR3: Gjennomsnittlig tid fra søknad til vedtak: 24 timer (ned fra 5 dager)
**Team Digital utvikling OKR (syklus 2)**: **Team Digital utvikling OKR (syklus 2)**:
``` ```
Objective: Bygge robust backend for automatisk saksbehandlingbehandling Objective: Bygge robust backend for automatisk saksbehandling
KR1: API-responstid under 200ms for alle kall KR1: API-responstid under 200ms for alle kall
KR2: Integrasjon med folkeregisteret og politiets registre ferdig testet KR2: Integrasjon med folkeregisteret og politiets registre ferdig testet
@ -437,7 +440,7 @@ Ikke alt i tildelingsbrevet egner seg som OKR. Her er et eksempel på vurdering:
4. "Rapportere månedlig til departementet" → **Ikke OKR** (prosess/krav) 4. "Rapportere månedlig til departementet" → **Ikke OKR** (prosess/krav)
5. "Utvikle ny strategi for nullutslipps anleggsplasser" → **OKR-kandidat** (nytt initiativ) 5. "Utvikle ny strategi for nullutslipps anleggsplasser" → **OKR-kandidat** (nytt initiativ)
**Resultat**: **Resultat** (DDT-eksempelet under er fiktivt):
``` ```
OKR: OKR:
Objective: Gjøre DDTs drift klimanøytral Objective: Gjøre DDTs drift klimanøytral
@ -453,6 +456,21 @@ KPI-er (ikke OKR):
--- ---
## Tillitsreformen og styringsspenningen
Tillitsreformen (lansert av Støre-regjeringen 202122) satte ord på en varig spenning i norsk forvaltning: balansen mellom **detaljstyring/kontroll** og **faglig handlingsrom**. Målet var færre og tydeligere mål, mindre statlig detaljstyring og økt lokalt handlingsrom, drevet nedenfra og opp og forankret i partssamarbeidet (Hovedavtalen).
**Status (per 2026 — sjekk gjeldende):** Reformen mistet politisk flaggskip-synlighet etter 2024 — den er ikke et navngitt prosjekt i regjeringens *Plan for Norge (20252029)*, og regjeringens egen reformside ble lenge ikke oppdatert. Den er **ikke formelt avsluttet** (videreføres bl.a. via tildelingsbrev og «tillitsbasert styring»-språk), men har tapt sentral drivkraft. Det er den **underliggende spenningen** — ikke reformens dagsaktuelle status — OKR-rådgivningen bør forholde seg til.
**Er OKR forenlig med tillitsbasert styring? (omstridt):** Forskningen er delt:
- **Kuvaas (BI, 2017):** tillitsbasert styring står «i direkte motsetning til» mål- og resultatstyring — kvantifiserte mål fortrenger faglig skjønn.
- **Bjurstrøm (NAT, 2021):** motsetningen er i stor grad *kunstig* — MRS og tillit er forenlige; spørsmålet er balansen mellom kontroll (prinsipal-agent) og handlingsrom (forvalterteori), normalt en miks.
- Akademikerne: «tillit betyr ikke fravær av styring og kontroll» — de kan kombineres.
En nøktern realitetssjekk: tillitsbasert styring er «mye hørt, men lite sett» i praksis (forskning 2022) — å *erklære* et skifte gjør det ikke. OKR forsøker å innta lærings-/handlingsrom-polen, men det er et omstridt, ikke et løst, trekk. Bruk OKR bevisst for å gi team retning og handlingsrom — men vær klar over målforskyvnings- og sandbagging-risikoen (se `okr-antipatterns.md` 2.5).
---
## Ressurser ## Ressurser
### Interne referanser ### Interne referanser
@ -466,6 +484,8 @@ KPI-er (ikke OKR):
- [Regjeringen: Tildelingsbrev](https://www.regjeringen.no/no/dokument/tildelingsbrev-og-arsrapportar/id2357472/) - [Regjeringen: Tildelingsbrev](https://www.regjeringen.no/no/dokument/tildelingsbrev-og-arsrapportar/id2357472/)
- [Riksrevisjonen: Undersøkelse av mål- og resultatstyring](https://www.stortinget.no/no/Saker-og-publikasjoner/Publikasjoner/Dokumenter/) - [Riksrevisjonen: Undersøkelse av mål- og resultatstyring](https://www.stortinget.no/no/Saker-og-publikasjoner/Publikasjoner/Dokumenter/)
- [NAV: Mål- og resultatstyring - kan det bidra til å få flere i arbeid?](https://arbeidogvelferd.nav.no/) - [NAV: Mål- og resultatstyring - kan det bidra til å få flere i arbeid?](https://arbeidogvelferd.nav.no/)
- [Regjeringen: Tillitsreformen](https://www.regjeringen.no/no/tema/statlig-forvaltning/forvaltningsutvikling/tillitsreform/id2894124)
- Kuvaas, B. (2017): «Erstatt målstyring med tillitsbasert styring» (BI Business Review); Bjurstrøm, K.H. (2021): «Mål- og resultatstyring og tillitsbasert styring: ulike alternativer eller kunstige motsetninger?» (Nordisk Administrativt Tidsskrift 98(2))
- Doerr, John (2018): *Measure What Matters*, Portfolio/Penguin - Doerr, John (2018): *Measure What Matters*, Portfolio/Penguin
### Relevante Riksrevisjonsrapporter ### Relevante Riksrevisjonsrapporter

View file

@ -0,0 +1,105 @@
# OKR-kvalitetsrubrikker — ankret scoringsguide
Felles sannhetskilde for OKR-kvalitetsvurdering i norsk offentlig sektor (Google/Doerr-metodikk tilpasset tertialsyklus). Hver kvalitetsdimensjon har **fem ankere** fra svakest til sterkest med konkret nivåbeskrivelse, slik at scoring blir deterministisk og reproduserbar på tvers av vurderinger og vurderere.
**Bruk:** les ankerbeskrivelsene per dimensjon, finn det nivået OKR-en faktisk treffer, og scor dimensjonen 1-5. Skaler til 0-10 der en ti-skala kreves: anker 1 -> 1-2, anker 2 -> 3-4, anker 3 -> 5-6, anker 4 -> 7-8, anker 5 -> 9-10 (jf. scoring-guiden i `kvalitetssjekker`-agenten og samlet-scoring-tabellen i `/okr:kvalitet`). Denne fila erstatter den inline-rubrikken som tidligere lå i `commands/kvalitet.md` og forkortet i `agents/kvalitetssjekker-agent.md` — kommandoen og agenten refererer hit i stedet for å bære ankrene selv.
## Objective-dimensjoner
### Inspirerende
Måler om Objective-et motiverer teamet og kommuniserer hvorfor arbeidet betyr noe.
1. **Anker 1 (svakest)** — Kjedelig eller rent byråkratisk; ingen blir engasjert av formuleringen.
2. **Anker 2** — Tørt og oppgavepreget; saklig, men vekker ikke eierskap.
3. **Anker 3** — Nøytralt; akseptabelt, men hverken løfter eller demotiverer.
4. **Anker 4** — Engasjerende for de fleste; tydelig retning og en antydning av hvorfor.
5. **Anker 5 (sterkest)** — Motiverer hele teamet; kommuniserer mening og ambisjon, og folk husker det.
### Klarhet
Måler om Objective-et gir en entydig retning som alle tolker likt.
1. **Anker 1 (svakest)** — Flertydig; kan tolkes i flere ulike retninger.
2. **Anker 2** — Et kjernebegrep er uklart og krever oppklaring før arbeid kan starte.
3. **Anker 3** — Noe vagt, men hovedretningen anes.
4. **Anker 4** — Stort sett entydig; kun små tolkningsrom igjen.
5. **Anker 5 (sterkest)** — Entydig retning; alle som leser det forstår det samme.
### Outcome-fokus
Måler om Objective-et beskriver en ønsket tilstand (resultat) heller enn en aktivitet.
1. **Anker 1 (svakest)** — Ren aktivitet ("gjennomføre", "lage", "innføre"); ingen resultat.
2. **Anker 2** — Overveiende aktivitet med en vag henvisning til effekt.
3. **Anker 3** — Blanding av aktivitet og resultat.
4. **Anker 4** — Overveiende resultat, med en gjenværende aktivitetsrest.
5. **Anker 5 (sterkest)** — Rent outcome; beskriver tilstanden vi vil oppnå, ikke veien dit.
### Scope
Måler om Objective-et er riktig dimensjonert for én tertial (fire måneder).
1. **Anker 1 (svakest)** — Helt feil scope; en flerårig visjon eller en triviell enkeltoppgave.
2. **Anker 2** — Klart for stort eller for lite for én tertial.
3. **Anker 3** — Litt for stort eller for lite, men håndterbart.
4. **Anker 4** — Passer tertialen med rimelig stretch.
5. **Anker 5 (sterkest)** — Perfekt dimensjonert for én tertial; ambisiøst men oppnåelig i perioden.
### Alignment
Måler om Objective-et er koblet oppover til org-OKR, tildelingsbrev eller overordnet strategi.
1. **Anker 1 (svakest)** — Ingen kobling til overordnet mål eller tildelingsbrev.
2. **Anker 2** — Kobling påstås, men kan ikke spores til et konkret overordnet mål.
3. **Anker 3** — Implisitt kobling oppover; leseren må selv slutte sammenhengen.
4. **Anker 4** — Tydelig koblet, men ikke eksplisitt sitert.
5. **Anker 5 (sterkest)** — Tydelig og eksplisitt koblet til navngitt org-OKR eller tildelingsbrevspunkt.
## Key Result-dimensjoner
### Målbarhet
Måler om Key Result-et har konkrete tall med baseline og target.
1. **Anker 1 (svakest)** — Ikke målbart; ingen tall, kun kvalitativ påstand.
2. **Anker 2** — Et tall er nevnt, men uten baseline eller uten target.
3. **Anker 3** — Delvis målbart (target uten baseline, eller omvendt).
4. **Anker 4** — Tall med baseline og target, men et mindre presisjonshull (uklar enhet/avgrensning).
5. **Anker 5 (sterkest)** — Tall med tydelig baseline -> target og entydig enhet.
### Outcome
Måler om Key Result-et fanger reell effekt heller enn output/aktivitet.
1. **Anker 1 (svakest)** — Ren output/aktivitet (antall møter, leveranser, kurs).
2. **Anker 2** — Output brukt som svak proxy for et udokumentert resultat.
3. **Anker 3** — Blanding av output og outcome.
4. **Anker 4** — Overveiende outcome, med en mindre output-rest.
5. **Anker 5 (sterkest)** — Måler reell effekt eller resultat for bruker/samfunn.
### Ambisjon
Måler om Key Result-et har riktig stretch — ambisiøst, men ikke urealistisk eller sandbagget.
1. **Anker 1 (svakest)** — Urealistisk (praktisk umulig) eller åpenbar sandbagging (garantert 1.0).
2. **Anker 2** — Tydelig for lett eller for hardt for perioden.
3. **Anker 3** — For lett eller for vanskelig, men i nærheten av riktig nivå.
4. **Anker 4** — Rimelig stretch, men litt for konservativ eller litt for aggressiv.
5. **Anker 5 (sterkest)** — Riktig stretch; om lag 70 % forventet måloppnåelse, ambisiøst men mulig.
### Datakilde
Måler om Key Result-et har en spesifisert og faktisk tilgjengelig datakilde.
1. **Anker 1 (svakest)** — Ukjent datakilde; ingen vet hvor tallet skal hentes fra.
2. **Anker 2** — Datakilde antydet, men ikke bekreftet tilgjengelig.
3. **Anker 3** — Datakilde antas tilgjengelig, men er ikke verifisert.
4. **Anker 4** — Spesifisert kilde med mindre usikkerhet om målefrekvens eller tilgang.
5. **Anker 5 (sterkest)** — Spesifisert OG tilgjengelig kilde med kjent målefrekvens.
### Uavhengighet
Måler i hvilken grad teamet selv kontrollerer utfallet av Key Result-et.
1. **Anker 1 (svakest)** — Utfallet ligger helt utenfor teamets kontroll.
2. **Anker 2** — Sterkt avhengig av andre enheter eller eksterne aktører.
3. **Anker 3** — Delvis avhengig av andre.
4. **Anker 4** — Stort sett innenfor teamets kontroll, med en mindre ekstern avhengighet.
5. **Anker 5 (sterkest)** — Teamet kontrollerer utfallet direkte.
---
*Kilde: Google re:Work OKR-rubrikk + Doerr "Measure What Matters", tilpasset norsk offentlig tertialsyklus. Ankrene konsoliderer scoringsbåndene fra `/okr:kvalitet` og `kvalitetssjekker`-agenten til én delt sannhetskilde.*
*Sist oppdatert: Juni 2026*

View file

@ -47,7 +47,8 @@ Googles offisielle, åpne ressurs for hvordan de bruker OKR internt.
- Kvartalsvis syklus med midtveis-sjekk - Kvartalsvis syklus med midtveis-sjekk
- Ikke koble OKR direkte til kompensasjon - Ikke koble OKR direkte til kompensasjon
**Lenke:** [rework.withgoogle.com/guides/set-goals-with-okrs](https://rework.withgoogle.com/en/guides/set-goals-with-okrs) **Lenke:** [rework.withgoogle.com/en/guides/set-goals-with-okrs](https://rework.withgoogle.com/en/guides/set-goals-with-okrs)
<!-- Wayback-fallback hvis lenken dør: https://web.archive.org/web/https://rework.withgoogle.com/en/guides/set-goals-with-okrs -->
**Video:** Rick Klau's OKR-presentasjon fra Google Ventures: [youtube.com/watch?v=mJB83EZtAjc](https://www.youtube.com/watch?v=mJB83EZtAjc) **Video:** Rick Klau's OKR-presentasjon fra Google Ventures: [youtube.com/watch?v=mJB83EZtAjc](https://www.youtube.com/watch?v=mJB83EZtAjc)
@ -60,7 +61,7 @@ Googles offisielle, åpne ressurs for hvordan de bruker OKR internt.
Edwin Locke og Gary Lathams forskning fra 1960-tallet og fremover er det vitenskapelige fundamentet for all målstyring. Edwin Locke og Gary Lathams forskning fra 1960-tallet og fremover er det vitenskapelige fundamentet for all målstyring.
**Hovedfunn:** **Hovedfunn:**
- Spesifikke, utfordrende mål gir 250% høyere ytelse enn "gjør ditt beste" - Spesifikke, vanskelige mål slår enkle/"gjør ditt beste"-mål i rundt 90% av studiene (Locke et al. 1981; ~96% i 2002-gjennomgangen), med en positiv, tilnærmet lineær sammenheng mellom målvanskelighet og ytelse — betinget av commitment, evne og fravær av motstridende mål — opp til evnegrensen
- Effect size 0.52-0.82 i meta-analyser (veldig sterk effekt) - Effect size 0.52-0.82 i meta-analyser (veldig sterk effekt)
- Fire mekanismer: Fokus, energi, utholdenhet, strategiutvikling - Fire mekanismer: Fokus, energi, utholdenhet, strategiutvikling
- Moderatorer: Feedback, self-efficacy, oppgavekompleksitet - Moderatorer: Feedback, self-efficacy, oppgavekompleksitet
@ -100,7 +101,7 @@ Viktig forskning på hvordan OKR fungerer i team-kontekst.
**Implikasjon:** I offentlig sektor bør mål formuleres som bidrag til felles resultat, ikke individuell prestasjon. **Implikasjon:** I offentlig sektor bør mål formuleres som bidrag til felles resultat, ikke individuell prestasjon.
**Referanse:** Kleingeld, A., van Mierlo, H., & Arends, L. (2011). "The effect of goal setting on group performance: A meta-analysis." **Referanse:** Kleingeld, A., van Mierlo, H., & Arends, L. (2011). "The effect of goal setting on group performance: A meta-analysis." *Journal of Applied Psychology*, 96(6), 1289-1304. DOI: 10.1037/a0024315.
--- ---
@ -122,17 +123,19 @@ Viktig forskning på hvordan OKR fungerer i team-kontekst.
### Intel: Operation Crush (1979-80) ### Intel: Operation Crush (1979-80)
**Kontekst:** Intel tapte mikroprosesormarkedet til Motorola. **Kontekst:** Intel var 16-bit-inkumbenten som mistet andeler til Motorolas 68000 — en eroderende posisjon, ikke fra null.
**OKR:** "Etablere 8086 som den høyest ytende 16-bit mikroprosessorfamilien" **OKR:** "Etablere 8086 som den høyest ytende 16-bit mikroprosessorfamilien"
**Resultat:** **Resultat:**
- 2000 "design wins" (kundeavtaler) - Mål: 2000 "design wins" (kundeavtaler); oppnådd over 2 300 (noen Intel-kilder sier ~2 500), inkludert IBM PCs Intel 8088
- Fra 0% til 85% markedsandel innen 1986 - Gjenvunnet ~85% av 16-bit-markedet innen 1986 — en posisjon Operation Crush (1979-80) bidro til å sette i gang, ikke et direkte kampanjeresultat
- Ingen produktendringer - kun strategisk reposisjonering - Ingen produktendringer - kun strategisk reposisjonering
**Lærdom:** Ett tydelig OKR kan samle hele organisasjonen mot en eksistensiell trussel. **Lærdom:** Ett tydelig OKR kan samle hele organisasjonen mot en eksistensiell trussel.
**Kilde:** Computer History Museum (Intel «Crush» oral history); Doerr, *Measure What Matters*. Merk: tallene stammer fra Doerr/Intel-kilder, ikke uavhengig revidert markedsforskning.
--- ---
### Spotify: OKR + Squad-modellen ### Spotify: OKR + Squad-modellen
@ -204,30 +207,43 @@ Viktig forskning på hvordan OKR fungerer i team-kontekst.
--- ---
### NRK, Telia, Ruter ### Digdir — produktorganisering med OKR (offentlig forvaltning)
Knowit Insight har samlet erfaringer fra disse store norske virksomhetene. **Kontekst:** Digitaliseringsdirektoratet (statlig forvaltningsorgan) dokumenterer egen OKR-bruk i sin produktleveransemodell: team planlegger OKR kvartalsvis, typisk 13 Objectives og 23 Key Results, med vekt på outcome over leveranser. Digdir anskaffet også ekstern OKR-fasilitering i 2025.
**Tre nøkkelpunkter for suksess:** **Lærdom:** OKR brukes reelt i norsk statsforvaltning på team-/produktnivå, ikke bare i privat sektor.
1. **Forankring:** Tydelig mandat fra ledelsen, bruk tid på allmøter
2. **Fokus:** Tør å prioritere, minimer antall mål
3. **Kontinuerlig arbeid:** OKR er en reise, ikke en destinasjon
**Lenke:** [Knowit blogg](https://blogg.knowit.no/vi-har-samlet-virksomheter-med-erfaring-fra-okr) **Kilde:** [baksia.digdir.no produktleveransemodell/OKR](https://baksia.digdir.no/produktleveransemodell/okr/) (first-party); [doffin.no anskaffelse 2025-115354](https://doffin.no/notices/2025-115354) (nøytralt anskaffelsesregister). *Provenans: first-party + uavhengig register.*
--- ---
### Futureworks / Kim Leinan ### Oslo kommune (Origo) — egen OKR-tracker (kommunal)
OKR-coaching spesialisert for norsk offentlig sektor. **Kontekst:** Oslo Origo, Oslo kommunes digitale byrå, bruker OKR for sine team og har bygget — og åpen-kildekode-publisert — sitt eget OKR-verktøy («OKR-tracker»).
**Innsikter:** **Lærdom:** Et internt bygget, MIT-lisensiert OKR-verktøy er sterk dokumentasjon på reell adopsjon — et verifiserbart artefakt, ikke en markedsføringspåstand.
- "Vi er flinke på planer, men mangler gjennomføringsrytme"
- OKR er ikke et prosjekt, det er en måte å tenke og jobbe på
- Start med pilot - én seksjon, ett team
- 15 minutters ukentlige innsjekker er nok
**Lenke:** [futureworks.no](https://futureworks.no/fra-strategiplan-til-faktisk-fremdrift-slik-lykkes-offentlig-sektor-med-okr/) **Kilde:** [labs.oslo.kommune.no](https://labs.oslo.kommune.no/tema/utviklingsopplevelse) (offisiell); [github.com/oslokommune/okr-tracker](https://github.com/oslokommune/okr-tracker) (open source, MIT). *Provenans: offisiell + verifiserbart artefakt.*
---
### Ruter — OKR i styringsmodellen (offentlig eid selskap)
**Kontekst:** Ruter AS (kollektivtrafikk, eid av Oslo kommune 60 % og Akershus fylkeskommune 40 % — et offentlig eid **selskap**, ikke en forvaltningsetat) oppgir i sin egen årsrapport 2024 at «OKR reflekterer selskapets strategiske satsinger og setter tydelige prioriteringer for en gitt periode».
**Lærdom:** OKR brukes også i offentlig eide selskaper, men merk at styringskonteksten (selskapsstyring) skiller seg fra forvaltningsorganenes tildelingsbrev-/Riksrevisjon-regime.
**Kilde:** [Ruter årsrapport 2024 organisasjon og styringsmodell](https://aarsrapport2024.ruter.no/baerekraft/generell-informasjon/ruters-organisasjon-og-styringsmodell/) (first-party). *Provenans: first-party.*
---
### Knowit: «Fire fallgruver med OKR» (praktiker-perspektiv)
**Kontekst:** Konsulentselskapet Knowit (Herman Ismar Gulbrandsen, 2021) beskriver fire vanlige fallgruver: (1) for mange OKR (utforming), (2) for mye top-down (innføring), (3) manglende oppfølging, (4) manglende horisontal koordinering (skalering).
**Lærdom:** Nyttig sjekkliste fra praksis — men merk at dette er et vendor-/praktiker-perspektiv, og at eksempel-organisasjonene i artikkelen er anonymiserte.
**Kilde:** [Knowit Fire fallgruver med OKRs](https://blogg.knowit.no/okr-fallgruver) (vendor/praktiker). *Provenans: vendor.*
--- ---
@ -296,7 +312,7 @@ Norsk podcast med flere episoder om OKR og målstyring.
## 5. Verktøy og plattformer ## 5. Verktøy og plattformer
### Oboard (Direktoratet for digital tjenesteutviklings valg) ### Oboard
Norskutviklet OKR-plattform, integrert med Microsoft 365. Norskutviklet OKR-plattform, integrert med Microsoft 365.
@ -348,7 +364,7 @@ Norskutviklet OKR-plattform, integrert med Microsoft 365.
### Nettsteder ### Nettsteder
- **[whatmatters.com](https://whatmatters.com)** - John Doerrs offisielle ressurs - **[whatmatters.com](https://whatmatters.com)** - John Doerrs offisielle ressurs
- **[rework.withgoogle.com](https://rework.withgoogle.com)** - Google re:Work - **[rework.withgoogle.com/en](https://rework.withgoogle.com/en)** - Google re:Work
- **[felipecastro.com](https://felipecastro.com)** - OKR-blogger og konsulent - **[felipecastro.com](https://felipecastro.com)** - OKR-blogger og konsulent
--- ---
@ -375,7 +391,7 @@ Når brukeren spør "Hvor kan jeg lære mer om OKR?", kan du referere til denne
1. **For nybegynnere:** Google re:Work guide + Doerrs TED Talk 1. **For nybegynnere:** Google re:Work guide + Doerrs TED Talk
2. **For dypere forståelse:** "Measure What Matters" + Locke & Latham 2. **For dypere forståelse:** "Measure What Matters" + Locke & Latham
3. **For norsk kontekst:** Smidigpodden episode 53 + Futureworks-artikkelen 3. **For norsk kontekst:** Smidigpodden episode 53
4. **For offentlig sektor:** San Jose-casen + NACP Ukraine 4. **For offentlig sektor:** San Jose-casen + NACP Ukraine
--- ---

View file

@ -0,0 +1,149 @@
---
name: okr-second-brain-search
description: >-
Search the user's personal OKR/organization "second brain" — an OKF-compatible
markdown wiki under .claude/okr/ (project) and ~/.claude/okr/org/ (home) — to
retrieve the right strategic, governance, or cycle context on demand, in free
chat and during /okr:* commands, without pre-injecting everything. Use whenever
the user refers to their own goals, tildelingsbrev, strategy, steering signals,
or a previous cycle and the answer likely lives in their wiki rather than the
prompt. Triggers on: "våre mål", "overordnede mål", "mål dette tertialet",
"tertialmål", "tildelingsbrev", "hva sier OKR-ene våre", "forrige syklus",
"strategi", "styringssignaler".
version: "1.6.1"
---
# OKR Second-Brain Search
Retrieve the *right* personal/organizational OKR context at the *right* moment by
searching a user-owned, OKF-compatible markdown wiki with native tools only —
**Glob, Read, Grep**. No MCP, no search engine, no pre-injection. This is the
on-demand counterpart to the `inject-okr-context` hook: the hook emits only a
tiny pointer; this skill does the actual retrieval when the conversation needs it.
## When to use
Activate when the user references their own goals, governance documents, strategy,
or prior cycles — in free chat **or** under an `/okr:*` command — and the answer
plausibly lives in their wiki rather than in the prompt. Typical Norwegian cues
are listed in `Triggers on:` above ("våre mål", "tildelingsbrev", "forrige
syklus", "styringssignaler", …). Do **not** activate for generic OKR methodology
questions — those belong to the `okr-offentlig-sektor` skill.
## The two bundle roots
The second brain lives in **two roots with different lifecycles**; always search
both, project first:
1. **Project bundle**`.claude/okr/` in the current working directory
(cycle/work data, cwd-bound): `strategisk-kontekst/`, `syklus/<id>/`,
`historikk/`, `dokumenter/`.
2. **Home bundle**`~/.claude/okr/org/` (organization identity, survives
reinstall).
Each root carries its own `index.md` per level and an `okf_version` marker on its
root `index.md`. Project content overrides home content on conflict
(most-specific-wins, mirroring the hook's resolution).
## OKF layout (what you are searching)
The wiki follows the Knowledge Catalog **"Documents / `kb` Layout"** ("Metadata as
Code"). Each **concept file** carries YAML frontmatter in this field order:
```yaml
---
type: Tildelingsbrev # Title-Case human string; the only de-facto required field
resource: https://… # URL/path to the source
title: …
description: …
tags: # YAML list (multi-line) — optional
- styring
timestamp: '2026-01-15T09:00:00+00:00' # ISO-8601, quoted
---
# Heading
…body…
```
`type` values you will encounter: `Tildelingsbrev`, `OKR`, `Retrospektiv`,
`Organisasjonsprofil`, `Virksomhetsplan`, `Status` (and occasionally others —
treat unknown types as valid, never error on them).
Each level has an **`index.md`** with **no frontmatter**, formatted as a heading
plus one bullet per concept file:
```
# Heading
* [title](relative.md) - description
```
The `index.md` is the curated table of contents for its level — use it to
navigate and to break ranking ties (below).
## Retrieval procedure
Use the native file tools as a retrieval triad — **Glob** (list) / **Read** (open)
/ **Grep** (search) — together with semantic decomposition (the `discovery`
pattern: expand the query before searching rather than grepping verbatim). Concretely:
1. **Locate & navigate.** Glob both roots — the project root `.claude/okr/**/*.md`,
and the home root `<HOME>/.claude/okr/org/**/*.md` (expand `~` to the absolute
home path first — the Glob tool does **not** expand a literal `~`). Read each
level's `index.md` first to understand what exists before reading any concept file.
2. **Semantic decomposition***do not* grep the user's words verbatim. Generate
**up to 3 query variations** and search all of them:
- **(i) Direct + synonyms** — the literal term plus close synonyms
(e.g. "mål" → "mål", "objective", "OKR").
- **(ii) Domain translation** — map everyday phrasing to the domain/governance
vocabulary actually written in the files (e.g. "hva må vi levere" →
"tildelingsbrev", "styringssignal"; "forrige runde" → "retrospektiv",
"historikk").
- **(iii) Broader category** — fall back to the `type:` or the containing level
(e.g. search `type: OKR` under `syklus/`, or `type: Tildelingsbrev` under
`strategisk-kontekst/`).
3. **Grep** each variation across the concept files (token in body/frontmatter,
or `type:` for category queries). Exclude `index.md` from concept matches.
4. **Merge, dedup, and rank.** Combine the hits from all variations, dedup by
path, then rank by this **deterministic tie-break**:
- **+2** if the token appears in the file's **frontmatter** (`title`/`type`/`tags`).
- **+1** if the token appears in the file's entry in the nearest **`index.md`**.
- On equal score, the file listed **earlier in its `index.md`** wins; final ties
break by path order.
This is exactly the ranking the regression test (`tests/okf-retrieval.test.mjs`)
pins: a frontmatter/index match outranks a body-only match, so a shared token
resolves to the concept that *names* it rather than one that merely mentions it.
5. **Read only the top concept file(s)** — usually one. Do not bulk-read the tree;
the point is selective retrieval. Cite the file path you used.
### Robustness
- **Dangling links** in an `index.md` (a bullet pointing at a missing file): skip
silently, never fail.
- **Unknown `type:`**: accept it; rank and read as normal.
- **Empty/greenfield wiki**: if neither root exists or has concept files, say so
briefly and proceed from the prompt — never fabricate wiki content.
## Trigger smoke-test (Assumption 1 — manual)
Skill activation is model-judged, not unit-testable. Verify it manually:
- **`/doctor`** — confirm `okr-second-brain-search` is listed and its
`description` is not trimmed by the skill-listing budget. If trimmed, raise
`skillListingBudgetFraction` (the `Triggers on:` phrases are placed early in the
description precisely so the highest-signal cues survive trimming).
- **Free-chat probe** — with a populated `.claude/okr/`, send a prompt with **no**
`/okr:*` command, e.g. *"Hva sier tildelingsbrevet vårt om mål dette tertialet?"*
The skill should activate and retrieve from `strategisk-kontekst/` and
`syklus/<id>/` without being told where to look.
- **Fallback** — if activation does not fire, the `inject-okr-context` hook still
emits a deterministic pointer to the resolved `index.md`, and any `/okr:*`
command can invoke this skill explicitly. Retrieval degrades, it does not break.
## Resources
- **Wiki data (project):** `.claude/okr/``strategisk-kontekst/`, `syklus/<id>/`,
`historikk/`, `dokumenter/`, each with an `index.md`.
- **Wiki data (home):** `~/.claude/okr/org/` — organization profile bundle.
- **Index/check tooling:** `scripts/okf-index.mjs` (regenerate `index.md` per root)
and `scripts/okf-check.mjs` (validate every concept file has `type:`).
- **Companion skill:** `okr-offentlig-sektor` for OKR methodology (writing,
scoring, cascading) — this skill only *retrieves* the user's own context.

View file

@ -2,27 +2,57 @@
# OKR Plugin - Lokal Konfigurasjon # OKR Plugin - Lokal Konfigurasjon
# Plasser denne filen som .claude/okr.local.md i ditt prosjekt # Plasser denne filen som .claude/okr.local.md i ditt prosjekt
# Kjør /okr:oppsett for automatisk konfigurasjon # Kjør /okr:oppsett for automatisk konfigurasjon
# Skjemaet under speiler /okr:oppsett mvp — feltnavn er i paritet med onboarding og hook
# Org-identitet (organisasjon:/program:) kan ogsaa bo i en reinstall-overlevende
# hjem-profil: ~/.claude/okr/org/profil.md (skrevet av /okr:oppsett). En prosjektlokal
# fil her vinner over hjem-profilen (mest-spesifikk-vinner); syklus-/kontekstdata er
# alltid prosjektlokalt.
# OKF-frontmatter (second-brain-kompatibel): hjem-profilen faar disse tre top-level-
# noeklene av /okr:oppsett via compose-org-profile.mjs. Kun type/resource/timestamp er
# trygge top-level (leses ikke av hooks); legg ALDRI til top-level navn/id/fase/domene/
# sektor -- de ville skygge de nestede verdiene under organisasjon:/gjeldende_syklus:.
type: Organisasjonsprofil
resource: ~/.claude/okr/org/profil.md
timestamp: '' # ISO-8601, settes ved skriv
onboarding_status: partial # partial | fullfort
organisasjon: organisasjon:
navn: "Din organisasjon" navn: "Din organisasjon"
kortform: "" # kort forkortelse, f.eks. SVV
type: "offentlig" # offentlig | privat type: "offentlig" # offentlig | privat
sektor: "transport" # transport | helse | justis | digitalisering | annet departement: "" # overordnet departement
ansatte_i_okr_program: 0
syklus: domene: "transport" # transport | helse | justis | digitalisering | annet
modell: "tertial" # tertial (4-mnd) | kvartal (3-mnd) geografi: "nasjonal" # nasjonal | regional | lokal
gjeldende: "T1-2026" # Format: T[1-3]-YYYY eller Q[1-4]-YYYY program:
startdato: "2026-01-01" modenhetsnivaa: "utforsker" # ikke-startet | utforsker | pilot | skalering | moden
sykluser_gjennomfort: 0
sponsor: ""
champion: ""
okr_frikoblet_fra_loenn: true # true = OKR ikke koblet til lønnssamtale (anbefalt)
alignment_tilnaerming: "bidireksjonell" # top-down | bottom-up | bidireksjonell
gjeldende_syklus:
id: "T1-2026" # Format: T[1-3]-YYYY eller Q[1-4]-YYYY
periode: "2026-01-01 til 2026-04-30"
fase: "planlegging" # planlegging | gjennomforing | review | avslutning
antall_team: 1
verktoy:
oppgavestyring: ""
okr_tracking: ""
moeteverktoy: ""
leveransemetodikk: ""
kultur:
sjekk_inn_rytme: "annenhver uke"
psykologisk_trygghet: "middels" # lav | middels | hoey
kjente_utfordringer: []
integrasjoner: integrasjoner:
linear: linear:
aktivert: false aktivert: false
team_id: "" # Linear team ID team_id: "" # Linear team ID
project_id: "" # Linear project ID for OKR project_id: "" # Linear project ID for OKR
preferanser: preferanser:
språk: "no" # no | en spraak: "no" # no | en
vis_eksempler: true vis_eksempler: true
ambisjonsnivå: "balansert" # konservativ | balansert | ambisiøs ambisjonsnivaa: "balansert" # konservativ | balansert | ambisiøs
--- ---
# Notater # Notater

View file

@ -0,0 +1,99 @@
// coaching-hook.test.mjs
// Tester coaching-hook (SC4): graceful-exit-grener + gyldig-syklus-emisjon +
// deterministisk fase via OKR_NOW klokke-seam. Spawner hooken som subprosess
// med kontrollert cwd + env. Zero npm deps. Coaching leser kun cwd, saa én
// temp-dir per test holder. Moenster: tests/inject-okr-context.test.mjs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HOOK = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'hooks',
'scripts',
'coaching-hook.mjs',
);
function writeConfig(workDir, body) {
const p = join(workDir, '.claude');
mkdirSync(p, { recursive: true });
writeFileSync(join(p, 'okr.local.md'), body);
}
function runHook(cwd, okrNow) {
// execFileSync returnerer stdout; hooken avslutter alltid med exit 0.
const env = { ...process.env };
if (okrNow) env.OKR_NOW = okrNow;
else delete env.OKR_NOW;
return execFileSync('node', [HOOK], { cwd, env, encoding: 'utf8' });
}
function withWork(fn) {
const work = mkdtempSync(join(tmpdir(), 'okrcoach-'));
try {
fn(work);
} finally {
rmSync(work, { recursive: true, force: true });
}
}
test('ingen config: exit 0, tom stdout (graceful)', () => {
withWork((work) => {
const out = runHook(work);
assert.equal(out.trim(), '', 'ingen config -> tom stdout, ingen blokkering');
});
});
test('config uten id: exit 0, tom stdout', () => {
withWork((work) => {
writeConfig(work, '---\nnavn: "Org"\n---\n');
const out = runHook(work);
assert.equal(out.trim(), '', 'manglende id -> tom stdout');
});
});
test('ugyldig id: exit 0, tom stdout', () => {
withWork((work) => {
writeConfig(work, '---\nid: "ugyldig"\n---\n');
const out = runHook(work);
assert.equal(out.trim(), '', 'id som ikke matcher T/Q-moenster -> tom stdout');
});
});
test('gyldig syklus: emitterer systemMessage', () => {
withWork((work) => {
writeConfig(work, '---\nid: "T2-2026"\n---\n');
const out = runHook(work);
assert.match(out, /OKR coaching/, 'gyldig syklus skal emittere coaching-melding');
});
});
test('OKR_NOW tidlig fase: early-coaching', () => {
withWork((work) => {
writeConfig(work, '---\nid: "T2-2026"\n---\n');
const out = runHook(work, '2026-05-05');
assert.match(out, /Tidlig i syklusen/, 'uke 1 av T2 -> tidlig fase');
});
});
test('OKR_NOW midtveis fase: mid-coaching', () => {
withWork((work) => {
writeConfig(work, '---\nid: "T2-2026"\n---\n');
const out = runHook(work, '2026-06-20');
assert.match(out, /Midtveis i syklusen/, 'uke ~8 av T2 -> midtveis fase');
});
});
test('OKR_NOW sen fase: late-coaching', () => {
withWork((work) => {
writeConfig(work, '---\nid: "T2-2026"\n---\n');
const out = runHook(work, '2026-08-25');
assert.match(out, /sluttspurt/, 'uke ~16 av T2 -> sen fase');
});
});

View file

@ -0,0 +1,7 @@
# Tildelingsbrev 2026
Tildelingsbrevet gir overordnede foringer for virksomheten i 2026.
## Oppfolging mot Virksomhetsplan 2026
Tildelingsbrevet bygger paa Virksomhetsplan 2026 og maalene som er satt der.

View file

@ -0,0 +1,7 @@
# Virksomhetsplan 2026
Virksomhetsplanen konkretiserer de overordnede maalene for perioden.
## Mal og rammer (2026) [utkast]
Rammene for perioden er forelopige, jf. tildelingsbrev.

7
tests/fixtures/inbox-sample/dok-a.txt vendored Normal file
View file

@ -0,0 +1,7 @@
# Tildelingsbrev 2026
Tildelingsbrevet gir overordnede foringer for virksomheten i 2026.
## Oppfolging mot Virksomhetsplan 2026
Tildelingsbrevet bygger paa Virksomhetsplan 2026 og maalene som er satt der.

7
tests/fixtures/inbox-sample/dok-b.txt vendored Normal file
View file

@ -0,0 +1,7 @@
# Virksomhetsplan 2026
Virksomhetsplanen konkretiserer de overordnede maalene for perioden.
## Mal og rammer (2026) [utkast]
Rammene for perioden er forelopige, jf. tildelingsbrev.

5
tests/fixtures/okf-minimal/index.md vendored Normal file
View file

@ -0,0 +1,5 @@
# OKF second brain (minimal testfixtur)
okf_version: kb-layout-2026-06
* [Tildelingsbrev](tildelingsbrev.md) - Styringssignal med unik testtoken.

View file

@ -0,0 +1,12 @@
---
type: Tildelingsbrev
resource: https://example.no/min/tildelingsbrev
title: Minimalt tildelingsbrev
description: Et minimalt OKF-konsept med en unik token.
tags:
- styring
timestamp: '2026-01-10T08:00:00+00:00'
---
# Tildelingsbrev
Dette dokumentet inneholder den unike testtokenen kvikkleireskred.

View file

@ -0,0 +1,3 @@
# Dokumenter
* [Arbeidsnotat](notat.md) - Internt arbeidsnotat med ukjent type.

View file

@ -0,0 +1,12 @@
---
type: Notat
resource: local
title: Arbeidsnotat
description: Internt arbeidsnotat (ukjent OKF-type for robusthetstest).
tags:
- internt
timestamp: '2026-03-01T12:00:00+00:00'
---
# Arbeidsnotat
Dette arbeidsnotat har en type som ikke er i det kjente OKR-settet.

View file

@ -0,0 +1,4 @@
# Historikk
* [Retrospektiv T3-2025](retrospektiv-T3-2025.md) - Laeringssloeyfe fra forrige syklus.
* [Retrospektiv T2-2025](retrospektiv-T2-2025.md) - Mangler med vilje (dangling-test).

View file

@ -0,0 +1,12 @@
---
type: Retrospektiv
resource: local
title: Retrospektiv T3-2025
description: Laering fra forrige syklus.
tags:
- retro
timestamp: '2025-12-20T14:00:00+00:00'
---
# Retrospektiv T3-2025
Viktigste laeringssloeyfe: tettere oppfoelging av KR-data.

8
tests/fixtures/okf-realistic/index.md vendored Normal file
View file

@ -0,0 +1,8 @@
# OKF second brain - Vegdirektoratet (realistisk testfixtur)
okf_version: kb-layout-2026-06
* [Strategisk kontekst](strategisk-kontekst/index.md) - Tildelingsbrev, virksomhetsplan og profil.
* [Syklus T1-2026](syklus/T1-2026/index.md) - Aktive OKR og statusrapport.
* [Historikk](historikk/index.md) - Arkiverte sykluser og retrospektiv.
* [Dokumenter](dokumenter/index.md) - Lose arbeidsdokumenter.

View file

@ -0,0 +1,5 @@
# Strategisk kontekst
* [Tildelingsbrev 2026](tildelingsbrev-2026.md) - Klimaomstilling og sikkerhet.
* [Virksomhetsplan 2026](virksomhetsplan.md) - Kompetanseloeft og effektivisering.
* [Organisasjonsprofil](organisasjonsprofil.md) - Vegdirektoratet, offentlig sektor.

View file

@ -0,0 +1,12 @@
---
type: Organisasjonsprofil
resource: local
title: Organisasjonsprofil
description: Identitet og sektor for organisasjonen.
tags:
- profil
timestamp: '2026-01-05T08:00:00+00:00'
---
# Organisasjonsprofil
Organisasjonen er Vegdirektoratet, en etat i offentlig sektor.

View file

@ -0,0 +1,13 @@
---
type: Tildelingsbrev
resource: https://example.no/tildelingsbrev-2026
title: Tildelingsbrev 2026
description: Departementets styringssignaler for budsjettaaret 2026.
tags:
- styring
- klima
timestamp: '2026-01-15T09:00:00+00:00'
---
# Tildelingsbrev 2026
Hovedprioriteten for 2026 er klimaomstilling i transportsektoren.

View file

@ -0,0 +1,12 @@
---
type: Virksomhetsplan
resource: https://example.no/virksomhetsplan-2026
title: Virksomhetsplan 2026
description: Intern operasjonalisering av tildelingsbrevet.
tags:
- plan
timestamp: '2026-01-20T10:00:00+00:00'
---
# Virksomhetsplan 2026
Et sentralt tiltak er et kompetanseloeft for digital forvaltning.

View file

@ -0,0 +1,5 @@
# Syklus T1-2026
* [Trafikksikkerhet og tunnelsikkerhet](okr-trafikksikkerhet.md) - Maal om nullvisjon i tunneler.
* [Digitalisering av tjenester](okr-digitalisering.md) - Oekt selvbetjeningsgrad.
* [Statusrapport T1](status.md) - Fremdriftsindikator for tertialet.

View file

@ -0,0 +1,12 @@
---
type: OKR
resource: local
title: Digitalisering av tjenester
description: Tertialmaal for digital selvbetjening.
tags:
- digital
timestamp: '2026-02-01T09:05:00+00:00'
---
# Digitalisering av tjenester
Objective: Oeke selvbetjeningsgrad i publikumstjenester.

View file

@ -0,0 +1,14 @@
---
type: OKR
resource: local
title: Trafikksikkerhet og tunnelsikkerhet
description: Tertialmaal for sikrere veier og tunneler.
tags:
- sikkerhet
- tunnel
timestamp: '2026-02-01T09:00:00+00:00'
---
# Trafikksikkerhet og tunnelsikkerhet
Objective: Styrke tunnelsikkerhet i tertialet.
KR1: Naa nullvisjon for alvorlige tunnelhendelser.

View file

@ -0,0 +1,12 @@
---
type: Status
resource: local
title: Statusrapport T1-2026
description: Fremdrift for innevaerende tertial.
tags:
- status
timestamp: '2026-04-30T16:00:00+00:00'
---
# Statusrapport T1-2026
Fremdriftsindikator viser god progresjon. Arbeidet med tunnelsikkerhet ligger noe bak skjema.

View file

@ -0,0 +1,109 @@
// frontmatter-compat.test.mjs
// Karakteriserings-/kompatibilitetstest (SC2): beviser at ruting av
// inject-okr-context.mjs gjennom den delte lib/frontmatter.mjs er
// ATFERDS-BEVARENDE for de 9 leste noeklene, med EN tilsiktet endring:
// comment-leak paa usitert `okr_frikoblet_fra_loenn`-linje forsvinner.
// Spawner hooken som subprosess (full 9-felts frontmatter, rikere enn
// inject-okr-context.test.mjs sin makeProjectConfig som kun skriver navn).
// Zero npm deps. Moenster: tests/inject-okr-context.test.mjs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HOOK = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'hooks',
'scripts',
'inject-okr-context.mjs',
);
// Bygger en full 9-felts okr.local.md. okr_frikoblet_fra_loenn skrives USITERT
// MED en trailing "# ..."-kommentar — det er denne lekkasjen den delte
// parseren skal fjerne.
function makeFullConfig(workDir, fields = {}) {
const f = {
navn: 'TestOrg',
kortform: 'TO',
gjeldende: 'T2-2026',
sektor: 'transport',
modenhetsnivaa: 'nivaa-2',
fase: 'utforsker',
okr_frikoblet_fra_loenn: 'true',
psykologisk_trygghet: 'hoy',
linear: true,
...fields,
};
const body =
[
'---',
`navn: "${f.navn}"`,
`kortform: "${f.kortform}"`,
`gjeldende: "${f.gjeldende}"`,
`sektor: "${f.sektor}"`,
`modenhetsnivaa: "${f.modenhetsnivaa}"`,
`fase: "${f.fase}"`,
`okr_frikoblet_fra_loenn: ${f.okr_frikoblet_fra_loenn} # frikoblet fra lonn`,
`psykologisk_trygghet: "${f.psykologisk_trygghet}"`,
'linear:',
` aktivert: ${f.linear}`,
'---',
'',
].join('\n');
const p = join(workDir, '.claude');
mkdirSync(p, { recursive: true });
writeFileSync(join(p, 'okr.local.md'), body);
}
const OKR_PROMPT = JSON.stringify({ prompt: 'hjelp meg skrive OKR' });
function runHook(cwd, home, input = OKR_PROMPT) {
return execFileSync('node', [HOOK], {
cwd,
env: { ...process.env, HOME: home },
input,
encoding: 'utf8',
});
}
function withDirs(fn) {
const home = mkdtempSync(join(tmpdir(), 'okrhome-'));
const work = mkdtempSync(join(tmpdir(), 'okrwork-'));
try {
fn(home, work);
} finally {
rmSync(home, { recursive: true, force: true });
rmSync(work, { recursive: true, force: true });
}
}
test('full config: alle 9 noekler resolver likt som foer', () => {
withDirs((home, work) => {
makeFullConfig(work);
const out = runHook(work, home);
assert.match(out, /Organisasjon: TestOrg \(TO\)/, 'navn + kortform');
assert.match(out, /Syklus: T2-2026 \[utforsker\]/, 'gjeldende + fase');
assert.match(out, /Sektor: transport/, 'sektor');
assert.match(out, /Modenhet: nivaa-2/, 'modenhetsnivaa');
assert.match(out, /OKR frikoblet fra lonn: true/, 'frikoblet-verdi');
assert.match(out, /Psykologisk trygghet: hoy/, 'psykologisk_trygghet');
assert.match(out, /Linear: aktivert/, 'linear aktivert: true');
});
});
test('comment-leak fjernet: ingen "#" i injisert melding', () => {
withDirs((home, work) => {
makeFullConfig(work);
const out = runHook(work, home);
assert.doesNotMatch(
out,
/#/,
'usitert frikoblet-linjes "# ..."-kommentar skal ikke lekke inn i meldingen',
);
});
});

135
tests/frontmatter.test.mjs Normal file
View file

@ -0,0 +1,135 @@
// frontmatter.test.mjs
// Tester den delte frontmatter-modulen (SC2): parseFrontmatter/get reproduserer
// hookenes flate first-match-parser MEN (1) strip trailing " #..." KUN fra
// USITERTE verdier, (2) anker key-match til linjestart (ingen midt-i-linje
// falske treff), og (3) tolererer fler-linje OKF-list-verdier (`tags:`) uten
// krasj. writeFrontmatter round-tripper og siterer `#`. Importerer modulen
// direkte (zero npm deps). Moenster: tests/coaching-hook.test.mjs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseFrontmatter, writeFrontmatter } from '../lib/frontmatter.mjs';
test('basic: sitert skalar leses', () => {
const { get } = parseFrontmatter('---\nnavn: "Org"\n---\n');
assert.equal(get('navn'), 'Org');
});
test('basic: usitert skalar leses', () => {
const { get } = parseFrontmatter('---\nnavn: Org\n---\n');
assert.equal(get('navn'), 'Org');
});
test('usitert verdi: trailing "# kommentar" strippes (comment-leak-fix)', () => {
const { get } = parseFrontmatter(
'---\nokr_frikoblet_fra_loenn: true # frikoblet fra lonn\n---\n',
);
assert.equal(get('okr_frikoblet_fra_loenn'), 'true', 'usitert: # strippes');
});
test('sitert verdi: intern "#" bevares (space-hash i quotes)', () => {
const { get } = parseFrontmatter('---\nkortform: "A #B"\n---\n');
assert.equal(get('kortform'), 'A #B', 'sitert: # er en del av verdien, ikke kommentar');
});
test('sitert verdi med kommentar UTENFOR quotes: kommentar ignoreres', () => {
const { get } = parseFrontmatter('---\nkortform: "A #B" # en note\n---\n');
assert.equal(get('kortform'), 'A #B');
});
test('first-match: forste forekomst vinner', () => {
const { get } = parseFrontmatter('---\nnavn: "Forste"\nnavn: "Andre"\n---\n');
assert.equal(get('navn'), 'Forste');
});
test('nestet: innrykket key resolver (load-bearing inject:69)', () => {
const { get } = parseFrontmatter(
'---\norganisasjon:\n navn: "NestetOrg"\n type: "offentlig"\n---\n',
);
assert.equal(get('navn'), 'NestetOrg', 'innrykket navn under organisasjon: skal resolve');
});
test('linjeanker: key matcher IKKE som substring av annen key', () => {
const { get } = parseFrontmatter('---\netternavn: "Hansen"\n---\n');
assert.equal(get('navn'), null, 'navn skal ikke matche etternavn: (anker linjestart)');
});
test('OKF type: Title-Case enkeltlinje', () => {
const { get } = parseFrontmatter('---\ntype: Organisasjonsprofil\n---\n');
assert.equal(get('type'), 'Organisasjonsprofil');
});
test('OKF resource: URL med ":" bevares', () => {
const { get } = parseFrontmatter('---\nresource: https://example.com/path\n---\n');
assert.equal(get('resource'), 'https://example.com/path');
});
test('OKF timestamp: sitert ISO-8601 med ":" og "+" bevares', () => {
const { get } = parseFrontmatter("---\ntimestamp: '2026-05-28T22:49:59+00:00'\n---\n");
assert.equal(get('timestamp'), '2026-05-28T22:49:59+00:00');
});
test('OKF fler-linje tags-liste: krasjer ikke + folgende skalar resolver', () => {
const content =
'---\n' +
'type: BigQuery Dataset\n' +
'tags:\n' +
'- ecommerce\n' +
'- web analytics\n' +
"timestamp: '2026-05-28T22:49:59+00:00'\n" +
'---\n# body\n';
const { get } = parseFrontmatter(content);
assert.doesNotThrow(() => get('tags'), 'get paa list-key skal ikke krasje');
assert.equal(get('type'), 'BigQuery Dataset');
assert.equal(
get('timestamp'),
'2026-05-28T22:49:59+00:00',
'skalar etter list-blokk skal fortsatt resolve',
);
});
test('ingen frontmatter: raw null, get returnerer null', () => {
const { raw, get } = parseFrontmatter('ingen frontmatter her\n');
assert.equal(raw, null);
assert.equal(get('navn'), null);
});
test('writeFrontmatter: round-trip via parseFrontmatter', () => {
const block = writeFrontmatter({ navn: 'Org', type: 'Organisasjonsprofil' });
const { get } = parseFrontmatter(block);
assert.equal(get('navn'), 'Org');
assert.equal(get('type'), 'Organisasjonsprofil');
});
test('writeFrontmatter: verdi med "#" siteres og round-tripper', () => {
const block = writeFrontmatter({ kortform: 'A #B' });
assert.match(block, /kortform: "A #B"/, 'verdi med # skal siteres');
const { get } = parseFrontmatter(block);
assert.equal(get('kortform'), 'A #B', 'sitert # round-tripper');
});
test('writeFrontmatter: array-verdi (tags) -> OKF multi-linje liste (Step 3)', () => {
const block = writeFrontmatter({ type: 'OKR', tags: ['a', 'b'], kilde: 'innboks' });
assert.match(block, /tags:\n - a\n - b/, 'array -> innrykket OKF-liste');
const { get } = parseFrontmatter(block);
assert.equal(get('type'), 'OKR', 'skalar FOR list-blokk resolver');
assert.equal(get('kilde'), 'innboks', 'skalar ETTER innrykket list-blokk resolver fortsatt');
// Lese-siden uendret (Step 3 scope = kun skrive-siden): get() paa en list-key
// krasjer ikke (eksisterende tolerance-kontrakt, frontmatter.mjs:14-16). Den
// returnerer foerste list-element fordi parser-\s* spiser newline -- IKKE
// null; konsumentene leser aldri tag-VERDIER, kun at nabo-skalarer resolver.
assert.doesNotThrow(() => get('tags'), 'get paa list-key krasjer ikke');
});
test('writeFrontmatter: skalar uendret av array-gren (additiv)', () => {
const block = writeFrontmatter({ navn: 'Org', type: 'Notat' });
assert.match(block, /navn: Org/);
assert.match(block, /type: Notat/);
assert.doesNotMatch(block, /^\s*-\s/m, 'ingen list-syntaks for skalarer');
});
test('writeFrontmatter: tom array -> kun key-linje, ingen items', () => {
const block = writeFrontmatter({ tags: [] });
assert.match(block, /tags:\n/, 'tom array gir key-linje');
assert.doesNotMatch(block, /^\s*-\s/m, 'ingen item-linjer for tom array');
});

View file

@ -0,0 +1,138 @@
// inject-core-cap.test.mjs
// Verifiserer hook-splitten (SC5): inject-okr-context emitterer kun kjerne-
// kontekst + EN resolvert index.md-peker, ALDRI tre-enumerering. Beviser:
// (a) byte-cap < 512; (b) payload uavhengig av filantall (lite vs stort tre);
// (c) ingen konsept-filnavn lekker; (d) home-only -> home-bundle-sti i peker;
// (e) ingen '#'.
// Spawner hooken som subprosess m/ kontrollert cwd + HOME. Zero npm deps.
// Monster: tests/inject-okr-context.test.mjs + tre-bygger-helper.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HOOK = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'hooks',
'scripts',
'inject-okr-context.mjs',
);
const OKR_PROMPT = JSON.stringify({ prompt: 'hjelp meg skrive OKR' });
function makeProjectConfig(work, navn = 'CapTestOrg') {
const p = join(work, '.claude');
mkdirSync(p, { recursive: true });
writeFileSync(
join(p, 'okr.local.md'),
`---\nnavn: "${navn}"\ngjeldende: "T1-2026"\nsektor: "transport"\n---\n`,
);
}
// Idempotent paa index.md, additiv paa konsept-filer (0..count-1). Distinkte
// filnavn (konseptfil-N) for lekkasje-testen.
function makeProjectTree(work, count) {
const lvl = join(work, '.claude', 'okr', 'strategisk-kontekst');
mkdirSync(lvl, { recursive: true });
writeFileSync(
join(work, '.claude', 'okr', 'index.md'),
'# OKR-rot\n\nokf_version: kb-layout-2026-06\n',
);
for (let i = 0; i < count; i++) {
writeFileSync(join(lvl, `konseptfil-${i}.md`), `---\ntype: OKR\n---\n# K${i}\n`);
}
}
function makeHomeBundle(home, navn = 'HomeCapOrg') {
const org = join(home, '.claude', 'okr', 'org');
mkdirSync(org, { recursive: true });
writeFileSync(join(org, 'profil.md'), `---\nnavn: "${navn}"\n---\n`);
writeFileSync(join(org, 'index.md'), '# Org-rot\n\nokf_version: kb-layout-2026-06\n');
}
function runHook(cwd, home, input = OKR_PROMPT) {
return execFileSync('node', [HOOK], {
cwd,
env: { ...process.env, HOME: home },
input,
encoding: 'utf8',
});
}
function systemMessage(out) {
return JSON.parse(out).systemMessage;
}
function withDirs(fn) {
const home = mkdtempSync(join(tmpdir(), 'okrhome-'));
const work = mkdtempSync(join(tmpdir(), 'okrwork-'));
try {
fn(home, work);
} finally {
rmSync(home, { recursive: true, force: true });
rmSync(work, { recursive: true, force: true });
}
}
// (a) byte-cap < 512
test('cap: systemMessage < 512 byte (utf8) selv med 50 konsept-filer', () => {
withDirs((home, work) => {
makeProjectConfig(work);
makeProjectTree(work, 50);
const msg = systemMessage(runHook(work, home));
const bytes = Buffer.byteLength(msg, 'utf8');
assert.ok(bytes < 512, `payload skal vaere < 512 B, var ${bytes}`);
});
});
// (b) invariant: lite (index+3) vs stort (index+50) tre -> byte-identisk payload.
// SAMME work-dir i begge maalinger -> peker-stien er konstant; eneste variabel
// er filantallet, som splitten skal gjore irrelevant.
test('invariant: payload byte-identisk for lite og stort tre (ingen enumerering)', () => {
withDirs((home, work) => {
makeProjectConfig(work);
makeProjectTree(work, 3);
const small = systemMessage(runHook(work, home));
makeProjectTree(work, 50);
const large = systemMessage(runHook(work, home));
assert.equal(small, large, 'filantall skal ikke paavirke payload');
});
});
// (c) ingen konsept-filnavn enumerert
test('ingen lekkasje: konsept-filnavn opptrer ikke i payload; peker bruker index.md', () => {
withDirs((home, work) => {
makeProjectConfig(work);
makeProjectTree(work, 5);
const msg = systemMessage(runHook(work, home));
assert.doesNotMatch(msg, /konseptfil-/, 'konsept-filnavn skal ikke enumereres');
assert.match(msg, /index\.md/, 'peker skal referere index.md');
});
});
// (d) home-only -> home-bundle index.md i peker
test('home-only: peker bruker home-bundle index.md (~/.claude/okr/org/index.md)', () => {
withDirs((home, work) => {
makeHomeBundle(home); // tomt prosjekt, kun home
const msg = systemMessage(runHook(work, home));
assert.match(msg, /HomeCapOrg/, 'org leses fra home-profil');
assert.match(msg, /okr-second-brain-search/, 'peker nevner skillen');
const expected = join(home, '.claude', 'okr', 'org', 'index.md');
assert.ok(msg.includes(expected), `peker skal inneholde home-index-sti: ${expected}`);
});
});
// (e) ingen '#'
test('ingen "#" i payload (peker + sti ASCII-rent)', () => {
withDirs((home, work) => {
makeProjectConfig(work);
makeProjectTree(work, 3);
const msg = systemMessage(runHook(work, home));
assert.doesNotMatch(msg, /#/, 'payload skal ikke inneholde "#"');
});
});

View file

@ -0,0 +1,84 @@
// inject-okr-context.test.mjs
// Tester hybrid org-profil-resolusjon (SC6): prosjekt-lokal -> hjem-profil.
// Spawner hooken som subprosess med kontrollert cwd + HOME. Zero npm deps.
// Plassert i tests/ (ikke hooks/scripts/) pga. pathguard-vern av hook-mappa.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HOOK = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'hooks',
'scripts',
'inject-okr-context.mjs',
);
function makeHomeProfil(homeDir, navn) {
const p = join(homeDir, '.claude', 'okr', 'org');
mkdirSync(p, { recursive: true });
writeFileSync(join(p, 'profil.md'), `---\nnavn: "${navn}"\n---\n`);
}
function makeProjectConfig(workDir, navn) {
const p = join(workDir, '.claude');
mkdirSync(p, { recursive: true });
writeFileSync(join(p, 'okr.local.md'), `---\nnavn: "${navn}"\n---\n`);
}
function runHook(cwd, home, input = '') {
// execFileSync returns stdout; the hook always exits 0.
// input pipes a UserPromptSubmit payload through stdin so the emne-guard
// (topic guard) sees an explicit OKR-relevant prompt instead of suppressing.
return execFileSync('node', [HOOK], {
cwd,
env: { ...process.env, HOME: home },
input,
encoding: 'utf8',
});
}
// OKR-relevant prompt slik at emne-guarden injiserer i stedet for aa suppresse.
const OKR_PROMPT = JSON.stringify({ prompt: 'hjelp meg skrive OKR' });
function withDirs(fn) {
const home = mkdtempSync(join(tmpdir(), 'okrhome-'));
const work = mkdtempSync(join(tmpdir(), 'okrwork-'));
try {
fn(home, work);
} finally {
rmSync(home, { recursive: true, force: true });
rmSync(work, { recursive: true, force: true });
}
}
test('kun-hjem: leser org fra hjem-profil, ingen syklus-kontekst', () => {
withDirs((home, work) => {
makeHomeProfil(home, 'HjemOrg');
const out = runHook(work, home, OKR_PROMPT);
assert.match(out, /HjemOrg/, 'org skal komme fra hjem-profil naar prosjekt-config mangler');
assert.doesNotMatch(out, /Tilgjengelige kontekstfiler/, 'kun-hjem har ingen prosjekt-lokalt syklus-tre (okrDir er cwd-bundet)');
});
});
test('begge: prosjekt-lokal vinner over hjem (mest-spesifikk-vinner)', () => {
withDirs((home, work) => {
makeHomeProfil(home, 'HjemOrg');
makeProjectConfig(work, 'ProsjektOrg');
const out = runHook(work, home, OKR_PROMPT);
assert.match(out, /ProsjektOrg/, 'prosjekt-lokal skal vinne');
assert.doesNotMatch(out, /HjemOrg/, 'hjem skal ikke leses naar prosjekt-config finnes');
});
});
test('ingen: exit 0, tom output (graceful, regresjonsvakt)', () => {
withDirs((home, work) => {
const out = runHook(work, home);
assert.equal(out.trim(), '', 'ingen config -> tom stdout, ingen blokkering');
});
});

View file

@ -0,0 +1,124 @@
// innboks-frontmatter.test.mjs
// Step 5 (SC idempotens): deterministisk frontmatter-projeksjon. projectFrontmatter
// utleder type/tags regel-basert (INGEN LLM) og snapper mot lukket vokab; resource =
// original relativ sti; description = foerste ikke-tomme avsnitt; timestamp = ISO av
// original-mtime (IKKE veggklokke); kilde:innboks; og fester concept.destRel =
// routeLevel(type)/slug.md FOER relasjons-steget. Asserterer paa parseFrontmatter().
// get() (aldri substring for verdier). Moenster: tests/oppsett-okf-write.test.mjs:93.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseFrontmatter } from '../lib/frontmatter.mjs';
import { TYPE_VOCAB, routeLevel } from '../lib/okf-vocab.mjs';
import { splitConcepts } from '../lib/innboks-split.mjs';
import { projectFrontmatter } from '../lib/innboks-frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const FIX = join(HERE, 'fixtures', 'inbox-converted');
const dokA = readFileSync(join(FIX, 'dok-a.md'), 'utf8');
const MTIME = new Date('2026-03-15T08:30:00.000Z');
const OPTS = { sourcePath: 'innboks/dok-a.txt', sourceMtime: MTIME };
// Frontmatter-noekler i rekkefolge (kun linjestart-noekler; list-elementer hoppes over).
function fmKeyOrder(fm) {
const { raw } = parseFrontmatter(fm);
return (raw || '')
.split('\n')
.map((l) => {
const m = l.match(/^([a-z_]+):/);
return m ? m[1] : null;
})
.filter(Boolean);
}
test('projectFrontmatter: type snappes til lukket vokab; keyword i title -> kanonisk type', () => {
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
const e = projectFrontmatter(c1, OPTS);
const { get } = parseFrontmatter(e.frontmatter);
assert.ok(TYPE_VOCAB.includes(get('type')), 'type i lukket vokab');
assert.equal(get('type'), 'Tildelingsbrev', 'keyword "Tildelingsbrev" i title -> kanonisk type');
});
test('projectFrontmatter: ukjent emne -> safe default Dokument (negativ vokab-sjekk)', () => {
const concept = {
sourceSlug: 'x',
title: 'Helt annerledes emne',
slug: 'helt-annerledes-emne',
level: 1,
body: 'Innhold uten vokab-noekkelord.',
};
const e = projectFrontmatter(concept, { sourcePath: 'innboks/x.txt', sourceMtime: MTIME });
assert.equal(parseFrontmatter(e.frontmatter).get('type'), 'Dokument');
assert.doesNotMatch(
e.frontmatter,
/^type: (Tildelingsbrev|Virksomhetsplan|OKR)$/m,
'ikke feil-snappet til en foeringstype',
);
});
test('projectFrontmatter: resource = original relativ sti (kanonisk navn, ikke "source")', () => {
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
const e = projectFrontmatter(c1, OPTS);
const { get } = parseFrontmatter(e.frontmatter);
assert.equal(get('resource'), 'innboks/dok-a.txt');
assert.equal(get('source'), null, 'noekkelen heter resource, ikke source');
});
test('projectFrontmatter: description deterministisk (2 kjoeringer byte-identiske, foerste avsnitt)', () => {
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
const e1 = projectFrontmatter(c1, OPTS);
const e2 = projectFrontmatter(c1, OPTS);
assert.equal(e1.frontmatter, e2.frontmatter, 'byte-identisk projeksjon');
assert.match(parseFrontmatter(e1.frontmatter).get('description'), /Tildelingsbrevet gir overordnede/);
});
test('projectFrontmatter: lang body -> description trunkeres deterministisk', () => {
const longBody = 'A'.repeat(500);
const concept = { sourceSlug: 'lang', title: 'Lang', slug: 'lang', level: 1, body: longBody };
const e = projectFrontmatter(concept, { sourcePath: 'innboks/lang.txt', sourceMtime: MTIME });
const desc = parseFrontmatter(e.frontmatter).get('description');
assert.ok(desc.length <= 244, 'trunkert til <= 240 + ellipsis');
assert.match(desc, /\.\.\.$/, 'ellipsis-markoer ved trunkering');
});
test('projectFrontmatter: timestamp = ISO av sourceMtime (idempotent paa tvers av veggklokke)', () => {
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
const e = projectFrontmatter(c1, OPTS);
assert.equal(parseFrontmatter(e.frontmatter).get('timestamp'), MTIME.toISOString());
});
test('projectFrontmatter: kilde:innboks provenans-markoer alltid satt', () => {
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
const e = projectFrontmatter(c1, OPTS);
assert.equal(parseFrontmatter(e.frontmatter).get('kilde'), 'innboks');
assert.equal(e.kilde, 'innboks', 'kilde ogsaa paa konsept-objektet');
});
test('projectFrontmatter: concept.destRel satt + konsistent med routeLevel(type)', () => {
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
const e = projectFrontmatter(c1, OPTS);
assert.ok(e.destRel, 'destRel satt FOER relasjons-steget');
assert.equal(e.destRel, `${routeLevel(e.type)}/${c1.slug}.md`);
assert.equal(e.destRel, 'strategisk-kontekst/tildelingsbrev-2026.md', 'Tildelingsbrev -> strategisk-kontekst');
});
test('projectFrontmatter: kanonisk noekkel-rekkefolge (type/resource/title/desc/tags/timestamp/kilde)', () => {
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
const e = projectFrontmatter(c1, OPTS);
const order = fmKeyOrder(e.frontmatter);
const canon = ['type', 'resource', 'title', 'description', 'tags', 'timestamp', 'kilde'];
assert.deepEqual(order, canon.filter((k) => order.includes(k)), 'present keys i kanonisk rekkefolge');
assert.equal(order[0], 'type', 'type foerst');
assert.equal(order[order.length - 1], 'kilde', 'kilde sist');
});
test('projectFrontmatter: tags er multi-linje liste (writeFrontmatter array-gren) + i vokab', () => {
const [c1] = splitConcepts(dokA, { sourceSlug: 'dok-a' });
const e = projectFrontmatter(c1, OPTS);
assert.match(e.frontmatter, /^tags:$/m, 'multi-linje tags-noekkel');
assert.match(e.frontmatter, /^ {2}- Tildelingsbrev$/m, 'tag-element i vokab');
});

View file

@ -0,0 +1,86 @@
// innboks-relations.test.mjs
// Step 6 (SC relasjoner): delt bundle-root-relativ lenke-resolver (okf-links) +
// deterministisk relasjons-resolusjon (innboks-relations). Beviser: >=1 emittert
// lenke matcher SC-regex /\]\(\/.*\.md\)/ (leading '/'); hver lenke resolverer til
// et emittert konsept (zero-dangling); '../'/absolutt/file://-/http-lenker avvises
// av isSafeBundleLink (allow-list). Bygger det emitterte settet gjennom Step 4+5.
// Moenster: tests/okf-retrieval.test.mjs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isSafeBundleLink, resolveBundleLink } from '../lib/okf-links.mjs';
import { resolveRelations } from '../lib/innboks-relations.mjs';
import { splitConcepts } from '../lib/innboks-split.mjs';
import { projectFrontmatter } from '../lib/innboks-frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const FIX = join(HERE, 'fixtures', 'inbox-converted');
const MTIME = new Date('2026-03-15T08:30:00.000Z');
const SC_REGEX = /\]\(\/.*\.md\)/;
// Det emitterte konsept-settet (split + project) for begge fixture-dokumentene.
function buildConcepts() {
const out = [];
for (const slug of ['dok-a', 'dok-b']) {
const md = readFileSync(join(FIX, `${slug}.md`), 'utf8');
for (const c of splitConcepts(md, { sourceSlug: slug })) {
out.push(projectFrontmatter(c, { sourcePath: `innboks/${slug}.txt`, sourceMtime: MTIME }));
}
}
return out;
}
test('resolveRelations: >=1 emittert lenke matcher SC-regex (leading-/ bundle-link)', () => {
const related = resolveRelations(buildConcepts());
const targets = related.flatMap((c) => c.relations.map((r) => r.target));
assert.ok(targets.length >= 1, 'minst en relasjon emitteres fra fixturene');
assert.ok(targets.every((t) => SC_REGEX.test(`](${t})`)), 'hver lenke matcher SC-regex');
// Samme lenke maa finnes i body-en som faktisk skrives + grepes av SC4.
const bodies = related.map((c) => c.body).join('\n');
assert.match(bodies, SC_REGEX, 'relasjons-lenke finnes i konsept-body');
});
test('resolveRelations: hver lenke resolverer til et emittert konsept (zero-dangling)', () => {
const concepts = buildConcepts();
const emitted = new Set(concepts.map((c) => c.destRel));
const related = resolveRelations(concepts);
for (const c of related) {
for (const r of c.relations) {
assert.ok(emitted.has(r.destRel), `lenke-mal ${r.destRel} er et emittert konsept`);
assert.equal(r.target, `/${r.destRel}`, 'leading-/ bundle-root-lenke');
assert.equal(isSafeBundleLink(r.target), true, 'emittert lenke er trygg');
}
}
});
test('resolveRelations: deterministisk (2 kjoeringer byte-like body + relations)', () => {
const pick = (arr) => arr.map((c) => ({ slug: c.slug, body: c.body, relations: c.relations }));
assert.deepEqual(pick(resolveRelations(buildConcepts())), pick(resolveRelations(buildConcepts())));
});
test('isSafeBundleLink: aksepterer leading-/ .md bundle-lenke', () => {
assert.equal(isSafeBundleLink('/strategisk-kontekst/virksomhetsplan-2026.md'), true);
assert.equal(isSafeBundleLink('/dokumenter/notat.md'), true);
});
test('isSafeBundleLink: avviser escape/scheme/relativ/ikke-md/backslash', () => {
assert.equal(isSafeBundleLink('../escape.md'), false, 'relativ ..');
assert.equal(isSafeBundleLink('/foo/../../etc/passwd.md'), false, '.. segment');
assert.equal(isSafeBundleLink('file:///etc/passwd.md'), false, 'file:// scheme');
assert.equal(isSafeBundleLink('http://evil.example/x.md'), false, 'http scheme');
assert.equal(isSafeBundleLink('relativ/sti.md'), false, 'ingen leading /');
assert.equal(isSafeBundleLink('/strategisk-kontekst/uten-ext'), false, 'ikke .md');
assert.equal(isSafeBundleLink('C:\\win\\path.md'), false, 'backslash');
assert.equal(isSafeBundleLink(''), false, 'tom streng');
assert.equal(isSafeBundleLink(null), false, 'ikke-streng');
});
test('resolveBundleLink: trygg lenke -> sti under bundleRoot; escape/scheme -> null', () => {
const root = '/tmp/bundle';
assert.equal(resolveBundleLink('/dokumenter/notat.md', root), join(root, 'dokumenter/notat.md'));
assert.equal(resolveBundleLink('../escape.md', root), null);
assert.equal(resolveBundleLink('http://x/y.md', root), null);
});

View file

@ -0,0 +1,77 @@
// innboks-split.test.mjs
// Step 4 (SC idempotens): deterministisk heading-split av (allerede konvertert)
// markdown til konsepter. splitConcepts er en REN funksjon -- samme input gir
// identisk konsept-sett + filnavn (kebab-slug). Header-tekst -> title; flat
// dokument (ingen #/##) -> ett konsept; slug-kollisjon -> stabil numerisk
// disambiguering; preamble foer foerste heading bevares (ingen datatap).
// Direkte import (zero npm deps). Moenster: tests/frontmatter.test.mjs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { splitConcepts } from '../lib/innboks-split.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const FIX = join(HERE, 'fixtures', 'inbox-converted');
const dokA = readFileSync(join(FIX, 'dok-a.md'), 'utf8');
const dokB = readFileSync(join(FIX, 'dok-b.md'), 'utf8');
test('splitConcepts: deterministisk -- samme input 2x gir identisk konsept-sett', () => {
const a1 = splitConcepts(dokA, { sourceSlug: 'dok-a' });
const a2 = splitConcepts(dokA, { sourceSlug: 'dok-a' });
assert.deepEqual(a1, a2, 'ren funksjon: identisk output for identisk input');
});
test('splitConcepts: #/## heading -> ett konsept per seksjon, header-tekst -> title', () => {
const concepts = splitConcepts(dokA, { sourceSlug: 'dok-a' });
assert.equal(concepts.length, 2, 'dok-a har en H1 + en H2 -> 2 konsepter');
assert.equal(concepts[0].title, 'Tildelingsbrev 2026');
assert.equal(concepts[0].slug, 'tildelingsbrev-2026');
assert.equal(concepts[0].level, 1);
assert.equal(concepts[1].title, 'Oppfolging mot Virksomhetsplan 2026');
assert.equal(concepts[1].level, 2);
// body baerer seksjons-innholdet (uten heading-linja).
assert.match(concepts[0].body, /Tildelingsbrevet gir overordnede/);
assert.doesNotMatch(concepts[0].body, /^#/, 'heading-linja er ikke i body');
});
test('splitConcepts: slug stripper klammer/parenteser (adversarial heading)', () => {
const concepts = splitConcepts(dokB, { sourceSlug: 'dok-b' });
assert.equal(concepts.length, 2);
assert.equal(concepts[1].title, 'Mal og rammer (2026) [utkast]');
assert.equal(concepts[1].slug, 'mal-og-rammer-2026-utkast', 'klammer/parenteser strippet fra slug');
});
test('splitConcepts: ### og dypere er IKKE split-punkt (forblir body)', () => {
const md = '# Topp\n\nIntro.\n\n### Underseksjon\n\nDetalj.';
const concepts = splitConcepts(md, { sourceSlug: 'dyp' });
assert.equal(concepts.length, 1, 'kun H1 splitter; ### forblir i body');
assert.match(concepts[0].body, /### Underseksjon/, '### bevart som body-innhold');
});
test('splitConcepts: flat dokument (ingen #/##) -> ett konsept', () => {
const flat = 'Bare en paragraf uten overskrift.\n\nEnda en linje.';
const concepts = splitConcepts(flat, { sourceSlug: 'notat' });
assert.equal(concepts.length, 1);
assert.equal(concepts[0].slug, 'notat', 'flat-fallback slug fra sourceSlug');
assert.match(concepts[0].body, /Bare en paragraf/);
});
test('splitConcepts: slug-kollisjon -> stabil numerisk disambiguering', () => {
const md = '# Samme tittel\n\nA\n\n## Samme tittel\n\nB';
const concepts = splitConcepts(md, { sourceSlug: 'kollisjon' });
assert.equal(concepts.length, 2);
assert.equal(concepts[0].slug, 'samme-tittel');
assert.equal(concepts[1].slug, 'samme-tittel-2', 'andre forekomst faar -2 suffiks');
});
test('splitConcepts: preamble foer foerste heading bevares som ledende konsept', () => {
const md = 'Forord uten overskrift.\n\n# Ekte overskrift\n\nKropp.';
const concepts = splitConcepts(md, { sourceSlug: 'med-forord' });
assert.equal(concepts.length, 2, 'preamble + en heading -> 2 konsepter (ingen datatap)');
assert.equal(concepts[0].slug, 'med-forord');
assert.match(concepts[0].body, /Forord uten overskrift/);
assert.equal(concepts[1].title, 'Ekte overskrift');
});

View file

@ -0,0 +1,152 @@
// innboks-write.test.mjs
// Step 7 (SC original + riktig nivaa): konsept-skriver med type->nivaa-ruting og
// ikke-destruktiv original-bevaring. Driver den reelle kjernen
// (splitConcepts -> projectFrontmatter -> resolveRelations) per original og lar
// writeConcepts persistere settet i en temp-bundle. Verifiserer:
// - konsept med strategisk-kontekst/-destRel skrives dit, dokumenter/-destRel dit
// - skrevet fil = frontmatter (verbatim) + body (verbatim), trailing newline
// - originalens sha256 er uendret etter skriv (ikke-destruktiv, SC3)
// - peker-fil i konseptets nivaa lenker til originalen i drop-zonen (grep-bar)
// - mal-sti utenfor bundle-rot avvist (../-escape) og home-org-rot avvist
// - ingen .tmp lekker (atomisk temp+renameSync)
// Zero npm deps. Moenster: tests/org-profile-write.test.mjs (mkdtemp + realpath/sha).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync } from 'node:fs';
import { tmpdir, homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { splitConcepts } from '../lib/innboks-split.mjs';
import { projectFrontmatter } from '../lib/innboks-frontmatter.mjs';
import { resolveRelations } from '../lib/innboks-relations.mjs';
import { writeConcepts } from '../lib/innboks-write.mjs';
const MTIME = new Date('2026-03-15T08:30:00.000Z');
function sha256(path) {
return createHash('sha256').update(readFileSync(path)).digest('hex');
}
// Kjor den reelle kjernen for ett dokument -> konsepter (m/ destRel + frontmatter + body).
function ingestOne(markdown, sourceSlug, sourcePath) {
const raw = splitConcepts(markdown, { sourceSlug });
const projected = raw.map((c) => projectFrontmatter(c, { sourcePath, sourceMtime: MTIME }));
return resolveRelations(projected);
}
// Temp-bundle: <tmp>/.claude/okr (+ innboks/ drop-zone). Ryddes alltid.
function withBundle(fn) {
const tmp = mkdtempSync(join(tmpdir(), 'okrwrite-'));
const bundleRoot = join(tmp, '.claude', 'okr');
const inbox = join(bundleRoot, 'innboks');
mkdirSync(inbox, { recursive: true });
try {
fn({ tmp, bundleRoot, inbox });
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}
const TILDELING = '# Tildelingsbrev 2026\n\nStatens vegvesen skal levere paa foelgende maal i 2026.\n';
const MERKNAD = '# Generell merknad\n\nEt fritt notat uten foeringsord i tittel.\n';
test('writeConcepts: type->nivaa-ruting (strategisk-kontekst + dokumenter)', () => {
withBundle(({ bundleRoot, inbox }) => {
const tPath = join(inbox, 'tildeling.txt');
const mPath = join(inbox, 'merknad.txt');
writeFileSync(tPath, TILDELING);
writeFileSync(mPath, MERKNAD);
const tConcepts = ingestOne(TILDELING, 'tildeling', 'innboks/tildeling.txt');
const mConcepts = ingestOne(MERKNAD, 'merknad', 'innboks/merknad.txt');
const [tc] = tConcepts;
const [mc] = mConcepts;
// Forventning fra Step 5-ruting (sanity foer skriv).
assert.equal(dirname(tc.destRel), 'strategisk-kontekst', 'Tildelingsbrev -> strategisk-kontekst/');
assert.equal(dirname(mc.destRel), 'dokumenter', 'default Dokument -> dokumenter/');
writeConcepts([...tConcepts, ...mConcepts], {
bundleRoot,
originals: [
{ sourceSlug: 'tildeling', path: tPath },
{ sourceSlug: 'merknad', path: mPath },
],
});
const tFile = join(bundleRoot, tc.destRel);
const mFile = join(bundleRoot, mc.destRel);
assert.ok(existsSync(tFile), 'tildelingsbrev-konsept skrevet til strategisk-kontekst/');
assert.ok(existsSync(mFile), 'merknad-konsept skrevet til dokumenter/');
// Skrevet fil = frontmatter (verbatim) + body (verbatim), trailing newline. IKKE re-serialisert.
const tContent = readFileSync(tFile, 'utf8');
assert.ok(tContent.startsWith(tc.frontmatter), 'frontmatter verbatim oeverst');
assert.ok(tContent.includes(tc.body), 'body verbatim bevart');
assert.ok(tContent.endsWith('\n'), 'fil avsluttes med newline');
});
});
test('writeConcepts: original uendret (sha256) + peker-fil lenker til drop-zone-original', () => {
withBundle(({ bundleRoot, inbox }) => {
const tPath = join(inbox, 'tildeling.txt');
writeFileSync(tPath, TILDELING);
const before = sha256(tPath);
const concepts = ingestOne(TILDELING, 'tildeling', 'innboks/tildeling.txt');
writeConcepts(concepts, {
bundleRoot,
originals: [{ sourceSlug: 'tildeling', path: tPath }],
});
// SC3: ikke-destruktiv -- originalen er byte-uendret.
assert.equal(sha256(tPath), before, 'original-sha256 uendret etter skriv');
assert.ok(existsSync(tPath), 'original finnes fortsatt i drop-zonen');
// Peker-fil i konseptets nivaa, grep-bar lenke til originalen.
const level = dirname(concepts[0].destRel);
const pointer = join(bundleRoot, level, 'tildeling.kilde.md');
assert.ok(existsSync(pointer), 'peker-fil skrevet i konseptets nivaa');
const pointerBody = readFileSync(pointer, 'utf8');
assert.match(pointerBody, /\(\/innboks\/tildeling\.txt\)/, 'peker lenker til original i drop-zonen');
assert.match(pointerBody, /^type: Notat$/m, 'peker baerer gyldig OKF-type (vokab)');
});
});
test('writeConcepts: ../-escape destRel avvist (path-confinement)', () => {
withBundle(({ bundleRoot }) => {
const evil = { slug: 'evil', sourceSlug: 'evil', destRel: '../escape.md', frontmatter: '---\ntype: Dokument\n---\n', body: 'x' };
assert.throws(
() => writeConcepts([evil], { bundleRoot, originals: [] }),
/utenfor bundle-rot/,
'destRel som escaper bundle-rota skal kaste',
);
assert.ok(!existsSync(join(bundleRoot, '..', 'escape.md')), 'ingenting skrevet utenfor bundle');
});
});
test('writeConcepts: skriv til home-org-rot avvist (ingestion roerer aldri ~/.claude/okr/org)', () => {
const homeOrg = join(homedir(), '.claude', 'okr', 'org');
const concept = { slug: 'p', sourceSlug: 'p', destRel: 'dokumenter/p.md', frontmatter: '---\ntype: Dokument\n---\n', body: 'x' };
// Kaster FOER noen skriv -- home-profilen roeres aldri.
assert.throws(
() => writeConcepts([concept], { bundleRoot: homeOrg, originals: [] }),
/home-org-rot avvist/,
'home-org bundleRoot skal kaste',
);
});
test('writeConcepts: atomisk -- ingen .tmp lekker etter skriv', () => {
withBundle(({ bundleRoot, inbox }) => {
const tPath = join(inbox, 'tildeling.txt');
writeFileSync(tPath, TILDELING);
const concepts = ingestOne(TILDELING, 'tildeling', 'innboks/tildeling.txt');
writeConcepts(concepts, { bundleRoot, originals: [{ sourceSlug: 'tildeling', path: tPath }] });
const level = dirname(concepts[0].destRel);
const leftover = readdirSync(join(bundleRoot, level)).filter((f) => f.endsWith('.tmp'));
assert.deepEqual(leftover, [], 'ingen temp-fil igjen etter atomisk renameSync');
});
});

312
tests/okf-check.test.mjs Normal file
View file

@ -0,0 +1,312 @@
// okf-check.test.mjs
// Tester OKF-mekanismen (Step 6): okf-index genererer index.md per nivaa i ekte
// OKF-form, og okf-check validerer at hver konsept-fil baerer `type:`.
// okf-index kjoeres mot en TEMP-KOPI av okf-realistic (cpSync) saa de committede
// fixturene (lest read-only av okf-retrieval.test.mjs, m/ en bevisst dangling-link)
// forblir uroert. okf-check kjoeres som subprosess for aa fange exit-koden (kontrakt).
// Zero npm deps. Plassert i tests/ (samme moenster som org-profile-write.test.mjs).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import {
mkdtempSync, cpSync, writeFileSync, readFileSync, existsSync, readdirSync, rmSync, mkdirSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { generateIndexes } from '../scripts/okf-index.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const CHECK = join(ROOT, 'scripts', 'okf-check.mjs');
const REALISTIC = join(ROOT, 'tests', 'fixtures', 'okf-realistic');
function tmpRoot() {
return mkdtempSync(join(tmpdir(), 'okf-'));
}
// Kjoer okf-check som subprosess; fang non-zero exit (execFileSync kaster da).
// extra: ekstra CLI-flagg (f.eks. '--strict-ingest').
function runCheck(root, ...extra) {
try {
const stdout = execFileSync('node', [CHECK, root, ...extra], { encoding: 'utf8' });
return { status: 0, stdout };
} catch (e) {
return { status: e.status ?? 1, stdout: `${e.stdout || ''}${e.stderr || ''}` };
}
}
// Bygg en ren ingestion-bundle: kanoniske vokab-typer + en trygg, on-disk relasjon.
function buildCleanIngest(dir) {
mkdirSync(join(dir, 'strategisk-kontekst'), { recursive: true });
mkdirSync(join(dir, 'dokumenter'), { recursive: true });
writeFileSync(join(dir, 'index.md'), '# Bundle\n\nokf_version: kb-layout-2026-06\n');
writeFileSync(
join(dir, 'strategisk-kontekst', 'tildelingsbrev.md'),
'---\ntype: Tildelingsbrev\nresource: urn:okr:tb\ntitle: Tildelingsbrev 2026\n'
+ "description: Styringssignaler.\ntimestamp: '2026-01-15T09:00:00+00:00'\n---\n"
+ '# Tildelingsbrev 2026\n\nHoveddokument.\n',
);
writeFileSync(
join(dir, 'dokumenter', 'notat.md'),
'---\ntype: Notat\nresource: urn:okr:notat\ntitle: Internt notat\n'
+ "description: Et notat.\ntimestamp: '2026-02-01T09:00:00+00:00'\n---\n"
+ '# Internt notat\n\n## Relaterte dokumenter\n\n'
+ '- [Tildelingsbrev 2026](/strategisk-kontekst/tildelingsbrev.md)\n',
);
}
// Alle kataloger under root (inkl. root selv), rekursivt.
function allDirs(root) {
const out = [root];
for (const e of readdirSync(root, { withFileTypes: true })) {
if (e.isDirectory()) out.push(...allDirs(join(root, e.name)));
}
return out;
}
// Byte-snapshot av alle index.md under root (for idempotens-sammenligning).
function snapshotIndexes(root) {
return allDirs(root)
.map((d) => join(d, 'index.md'))
.filter((p) => existsSync(p))
.sort()
.map((p) => `${p}\n${readFileSync(p, 'utf8')}`)
.join('\n=====\n');
}
// --- okf-index ---
test('okf-index: genererer index.md per nivaa (hver katalog) i temp-kopi', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
for (const d of allDirs(dir)) {
assert.ok(existsSync(join(d, 'index.md')), `index.md skal finnes i ${d}`);
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: entry-linjer matcher OKF-form `* [t](l) - d`, uten frontmatter', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const sk = readFileSync(join(dir, 'strategisk-kontekst', 'index.md'), 'utf8');
assert.ok(!sk.startsWith('---'), 'index.md skal IKKE ha frontmatter');
const entries = sk.split('\n').filter((l) => l.startsWith('* '));
assert.ok(entries.length >= 3, 'strategisk-kontekst skal liste sine 3 konsept-filer');
for (const l of entries) {
assert.match(l, /^\* \[[^\]]+\]\([^)]+\) - .+$/, `OKF-form: ${l}`);
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: rot-index baerer okf_version, undernivaa gjoer ikke', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root, /^okf_version:\s*\S+/m, 'rot-index skal ha okf_version');
const sub = readFileSync(join(dir, 'strategisk-kontekst', 'index.md'), 'utf8');
assert.ok(!/^okf_version:/m.test(sub), 'undernivaa skal IKKE ha okf_version');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: konsept-tittel/beskrivelse hentes fra frontmatter', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const sk = readFileSync(join(dir, 'strategisk-kontekst', 'index.md'), 'utf8');
assert.match(
sk,
/\* \[Tildelingsbrev 2026\]\(tildelingsbrev-2026\.md\) - Departementets/,
'entry skal bruke frontmatter-title + -description + lenke til konsept-fil',
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
// --- okf-check ---
test('okf-check: gyldig bundle (alle konsept har type:) -> exit 0, «0 filer uten type:»', () => {
const { status, stdout } = runCheck(REALISTIC);
assert.equal(status, 0, 'gyldig bundle skal gi exit 0');
assert.match(stdout, /0 filer uten type:/, 'skal rapportere null manglende type');
});
test('okf-check: konsept-fil uten type: -> exit != 0 + teller > 0 + navngir filen', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
writeFileSync(
join(dir, 'strategisk-kontekst', 'mangler-type.md'),
'---\ntitle: Uten type\ndescription: En konsept-fil uten paakrevd type.\n---\n# Uten type\n',
);
const { status, stdout } = runCheck(dir);
assert.notEqual(status, 0, 'type-loes fil skal gi exit != 0');
assert.match(stdout, /[1-9]\d* filer uten type:/, 'teller skal vaere > 0');
assert.match(stdout, /mangler-type\.md/, 'skal navngi den feilende filen');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check: rapporterer okf_version fra rot-index', () => {
const { stdout } = runCheck(REALISTIC);
assert.match(stdout, /okf_version:\s*kb-layout-2026-06/, 'skal ekko okf_version for menneskelig sammenligning');
});
// --- okf-check --strict-ingest (Step 8): lukket vokab + lenke-allow-liste ---
test('okf-check --strict-ingest: ren ingestion-bundle (vokab-type + trygg on-disk relasjon) -> exit 0', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
const { status, stdout } = runCheck(dir, '--strict-ingest');
assert.equal(status, 0, `ren bundle skal gi exit 0:\n${stdout}`);
assert.match(stdout, /0 strict-ingest-feil/, 'skal rapportere null strict-feil');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check --strict-ingest: type utenfor vokabular -> exit 1 + navngir filen', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'dokumenter', 'rar.md'),
'---\ntype: Tilfeldig\ntitle: Rar\ndescription: x\n---\n# Rar\n',
);
const { status, stdout } = runCheck(dir, '--strict-ingest');
assert.equal(status, 1, 'out-of-vocab type skal gi exit 1');
assert.match(stdout, /rar\.md: type .* utenfor ingestion-vokabular/, 'skal navngi feilen');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check --strict-ingest: usikker lenke (escape ut av bundle) -> exit 1', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'dokumenter', 'ond.md'),
'---\ntype: Notat\ntitle: Ond\ndescription: x\n---\n# Ond\n\n- [exfil](../../../etc/passwd)\n',
);
const { status, stdout } = runCheck(dir, '--strict-ingest');
assert.equal(status, 1, 'utrygg lenke skal gi exit 1');
assert.match(stdout, /utrygg lenke/, 'skal rapportere utrygg lenke');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check --strict-ingest: dangling bundle-lenke (trygg form, mangler on-disk) -> exit 1', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'dokumenter', 'henger.md'),
'---\ntype: Notat\ntitle: Henger\ndescription: x\n---\n# Henger\n\n'
+ '- [Mangler](/strategisk-kontekst/finnes-ikke.md)\n',
);
const { status, stdout } = runCheck(dir, '--strict-ingest');
assert.equal(status, 1, 'dangling lenke skal gi exit 1');
assert.match(stdout, /dangling lenke/, 'skal rapportere dangling lenke');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check --strict-ingest: lenke-baerende title -> exit 1', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'dokumenter', 'tittel.md'),
'---\ntype: Notat\ntitle: "[Klikk her](http://evil.example)"\ndescription: x\n---\n# T\n',
);
const { status, stdout } = runCheck(dir, '--strict-ingest');
assert.equal(status, 1, 'lenke-baerende title skal gi exit 1');
assert.match(stdout, /lenke-baerende title/, 'skal rapportere lenke-baerende title');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
// --- walk-eksklusjon (Step 8): drop-zone + dot-kataloger skannes aldri ---
test('okf-check: raa filer i innboks/ og .cache/ ignoreres av walk (ingen falsk exit 1)', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
mkdirSync(join(dir, 'innboks'), { recursive: true });
mkdirSync(join(dir, '.cache'), { recursive: true });
// Raa filer UTEN type -- ville gitt exit 1 hvis walk ikke ekskluderte dem.
writeFileSync(join(dir, 'innboks', 'raa.md'), '# Raa innboks-fil uten frontmatter\n');
writeFileSync(join(dir, '.cache', 'c.md'), '# Cache uten type\n');
const { status, stdout } = runCheck(dir);
assert.equal(status, 0, `drop-zone/dot-filer skal ignoreres:\n${stdout}`);
assert.match(stdout, /0 filer uten type:/, 'walk skal ikke telle drop-zone-filer');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
// --- okf-index herding (Step 9): saner title/desc + skip innboks/dot ---
test('okf-index: tittel/beskrivelse med ]/) + lenke saneres -> 2x byte-identisk + round-trip-trygg', () => {
const dir = tmpRoot();
try {
mkdirSync(join(dir, 'dokumenter'), { recursive: true });
writeFileSync(join(dir, 'index.md'), '# Bundle\n\nokf_version: kb-layout-2026-06\n');
writeFileSync(
join(dir, 'dokumenter', 'kr.md'),
'---\ntype: OKR\ntitle: "KR1 [resultat] (maal) [lenke](http://x)"\n'
+ 'description: "Status (delvis) [ref](http://y)"\n---\n# KR\n',
);
generateIndexes(dir);
const run1 = snapshotIndexes(dir);
generateIndexes(dir);
const run2 = snapshotIndexes(dir);
assert.equal(run1, run2, 'generateIndexes skal vaere byte-idempotent');
const idx = readFileSync(join(dir, 'dokumenter', 'index.md'), 'utf8');
const entry = idx.split('\n').find((l) => l.startsWith('* '));
assert.ok(entry, 'kr.md skal ha en entry');
assert.match(entry, /^\* \[[^\]]*\]\([^)]+\)/, 'entry skal vaere round-trip-trygg OKF-form');
assert.ok(!entry.includes('http'), 'injisert lenke skal vaere noytralisert');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: innboks/ + dot-katalog -> ingen egen index.md OG ikke listet i rot-index', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
mkdirSync(join(dir, 'innboks'), { recursive: true });
mkdirSync(join(dir, '.cache'), { recursive: true });
writeFileSync(join(dir, 'innboks', 'raa.md'), '# Raa\n');
writeFileSync(join(dir, '.cache', 'c.md'), '# Cache\n');
generateIndexes(dir);
assert.ok(!existsSync(join(dir, 'innboks', 'index.md')), 'innboks/ skal ikke faa index.md');
assert.ok(!existsSync(join(dir, '.cache', 'index.md')), 'dot-katalog skal ikke faa index.md');
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.ok(!root.includes('innboks/index.md'), 'rot-index skal ikke liste innboks/ som peker');
assert.ok(!root.includes('.cache/index.md'), 'rot-index skal ikke liste dot-katalog som peker');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

View file

@ -0,0 +1,131 @@
// okf-retrieval.test.mjs
// Beviser retrieval-MEKANISMEN for okr-second-brain-search SKILLen: native
// Glob/Read/Grep-seamen mot et OKF-kompatibelt bundle (fixture = parameterisert
// bundle-rot). Tester IKKE modell-trigger (Assumption 1 -> royktest i SKILL-body).
// Referanse-implementasjonen speiler SKILLens dokumenterte retrieval-algoritme;
// semantisk dekomponering (synonym-bridging) er modell-jobb og utelatt her.
// Zero npm deps. Plassert i tests/ (samme monster som org-profile-write.test.mjs).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseFrontmatter } from '../lib/frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const MINIMAL = join(HERE, 'fixtures', 'okf-minimal');
const REALISTIC = join(HERE, 'fixtures', 'okf-realistic');
// --- Reference retrieval (native-tool seam: Glob + Read + Grep + rank) ---
// Glob: alle konsept-filer (.md unntatt index.md) under en bundle-rot, rekursivt.
function globConcepts(root) {
const out = [];
const walk = (dir) => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) walk(p);
else if (e.isFile() && e.name.endsWith('.md') && e.name !== 'index.md') out.push(p);
}
};
walk(root);
return out;
}
// Naermeste index.md-linje som lenker til <file>, ellers null. Kaster aldri.
function indexEntry(file) {
const idx = join(dirname(file), 'index.md');
if (!existsSync(idx)) return { line: null, order: Number.MAX_SAFE_INTEGER };
const base = basename(file);
const lines = readFileSync(idx, 'utf8').split('\n').filter((l) => l.trimStart().startsWith('*'));
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(`](${base})`)) return { line: lines[i], order: i };
}
return { line: null, order: Number.MAX_SAFE_INTEGER };
}
// Grep + rank: konsept-filer som inneholder token, rangert etter dokumentert
// tie-break: +2 token i frontmatter, +1 token i nivaaets index.md-entry; ekte
// tie -> index-rekkefolge; siste tie -> sti-sortering (deterministisk).
function retrieve(root, token) {
const lc = token.toLowerCase();
const hits = [];
for (const f of globConcepts(root)) {
const content = readFileSync(f, 'utf8');
if (!content.toLowerCase().includes(lc)) continue;
const { raw } = parseFrontmatter(content);
const inFm = raw !== null && raw.toLowerCase().includes(lc);
const { line, order } = indexEntry(f);
const inIdx = line !== null && line.toLowerCase().includes(lc);
hits.push({ path: f, score: (inFm ? 2 : 0) + (inIdx ? 1 : 0), order });
}
hits.sort((a, b) => b.score - a.score || a.order - b.order || a.path.localeCompare(b.path));
return hits;
}
// Les index.md-lenker, returner kun de som finnes paa disk (dangling droppes, kaster ikke).
function resolveIndexLinks(indexPath) {
const dir = dirname(indexPath);
const links = [];
for (const l of readFileSync(indexPath, 'utf8').split('\n')) {
const m = l.match(/\]\(([^)]+)\)/);
if (m) links.push(m[1]);
}
return links.filter((relPath) => existsSync(join(dir, relPath)));
}
const rel = (root, p) => p.slice(root.length + 1).split('\\').join('/');
// (a) MEKANISME: unik token (excl. index.md) -> forventet sti.
test('mekanisme: unik token resolver til forventet konsept-fil (okf-minimal)', () => {
const hits = retrieve(MINIMAL, 'kvikkleireskred');
assert.equal(hits.length, 1, 'unik token skal treffe noyaktig en konsept-fil');
assert.equal(rel(MINIMAL, hits[0].path), 'tildelingsbrev.md');
});
// (b) BRUKERVERDI-PROXY: query -> token -> konsept-tabell mot okf-realistic.
test('brukerverdi: token-tabell resolver hver til forventet konsept (okf-realistic)', () => {
const table = [
{ token: 'klimaomstilling', expect: 'strategisk-kontekst/tildelingsbrev-2026.md' },
{ token: 'kompetanseloeft', expect: 'strategisk-kontekst/virksomhetsplan.md' },
{ token: 'Vegdirektoratet', expect: 'strategisk-kontekst/organisasjonsprofil.md' },
{ token: 'selvbetjeningsgrad', expect: 'syklus/T1-2026/okr-digitalisering.md' },
{ token: 'nullvisjon', expect: 'syklus/T1-2026/okr-trafikksikkerhet.md' },
{ token: 'fremdriftsindikator', expect: 'syklus/T1-2026/status.md' },
{ token: 'laeringssloeyfe', expect: 'historikk/retrospektiv-T3-2025.md' },
{ token: 'arbeidsnotat', expect: 'dokumenter/notat.md' },
];
for (const { token, expect } of table) {
const hits = retrieve(REALISTIC, token);
assert.ok(hits.length >= 1, `token «${token}» skal gi minst ett treff`);
assert.equal(rel(REALISTIC, hits[0].path), expect, `token «${token}» -> ${expect}`);
}
});
// (c) DISAMBIGUERING (Pass 2): delt token mellom 2 filer -> index.md-routing/ranking
// velger riktig topp-treff (frontmatter+index-entry tie-break).
test('disambiguering: delt token rangerer frontmatter/index-treff over body-treff', () => {
const hits = retrieve(REALISTIC, 'tunnelsikkerhet');
assert.equal(hits.length, 2, 'delt token skal treffe begge filer');
assert.equal(rel(REALISTIC, hits[0].path), 'syklus/T1-2026/okr-trafikksikkerhet.md',
'frontmatter+index-entry-treff skal rangeres over body-only-treff');
assert.equal(rel(REALISTIC, hits[1].path), 'syklus/T1-2026/status.md');
assert.ok(hits[0].score > hits[1].score, 'topp-treff skal ha hoyere score (deterministisk tie-break)');
});
// (d) ROBUSTHET: dangling-link + ukjent-type kaster ikke.
test('robusthet: dangling index-link droppes uten kast', () => {
const histIdx = join(REALISTIC, 'historikk', 'index.md');
const resolved = resolveIndexLinks(histIdx);
assert.ok(resolved.includes('retrospektiv-T3-2025.md'), 'eksisterende lenke beholdes');
assert.ok(!resolved.some((r) => r.includes('T2-2025')), 'dangling lenke (T2-2025) droppes');
});
test('robusthet: ukjent type parses uten kast, retrieval treffer fortsatt', () => {
const notat = join(REALISTIC, 'dokumenter', 'notat.md');
const { get } = parseFrontmatter(readFileSync(notat, 'utf8'));
assert.equal(get('type'), 'Notat', 'ukjent type leses raatt uten kast');
const hits = retrieve(REALISTIC, 'arbeidsnotat');
assert.equal(rel(REALISTIC, hits[0].path), 'dokumenter/notat.md');
});

65
tests/okf-vocab.test.mjs Normal file
View file

@ -0,0 +1,65 @@
// okf-vocab.test.mjs
// Tester det lukkede OKF-vokabularet (type+tags) og type->nivaa-ruting for
// innboks-ingestion (Step 2). snapType: exact-match -> kanonisk, ellers safe
// default Dokument; snapTags dropper ukjente; routeLevel mapper type til
// bundle-nivaa. Direkte import (zero npm deps). Moenster: tests/frontmatter.test.mjs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
TYPE_VOCAB,
TAGS_VOCAB,
snapType,
snapTags,
routeLevel,
} from '../lib/okf-vocab.mjs';
test('snapType: kjent type beholdes kanonisk', () => {
assert.equal(snapType('OKR'), 'OKR');
assert.equal(snapType('Tildelingsbrev'), 'Tildelingsbrev');
});
test('snapType: ukjent type -> safe default Dokument', () => {
assert.equal(snapType('ukjent'), 'Dokument');
assert.equal(snapType(''), 'Dokument');
assert.equal(snapType(undefined), 'Dokument');
});
test('snapTags: dropper ukjente, beholder kjente (bevart rekkefolge)', () => {
assert.deepEqual(snapTags(['Strategi', 'xyz']), ['Strategi']);
assert.deepEqual(snapTags(['xyz', 'abc']), []);
assert.deepEqual(snapTags([]), []);
});
test('snapTags: ikke-array -> tom liste (ingen krasj)', () => {
assert.deepEqual(snapTags(undefined), []);
assert.deepEqual(snapTags(null), []);
});
test('routeLevel: strategisk-kontekst for foringsdokumenter', () => {
assert.equal(routeLevel('Tildelingsbrev'), 'strategisk-kontekst');
assert.equal(routeLevel('Virksomhetsplan'), 'strategisk-kontekst');
assert.equal(routeLevel('Overordnede OKR'), 'strategisk-kontekst');
});
test('routeLevel: historikk for Retrospektiv', () => {
assert.equal(routeLevel('Retrospektiv'), 'historikk');
});
test('routeLevel: dokumenter for OKR/Status/Notat/Dokument/Organisasjonsprofil', () => {
assert.equal(routeLevel('OKR'), 'dokumenter');
assert.equal(routeLevel('Status'), 'dokumenter');
assert.equal(routeLevel('Notat'), 'dokumenter');
assert.equal(routeLevel('Dokument'), 'dokumenter');
assert.equal(routeLevel('Organisasjonsprofil'), 'dokumenter');
});
test('routeLevel: ukjent type -> default dokumenter', () => {
assert.equal(routeLevel('Whatever'), 'dokumenter');
});
test('vokabular non-tomt + default i settet', () => {
assert.ok(TYPE_VOCAB.length > 0, 'TYPE_VOCAB non-tomt');
assert.ok(TAGS_VOCAB.length > 0, 'TAGS_VOCAB non-tomt');
assert.ok(TYPE_VOCAB.includes('Dokument'), 'default Dokument er i vokabularet');
});

View file

@ -0,0 +1,143 @@
// oppsett-okf-write.test.mjs
// Step 7 (SC6): onboarding skriver OKF-frontmatter paa org-profilen.
// Profilen er NESTET (organisasjon: { navn, type }), saa flat writeFrontmatter
// er feil verktoey. compose-org-profile.mjs bygger den fulle nestede YAML-en og
// PREPENDER tre top-level OKF-noekler (type/resource/timestamp) FOER organisasjon:.
// Pipes til write-org-profile.mjs (uendret) som skriver atomisk til hjem.
//
// Kritisk invariant: kun type/resource/timestamp top-level (leses ikke av hooks).
// En NY top-level navn/id/fase/domene/sektor ville matches FOERST av den flate
// parseren og skygge den nestede verdien -> brutt org-/syklus-resolusjon.
// Zero npm deps. Moenster: tests/org-profile-write.test.mjs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseFrontmatter } from '../lib/frontmatter.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const COMPOSE = join(ROOT, 'scripts', 'compose-org-profile.mjs');
const WRITER = join(ROOT, 'scripts', 'write-org-profile.mjs');
const HOOK = join(ROOT, 'hooks', 'scripts', 'inject-okr-context.mjs');
// Nestet profil-body slik /okr:oppsett produserer den (organisasjon: + program:).
// Bevisst UTEN '#'-kommentarer her -- comment-strip dekkes av frontmatter-testen.
const NESTED_BODY = [
'onboarding_status: fullfort',
'organisasjon:',
' navn: "Testdirektoratet"',
' kortform: "TDIR"',
' type: "offentlig"',
' domene: "digitalisering"',
'program:',
' modenhetsnivaa: "pilot"',
' okr_frikoblet_fra_loenn: true',
'',
].join('\n');
const NOW = '2026-06-26T10:00:00+00:00';
function compose(input, env = {}) {
return execFileSync('node', [COMPOSE], {
env: { ...process.env, ...env },
input,
encoding: 'utf8',
});
}
function write(cwd, home, input) {
return execFileSync('node', [WRITER], {
cwd,
env: { ...process.env, HOME: home },
input,
encoding: 'utf8',
});
}
function runHook(cwd, home) {
return execFileSync('node', [HOOK], {
cwd,
env: { ...process.env, HOME: home },
encoding: 'utf8',
});
}
function withDirs(fn) {
const home = mkdtempSync(join(tmpdir(), 'okrhome-'));
const work = mkdtempSync(join(tmpdir(), 'okrwork-'));
try {
fn(home, work);
} finally {
rmSync(home, { recursive: true, force: true });
rmSync(work, { recursive: true, force: true });
}
}
test('compose: prepender top-level type/resource/timestamp, bevarer nestet organisasjon/navn', () => {
const composed = compose(NESTED_BODY, { OKR_NOW: NOW });
// Top-level OKF-noekler (linjestart):
assert.match(composed, /^type: Organisasjonsprofil$/m, 'top-level type');
assert.match(composed, /^resource: /m, 'top-level resource');
assert.match(composed, /^timestamp: '2026-06-26T10:00:00\+00:00'$/m, 'top-level timestamp (ISO, sitert)');
// Nestet struktur bevart:
assert.match(composed, /^organisasjon:$/m, 'nestet organisasjon: bevart');
assert.match(composed, /^ {2}navn: "Testdirektoratet"$/m, 'nestet navn bevart');
// ETT samlet frontmatter-blokk: parseFrontmatter ser BAADE OKF-noekler OG nestet navn.
const { get } = parseFrontmatter(composed);
assert.equal(get('navn'), 'Testdirektoratet', 'navn resolver (ingen intern --- som trunkerer blokken)');
assert.equal(get('type'), 'Organisasjonsprofil', 'type resolver til OKF-verdi');
// FORBID-regel (Critical risk): ingen NY top-level noekkel som skygger nestet verdi.
assert.doesNotMatch(composed, /^navn:/m, 'ingen top-level navn (ville skygge nestet org-navn)');
assert.doesNotMatch(composed, /^id:/m, 'ingen top-level id (ville skygge nestet cycle-id)');
assert.doesNotMatch(composed, /^fase:/m, 'ingen top-level fase');
assert.doesNotMatch(composed, /^domene:/m, 'ingen top-level domene');
assert.doesNotMatch(composed, /^sektor:/m, 'ingen top-level sektor');
});
test('compose: tolererer body som allerede baerer --- fences (samler til ett blokk)', () => {
const fenced = `---\n${NESTED_BODY}---\n`;
const composed = compose(fenced, { OKR_NOW: NOW });
const { get } = parseFrontmatter(composed);
assert.equal(get('navn'), 'Testdirektoratet', 'navn resolver (ingen dobbel-fence trunkerer blokken)');
assert.equal(get('type'), 'Organisasjonsprofil');
const fences = composed.match(/^---$/gm) || [];
assert.equal(fences.length, 2, 'noeyaktig to frontmatter-fences');
});
test('compose -> write-org-profile: hjem-profil faar OKF-frontmatter + bevart nestet navn', () => {
withDirs((home, work) => {
const composed = compose(NESTED_BODY, { OKR_NOW: NOW });
const out = write(work, home, composed);
const target = join(home, '.claude', 'okr', 'org', 'profil.md');
assert.ok(existsSync(target), 'hjem-profil skal finnes');
const written = readFileSync(target, 'utf8');
assert.match(written, /^type: Organisasjonsprofil$/m, 'skrevet profil har top-level type');
assert.match(written, /^resource: /m, 'skrevet profil har resource');
assert.match(written, /^timestamp: /m, 'skrevet profil har timestamp');
assert.match(written, /navn: "Testdirektoratet"/, 'skrevet profil bevarer nestet navn');
assert.equal(written, composed, 'write-org-profile er byte-passthrough (skriver stdin uendret)');
assert.ok(out.trim().length > 0, 'helper rapporterer brukt sti paa stdout');
});
});
test('inject-roundtrip: get(navn) resolver org-navn (ikke "Organisasjonsprofil"), ingen #, cap < 512 B', () => {
withDirs((home, work) => {
const composed = compose(NESTED_BODY, { OKR_NOW: NOW });
write(work, home, composed);
// Tomt prosjekt-cwd -> hooken faller til hjem-profil (mest-spesifikk-vinner).
const injected = runHook(work, home);
const { systemMessage } = JSON.parse(injected);
assert.match(systemMessage, /Testdirektoratet/, 'org-navn injiseres');
assert.doesNotMatch(systemMessage, /Organisasjonsprofil/, 'top-level type skal IKKE skygge nestet navn');
assert.ok(!systemMessage.includes('#'), 'ingen # i payload');
assert.ok(Buffer.byteLength(systemMessage, 'utf8') < 512, 'kjerne-payload < 512 B (SC5)');
});
});

View file

@ -0,0 +1,83 @@
// org-profile-write.test.mjs
// Tester atomisk org-profil-skrivehelper (SC1): hjem-skriv, circuit-breaker
// fallback til prosjektlokal, og round-trip mot inject-okr-context.
// Spawner helperen som subprosess med kontrollert HOME + cwd + stdin.
// Zero npm deps. Plassert i tests/ (samme moenster som inject-okr-context.test.mjs).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, writeFileSync, readFileSync, existsSync, realpathSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const HELPER = join(ROOT, 'scripts', 'write-org-profile.mjs');
const HOOK = join(ROOT, 'hooks', 'scripts', 'inject-okr-context.mjs');
function runHelper(cwd, home, input) {
// execFileSync returnerer stdout; helperen avslutter alltid med exit 0.
return execFileSync('node', [HELPER], {
cwd,
env: { ...process.env, HOME: home },
input,
encoding: 'utf8',
});
}
function runHook(cwd, home) {
return execFileSync('node', [HOOK], {
cwd,
env: { ...process.env, HOME: home },
encoding: 'utf8',
});
}
function withDirs(fn) {
const home = mkdtempSync(join(tmpdir(), 'okrhome-'));
const work = mkdtempSync(join(tmpdir(), 'okrwork-'));
try {
fn(home, work);
} finally {
rmSync(home, { recursive: true, force: true });
rmSync(work, { recursive: true, force: true });
}
}
test('hjem-skriv: helper skriver org-profil til ~/.claude/okr/org/profil.md', () => {
withDirs((home, work) => {
const profil = '---\nnavn: "HjemskrivOrg"\n---\n';
const out = runHelper(work, home, profil);
const target = join(home, '.claude', 'okr', 'org', 'profil.md');
assert.ok(existsSync(target), 'hjem-profil skal finnes etter skriv');
assert.equal(readFileSync(target, 'utf8'), profil, 'innhold skal matche stdin');
// realpathSync normaliserer macOS /var -> /private/var-symlink paa begge sider.
assert.equal(realpathSync(out.trim()), realpathSync(target), 'stdout skal rapportere faktisk brukt sti (hjem)');
});
});
test('circuit-breaker: uskrivbart hjem -> fallback til prosjektlokal uten error', () => {
withDirs((home, work) => {
// Gjoer hjem-skriv umulig: ~/.claude er en FIL, ikke katalog -> mkdirSync
// (recursive) under den feiler med ENOTDIR (rot-uavhengig, deterministisk).
writeFileSync(join(home, '.claude'), 'not a directory\n');
const profil = '---\nnavn: "FallbackOrg"\n---\n';
// execFileSync kaster hvis exit != 0; at dette IKKE kaster beviser exit 0.
const out = runHelper(work, home, profil);
const fallback = join(work, '.claude', 'okr.local.md');
assert.ok(existsSync(fallback), 'fallback-profil skal finnes i prosjektlokal sti');
assert.equal(readFileSync(fallback, 'utf8'), profil, 'fallback-innhold skal matche stdin');
assert.equal(realpathSync(out.trim()), realpathSync(fallback), 'stdout skal rapportere fallback-sti');
});
});
test('round-trip: hjem-skrevet org reflekteres av inject-okr-context', () => {
withDirs((home, work) => {
const profil = '---\nnavn: "RoundTripOrg"\n---\n';
runHelper(work, home, profil);
// Tom prosjektkatalog (work) -> hooken faller til hjem-profil (mest-spesifikk-vinner).
const injected = runHook(work, home);
assert.match(injected, /RoundTripOrg/, 'inject skal lese hjem-profilen helperen skrev');
});
});

View file

@ -0,0 +1,79 @@
// topic-guard.test.mjs
// Tester UserPromptSubmit emne-guard (SC6) i inject-okr-context: kun
// OKR-relevante prompter injiserer kontekst; en eksplisitt ikke-matchende
// prompt suppresses; tvil (tomt felt / ingen stdin) -> default-inject.
// Spawner hooken som subprosess med kontrollert cwd + stdin. Zero npm deps.
// Moenster: tests/inject-okr-context.test.mjs (begge-casen + input-opsjon).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HOOK = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'hooks',
'scripts',
'inject-okr-context.mjs',
);
function makeProjectConfig(workDir, navn) {
const p = join(workDir, '.claude');
mkdirSync(p, { recursive: true });
writeFileSync(join(p, 'okr.local.md'), `---\nnavn: "${navn}"\n---\n`);
}
function runHook(cwd, input = '') {
// execFileSync returnerer stdout; hooken avslutter alltid med exit 0.
return execFileSync('node', [HOOK], {
cwd,
env: { ...process.env },
input: input,
encoding: 'utf8',
});
}
function withWork(fn) {
const work = mkdtempSync(join(tmpdir(), 'okrtopic-'));
try {
fn(work);
} finally {
rmSync(work, { recursive: true, force: true });
}
}
test('irrelevant prompt: suppresses injeksjon (tom stdout)', () => {
withWork((work) => {
makeProjectConfig(work, 'TopicOrg');
const out = runHook(work, JSON.stringify({ prompt: 'hva er vaeret i dag' }));
assert.equal(out.trim(), '', 'ikke-OKR-prompt skal suppresses selv med gyldig config');
});
});
test('relevant prompt: injiserer OKR-kontekst', () => {
withWork((work) => {
makeProjectConfig(work, 'TopicOrg');
const out = runHook(work, JSON.stringify({ prompt: 'hjelp meg skrive en OKR for neste tertial' }));
assert.match(out, /OKR-kontekst/, 'OKR-relevant prompt skal injisere kontekst');
});
});
test('tomt prompt-felt: bevarer inject-default (tvil -> injiser)', () => {
withWork((work) => {
makeProjectConfig(work, 'TopicOrg');
const out = runHook(work, JSON.stringify({ prompt: '' }));
assert.match(out, /OKR-kontekst/, 'tomt prompt-felt -> tvil -> injiser');
});
});
test('ingen stdin: bevarer inject-default (regresjonsvakt)', () => {
withWork((work) => {
makeProjectConfig(work, 'TopicOrg');
const out = runHook(work); // input utelatt -> tom stdin
assert.match(out, /OKR-kontekst/, 'ingen stdin -> tvil -> injiser');
});
});