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

@ -92,6 +92,8 @@ v2.1 (non-breaking) replaced the hardcoded Q1Q8 interview with a dynamic, qua
v1.7 self-verifying chain (preserved): a step may not be marked `completed` unless its manifest verifies. v1.8 Opus 4.7 literalism fixes (preserved): literal Step+Manifest template, forbidden narrative headers, schema self-check. v1.7 self-verifying chain (preserved): a step may not be marked `completed` unless its manifest verifies. v1.8 Opus 4.7 literalism fixes (preserved): literal Step+Manifest template, forbidden narrative headers, schema self-check.
v3.1.0 (in progress) adds a `lib/`-tree of zero-dep validators (`brief-validator`, `research-validator`, `plan-validator`, `progress-validator`, `architecture-discovery`) wired into the four commands as CLI shims, plus 109 `node:test` cases and a doc-consistency invariant test. The Phase 5.5 schema self-check now runs as `node lib/validators/plan-validator.mjs --strict` instead of three `grep -cE` calls — same checks, single source of truth, machine-readable error codes. Architecture discovery treats the `ultra-cc-architect` contract as drift-WARN, never drift-FAIL. Forking the plugin? `npm test` is the readiness gate.
Defense-in-depth security: plugin hooks block destructive commands and sensitive path writes, prompt-level denylist works in headless sessions, pre-execution plan scan catches dangerous commands before they run, scoped `--allowedTools` replaces `--dangerously-skip-permissions` in parallel sessions. Defense-in-depth security: plugin hooks block destructive commands and sensitive path writes, prompt-level denylist works in headless sessions, pre-execution plan scan catches dangerous commands before they run, scoped `--allowedTools` replaces `--dangerously-skip-permissions` in parallel sessions.
Modes: default, brief-driven, project-scoped, research-enriched, foreground, quick, decompose, export Modes: default, brief-driven, project-scoped, research-enriched, foreground, quick, decompose, export

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/);
});

View file

