feat(graceful-handoff): 2.0 — migrate to skills/ with disable-model-invocation [skip-docs]

Step 1 of v2.0 plan. Hard cut from commands/ to skills/ per Anthropic
recommendation for new plugins. Frontmatter sets disable-model-invocation:
true and pins model: claude-sonnet-4-6. Docs (README, CLAUDE.md, root
README) deferred to Step 9 per plan.
This commit is contained in:
Kjell Tore Guttormsen 2026-05-01 05:45:26 +02:00
commit 1a65d8e4d5
12 changed files with 331 additions and 355 deletions

View file

@ -1,329 +0,0 @@
---
name: graceful-handoff
description: Produser handoff-artefakt, commit+push, og copy-paste-prompt for neste sesjon. Bruk når du nærmer deg 60-70% kontekst og må fortsette arbeidet i en ny sesjon uten tap.
argument-hint: "[topic-slug] [--no-commit] [--dry-run]"
allowed-tools: Read, Write, Edit, Bash, Glob
---
# Graceful Handoff — sesjonsoverlevering
Produser en komplett handoff-pakke i ett steg: analyser state, skriv NEXT-SESSION-artefakt, oppdater REMEMBER/TODO, commit+push hvis mulig, og print en copy-paste-prompt for neste sesjon.
**Tidsbudsjett:** Hele kjøringen (fase 1-6) skal ligge under 60 sekunder reell tid. Bruker er typisk på 60-70% kontekst når de trigger dette — ikke bruk Agent-delegering eller WebSearch.
## Argumenter
| Argument | Effekt |
|----------|--------|
| `[topic-slug]` | Kebab-case slug som styrer filnavnet. Default: `NEXT-SESSION-PROMPT.local.md`. Med slug: `NEXT-SESSION-<slug>.local.md` |
| `--no-commit` | Hopp over fase 5 (commit+push). Bruker håndterer commit manuelt |
| `--dry-run` | Ikke skriv filer eller gjør git-operasjoner. Print kun hva som ville skjedd |
Parse argumenter fra `$ARGUMENTS` (kombinert streng). Støtt flag i vilkårlig rekkefølge. Første ikke-flag-token er slug.
---
## Fase 1 — Detekter arbeidsmappe og aktivt prosjekt
Kjør i parallell (én Bash-melding med flere tool calls):
```bash
pwd
git rev-parse --show-toplevel 2>/dev/null
git status --porcelain
git log --oneline -10
git branch --show-current
```
Let oppover i tre etter plugin-markør (maks 5 nivåer):
```bash
# Fra cwd og oppover, stopp på første treff
cur="$(pwd)"
for i in 1 2 3 4 5; do
if [ -f "$cur/.claude-plugin/plugin.json" ]; then
echo "PLUGIN_ROOT=$cur"
break
fi
cur="$(dirname "$cur")"
[ "$cur" = "/" ] && break
done
```
Finn ultraplan-prosjekt (nyeste først):
```bash
find . -maxdepth 4 -path '*/.claude/projects/*/brief.md' 2>/dev/null | sort -r | head -3
find . -maxdepth 4 -path '*/.claude/projects/*/progress.json' 2>/dev/null | sort -r | head -3
```
For hver progress.json-treff: kjør `jq -r '.status' <file>` for å se om prosjektet er aktivt.
## Fase 2 — Identifiser handoff-type
Prioritet: **multi-sesjon > plugin-arbeid > enkelt-oppgave**
| Type | Betingelse |
|------|------------|
| `multi-sesjon` | Finnes `.claude/projects/<NN>/progress.json` med `status != "completed"` OG `status != null`. Eller: finnes `.claude/projects/<NN>/plan.md` uten progress.json |
| `plugin-arbeid` | Fase 1 fant `PLUGIN_ROOT` OG ingen aktiv multi-sesjon |
| `enkelt-oppgave` | Ingen av over |
Lagre i intern state: `HANDOFF_TYPE`, `PROJECT_DIR` (hvis multi-sesjon), `PLUGIN_ROOT` (hvis plugin-arbeid), `WRITE_DIR` (hvor artefakten skal skrives).
Write-dir-logikk:
- `multi-sesjon``WRITE_DIR = <PROJECT_DIR>`
- `plugin-arbeid``WRITE_DIR = <PLUGIN_ROOT>`
- `enkelt-oppgave``WRITE_DIR = $(pwd)`
Rapporter til bruker i en kort linje:
```
Handoff-type: plugin-arbeid (llm-security). Skriver til /Users/ktg/.../plugins/llm-security/
```
Hvis overlapp eksisterer (f.eks. plugin-arbeid innenfor et aktivt ultraplan-prosjekt): rapporter begge, men velg multi-sesjon som primær.
## Fase 3 — Skriv/oppdater NEXT-SESSION-artefakt
Filnavn:
- Hvis `[topic-slug]` gitt: `NEXT-SESSION-<slug>.local.md`
- Ellers: `NEXT-SESSION-PROMPT.local.md`
Full sti: `<WRITE_DIR>/<filename>`.
**Hvis filen finnes**: les den først, bevar "Hvorfor dette eksisterer"-seksjonen hvis den er relevant, oppdater status-seksjoner med dagens dato og aktuell state.
**Template** (samme 7-seksjons-struktur som llm-security/config-audit bruker):
```markdown
# NEXT-SESSION-PROMPT — <plugin-eller-prosjekt-navn> <kort-tittel>
## Hvorfor dette eksisterer
<1-3 setninger om hva bruker jobber med og hvorfor denne sesjonen stoppet. Hent fra: siste commits, brief.md hvis multi-sesjon, git diff, samtale-kontekst. Vær konkret — ingen fluff.>
## Status ved sesjonshåndoff
### ✅ Ferdig
<List commits fra denne sesjonen med SHA + hva hver gjorde. Eks:
- `a9fb328` fix(config-audit): complete conflict-project fixture
- `94ce701` test(config-audit): add Opus 4.7 pattern fixtures
Hvis ingen commits: skriv "Ingen nye commits i denne sesjonen".>
### ⏳ Ikke startet / delvis
<Konkrete neste steg. Nevn filnavn og linjenummer hvis du vet dem. Eks:
- Commit 2: Kontekst-bevisst entropy — scanners/entropy-scanner.mjs:40-47, 90-137
- Commit 3: Policy-integrasjon
Hvis det finnes en plan-fil: peker til den.>
### ⚠️ Brutt / kjent risiko
<Tester som feiler, uncommittede endringer, antakelser som ikke er verifisert. Eks:
- 25/80 tester feiler i tests/lib/severity.test.mjs (forventet — tester gammel formel)
- Commit 1 er lokal, ikke pushet
Hvis ingenting er brutt: skriv "Ingen kjente broken tester".>
## Slik fortsetter du
1. `cd <absolute-path-til-WRITE_DIR>`
2. `cat <filename>` — les denne filen igjen
3. `git log --oneline -5` og `git status`
<4. (hvis multi-sesjon) Les planen: `cat .claude/projects/<slug>/plan.md`>
<5. Start med: <konkret neste handling>>
## Push-policy
- Direkte push til `main` på Forgejo er pre-autorisert (ikke spør om tillatelse)
- Aldri GitHub — kun Forgejo (`git.fromaitochitta.com`)
<evt. prosjekt- eller plugin-spesifikke regler, f.eks. versjonsbump-timing>
## Verifiseringskommandoer
```bash
<Copy-paste-klare sjekker som beviser state. Eks:
git log --oneline -5 # forventet: <siste commit SHA>
git status # forventet: clean (eller <N> uncommittede)
node --test tests/ # forventet: alle grønne
>
```
## Husk
- <Gotchas og antakelser som ikke er åpenbare fra koden. Eks:
- Hooks (pre-edit-secrets, pre-write-pathguard) blokkerer writes til .claude-plugin/
- Versjonsbump krever sync i 7+ filer — bruk grep-mønster
- Opus 4.7 fyller kontekst raskt — kjør /graceful-handoff ved 60-70%>
```
**Innholdsregler:**
- Ikke fyll inn plassholdere med placeholder-tekst. Les faktisk git-state, samtalekontekst, og evt. brief.md/plan.md for å fylle inn ekte info.
- Hvis en seksjon er irrelevant (f.eks. ingen push-policy avvik): skriv én setning som sier det, ikke utelat seksjonen (strukturen må være konsistent).
- Tone: nøktern, norsk, teknisk. Ingen "gøy å jobbe med deg" eller "vi har kommet langt". Hvis brukeren var sint eller frustrert: nevn det kort i "Husk" slik at neste sesjon ikke trår i samme fellen.
Hvis `--dry-run`: print artefakten til stdout i stedet for å skrive.
## Fase 4 — Oppdater REMEMBER.md og TODO.md
**Lokasjon:**
- `plugin-arbeid``<PLUGIN_ROOT>/REMEMBER.md` og `<PLUGIN_ROOT>/TODO.md`
- `multi-sesjon``<PROJECT_DIR>/REMEMBER.md` og `<PROJECT_DIR>/TODO.md` (hvis de finnes; ellers skip — ultraplan-prosjekter har ikke alltid disse)
- `enkelt-oppgave``$(pwd)/REMEMBER.md` og `$(pwd)/TODO.md` (kun hvis de allerede finnes; ikke opprett nye i random mapper)
**REMEMBER.md-oppdatering:**
Hvis filen finnes — les, oppdater:
- Øverst: `## Sist oppdatert\n<YYYY-MM-DD>` (absolutt dato, ikke "i dag")
- "PÅGÅENDE"-seksjon (eller lag den): plan-sti hvis relevant, 3-5 status-bullets (✅ ferdig, ⏳ pågående, ⚠️ blokkert)
- Flytt tidligere "PÅGÅENDE" til "TIDLIGERE" med datostempel hvis den peker på annet arbeid
Hvis filen ikke finnes og type er `plugin-arbeid`: opprett minimal REMEMBER.md med dagens dato og én PÅGÅENDE-bullet. Ellers skip.
**TODO.md-oppdatering:**
Hvis filen finnes — les, oppdater:
- Flytt items som ble gjort denne sesjonen til en "✅ Ferdig denne sesjonen (YYYY-MM-DD)"-seksjon øverst
- Legg til items for neste sesjon under "⏳ Neste"
- Ikke slett — kommenter ut eller flytt
Hvis filen ikke finnes: skip (ikke push onboarding i en handoff).
**Gitignore-verifisering:** Etter skriving, kjør `git check-ignore -v <path>` for hver fil. Hvis filen IKKE er ignorert: rapporter til bruker som advarsel (ikke blokker, men bruker må vite).
Hvis `--dry-run`: print diff i stedet for å skrive.
## Fase 5 — Commit + push hvis ucommittet arbeid finnes
Skip hele fasen hvis `--no-commit` eller `--dry-run`.
Skip hvis `git status --porcelain` er tom (ingenting å commite).
**Steg 5a — Klassifiser endringer:**
```bash
git diff --stat
git diff --cached --stat
git status --porcelain
```
Kategoriser endrede filer for å utlede commit-type:
| Filmønster | Type |
|------------|------|
| `tests/**`, `**/*.test.*` | `test` |
| `*.md`, `README`, `CHANGELOG`, `CLAUDE.md` (kun docs-endring) | `docs` |
| `plugins/<name>/hooks/**`, `plugins/<name>/scanners/**` (ny funksjon) | `feat` |
| Samme mønster, men fikser eksisterende atferd | `fix` |
| `package.json`, config, lockfile kun | `chore` |
| Blandet | Bruk dominerende kategori; hvis uklart, `chore` |
Scope: plugin-navn hvis `plugin-arbeid` eller hvis alle endringer er i én plugin. Ellers ingen scope.
**Steg 5b — Generer commit-melding:**
Format: `<type>(<scope>): <kort beskrivelse>` (Conventional Commits).
Krav:
- Første linje ≤ 72 tegn
- Beskrivelse på norsk eller engelsk (match eksisterende commits i repoet — sjekk `git log --oneline -10`)
- Beskriv **hva** som endres, ikke hvorfor (hvorfor hører hjemme i body hvis nødvendig)
- Hvis flere uavhengige endringer: vurder om de skal splittes i to commits. Hvis bruker er i hastverk (kontekst-trøskel): ett commit med samlet beskrivelse er akseptabelt, men noter at det er samlet i body.
Eksempler (match stil fra eksisterende repo):
- `test(config-audit): add Opus 4.7 pattern fixtures`
- `fix(llm-security): complete conflict-project fixture for CNF cross-scope tests`
- `feat(graceful-handoff): initial plugin with /graceful-handoff command`
Body (valgfri, bruk hvis commit dekker flere endringer):
```
- Endring 1
- Endring 2
Samlet som ett commit pga. graceful-handoff-flyt.
```
**Steg 5c — Vis meldingen og commit:**
Print den genererte meldingen ordrett til bruker FØR commit:
```
Commit-melding:
---
feat(graceful-handoff): initial plugin with /graceful-handoff command
---
```
Deretter:
```bash
git add -A # eller eksplisitt filvalg hvis endringene spenner flere uavhengige områder
git commit -m "$(cat <<'EOF'
<meldingen>
EOF
)"
```
Hvis commit feiler pga. pre-commit hook (secrets, pathguard, osv.): STOPP, rapporter feilmeldingen, **ikke** bypass med `--no-verify`. Be bruker håndtere manuelt.
**Steg 5d — Push:**
```bash
BRANCH="$(git branch --show-current)"
if [ -n "$BRANCH" ]; then
git push origin "$BRANCH"
else
git push origin HEAD
fi
```
Bekreft push med `git rev-parse @{u}` (at upstream er satt) eller hent siste output fra push-kommandoen.
Hvis push feiler: rapporter, men ikke force-push. Bruker kan håndtere manuelt.
## Fase 6 — Print copy-paste-prompt for neste sesjon
Print en kompakt prompt til terminal som bruker kan kopiere direkte inn i ny Claude Code-sesjon:
```
════════════════════════════════════════════════════════════
NESTE SESJON — copy-paste til ny Claude:
════════════════════════════════════════════════════════════
cd <absolute-WRITE_DIR>
cat <NEXT-SESSION-filnavn>
git log --oneline -5
git status
Fortsett fra <konkret neste handling - én setning>.
════════════════════════════════════════════════════════════
Artefakt: <full sti til NEXT-SESSION-filen>
Commit: <siste SHA eller "ingen endringer">
Push: <"pushet til Forgejo" eller "skippet (flag / ingenting)">
════════════════════════════════════════════════════════════
```
Dette er den KRITISKE output-en — hvis alt annet feiler, må minst denne blokken nå bruker slik at de kan fortsette. Bruk én final assistant-tekst-melding til å printe dette.
---
## Tidsbudsjett og escalation
- Hele kommandoen: < 60 sekunder
- Hvis du oppdager at du trenger å lese >5 filer eller kjøre >3 Agent-delegeringer: stopp, skriv en minimal handoff med det du har, og noter i "Brutt/risiko" at handoff var ufullstendig.
- **Aldri Agent-delegering i denne kommandoen** — main-sesjonen er raskere enn å spinne opp en subagent for noe mekanisk som dette.
- **Aldri WebSearch** — ingen ekstern info trengs.
## Fallback hvis state er helt uklart
Hvis fase 1-2 ikke kan identifisere noen av de tre handoff-typene (f.eks. cwd er utenfor git, ingen plugin-markør, ingen ultraplan-prosjekt):
1. Sett `HANDOFF_TYPE = enkelt-oppgave`
2. `WRITE_DIR = $(pwd)`
3. Varsle bruker: "Kunne ikke detektere handoff-type. Skriver til cwd."
4. Fortsett som vanlig — prompten er fremdeles nyttig

