From 1a65d8e4d50b092d9a080bb4db23b306d7fd1cf0 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 1 May 2026 05:45:26 +0200 Subject: [PATCH] =?UTF-8?q?feat(graceful-handoff):=202.0=20=E2=80=94=20mig?= =?UTF-8?q?rate=20to=20skills/=20with=20disable-model-invocation=20[skip-d?= =?UTF-8?q?ocs]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 2 + .../commands/graceful-handoff.md | 329 ------------------ .../skills/graceful-handoff/SKILL.md | 98 ++++++ .../tests/skill-structure.test.mjs | 61 ++++ plugins/ultraplan-local/CLAUDE.md | 19 + plugins/ultraplan-local/README.md | 25 ++ .../agents/planning-orchestrator.md | 39 ++- .../commands/ultrabrief-local.md | 14 + .../commands/ultraexecute-local.md | 17 + .../commands/ultraplan-local.md | 24 +- .../ultraplan-local/lib/util/frontmatter.mjs | 28 +- .../tests/lib/manifest-yaml.test.mjs | 28 ++ 12 files changed, 330 insertions(+), 354 deletions(-) delete mode 100644 plugins/graceful-handoff/commands/graceful-handoff.md create mode 100644 plugins/graceful-handoff/skills/graceful-handoff/SKILL.md create mode 100644 plugins/graceful-handoff/tests/skill-structure.test.mjs diff --git a/README.md b/README.md index 3b4713a..86d782c 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ v2.1 (non-breaking) replaced the hardcoded Q1–Q8 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. +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. Modes: default, brief-driven, project-scoped, research-enriched, foreground, quick, decompose, export diff --git a/plugins/graceful-handoff/commands/graceful-handoff.md b/plugins/graceful-handoff/commands/graceful-handoff.md deleted file mode 100644 index 6d77709..0000000 --- a/plugins/graceful-handoff/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/plugins/graceful-handoff/skills/graceful-handoff/SKILL.md b/plugins/graceful-handoff/skills/graceful-handoff/SKILL.md new file mode 100644 index 0000000..79c2865 --- /dev/null +++ b/plugins/graceful-handoff/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/plugins/graceful-handoff/tests/skill-structure.test.mjs b/plugins/graceful-handoff/tests/skill-structure.test.mjs new file mode 100644 index 0000000..1e16f0a --- /dev/null +++ b/plugins/graceful-handoff/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/); +}); diff --git a/plugins/ultraplan-local/CLAUDE.md b/plugins/ultraplan-local/CLAUDE.md index c6a0c7a..3235a32 100644 --- a/plugins/ultraplan-local/CLAUDE.md +++ b/plugins/ultraplan-local/CLAUDE.md @@ -90,6 +90,25 @@ If `{project_dir}/architecture/overview.md` exists (typically produced by the se | contrarian-researcher | sonnet | Counter-evidence, overlooked alternatives | | 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 `) +- `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 **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). diff --git a/plugins/ultraplan-local/README.md b/plugins/ultraplan-local/README.md index ff6b1c6..d7ac23a 100644 --- a/plugins/ultraplan-local/README.md +++ b/plugins/ultraplan-local/README.md @@ -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** | | 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/.mjs --json ` 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 **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. diff --git a/plugins/ultraplan-local/agents/planning-orchestrator.md b/plugins/ultraplan-local/agents/planning-orchestrator.md index 134c532..2e88029 100644 --- a/plugins/ultraplan-local/agents/planning-orchestrator.md +++ b/plugins/ultraplan-local/agents/planning-orchestrator.md @@ -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) 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 -# Count canonical step headings -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" +node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs --strict --json "$plan_path" ``` -**Pass criteria:** -- Step count ≥ 1 -- Manifest count == Step count -- Forbidden narrative count == 0 +**Pass criteria:** validator exits 0 with `valid: true` in its JSON output. +Internally the validator enforces (same checks as before, now in one place): +- Step count ≥ 1, numbering is 1..N contiguous +- 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 -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. +Each error has a `code` field — read these to localize the fix. Common codes: +- `PLAN_FORBIDDEN_HEADING` — narrative drift; rewrite the section using the + literal template from Phase 5 +- `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) diff --git a/plugins/ultraplan-local/commands/ultrabrief-local.md b/plugins/ultraplan-local/commands/ultrabrief-local.md index 89c29ac..e66e6e5 100644 --- a/plugins/ultraplan-local/commands/ultrabrief-local.md +++ b/plugins/ultraplan-local/commands/ultrabrief-local.md @@ -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 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: ``` Brief written: {PROJECT_DIR}/brief.md Review iterations: {1..3} Final quality: {complete | partial} +Validator: {PASS | warnings(N)} Research topics identified: {N} ``` diff --git a/plugins/ultraplan-local/commands/ultraexecute-local.md b/plugins/ultraplan-local/commands/ultraexecute-local.md index a0e6f30..b411101 100644 --- a/plugins/ultraplan-local/commands/ultraexecute-local.md +++ b/plugins/ultraplan-local/commands/ultraexecute-local.md @@ -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 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 Manifest block in strict mode, or synthesized manifests in legacy mode): diff --git a/plugins/ultraplan-local/commands/ultraplan-local.md b/plugins/ultraplan-local/commands/ultraplan-local.md index 6bc6e5e..6f11ce9 100644 --- a/plugins/ultraplan-local/commands/ultraplan-local.md +++ b/plugins/ultraplan-local/commands/ultraplan-local.md @@ -61,11 +61,27 @@ Parse `$ARGUMENTS` for mode flags. Order of precedence: Missing: {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_architecture_note = true** if `{dir}/architecture/overview.md` exists. - If set, **architecture_note_path = {dir}/architecture/overview.md**. Produced by - the optional `/ultra-cc-architect-local` command from the separate `ultra-cc-architect` - plugin. Missing file is fine — this is additive discovery, not a requirement. + - Read the architecture-discovery JSON output: set **has_architecture_note = true** + if `found == true`. The discovery module emits warnings if the file lives at a + non-canonical path (e.g. `architecture-overview.md`); preserve them for the + 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 `** — extract the brief path. If the file does not exist: ``` diff --git a/plugins/ultraplan-local/lib/util/frontmatter.mjs b/plugins/ultraplan-local/lib/util/frontmatter.mjs index f3b8e8f..e74ddf9 100644 --- a/plugins/ultraplan-local/lib/util/frontmatter.mjs +++ b/plugins/ultraplan-local/lib/util/frontmatter.mjs @@ -79,10 +79,30 @@ export function parseFrontmatter(yamlText) { while (j < lines.length) { const next = lines[j]; if (next.trim() === '') { j++; continue; } - const m2 = next.match(/^\s+-\s+(.*)$/); - if (!m2) break; - list.push(parseScalar(m2[1])); - j++; + const itemMatch = next.match(/^(\s+)-\s+(.*)$/); + if (!itemMatch) break; + const itemIndent = itemMatch[1].length; + 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) { out[key] = list; diff --git a/plugins/ultraplan-local/tests/lib/manifest-yaml.test.mjs b/plugins/ultraplan-local/tests/lib/manifest-yaml.test.mjs index 58fcaf0..bd6a68e 100644 --- a/plugins/ultraplan-local/tests/lib/manifest-yaml.test.mjs +++ b/plugins/ultraplan-local/tests/lib/manifest-yaml.test.mjs @@ -99,6 +99,34 @@ test('parseManifest — commit_message_pattern compiles via new RegExp', () => { 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', () => { const steps = [ { n: 1, body: STEP_BODY_GOOD },