@ -90,6 +90,25 @@ If `{project_dir}/architecture/overview.md` exists (typically produced by the se
| contrarian-researcher | sonnet | Counter-evidence, overlooked alternatives | | contrarian-researcher | sonnet | Counter-evidence, overlooked alternatives |
| gemini-bridge | sonnet | Gemini Deep Research second opinion (conditional) | | gemini-bridge | sonnet | Gemini Deep Research second opinion (conditional) |
## Quality infrastructure (v3.1.0)
`lib/` contains zero-dep validators and parsers wired into the four commands:
- `lib/util/{frontmatter,result}.mjs` — shared YAML-frontmatter parser + Result helpers
- `lib/parsers/{plan-schema,manifest-yaml,project-discovery,arg-parser,bash-normalize}.mjs` — pure parsers (no I/O), unit-tested
- `lib/validators/{brief,research,plan,progress}-validator.mjs` — schema validators with CLI shims (`node lib/validators/X.mjs --json <path>`)
- `lib/validators/architecture-discovery.mjs` — drift-WARN external-contract discovery for `architecture/overview.md`
Wiring points (replaces previous prose-grep instructions):
- `/ultrabrief-local` Phase 4g → `brief-validator` (post-write sanity check)
- `/ultraplan-local` Phase 1 → `brief-validator --soft`, `research-validator --dir`, `architecture-discovery`
- `planning-orchestrator` Phase 5.5 → `plan-validator --strict` (replaces 3 `grep -cE` calls)
- `/ultraexecute-local --validate``plan-validator --strict` + `progress-validator`
Tests under `tests/**/*.test.mjs` (109 tests, 0 deps). `npm test` is the fork-readiness gate.
Doc-consistency test at `tests/lib/doc-consistency.test.mjs` pins agent-table count, command-table coverage, plan_version invariant, and settings.json scope cleanliness.
## Architecture ## Architecture
**Brief:** 7-phase workflow: Parse mode → Create project dir → Phase 3 completeness loop (section-driven, no question cap) → Phase 4 draft/review/revise with `brief-reviewer` as stop-gate (max 3 iterations; gate = all dimensions ≥ 4 and research plan = 5) → Finalize (`brief.md` on pass, or `brief_quality: partial` on cap/force-stop) → Manual/auto opt-in → Stats. Always interactive. Auto mode runs research + plan inline in the main context (v2.4.0). **Brief:** 7-phase workflow: Parse mode → Create project dir → Phase 3 completeness loop (section-driven, no question cap) → Phase 4 draft/review/revise with `brief-reviewer` as stop-gate (max 3 iterations; gate = all dimensions ≥ 4 and research plan = 5) → Finalize (`brief.md` on pass, or `brief_quality: partial` on cap/force-stop) → Manual/auto opt-in → Stats. Always interactive. Auto mode runs research + plan inline in the main context (v2.4.0).

View file

@ -473,6 +473,31 @@ v2.0.0 is a **breaking release**. See [MIGRATION.md](MIGRATION.md) for a step-by
| Requires GitHub | Yes | Yes | No | **No** | | Requires GitHub | Yes | Yes | No | **No** |
| Cross-platform | Web only | Web only | Desktop | **Mac, Linux, Windows** | | Cross-platform | Web only | Web only | Desktop | **Mac, Linux, Windows** |
## Quality infrastructure (since v3.1.0)
The plugin ships with `node:test`-based unit tests and a `lib/` directory of pure-JS validators wired into the commands. Forking the plugin for internal use? Run `npm test` to confirm the parsers, validators, and doc-consistency invariants still hold:
```bash
cd plugins/ultraplan-local
npm test # runs all tests under tests/**/*.test.mjs
```
Validators (zero npm deps, hand-rolled YAML subset):
| Module | Purpose |
|---|---|
| `lib/validators/brief-validator.mjs` | brief.md frontmatter + state machine (research_topics + status coherence) + body sections |
| `lib/validators/research-validator.mjs` | research-brief frontmatter (confidence ∈ [0,1], dimensions ≥ 1) + body sections; `--dir` mode validates a whole `research/` folder |
| `lib/validators/plan-validator.mjs` | wraps plan-schema + manifest-yaml; enforces v1.7 step heading, manifest count match, and forbidden-narrative-form denylist (`### Fase/Phase/Stage/Steg N`) — replaces the Phase 5.5 grep checks |
| `lib/validators/progress-validator.mjs` | progress.json shape (schema_version, status enum, current_step in range) + resume-readiness check |
| `lib/validators/architecture-discovery.mjs` | EXTERNAL CONTRACT — drift-WARN, never drift-FAIL. Discovers `architecture/overview.md` (owned by the separate `ultra-cc-architect` plugin) and tolerates non-canonical filenames with warnings. |
Each module exposes a CLI: `node lib/validators/<name>.mjs --json <path>` returns structured `{valid, errors, warnings, parsed}`. Commands invoke the CLI as their schema check.
A doc-consistency test (`tests/lib/doc-consistency.test.mjs`) pins prose-vs-source invariants — the agent table in `CLAUDE.md` must match the `agents/*.md` file count, every command's frontmatter `name:` must match its filename, and `templates/plan-template.md` must declare `plan_version: 1.7`.
Borrowed pattern from `llm-security` (commit `97c5c9d`); extending the plugin should preserve the invariants the test pins.
## Known limitations ## Known limitations
**Infrastructure-as-code (IaC) gets reduced value.** The exploration agents are designed for application code. Terraform, Helm, Pulumi, CDK projects will get a plan, but agents like `architecture-mapper` and `test-strategist` produce less useful output for IaC. Use ultraplan-local for the structural plan, then supplement IaC-specific steps manually. **Infrastructure-as-code (IaC) gets reduced value.** The exploration agents are designed for application code. Terraform, Helm, Pulumi, CDK projects will get a plan, but agents like `architecture-mapper` and `test-strategist` produce less useful output for IaC. Use ultraplan-local for the structural plan, then supplement IaC-specific steps manually.

View file

@ -320,28 +320,33 @@ If any validation fails, fix the plan before handing to Phase 6 review.
### Phase 5.5 — Schema self-check (REQUIRED before Phase 6) ### Phase 5.5 — Schema self-check (REQUIRED before Phase 6)
After writing the plan file, verify the output conforms to the executor's After writing the plan file, verify the output conforms to the executor's
parser BEFORE handing to plan-critic. Use Bash to grep the plan file: parser BEFORE handing to plan-critic. Run the plan validator:
```bash ```bash
# Count canonical step headings node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs --strict --json "$plan_path"
grep -c '^### Step [0-9]\+: ' "$plan_path"
# Count manifest blocks
grep -c '^ manifest:' "$plan_path"
# Detect forbidden narrative formats
grep -cE '^(##|###) (Fase|Phase|Stage) [0-9]' "$plan_path"
``` ```
**Pass criteria:** **Pass criteria:** validator exits 0 with `valid: true` in its JSON output.
- Step count ≥ 1 Internally the validator enforces (same checks as before, now in one place):
- Manifest count == Step count - Step count ≥ 1, numbering is 1..N contiguous
- Forbidden narrative count == 0 - Per-step Manifest YAML present, parses, and `commit_message_pattern` compiles
- Step count == manifest count
- Zero forbidden narrative headings (`### Fase N`, `### Phase N`, `### Stage N`,
`### Steg N`)
- `plan_version: 1.7` declared (warning only if older / missing)
**If the plan fails schema self-check:** rewrite the Implementation Plan Each error has a `code` field — read these to localize the fix. Common codes:
section using the exact literal template shown earlier in Phase 5. Do NOT - `PLAN_FORBIDDEN_HEADING` — narrative drift; rewrite the section using the
proceed to Phase 6 with a schema-failing plan — plan-critic cannot repair literal template from Phase 5
format drift, only content issues. - `PLAN_MANIFEST_COUNT_MISMATCH` — at least one step lost its manifest block
- `MANIFEST_PATTERN_INVALID` — a `commit_message_pattern` does not compile;
check escaping (use `\\(` not `\(` in YAML double-quoted strings)
- `PLAN_STEP_NUMBERING` — steps skip a number; renumber sequentially
**If the plan fails schema self-check:** rewrite the offending section using
the exact literal template shown earlier in Phase 5. Do NOT proceed to Phase 6
with a schema-failing plan — plan-critic cannot repair format drift, only
content issues.
### Failure recovery (REQUIRED for every step) ### Failure recovery (REQUIRED for every step)

View file

@ -460,11 +460,25 @@ After the loop exits (pass, cap, or force-stop), ensure:
Populate the "How to continue" footer with the actual project path and Populate the "How to continue" footer with the actual project path and
topic questions. topic questions.
**Schema sanity check (since v3.1.0):** before reporting, run the brief
validator. This catches frontmatter typos and state-machine inconsistencies
the brief-reviewer rubric does not check (e.g. `research_status: skipped`
with `research_topics: 3` and no `brief_quality: partial`).
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --json "{PROJECT_DIR}/brief.md"
```
If the validator returns errors, report them to the user and offer to
re-enter Phase 4 with the validator's hints in scope. If only warnings,
note them in the final report.
Report: Report:
``` ```
Brief written: {PROJECT_DIR}/brief.md Brief written: {PROJECT_DIR}/brief.md
Review iterations: {1..3} Review iterations: {1..3}
Final quality: {complete | partial} Final quality: {complete | partial}
Validator: {PASS | warnings(N)}
Research topics identified: {N} Research topics identified: {N}
``` ```

View file

@ -187,6 +187,23 @@ report. Do NOT run security scan, do NOT touch progress files, do NOT
execute any steps. This gives the user a fast sanity-check of plan execute any steps. This gives the user a fast sanity-check of plan
schema compliance without side effects. schema compliance without side effects.
**Preferred path (since v3.1.0):** invoke the plan validator directly. It
returns the same diagnostic info Phase 2 derives in prose, with stable
error codes for downstream tooling:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs --strict --json "{path}"
# When --project is in scope and progress.json exists, also validate it:
[ -f "{project_dir}/progress.json" ] && \
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/progress-validator.mjs --json "{project_dir}/progress.json"
```
Map the validator's `code` field to the error templates below (e.g.
`PLAN_FORBIDDEN_HEADING` → "Detected heading format" branch). When both
calls exit 0, render the READY report. Otherwise render FAIL with the
validator's first error code + message.
If Phase 2 parsing succeeded (no fatal errors, every step has a valid If Phase 2 parsing succeeded (no fatal errors, every step has a valid
Manifest block in strict mode, or synthesized manifests in legacy mode): Manifest block in strict mode, or synthesized manifests in legacy mode):