View file

@ -0,0 +1,98 @@
---
name: graceful-handoff
description: Produser handoff-artefakt, commit+push, og copy-paste-prompt for neste sesjon. Bruk når du nærmer deg 60-70% kontekst og må fortsette arbeidet i en ny sesjon uten tap.
argument-hint: "[topic-slug] [--no-commit] [--dry-run]"
disable-model-invocation: true
model: claude-sonnet-4-6
allowed-tools: Bash(git:*) Bash(jq:*) Bash(node:*) Bash(find:*) Bash(pwd:*) Read Write Glob
---
# Graceful Handoff — sesjonsoverlevering v2.0
Orkestrerer JSON-pipeline-skriptet og fyller copy-paste-template-en for neste sesjon. Selve pipelinen (state-deteksjon, classification, fil-skriving, commit, push) er deterministisk og lever i `scripts/handoff-pipeline.mjs`. Denne skill-en er en tynn wrapper.
**Tidsbudsjett:** Hele kjøringen skal ligge under 60 sekunder reell tid. Bruker er typisk på 60-70% kontekst når de trigger dette — ingen Agent-delegering, ingen WebSearch.
## Hvordan kjøres
1. **Parse `$ARGUMENTS`** (kombinert streng). Støtt flag i vilkårlig rekkefølge.
- `[topic-slug]` — kebab-case, styrer filnavnet
- `--no-commit` — hopp over commit/push, bruker håndterer manuelt
- `--dry-run` — print hva som ville skjedd, ingen filer/git
- `--no-push` — commit OK men ikke push (Stop hook bruker dette i auto-eksekvering)
- `--auto` — non-interactive, auto-Y på commit-bekreftelse (kun for hooks)
- `--non-interactive` — uten `--auto`: feil; med `--auto`: kjør uten prompts
2. **Kjør pipeline-skriptet:**
```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/handoff-pipeline.mjs <args>
```
3. **Parse JSON-output** fra stdout. Forventet schema:
```json
{
"handoff_type": "multi-sesjon | plugin-arbeid | enkelt-oppgave",
"write_dir": "/abs/path",
"artifact_path": "/abs/path/NEXT-SESSION-...",
"next_steps": ["..."],
"git_status": { "branch": "...", "dirty": true, "ahead": 2 },
"commit_message": "...",
"actions_taken": ["wrote artifact", "committed", "pushed"],
"errors": []
}
```
4. **Hvis `errors[]` non-empty:** rapporter feilene til bruker, ikke fortsett. Foreslå manuelle skritt fra `next_steps`.
5. **Hvis interaktiv (default):** Skriptet skriver commit-bekreftelses-prompten til stderr. Modellen leser stderr-output og presenterer Y/n-valget til bruker via AskUserQuestion. Send svaret tilbake til skriptet via stdin. (NB: I denne skill-konteksten kan modellen også vise commit-meldingen direkte og spørre — fleksibelt.)
6. **Når ferdig:** Print copy-paste-prompt fra `next_steps` JSON til bruker:
```
════════════════════════════════════════════════════════════
NESTE SESJON — copy-paste til ny Claude:
════════════════════════════════════════════════════════════
cd <absolute-WRITE_DIR>
cat <NEXT-SESSION-filnavn>
git log --oneline -5
git status
Fortsett fra <konkret neste handling — én setning>.
════════════════════════════════════════════════════════════
Artefakt: <full sti til NEXT-SESSION-filen>
Commit: <siste SHA eller "ingen endringer">
Push: <"pushet til Forgejo" eller "skippet (flag / ingenting)">
════════════════════════════════════════════════════════════
```
## Når brukes den
- **Manuelt:** kjør `/graceful-handoff` selv ved 60-70% kontekst
- **Automatisk:** Stop hook kaller `handoff-pipeline.mjs --auto --no-push` ved estimert ≥70%. Skill-en invokeres IKKE i auto-modus — hook-en kaller skriptet direkte for å bevare `disable-model-invocation: true`.
## Hva blir skrevet
- `NEXT-SESSION-PROMPT.local.md` (eller `NEXT-SESSION-<slug>.local.md`) i riktig WRITE_DIR
- `REMEMBER.md` oppdatert hvis den finnes
- `TODO.md` oppdatert hvis den finnes
- Git commit + push (med mindre `--no-commit` eller `--no-push`)
## Push-policy
- Direkte push til `main` på Forgejo er pre-autorisert
- Aldri GitHub — kun Forgejo (`git.fromaitochitta.com`)
- Pre-commit hooks respekteres uten `--no-verify`
## Begrensninger (v2.0)
- Auto-eksekvering ved kontekst-terskel er approksimasjon basert på transcript-størrelse, ikke Claude's reelle kontekst-måling. Estimat kan avvike ±10% — terskel satt konservativt til 70%.
- statusLine-plassering i `hooks/hooks.json` er antakelse; smoke-test før release.
- `disable-model-invocation: true` har en åpen issue (#26251) som potensielt kan blokkere user-invocation. Verifiser med smoke-test.
## Feilsøking
- Pipeline-skriptet feiler: kjør med `--dry-run` for å se hva det ville gjort
- Git-state uvanlig (detached HEAD, ingen remote): pipeline returnerer `errors[]`, ikke crash
- Stop hook trigger for tidlig/sent: terskel kan justeres i `hooks/scripts/stop-context-monitor.mjs` (look for `0.70`)

View file

@ -0,0 +1,61 @@
// skill-structure.test.mjs — Verifies SKILL.md frontmatter and commands/ deletion.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = join(__dirname, '..');
test('SKILL.md exists at expected path', () => {
const skillPath = join(PLUGIN_ROOT, 'skills', 'graceful-handoff', 'SKILL.md');
assert.ok(existsSync(skillPath), `SKILL.md missing at ${skillPath}`);
});
test('commands/ directory is deleted (hard cut to skills/)', () => {
const commandsDir = join(PLUGIN_ROOT, 'commands');
assert.ok(!existsSync(commandsDir), 'commands/ directory still exists — should be deleted in v2.0');
});
test('SKILL.md has disable-model-invocation: true', () => {
const skillPath = join(PLUGIN_ROOT, 'skills', 'graceful-handoff', 'SKILL.md');
const content = readFileSync(skillPath, 'utf-8');
assert.match(content, /^disable-model-invocation: true$/m);
});
test('SKILL.md has model: claude-sonnet-4-6', () => {
const skillPath = join(PLUGIN_ROOT, 'skills', 'graceful-handoff', 'SKILL.md');
const content = readFileSync(skillPath, 'utf-8');
assert.match(content, /^model: claude-sonnet-4-6$/m);
});
test('SKILL.md has Bash sub-scoped allowed-tools', () => {
const skillPath = join(PLUGIN_ROOT, 'skills', 'graceful-handoff', 'SKILL.md');
const content = readFileSync(skillPath, 'utf-8');
assert.match(content, /Bash\(git:\*\)/);
assert.match(content, /Bash\(node:\*\)/);
});
test('SKILL.md does not pre-approve curl or wget', () => {
const skillPath = join(PLUGIN_ROOT, 'skills', 'graceful-handoff', 'SKILL.md');
const content = readFileSync(skillPath, 'utf-8');
// Frontmatter only — find the allowed-tools line
const allowedToolsLine = content.match(/^allowed-tools:.*$/m);
assert.ok(allowedToolsLine, 'allowed-tools line missing');
assert.doesNotMatch(allowedToolsLine[0], /\bcurl\b/);
assert.doesNotMatch(allowedToolsLine[0], /\bwget\b/);
});
test('SKILL.md body references handoff-pipeline.mjs', () => {
const skillPath = join(PLUGIN_ROOT, 'skills', 'graceful-handoff', 'SKILL.md');
const content = readFileSync(skillPath, 'utf-8');
assert.match(content, /handoff-pipeline\.mjs/);
});
test('SKILL.md body has Tidsbudsjett (time budget) note', () => {
const skillPath = join(PLUGIN_ROOT, 'skills', 'graceful-handoff', 'SKILL.md');
const content = readFileSync(skillPath, 'utf-8');
assert.match(content, /Tidsbudsjett/);
});