diff --git a/commands/graceful-handoff.md b/commands/graceful-handoff.md deleted file mode 100644 index 6d77709..0000000 --- a/commands/graceful-handoff.md +++ /dev/null @@ -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-.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' ` 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//progress.json` med `status != "completed"` OG `status != null`. Eller: finnes `.claude/projects//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 = ` -- `plugin-arbeid` → `WRITE_DIR = ` -- `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-.local.md` -- Ellers: `NEXT-SESSION-PROMPT.local.md` - -Full sti: `/`. - -**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 — - -## 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 - - - -### ⏳ Ikke startet / delvis - - - -### ⚠️ Brutt / kjent risiko - - - -## Slik fortsetter du - -1. `cd ` -2. `cat ` — les denne filen igjen -3. `git log --oneline -5` og `git status` -<4. (hvis multi-sesjon) Les planen: `cat .claude/projects//plan.md`> -<5. Start med: > - -## Push-policy - -- Direkte push til `main` på Forgejo er pre-autorisert (ikke spør om tillatelse) -- Aldri GitHub — kun Forgejo (`git.fromaitochitta.com`) - - -## Verifiseringskommandoer - -```bash - -git status # forventet: clean (eller uncommittede) -node --test tests/ # forventet: alle grønne -> -``` - -## Husk - -- -``` - -**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` → `/REMEMBER.md` og `/TODO.md` -- `multi-sesjon` → `/REMEMBER.md` og `/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` (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 ` 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//hooks/**`, `plugins//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: `(): ` (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' - -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 -cat -git log --oneline -5 -git status - -Fortsett fra . - -════════════════════════════════════════════════════════════ -Artefakt: -Commit: -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 diff --git a/skills/graceful-handoff/SKILL.md b/skills/graceful-handoff/SKILL.md new file mode 100644 index 0000000..79c2865 --- /dev/null +++ b/skills/graceful-handoff/SKILL.md @@ -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 + ``` + +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 + cat + git log --oneline -5 + git status + + Fortsett fra . + + ════════════════════════════════════════════════════════════ + Artefakt: + Commit: + 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-.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`) diff --git a/tests/skill-structure.test.mjs b/tests/skill-structure.test.mjs new file mode 100644 index 0000000..1e16f0a --- /dev/null +++ b/tests/skill-structure.test.mjs @@ -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/); +});