View file

@ -61,11 +61,27 @@ Parse `$ARGUMENTS` for mode flags. Order of precedence:
Missing: {dir}/brief.md Missing: {dir}/brief.md
``` ```
- Set **project_dir = {dir}**, **brief_path = {dir}/brief.md**. - Set **project_dir = {dir}**, **brief_path = {dir}/brief.md**.
- **Validate inputs** (soft mode — warnings do not block, errors do):
```bash
# Brief schema sanity check (frontmatter + state machine, soft on body sections)
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"
# Research briefs (if any) — drift-warn only, none of these block the run
[ -d "{dir}/research" ] && \
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/research-validator.mjs --soft --dir "{dir}/research" --json
# Architecture note discovery (EXTERNAL CONTRACT — drift-WARN, never drift-FAIL)
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/architecture-discovery.mjs --json "{dir}"
```
Each call exits 0 on success or with a structured JSON error report on stderr.
Surface any warnings in the user-facing summary at Phase 3, but do not abort.
- Set **has_research_brief = true** if `{dir}/research/*.md` matches ≥ 1 file. - Set **has_research_brief = true** if `{dir}/research/*.md` matches ≥ 1 file.
- Set **has_architecture_note = true** if `{dir}/architecture/overview.md` exists. - Read the architecture-discovery JSON output: set **has_architecture_note = true**
If set, **architecture_note_path = {dir}/architecture/overview.md**. Produced by if `found == true`. The discovery module emits warnings if the file lives at a
the optional `/ultra-cc-architect-local` command from the separate `ultra-cc-architect` non-canonical path (e.g. `architecture-overview.md`); preserve them for the
plugin. Missing file is fine — this is additive discovery, not a requirement. user-facing summary. If set, **architecture_note_path = {result.overview}**.
Produced by the optional `/ultra-cc-architect-local` command from the separate
`ultra-cc-architect` plugin. Missing file is fine — additive discovery, not required.
4. **`--brief <path>`** — extract the brief path. If the file does not exist: 4. **`--brief <path>`** — extract the brief path. If the file does not exist:
``` ```

View file

@ -79,10 +79,30 @@ export function parseFrontmatter(yamlText) {
while (j < lines.length) { while (j < lines.length) {
const next = lines[j]; const next = lines[j];
if (next.trim() === '') { j++; continue; } if (next.trim() === '') { j++; continue; }
const m2 = next.match(/^\s+-\s+(.*)$/); const itemMatch = next.match(/^(\s+)-\s+(.*)$/);
if (!m2) break; if (!itemMatch) break;
list.push(parseScalar(m2[1])); const itemIndent = itemMatch[1].length;
j++; const firstContent = itemMatch[2];
const dictKeyMatch = firstContent.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
if (dictKeyMatch) {
const item = {};
item[dictKeyMatch[1]] = parseScalar(dictKeyMatch[2]);
let k = j + 1;
while (k < lines.length) {
const cont = lines[k];
if (cont.trim() === '') { k++; continue; }
const contMatch = cont.match(/^(\s+)([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
if (!contMatch) break;
if (contMatch[1].length <= itemIndent + 1) break;
item[contMatch[2]] = parseScalar(contMatch[3]);
k++;
}
list.push(item);
j = k;
} else {
list.push(parseScalar(firstContent));
j++;
}
} }
if (list.length > 0) { if (list.length > 0) {
out[key] = list; out[key] = list;

View file

@ -99,6 +99,34 @@ test('parseManifest — commit_message_pattern compiles via new RegExp', () => {
assert.ok(!re.test('chore: not it')); assert.ok(!re.test('chore: not it'));
}); });
test('parseManifest — must_contain list-of-dicts (real-world template form)', () => {
const body = `### Step 1: Real
- Manifest:
\`\`\`yaml
manifest:
expected_paths:
- a.json
- b.md
min_file_count: 2
commit_message_pattern: "^chore:"
bash_syntax_check: []
forbidden_paths:
- CHANGELOG.md
must_contain:
- path: a.json
pattern: '"version": "2\\.3\\.0"'
- path: b.md
pattern: "version-blue"
\`\`\`
`;
const r = parseManifest(body);
assert.equal(r.valid, true, JSON.stringify(r.errors));
assert.equal(r.parsed.must_contain.length, 2);
assert.equal(r.parsed.must_contain[0].path, 'a.json');
assert.equal(r.parsed.must_contain[1].path, 'b.md');
assert.equal(r.parsed.forbidden_paths[0], 'CHANGELOG.md');
});
test('validateAllManifests — aggregates per-step issues', () => { test('validateAllManifests — aggregates per-step issues', () => {
const steps = [ const steps = [
{ n: 1, body: STEP_BODY_GOOD }, { n: 1, body: STEP_BODY_GOOD },