Compare commits

..

1 commit

Author SHA1 Message Date
Kjell Tore Guttormsen
0b795b1240 docs(llm-security): expand sandboxed cloning with platform matrix and links
Add detailed platform matrix with links to sandbox-exec, bubblewrap,
Windows Sandbox, Docker Desktop, WSL2, and AppContainer documentation.
CVE reference for .gitattributes attack vector. Git config flag table
with per-flag mitigation descriptions. Windows guidance with concrete
options and recommendations. Note on why Node.js --permission is not
applicable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 17:12:10 +02:00
451 changed files with 68695 additions and 6116 deletions

View file

@ -12,93 +12,18 @@
"plugins": [
{
"name": "llm-security",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/llm-security.git",
"ref": "v7.8.0"
},
"source": "./plugins/llm-security",
"description": "Security scanning, auditing, and threat modeling for Claude Code projects. OWASP LLM Top 10 (2025) and Agentic AI Top 10."
},
{
"name": "config-audit",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/config-audit.git",
"ref": "v5.12.5"
},
"source": "./plugins/config-audit",
"description": "Multi-agent workflow for analyzing, reporting, and optimizing Claude Code configuration across your entire machine"
},
{
"name": "voyage",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/voyage.git",
"ref": "v5.9.1"
},
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline with specialized agent swarms, external research triangulation, adversarial review, post-hoc independent review with Handover 6 feedback loop, multi-session resumption, session decomposition, and headless execution. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, modelled on claude-code-100x): pencil-toggle annotation mode, select text or click any element, pick intent (Fiks/Endre/Spørsmål), comment, Copy Prompt, paste back, Claude revises the .md."
},
{
"name": "linkedin-studio",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/linkedin-studio.git",
"ref": "v0.5.3"
},
"description": "LinkedIn Studio — a full-spectrum LinkedIn content engine: feed posts, carousels, video scripts, and long-form newsletter editions, built on algorithmic understanding, strategic consistency, and authentic engagement. Aligned to LinkedIn's 2026 topic-relevance ranking model."
},
{
"name": "graceful-handoff",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/graceful-handoff.git",
"ref": "v3.1.0"
},
"description": "One-command session handoff into the STATE.md continuity system. At a natural stopping point, overwrites the nearest STATE.md with a complete state-of-play (mandatory '👉 NEXT — START HERE' block) and commits per remote policy. Skill-only, no hooks."
},
{
"name": "ai-psychosis",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/ai-psychosis.git",
"ref": "v1.2.1"
},
"description": "Meta-awareness tools for healthy AI interaction patterns. Detects reinforcement loops, scope escalation, narrative crystallization, and other compulsive patterns."
},
{
"name": "ms-ai-architect",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/ms-ai-architect.git",
"ref": "v1.17.0"
},
"description": "Microsoft AI Solution Architect — structured architecture guidance for the full Microsoft AI stack."
},
{
"name": "okr",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/okr.git",
"ref": "v1.6.1"
},
"description": "Expert OKR guidance for Norwegian public sector. Write, review, cascade, track and govern OKR based on Google/Doerr methodology adapted for 4-month tertial cycles."
},
{
"name": "human-friendly-style",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/human-friendly-style.git",
"ref": "v1.1.0"
},
"description": "Shared Claude Code output style for the ktg-plugin-marketplace. Plain-language tone — explains what and why, hides paths/JSON/stack traces by default, matches the user's language."
},
{
"name": "claude-design",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/claude-design.git",
"ref": "v0.1.0"
},
"description": "End-to-end facilitator for prompting Claude Design (claude.ai/design) — idea to copy-paste-ready prompt with iteration coaching, citing Anthropic primary sources."
"name": "ultraplan-local",
"source": "./plugins/ultraplan-local",
"description": "Deep implementation planning with interview, specialized agent swarms, external research, adversarial review, session decomposition, and headless execution support"
}
]
}

16
.gitignore vendored
View file

@ -1,16 +0,0 @@
# Session state files (local only, not tracked)
REMEMBER.md
TODO.md
ROADMAP.md
STATE.md
*.local.md
# Per-plugin session directories (plans, research, execution progress)
plugins/*/.claude/
# Session-generated reports (not release artifacts)
plugins/*/reports/*-beskrivelse.*
# OS files
.DS_Store
Thumbs.db

View file

@ -1,14 +0,0 @@
title = "ktg-plugin-marketplace gitleaks config"
# Extend default rules
[extend]
useDefault = true
# Path-based allowlist: vendored design-system MANIFEST.json files
# contain SHA-256 hashes per file by design (drift detection).
# These are public file integrity hashes, not secrets.
[[allowlists]]
description = "Vendored design-system MANIFEST files (SHA-256 file hashes)"
paths = [
'''playground/vendor/playground-design-system/MANIFEST\.json$''',
]

View file

@ -1,5 +1,2 @@
# False positive: intentionally fake credential in llm-security malicious-skill demo
plugins/llm-security/examples/malicious-skill-demo/evil-project-health/lib/telemetry.mjs:generic-api-key:18
# False positive: word "conversational" matches linkedin-client-id entropy pattern
plugins/linkedin-studio/hooks/prompts/content-quality-gate.md:linkedin-client-id:14

View file

@ -1,4 +0,0 @@
# Konsoliderer Git-identiteter for statistikk og shortlog.
# Se: https://git-scm.com/docs/gitmailmap
Kjell Tore Guttormsen <hello@fromaitochitta.com> <ktg@humanize.no>

View file

@ -1,39 +0,0 @@
# ktg-plugin-marketplace (catalog)
Catalog repository for the ktg-plugin-marketplace. After the polyrepo migration this repo hosts only
the marketplace manifest and the catalog-level docs; every plugin and the shared design-system live in
their own Forgejo repositories under `https://git.fromaitochitta.com/open/`.
## What lives here
- `.claude-plugin/marketplace.json` — the marketplace manifest (plugin entries point at external repos)
- `README.md` — the landing/catalog page
- `CONVENTIONS.md` — marketplace-wide conventions inherited by every plugin repo
- `GOVERNANCE.md` — governance + fork-and-own model
- `.mailmap`, `.gitleaks.toml`, `.gitleaksignore` — shared git-hygiene baselines
## Catalog maintenance
- Marketplace conventions: see CONVENTIONS.md.
- Adding/updating a plugin entry: edit `.claude-plugin/marketplace.json` (external `source: "url"` with
a pinned `ref`) and re-state the plugin in README.md with its verified version.
- Plugin source, issues, and releases live in each plugin's own repository — not here.
- **Releasing a plugin (canonical path — `scripts/release-plugin.mjs`):** since the polyrepo split,
a release is a TWO-repo act — tag the plugin repo AND bump the catalog `ref`. Forgetting the second
step strands users on the old version (the exact drift this helper exists to prevent). Run
`node scripts/release-plugin.mjs <plugin> [--version X.Y.Z]` — dry-run by default; it REFUSES unless
`plugin.json` == README badge == the target version AND the `vX.Y.Z` tag exists, then prints the
planned bump. Apply with `--write [--commit] [--push]`; `--create-tag` mints+pushes a missing plugin
tag first. On `--write` it bumps the catalog `ref` AND the catalog README's per-plugin `` `vX.Y.Z` ``
label together (and `git add`s both on `--commit`). Because it only moves both to a verified, tagged,
consistent version, `check-versions.mjs` is green by construction. Never hand-edit a `ref` or a
README label for a release — use this. Pure planner + label reconciler covered by
`scripts/release-plugin.test.mjs`.
- **Version-consistency gate:** run `node scripts/check-versions.mjs` before committing any `ref`
change. For each plugin it checks (against the sibling repo) that the catalog `ref` resolves to a
real git tag (ERROR if dangling — breaks install), that `plugin.json` version == README
version-badge (ERROR), that the catalog README's per-plugin `` `vX.Y.Z` `` label == the catalog
`ref` (ERROR — the human-facing doc must not misstate the installed version), and that the catalog
`ref` matches `plugin.json` version (WARN — catalog lags or an unreleased bump). Exit 1 on any
ERROR; `--strict` also fails on WARN. Pure-function core covered by
`scripts/check-versions.test.mjs` (`node --test scripts/check-versions.test.mjs`).

View file

@ -1,19 +0,0 @@
# Conventions — ktg-plugin-marketplace catalog
> Extracted from the marketplace CLAUDE.md (D6). These are the marketplace-wide conventions
> every plugin repo inherits under fork-and-own. See GOVERNANCE.md for the governance model.
## Konvensjoner
- **Språk:** Norsk dialog, engelsk kode/docs
- **Commits:** Conventional Commits — `type(scope): description`
- **Git:** Forgejo-org `git.fromaitochitta.com/open/` — hvert plugin/design-system er sitt eget repo (`open/<navn>`), katalogen er `open/ktg-plugin-marketplace`. Aldri GitHub.
- **Hooks:** Alltid Node.js (.mjs), aldri bash. Cross-platform.
- **Avhengigheter:** Null npm dependencies i hooks/scannere. `node:test` for tester.
- **Bidrag:** Issues velkommen som signaler. PRs ikke akseptert. Fork-and-own er anbefalt adopsjonsmodell — se `GOVERNANCE.md`.
- **Lisens:** MIT, alle plugins
- **Docs ved endring (OBLIGATORISK):** Enhver feature-endring som pusher til Forgejo MÅ oppdatere dokumentasjonen. Etter polyrepo-splittet er dette to nivåer i to repo:
1. **I plugin-repoet** (SAMME commit): `README.md` (detaljert dokumentasjon av endringen) + `CLAUDE.md` (arkitektur/oversikt)
2. **I katalog-repoet** (`open/ktg-plugin-marketplace`), ved versjonsbump eller endret entry: oppdater `README.md`-landingssiden og `ref` i `.claude-plugin/marketplace.json` (egen commit i katalog-repoet)
- **Playground / design-system:** Playground-HTML er vendored inn i hvert plugin-repo under `playground/vendor/playground-design-system/`. Endringer i det delte design-systemet gjøres i `open/playground-design-system`, tagges der, og re-vendres deretter inn i hver konsument (llm-security, ms-ai-architect, config-audit, okr, voyage).

View file

@ -1,131 +0,0 @@
# Governance
How this marketplace is maintained, what you can expect from upstream, and how it's meant to be used.
## TL;DR
- Solo-maintained, AI-assisted development, MIT licensed.
- **Fork-and-own is the default model.** Upstream is a starting point, not a vendor.
- Issues welcome as signals. Pull requests are not accepted — see [Why no PRs](#pull-requests--no).
- No SLA. Best-effort bug fixes and security advisories. Breaking changes happen and are noted in each plugin's CHANGELOG.
---
## Can I trust this?
Be honest with yourself about what you're adopting:
- **One maintainer.** If I get hit by a bus, the bus wins. The repos stay up under MIT, but no one owes you a fix.
- **AI-generated code with human review.** Every plugin is built through dialog-driven development with Claude Code. I read, test, and judge the output before it ships, but I'm not auditing every line the way a security firm would. Treat it accordingly.
- **No commercial interests.** I'm not selling a SaaS, not steering you toward a paid tier, not collecting telemetry. The plugins run locally in your Claude Code installation.
- **MIT licensed.** Fork it, modify it, ship it under your own name.
If you work somewhere that needs vendor accountability, support contracts, or signed assurances — **this isn't that.** Use it as a reference implementation, fork it into your own organization, and own the result.
---
## How this is meant to be used
### Fork-and-own
The intended workflow:
1. **Fork** the marketplace (or a single plugin) into your own organization or namespace.
2. **Tailor** it to your context — terminology, integrations, cycle lengths, regulatory framing, whatever doesn't fit out of the box.
3. **Maintain it yourself.** Treat your fork as the canonical version for your team.
4. **Watch upstream selectively.** Cherry-pick changes that help, ignore changes that don't. There's no obligation to stay in sync.
This isn't a workaround for not accepting PRs. It's the actual recommended adoption pattern, especially for plugins like `okr` and `ms-ai-architect` where every Norwegian public sector organization will need its own tildelingsbrev mappings, terminology, and integrations. A central "one true plugin" would be wrong for everyone.
### What to change first when you fork
Each plugin differs, but the common edits are:
- **Identity** — rename the plugin, replace authorship, update README.
- **External integrations** — issue trackers, knowledge bases, dashboards, observability backends. The plugins ship as starting points, not pre-wired. Every organization must configure its own integrations.
- **Norwegian-specific framing** — relevant for `okr` and `ms-ai-architect`. Other plugins are jurisdiction-neutral. Rewrite for your jurisdiction if you're outside Norway.
- **Reference docs** — the knowledge base in each plugin reflects my reading. Replace with your organization's authoritative sources.
- **Hooks and policies** — security thresholds, blocked commands, and audit gates are tuned to my taste. Tune them to yours.
### Staying current with upstream
If you want to pull in upstream changes later:
- **Cherry-pick, don't merge.** Each plugin moves independently and breaking changes land without ceremony.
- **Read the CHANGELOG first.** Every plugin has one.
- **Keep your customizations in clearly-named files.** The harder upstream is to merge cleanly, the more painful staying current becomes. A `local/` directory or `*.local.md` convention helps.
---
## What upstream provides
| | What I do | What I don't |
|---|---|---|
| **Bug fixes** | Best-effort when I notice or get a clear report | No SLA, no triage commitment |
| **Security issues** | Investigate within reasonable time, document in CHANGELOG | No CVE process, no embargo coordination |
| **New features** | When they fit my own usage | Not on request |
| **Norwegian public sector context** | Kept current as long as the project lives | If I lose interest or change jobs, the framing freezes |
| **Breaking changes** | Documented in CHANGELOG | They happen — version pin if you need stability |
| **Compatibility** | Tracked against current Claude Code releases | No long-term support branches |
If any of this is a dealbreaker — fork now, version-pin, and stop reading upstream.
---
## How to contribute
### Issues — yes, please
Issues are the most valuable thing you can send me:
- **Bug reports** with reproduction steps. Even a screenshot helps.
- **Use-case feedback.** "I tried to use this in my organization and X didn't fit" is genuinely useful, even if I can't fix it for you.
- **Pointers to better sources.** If you know a DFØ veileder, an NSM guideline, or an academic paper that contradicts what's in a knowledge base, tell me.
- **Security findings.** See each plugin's `SECURITY.md` for disclosure preference where one exists; otherwise email rather than open a public issue.
### Pull requests — no
This is deliberate, not laziness:
- **Solo review is a bottleneck.** Honest PR review takes me longer than rewriting from scratch. The math doesn't work.
- **Forks are where the value is.** The fork-and-own model means upstream consolidation isn't the point. Your organization's adaptations belong in your fork, not mine.
- **AI-generated code complicates provenance.** Every line here is produced through dialog with Claude Code, with me as the judge. Mixing in PRs from contributors with different processes and licensing assumptions creates a mess I'd rather not untangle.
If you've built something useful on top of a fork, **publish it under your own name and link back.** I'll happily list notable forks here once they exist.
### Notable forks
*(To be populated as forks emerge. If you've forked one of these plugins for production use, open an issue and I'll add a link.)*
---
## Relationship between plugins
These plugins are **independent**. Install one without the others, fork one without the others. They share conventions (slash command naming, hook patterns, AI-generated disclosure) but no runtime dependencies.
The marketplace is a **catalog**, not a suite. Don't fork the whole repo unless you actually want to maintain everything.
---
## Versioning and stability
- **Semantic versioning per plugin.** Each plugin has its own `CHANGELOG.md` and version number.
- **Breaking changes happen.** I bump the major version when they do, but I don't run an LTS branch.
- **Pin your version.** If stability matters more than features, install a specific version and stay there until you choose to upgrade.
---
## Public sector adoption notes
For Norwegian etater specifically:
- **DPIA-relevant data flows are documented in the relevant plugin README where applicable.** Read them before installation.
- **No data leaves your machine** beyond what Claude Code itself sends to Anthropic. The plugins themselves do not call external services unless you configure an integration.
- **Drøftingsplikt and ledelsesansvar** are not replaced by these tools. The `okr` plugin coaches; it does not decide. The `ms-ai-architect` plugin advises; it does not approve.
- **Choose your Claude deployment carefully.** claude.ai vs. API direct vs. Bedrock in EU region have different data residency profiles. The plugins don't choose for you.
---
## License
MIT for all plugins in this marketplace. See each plugin's `LICENSE` file.

256
README.md
View file

@ -2,9 +2,81 @@
Open-source Claude Code plugins for AI-assisted development, security, and planning.
Built for my own Claude Code workflow and shared openly for anyone who finds them useful. Solo-maintained, AI-assisted, fork-and-own. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for what upstream provides and how this is meant to be used.
Built for my own Claude Code workflow and shared openly for anyone who finds them useful. Solo project — bug reports and feature requests are welcome, pull requests are not accepted.
All code here is generated by Claude Code through a dialog-based process: I direct, review, test, and validate; Claude writes. Treat the plugins as AI-authored, human-curated.
---
## Plugins
### [LLM Security](plugins/llm-security/) `v5.0.0`
Security scanning, auditing, and threat modeling for agentic AI projects.
Built on OWASP LLM Top 10 (2025), OWASP Agentic AI Top 10, and the AI Agent Traps taxonomy (Google DeepMind, 2025). Three layers of protection:
- **Automated enforcement** — 8 hooks that block dangerous operations in real time (prompt injection, secrets in code, destructive commands, supply chain guardrails)
- **Deterministic scanning** — 15 Node.js scanners for byte-level analysis: Shannon entropy, Unicode codepoints, typosquatting detection, taint flow, DNS resolution, git forensics
- **Advisory analysis** — 18 commands that scan, audit, and model threats with structured reports, letter grades, and actionable remediation
Key commands: `/security posture`, `/security audit`, `/security scan`, `/security threat-model`, `/security plugin-audit`
6 specialized agents · 15 scanners · 8 hooks · 13 knowledge docs
→ [Full documentation](plugins/llm-security/README.md)
---
### [Config-Audit](plugins/config-audit/) `v3.0.1`
Configuration intelligence for Claude Code — health checks, feature discovery, and auto-fix.
Claude Code reads instructions from 7+ file types across multiple scopes. This plugin tells you what's wrong, what's missing, and what's silently conflicting:
- **Health** — 7 deterministic scanners verify correctness across every configuration file (broken imports, deprecated settings, conflicting rules, permission contradictions)
- **Opportunities** — context-aware recommendations for Claude Code features you're not using
- **Action** — auto-fix with mandatory backups, syntax validation, rollback support, and human-in-the-loop workflow
Key commands: `/config-audit posture`, `/config-audit discover`, `/config-audit feature-gap`, `/config-audit fix`
6 agents · 8 scanners · 15 commands · 482+ tests
→ [Full documentation](plugins/config-audit/README.md)
---
### [Ultraplan Local and Ultra Execute Local](plugins/ultraplan-local/) `v1.4.0`
Deep implementation planning with specialized agent swarms and adversarial review, then autonomous execution with failure recovery.
Two commands, one pipeline: plan first, then execute. The plan is the contract between the two.
- **`/ultraplan-local`** — Interview, 6-8 specialized agents explore the codebase in parallel, adversarial review by plan-critic and scope-guardian
- **`/ultraexecute-local`** — Step-by-step implementation with git checkpoints, automatic failure recovery, and parallel session decomposition
Modes: default (interview + background), spec-driven, foreground, quick, decompose, export
13 specialized agents · 2 commands · No cloud dependency
→ [Full documentation](plugins/ultraplan-local/README.md)
---
### [AI Psychosis](plugins/ai-psychosis/) `v1.0.0`
Meta-awareness tools that counteract sycophancy, reinforcement loops, and compulsive AI interaction patterns.
AI assistants are structurally optimized to be agreeable. This creates reinforcement loops where productive collaboration is often a mirror showing you what you want to see. Research documents psychotic episodes triggered by sustained AI interaction in individuals with no prior psychiatric history.
- **Layer 1 — Behavioral instructions** — SKILL.md rules that modify Claude's behavior: no unearned affirmations, mandatory risk identification, pattern naming
- **Layer 2 — Programmatic detection** — 4 hooks that measure session duration, dependency language, rapid-fire bursts, edit ratios, and late-night usage with progressive alerts
Research-informed thresholds. Alerts are progressive and never blocking. Privacy-first: prompt text is never logged.
1 skill · 1 command · 4 hooks
→ [Full documentation](plugins/ai-psychosis/README.md)
---
## Installation
@ -12,183 +84,13 @@ All code here is generated by Claude Code through a dialog-based process: I dire
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
```
Then open Claude Code and type `/plugin` to browse and install. Works with the Claude Code CLI, desktop app, and IDE extensions on macOS, Linux, and Windows. No external dependencies.
Then open Claude Code and type `/plugin` to browse and install plugins from the marketplace.
Each plugin keeps its own full README and CHANGELOG; this page is just the catalog.
## Compatibility
---
## Plugins
### [LLM Security](https://git.fromaitochitta.com/open/llm-security) `v7.8.0`
Security scanning, auditing, and threat modeling for agentic AI projects. Built on OWASP LLM Top 10 (2025), OWASP Agentic AI Top 10, and Google DeepMind's AI Agent Traps taxonomy.
- **Automated enforcement** — 9 hooks block prompt injection, secrets in code, destructive commands, and supply-chain risks in real time
- **Deterministic scanning** — 26 Node.js scanners for entropy, Unicode codepoints, typosquatting, taint flow, git forensics, AI-BOM, trigger/signature/AST-taint, and IDE-extension prescan (VS Code + JetBrains)
- **Advisory analysis** — 20 commands that scan, audit, and model threats with letter-graded reports and remediation
- **Enterprise governance** — EU AI Act / NIST AI RMF / ISO 42001 mapping, SARIF 2.1.0 output, policy-as-code, standalone CLI
Key commands: `/security posture`, `/security audit`, `/security scan`, `/security ide-scan`, `/security threat-model`
6 agents · 26 scanners · 9 hooks · 1863 tests · [Full documentation →](https://git.fromaitochitta.com/open/llm-security)
---
### [Config-Audit](https://git.fromaitochitta.com/open/config-audit) `v5.12.5`
Configuration intelligence for Claude Code. Claude reads instructions from 7+ file types across multiple scopes; this plugin tells you what's wrong, what's missing, what's silently conflicting, what's actually loaded, and where you're burning tokens.
- **Health** — 13 deterministic scanners catch broken imports, deprecated settings, conflicting rules, permission contradictions, plugin-hygiene collisions, and token waste
- **Opportunities** — context-aware recommendations for Claude Code features you're not using
- **Action** — auto-fix with mandatory backups, syntax validation, and rollback
- **Inventory + hotspots** — read-only view of active plugins, skills, MCP servers, hooks, and CLAUDE.md cascade, plus a ranked map of token waste
- **Plain-language UX** — output leads with prose and groups findings by impact and urgency (`--raw` and `--json` available)
Key commands: `/config-audit posture`, `/config-audit feature-gap`, `/config-audit fix`, `/config-audit whats-active`, `/config-audit tokens`
6 agents · 13 scanners · 18 commands · 954+ tests · [Full documentation →](https://git.fromaitochitta.com/open/config-audit)
---
### [Voyage](https://git.fromaitochitta.com/open/voyage) `v5.9.1`
A six-command planning pipeline with specialized agent swarms, adversarial review, and zero-friction multi-session resumption. Renamed from `ultraplan-local`/`/ultra*-local` to avoid collision with Anthropic's `/ultraplan` and `/ultrareview`. No cloud dependency.
- **`/trekbrief`** — capture intent through a quality-gated interview; emits a reviewable brief
- **`/trekresearch`** — deep multi-source research with triangulation and confidence ratings
- **`/trekplan`** — transform the brief into an executable, manifest-verified plan
- **`/trekexecute`** — disciplined execution with independent audit and bounded failure recovery
- **`/trekreview`** — independent post-hoc review; severity-tagged findings feed back into planning
- **`/trekcontinue`** — resume the next session from saved state in one command
Per-phase effort and model dialog; `/trekbrief`, `/trekplan`, and `/trekreview` render an operator-annotation HTML view you can mark up and copy back into Claude.
23 agents · 6 commands (+1 helper) · 5 hooks · 500+ tests · [Full documentation →](https://git.fromaitochitta.com/open/voyage) · [Migration guide](https://git.fromaitochitta.com/open/voyage/src/branch/main/MIGRATION.md)
---
### [AI Psychosis](https://git.fromaitochitta.com/open/ai-psychosis) `v1.2.1`
Meta-awareness tools that counteract sycophancy, reinforcement loops, and compulsive AI interaction patterns. AI assistants are structurally optimized to be agreeable; this surfaces when that becomes a problem.
- **Behavioral instructions** — rules that modify Claude's behavior: no unearned affirmations, mandatory risk identification, pattern naming
- **Programmatic detection** — 4 hooks measure session duration, dependency language, rapid-fire bursts, edit ratios, and late-night usage, with progressive alerts
- **Interaction reports**`/interaction-report` for aggregated session statistics (opt-in)
- **Contemplative references** — optional pointers when interaction flags are elevated (opt-in)
Research-informed thresholds. Alerts are progressive and never blocking. Privacy-first: prompt text is never logged.
1 skill · 1 command · 4 hooks · [Full documentation →](https://git.fromaitochitta.com/open/ai-psychosis)
---
### [Graceful Handoff](https://git.fromaitochitta.com/open/graceful-handoff) `v3.1.0`
Auto-trigger session handoff at context threshold, so summarizing state, committing work, and writing a continuation prompt don't get rushed when you run low. Manual `/graceful-handoff` always works as backup. Built for Opus 4.7.
- **Auto-trigger via Stop hook** — at ~70% context, writes the handoff artifact and commits (push stays manual; irreversible operations remain user-triggered)
- **Model-aware context detection** — a 4-step fallback chain so Opus 4.7's 1M window fires at the right moment, not 57× too early
- **statusLine hint + SessionStart auto-load** — display-only warnings at 60%/70%, then injects the handoff into the resumed session
- **Deterministic JSON pipeline** — runs without an LLM, stages only the artifact (never `git add -A`), under a 60s budget
Key command: `/graceful-handoff [topic-slug] [--no-commit] [--no-push] [--dry-run]`
3 hooks · 1 skill · 1 pipeline · 57 tests · [Full documentation →](https://git.fromaitochitta.com/open/graceful-handoff)
---
### [MS AI Architect](https://git.fromaitochitta.com/open/ms-ai-architect) `v1.17.0` `🇳🇴 Norwegian`
Microsoft AI solution architecture guidance for Norwegian public sector and enterprise, through Cosmo Skyberg — a structured architect persona who understands the problem before recommending technology.
- **Structured advisory** — 7-phase methodology from business need to architecture recommendation and optional diagram
- **Regulatory assessments** — ROS (NS 5814), DPIA/PVK, 6×5 security scoring, EU AI Act classification, cost in NOK (P10/P50/P90)
- **Norwegian public sector** — Digdir architecture principles, Utredningsinstruksen, NSM, Schrems II data residency, EU AI Act workflow
- **Grounded and current** — 387 reference documents, verified against live Microsoft Learn via MCP; `/architect:kb-update` refreshes the knowledge base
- **Playground** — single-file decision-builder and report viewer covering all 17 report types, light/dark, runs offline
Key commands: `/architect`, `/architect:ros`, `/architect:security`, `/architect:dpia`, `/architect:utredning`, `/architect:cost`
12 agents · 25 commands · 5 skills (387 docs) · 2 hooks · [Full documentation →](https://git.fromaitochitta.com/open/ms-ai-architect)
---
### [LinkedIn Studio](https://git.fromaitochitta.com/open/linkedin-studio) `v0.5.3`
Build authentic LinkedIn authority through algorithmic understanding, strategic consistency, and AI-assisted content creation.
- **Long-form newsletter pipeline** — a multi-phase orchestrator (research → skeleton → prose → de-AI/voice scrub → fact-check → editorial and persona gates → visual assets → lock → delivery) with maintained edition state
- **Adversarial review** — cold headless content, language, and fact reviewers re-check a frozen draft with no drafting-session context; `/linkedin:pivot` re-opens cleared gates on major rewrites
- **Content engine** — Content Matrix (40+ ideas from one topic), voice training with drift detection, full ideation → publish → 48-hour monitoring
- **Honest analytics** — CSV-import pipeline with weekly/monthly reports and anomaly alerts. Adds an
**optional, manually-entered `saves`** count (add a `Saves` column with the number read off native
LinkedIn analytics; never auto-tracked, `unknown` ≠ 0, excluded from the engagement rate). `dwell`
time stays **explicitly unmeasurable** — no fabricated metric or surface
- **Growth and monetization** — phase-specific guidance from 0 to 10K+ followers; topic-relevance profile optimization aligned to LinkedIn's 2026 ranking model
Key commands: `/linkedin:create` + `/linkedin:measure` (journey front-doors), `/linkedin:onboarding`, `/linkedin:post`, `/linkedin:newsletter`, `/linkedin:report`
19 agents · 29 commands (five journeys) · 6 skills · 9 hooks · [Full documentation →](https://git.fromaitochitta.com/open/linkedin-studio)
---
### [OKR for Public Sector](https://git.fromaitochitta.com/open/okr) `v1.6.1` `🇳🇴 Norwegian`
Turn strategy into measurable goals. An AI coach that learns your organization once, then builds on that knowledge so you spend time on strategy, not re-explaining context.
- **Strategy to OKR** — transform goals from virksomhetsplan or tildelingsbrev into well-structured OKR with quality checks and alignment scoring
- **Gap analysis**`/okr:gap` compares strategy documents against current OKR: what's covered, what's missing, what to do
- **Cross-cycle learning**`/okr:analyse` tracks score trends and recurring antipatterns across cycles with charts
- **19 antipattern detection** — catches sandbagging, activity-disguised-as-KR, set-and-forget, and 16 more
- **Built for norsk offentlig sektor** — 4-month tertials, DFO terminology, tillitsvalgt involvement, Riksrevisjon-ready documentation
Key commands: `/okr:skriv`, `/okr:kvalitet`, `/okr:gap`, `/okr:analyse`, `/okr:kaskade`, `/okr:governance`
7 agents · 10 commands · 4 hooks · [Full documentation →](https://git.fromaitochitta.com/open/okr)
---
### [Human-Friendly Style](https://git.fromaitochitta.com/open/human-friendly-style) `v1.1.0`
A shared Claude Code [output style](https://code.claude.com/docs/en/output-styles) used across this marketplace, so the conversation feels like dialog rather than a console dump.
- Explains what and why, not how — reserving technical detail for when you ask
- Hides noise by default — long paths, raw commands, JSON, and stack traces are summarized
- Always shows irreversible actions verbatim — deploys, deletes, force-push, migrations
- Matches your language (Norwegian or English) and is honest about uncertainty
Optional and works alongside every other plugin. Activate with `/config` → Output style → Human-Friendly.
1 output style · [Full documentation →](https://git.fromaitochitta.com/open/human-friendly-style)
---
### [Claude Design](https://git.fromaitochitta.com/open/claude-design) `v0.1.0`
End-to-end facilitator for prompting Claude Design (`claude.ai/design`). Walks you from raw idea through prompt drafting, delivery, and iteration coaching. The output is the prompt; the artifact gets built in Claude Design.
- **Eight-phase flow** — disambiguate → intent preset → audience and destination → DESIGN.md anchor → five-layer prompt → delivery → iteration coaching → ship-readiness
- **Evidence-graded references** — five foundation plus eight per-preset references, each carrying an Anthropic-domain citation
- **Complements Anthropic's official design plugin** — this covers idea → prompt → iterate; theirs covers critique → handoff, with zero command overlap (enforced by test)
1 skill · 13 reference files · 5 tests · [Full documentation →](https://git.fromaitochitta.com/open/claude-design)
---
## Shared infrastructure
### [Playground Design System](https://git.fromaitochitta.com/open/playground-design-system) `v0.6.0`
Shared design system for plugin Playgrounds — the visual self-service UIs that complement terminal slash-commands. Aksel/Digdir-aligned, WCAG 2.1 AA, light + dark themes, print-ready. Used by `ms-ai-architect`, `okr`, `llm-security`, `voyage`, and `config-audit`.
- **Tokens and components** — self-hosted fonts (OFL 1.1), a deuteranopia-safe severity ramp, and Tier 13 components: radar, matrix-heatmap, findings-browser, AI Act pyramid and timeline, maturity-ladder, and more
- **Privacy-first** — all fonts self-hosted, zero CDN requests, works offline and behind air-gapped firewalls
- **Vendoring sync**`scripts/sync-design-system.mjs <plugin>` keeps each plugin standalone; a SHA-256 manifest detects local drift
[Full documentation →](https://git.fromaitochitta.com/open/playground-design-system) · [Browse showcase](https://git.fromaitochitta.com/open/playground-design-system/src/branch/main/playground-examples/index.html)
---
- Claude Code CLI, desktop app, and IDE extensions
- macOS, Linux, Windows
- No external dependencies (all scanners and hooks are self-contained)
## License

View file

@ -1,302 +0,0 @@
# Marketplace Polyrepo Migration · Brief
> **Voyage `/trekbrief`-style task brief.** Foundation document for splitting the
> `ktg-plugin-marketplace` monorepo into **one git repo per plugin + a thin catalog repo**.
> This brief defines *what the migration is and how we verify it* — it does **not** execute it.
> Decisions are surfaced as ratify-by-annotation `DECISION` blocks.
>
> - **Written:** 2026-06-17
> - **Status:** RATIFIED 2026-06-17 — all `DECISION` blocks D1D8 ratified as recommended (see each block). Next: run `/trekplan` in a fresh session at the repo root.
> - **Workstream:** Infrastructure (marketplace-wide, root-scoped). This is the first root-level workstream; all prior briefs were plugin-scoped.
> - **Grounded by:** (1) `.claude-plugin/marketplace.json` read; (2) official Claude Code marketplace docs verified via `claude-code-guide` agent ([plugin-marketplaces](https://code.claude.com/docs/en/plugin-marketplaces.md), [plugins-reference](https://code.claude.com/docs/en/plugins-reference.md)); (3) a read-only coupling inventory (Explore) of root infra + cross-references + per-plugin standalone-readiness, all on 2026-06-17. Counts/paths below are tool-verified, not recalled.
---
## 1. Context & motivation
The marketplace is a **monorepo**: 10 plugins under `plugins/`, a shared design system under `shared/`,
and a catalog (`marketplace.json` + landing `README.md`) at the root. Two findings make the monorepo the
wrong fit for how this repo is actually developed:
1. **A Claude Code marketplace does NOT require a monorepo.** Verified against official docs: a plugin entry's
`source` can be an external git repo (`github` / `url` / `git-subdir` / `npm`), a single `marketplace.json`
can **mix** in-repo and external sources, the "thin catalog that points to many plugin repos" is an
**officially supported pattern**, and each entry can **pin** to a `ref` (tag/branch) or `sha`. The monorepo
is a *packaging convenience, not an architectural necessity* (Explore verdict).
2. **The operator works on multiple plugins concurrently as the permanent norm, and plugin count is growing.**
10 plugins today (up from the ~8 the root CLAUDE.md still documents — `human-friendly-style` and
`claude-design` are new). A single working tree shares **one `.git/index` and one `HEAD`**, so concurrent
sessions race on global mutable git-state. Observed symptoms: cross-plugin commit bleed
(`f460814` "WIP across plugins", `69610d4` "roll up in-progress changes across plugins"); root-`README.md`
merge pressure (the three-doc gate forces *every* feature to touch the root README); noisy `git status`.
**The cost grows with each new plugin.**
The plugins are already **independent units in a monorepo costume**: each versions independently
(v1.15.0 / v4.1.0 / v7.7.2 …), each is self-contained (own `plugin.json` / `README` / `CLAUDE.md`), the design
system is **vendored, not live-linked**, and there are **no cross-plugin runtime imports**. Splitting aligns the
structure with that reality and removes the concurrency pain structurally rather than mitigating it.
---
## 2. Problem statement (grounded)
**Inventory (tool-verified 2026-06-17):**
| Surface | Finding |
|---|---|
| Plugins | **10**, `marketplace.json``plugins/` dirs are a **perfect match** (no drift) |
| Forgejo remote | `ssh://git@git.fromaitochitta.com/open/ktg-plugin-marketplace.git` (org `open/`) |
| Root catalog | `.claude-plugin/marketplace.json` (all 10 `source: "./plugins/<name>"`) |
| Root shared infra | `README.md` (landing, hand-maintained version badges), `GOVERNANCE.md`, `CLAUDE.md` (marketplace-wide conventions + a stale plugin overview), `.gitignore`, `.gitleaks.toml`, `.gitleaksignore`, `.mailmap`, `STATE.md` (gitignored) |
| Shared code | `shared/playground-design-system/` + `shared/playground-examples/` + `shared/PLAYGROUND-MAINTENANCE.md`; `scripts/sync-design-system.mjs` (the **only** root script) |
| Local state | 13 gitignored `STATE.md` / `*.local.md` files across plugins |
| Root CI/cron | **NONE** (no `.github/`, no `.forgejo/` at root; `voyage` has its own `.forgejo/`) |
**The coupling is minimal and resolvable — there are no live cross-plugin imports.** The only break-points a
split must address:
| # | Break-point | Detail |
|---|---|---|
| C1 | DS sync reaches the monorepo root | `scripts/sync-design-system.mjs:26-27` resolves `MARKETPLACE_ROOT/shared/playground-design-system` via `path.resolve(__dirname, '..')`; vendors into `plugins/<name>/playground/vendor/`. Runtime is fully decoupled — plugins load CSS from their own `vendor/`, **never** from `shared/`. Sync is a dev-time tool only. |
| C2 | `marketplace.json` source paths | All 10 are `./plugins/<name>`; must become external repo URLs. |
| C3 | `.gitignore` / `.gitleaks.*` per-plugin rewrite | Root `.gitignore` uses `plugins/*/` prefixes (e.g. `plugins/*/.claude/`, `plugins/*/reports/*-beskrivelse.*`) that must be re-rooted per plugin; `.gitleaks.toml`/`.gitleaksignore` carry per-plugin false-positive paths. |
| C4 | Root README version badges | Hand-maintained (`llm-security v7.7.2`, etc.); become cross-repo references in the thin catalog. |
| C5 | `shared/` relocation | DS + examples need a home that isn't a plugin and isn't the catalog's `plugins/`. |
| C6 | `linkedin-studio` storage resolver | `scripts/analytics/src/utils/storage.ts:18-33,48` — marker-based `findPluginRoot` (walks up to `.claude-plugin/plugin.json`) with a hardcoded `../../../../` **fallback**. Primary path is resilient across the repo boundary; the fallback must be re-checked at the new repo root. |
| C7 | `voyage` test helper | `tests/helpers/hook-helper.mjs:4` is a **verbatim copy** of llm-security's, with a provenance comment — not a live import; stays as a copy. |
---
## 3. Goal (definition of done)
**Each of the 10 plugins lives in its own Forgejo repo; the design system lives in its own versioned repo;
and `ktg-plugin-marketplace` becomes a thin catalog repo at the same URL users already added — installable and
live at every step of the migration, with no plugin functionality or version changed.**
After the migration:
- Each plugin is its own repo with its own history (per D2), `.gitignore`, `.gitleaks.*`, and continuity
(`STATE.md` + session hooks).
- The catalog repo has **no `plugins/` subdir** and **no `shared/`** — only `marketplace.json` (external
sources), the landing `README.md`, `GOVERNANCE.md`, and marketplace-wide conventions.
- `/plugin marketplace add <same URL>` + `/plugin install <plugin>@ktg-plugin-marketplace` works for all 10.
- Concurrent multi-plugin work is **structurally isolated** — separate repos, separate `index`/`HEAD`, zero
shared-state contention.
- The design system has a single source repo; consumers vendor from it via the (re-homed) sync script.
---
## 4. Success criteria (testable)
> Each is a concrete check; the planning phase turns these into commands.
- **SC1 — Install works post-migration.** `/plugin marketplace add ssh://…/open/ktg-plugin-marketplace.git`
then `/plugin install <plugin>@ktg-plugin-marketplace` succeeds for all 10. Verify: fresh-session install
smoke test per plugin.
- **SC2 — Each plugin repo is standalone.** A clean clone of each new repo (no marketplace parent) passes that
plugin's own validation/test suite. Verify: clone to `/tmp`, run its `tests/`/`validate`/`test-runner`.
- **SC3 — Catalog resolves externally at every step.** No `./plugins/…` source remains at completion; every
*intermediate* mixed-source state also resolves. Verify: `marketplace.json` schema check + install smoke
after each plugin graduates.
- **SC4 — Catalog is thin.** After completion the catalog repo contains **no** `plugins/` and **no** `shared/`.
Verify: `ls`.
- **SC5 — Design-system parity.** A DS change in the DS repo + re-sync produces byte-identical vendored output
in a consumer (sync script rewired to the DS-repo source; `MANIFEST.json` SHA-256 matches). Verify: run sync,
diff manifest.
- **SC6 — History per D2.** Each new repo's `git log` reflects the ratified history decision (filter-repo
preserved, or hard-cut with the pre-migration monorepo archived). Verify: `git log` per repo.
- **SC7 — Continuity intact.** Each plugin repo's `.gitignore` (prefix re-rooted) ignores its local state; a
session started in the repo injects *its* `STATE.md`; hooks run via `${CLAUDE_PLUGIN_ROOT}`. Verify: session
start in each repo.
- **SC8 — Marketplace never goes dark.** Install works at **every** intermediate commit (mixed sources). Verify:
install smoke after each graduation.
---
## 5. Non-goals (scope fence)
- **NOT changing plugin code, functionality, or versions.** Pure relocation + rewiring.
- **NOT changing the marketplace URL.** `open/ktg-plugin-marketplace` stays — existing user installs keep their
update path.
- **NOT GitHub.** Forgejo only (`git.fromaitochitta.com`); never `gh` CLI.
- **NOT touching content repos** (`~/repos/from-ai-to-chitta/…`).
- **NOT building new CI.** None exists at root; per-plugin CI (`voyage/.forgejo/`) moves with its plugin
unchanged.
- **NOT merging, splitting, or renaming plugin functionality.**
- **NOT a design-system redesign**`shared/` is relocated, not rewritten.
---
## 6. Target architecture
```
open/ktg-plugin-marketplace ← THIN CATALOG (same URL)
.claude-plugin/marketplace.json → external source + ref-pin per plugin
README.md GOVERNANCE.md CONVENTIONS .mailmap .gitleaks (baseline)
open/playground-design-system ← DS repo (versioned)
playground-design-system/ playground-examples/ PLAYGROUND-MAINTENANCE.md
scripts/sync-design-system.mjs (re-homed; --source points here)
open/<plugin> × 10 ← one repo each
.claude-plugin/plugin.json README CLAUDE CHANGELOG
.gitignore (re-rooted) .gitleaks.* (per-plugin)
hooks/ tests/ scripts/ playground/ vendor/playground-design-system/ …
```
`marketplace.json` entry shape (per D4 pinning):
```json
{ "name": "linkedin-studio",
"source": { "source": "url", "url": "ssh://git@git.fromaitochitta.com/open/linkedin-studio.git", "ref": "v0.4.0" },
"description": "…" }
```
---
## 7. Decisions (ratify by annotation)
> Each block: **recommendation** + rationale + alternatives. **All eight ratified as recommended on 2026-06-17** — D1, D2, D4, D8 via explicit operator selection; D3, D5, D6, D7 ratified-as-recommended.
### D1 — Forgejo org + repo naming
**✓ Ratified 2026-06-17 — bare plugin slug under `open/` (`open/<plugin>`).**
**Recommendation:** Org `open/` (matches the catalog); repo name = the **bare plugin slug** (`open/linkedin-studio`,
`open/ms-ai-architect`, …). *Rationale:* the marketplace already namespaces; a `ktg-`/`plugin-` prefix is
redundant. *Alternatives:* (a) prefix `open/ktg-<slug>`; (b) a dedicated org (e.g. `plugins/`).
### D2 — History preservation
**✓ Ratified 2026-06-17 — preserve per-plugin history via `git filter-repo` on a fresh clone; tag the pre-migration monorepo `pre-polyrepo-archive` regardless.**
**Recommendation:** Preserve per-plugin history with `git filter-repo --path plugins/<name>/ --path-rename plugins/<name>/:`
run on a **fresh clone** (never the working repo), keeping blame intact. Regardless of choice, tag the
pre-migration monorepo (`pre-polyrepo-archive`) and keep it as a read-only archive. *Alternatives:* (a) **hard-cut**
`git init` a new repo from the current tree, lose granular history but keep it in the archived monorepo (fast);
(b) hybrid — filter-repo the high-value plugins, hard-cut the rest. *This decision is the single biggest effort lever.*
### D3 — Design-system distribution
**✓ Ratified 2026-06-17 (as recommended) — DS becomes `open/playground-design-system`; consumers keep vendoring; `sync-design-system.mjs` re-homed with a `--source`/env pointer.**
**Recommendation:** DS becomes its own repo `open/playground-design-system`; `sync-design-system.mjs` moves there
(or to the catalog) and takes a `--source`/env pointing at the DS checkout; **consumers keep vendoring** (runtime
model unchanged — plugins load from `vendor/`). Pin the DS version per consumer in the vendored `MANIFEST.json`.
*Rationale:* single source of truth, minimal change to a proven sync. *Alternatives:* (a) `git-subdir` source in
each plugin (rejected — DS isn't a plugin); (b) npm package (adds a publish step); (c) git submodule per plugin
(rejected — submodule friction; vendoring already works). **Note for planning:** only ~5 plugins consume DS and
only **2** (`ms-ai-architect`, `llm-security`) currently have a `vendor/` dir — enumerate true consumers first.
### D4 — Marketplace pinning strategy
**✓ Ratified 2026-06-17 — pin each plugin to a release tag (`ref: "vX.Y.Z"`).**
**Recommendation:** Pin each plugin to a release **tag** (`ref: "vX.Y.Z"`) — reproducible installs, explicit
upgrades (bump the tag in `marketplace.json` to release). *Alternative:* floating (track default branch — simpler,
but installs move under users without a catalog change).
### D5 — Migration order + live strategy
**✓ Ratified 2026-06-17 (as recommended) — DS repo first → high-churn plugins → low-churn → thin the catalog last; mixed sources keep the marketplace live throughout.**
**Recommendation:** (0) Stand up the **DS repo first** (it's the spine 5 plugins vendor from). (1) Graduate
**high-churn** plugins first — `linkedin-studio`, `voyage`, `ms-ai-architect`, `llm-security` — each as one
reversible unit: create repo → push → flip its `marketplace.json` entry to external. (2) Mixed sources keep the
marketplace live throughout. (3) Low-churn plugins next (`okr`, `config-audit`, `graceful-handoff`,
`ai-psychosis`, `human-friendly-style`, `claude-design`). (4) **Thin the catalog last** — remove `plugins/` and
`shared/`, rewrite the landing README. *Alternative:* big-bang all-at-once (rejected — no reversible checkpoints,
marketplace dark during the cutover).
### D6 — What stays in the catalog repo
**✓ Ratified 2026-06-17 (as recommended) — catalog keeps `marketplace.json`, landing README, `GOVERNANCE.md`, `.mailmap`, a `.gitleaks` baseline, and the extracted marketplace-wide conventions; `shared/` + `sync-design-system.mjs` leave to the DS repo.**
**Recommendation:** `marketplace.json`, landing `README.md`, `GOVERNANCE.md`, `.mailmap`, a shared `.gitleaks`
baseline, and the **marketplace-wide conventions** extracted from the root `CLAUDE.md` (move them to `GOVERNANCE.md`
or a new `CONVENTIONS.md`). `shared/` + `scripts/sync-design-system.mjs` leave to the DS repo. The catalog
`CLAUDE.md` becomes thin (catalog-maintenance only). *Alternative:* keep `shared/` in the catalog and skip a
separate DS repo (rejected — re-creates a mini-monorepo and the cross-cutting README pressure).
### D7 — Gitignored state + continuity
**✓ Ratified 2026-06-17 (as recommended) — per-repo `.gitignore` with `plugins/*/` prefixes stripped; the 13 local state files stay uncommitted; `STATE.md` regenerates per repo; zero hook changes.**
**Recommendation:** Each new repo gets its own `.gitignore` with the `plugins/*/` prefixes stripped
(`.claude/`, `reports/*-beskrivelse.*`, `STATE.md`, `*.local.md`, OS files). The 13 local state files stay
**local and uncommitted**; `STATE.md` regenerates per repo (it is untracked, so `filter-repo`/clone won't carry
it — that is fine). The global `~/.claude` session-start hook already injects the nearest `STATE.md` from cwd, so
continuity works in any repo with zero hook changes. *Alternative:* seed an empty `STATE.md` template per repo.
### D8 — Push timing + irreversibility gate
**✓ Ratified 2026-06-17 — all extraction + validation stays local with NO push; creating Forgejo repos + the first push + the live `marketplace.json` rewrite are batched into one explicitly-authorized window (the weekend).**
**Recommendation:** All extraction + local validation happens with **NO push** (honoring "ikke push før helgen").
The outward, irreversible steps — **creating Forgejo repos, the first push, and rewriting the live
`marketplace.json`** — are **batched into one explicitly-authorized window** (the weekend). Forgejo repo creation
is operator-run (API/UI). *Rationale:* the marketplace is live; half-pushed external sources would break installs.
*Alternative:* graduate-and-push incrementally during the week (rejected — violates push policy + risks a
half-migrated public marketplace).
---
## 8. Risks
- **R1 — Marketplace goes dark mid-migration.** *Mitigation:* mixed sources (D5) + install smoke after each step
(SC8); never remove a `./plugins/x` source before its external repo is pushed and verified.
- **R2 — History loss.** *Mitigation:* `filter-repo` on a **clone** (D2); archive the pre-migration monorepo as a
tag/read-only repo regardless of D2.
- **R3 — DS drift across consumers.** *Mitigation:* single DS repo (D3); `MANIFEST.json` SHA verification (SC5).
- **R4 — Concurrent-session corruption during destructive git surgery.** The thinning + any history rewrite mutate
the working tree/`.git`; a second session committing there races on the shared index. *Mitigation:* run the
migration **from a dedicated fresh clone**, and quiesce other writers in the original checkout during the
destructive phase (see §10 + the session-hygiene answer).
- **R5 — `linkedin-studio` resolver fallback misfires.** The `../../../../` fallback in `storage.ts:48` assumes
the monorepo depth. *Mitigation:* verify the marker-based primary path finds `.claude-plugin/plugin.json` at the
new repo root (it will — the manifest sits at the standalone repo root); add a regression test.
- **R6 — Forgejo repo-creation irreversibility.** *Mitigation:* operator-gated, batched, one authorized window (D8).
- **R7 — `.gitignore`/`.gitleaks` path-rewrite errors.** *Mitigation:* generate from a template; verify per repo
(a committed local-state file is a failure).
- **R8 — Root README version badges go stale.** *Mitigation:* the thin catalog README references plugin repos;
a version-fetch helper is noted but out of scope.
---
## 9. Open questions for the planning phase
- Exact `filter-repo` invocation; whether to also preserve `shared/` history into the DS repo.
- True DS consumers — only 2 have `vendor/` dirs today vs 5 named; enumerate before standing up the DS repo, and
decide whether `claude-design` / `human-friendly-style` (no playground) need DS at all.
- Forgejo repo **visibility** per repo (the marketplace is under `open/` — confirm public vs private).
- Does `shared/playground-examples/` belong in the DS repo or stay referenced from the catalog?
- Per-repo `CHANGELOG` strategy (carry the plugin's existing history vs start fresh).
---
## 10. Execution model (for the planning phase)
- **Run from a dedicated fresh clone** of `ktg-plugin-marketplace`, not the operator's working checkout — so other
sessions' working trees are untouched and the destructive steps (filter-repo, thinning) can't race a concurrent
committer (R4).
- **Operator-run steps:** Forgejo repo creation (API/UI), the authorized push window (D8).
- **Claude-run steps:** filter-repo/extraction scripting, `.gitignore`/`.gitleaks` re-rooting, `marketplace.json`
rewrites, the DS-repo standup, validation per SC.
- **Reversible until push:** everything before D8's window is local and discardable; the migration is a sequence of
per-plugin units, each independently verifiable (SC2) and revertible.
---
## 11. Verification plan
1. **Per-plugin standalone:** clone each new repo to `/tmp`, run its own validate/test suite → **SC2**.
2. **Install smoke:** `/plugin marketplace add <URL>` + install each plugin in a fresh session, at the final state
**and** after each graduation → **SC1, SC3, SC8**.
3. **Catalog thinness:** `ls` the catalog — no `plugins/`, no `shared/`**SC4**.
4. **DS parity:** change a DS token in the DS repo, re-sync a consumer, diff `MANIFEST.json` SHA → **SC5**.
5. **History:** `git log` per new repo matches D2 → **SC6**.
6. **Continuity:** start a session in each repo; confirm its `STATE.md` injects and hooks run; `git status` shows
no committed local-state files → **SC7**.
---
## 12. References (tool-verified anchors, 2026-06-17)
- `.claude-plugin/marketplace.json` — 10 plugins, all `source: "./plugins/<name>"`.
- `scripts/sync-design-system.mjs:26-27``MARKETPLACE_ROOT` relative resolution + vendoring target.
- `plugins/linkedin-studio/scripts/analytics/src/utils/storage.ts:18-33,48` — marker-based `findPluginRoot` +
`../../../../` fallback.
- `plugins/voyage/tests/helpers/hook-helper.mjs:4` — verbatim-copy provenance comment.
- Root `.gitignore``plugins/*/` prefixed patterns to re-root.
- Forgejo remote: `ssh://git@git.fromaitochitta.com/open/ktg-plugin-marketplace.git`.
- Docs: [plugin-marketplaces](https://code.claude.com/docs/en/plugin-marketplaces.md),
[plugins-reference](https://code.claude.com/docs/en/plugins-reference.md) — external/mixed sources, thin catalog,
`ref`/`sha` pinning all officially supported.
---
_Counts and paths verified by direct codebase mapping + official-doc check on 2026-06-17, not recalled.
The root CLAUDE.md's plugin overview (~8 plugins) is stale — there are 10._

View file

@ -1,149 +0,0 @@
#!/usr/bin/env bash
# Step 1 — Preflight, archive tag, and the plugin map.
# Asserts tooling + repo hygiene, builds the dedicated extraction mirror ($WORK/_mirror)
# via `git clone --no-local` (R4/M6 — extraction never reads the working checkout),
# enumerates >1MB blob-strip candidates in ms-ai-architect and emits blob_strip_safe
# into plugin-map.json (M4/F3), and creates the local archive tag (D2).
# NULL push (D8): never pushes, never mutates plugins/ shared/ marketplace.json.
#
# Usage: 00-preflight.sh [--dry-run]
# --dry-run : validate tooling + map + print the 11-target table; NO mirror, NO tag, NO map mutation.
#
# Bash 3.2 compatible (no associative arrays / mapfile). Override WORK= to relocate the workspace.
set -euo pipefail
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
*) echo "preflight: unknown arg: $arg" >&2; exit 2 ;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)"
WORK="${WORK:-/tmp/polyrepo-migration}"
MIRROR="$WORK/_mirror"
ARCHIVE_TAG="pre-polyrepo-archive"
BASELINE="f2d41c8"
fail() { printf 'PREFLIGHT FAIL: %s\n' "$*" >&2; exit 1; }
# --- tooling ---
git filter-repo --version >/dev/null 2>&1 || fail "git filter-repo not available — brew install git-filter-repo"
command -v python3 >/dev/null 2>&1 || fail "python3 not found"
python3 -c 'import sys; sys.exit(0 if sys.version_info[:2] >= (3, 6) else 1)' \
|| fail "python3 >= 3.6 required"
# --- map presence + parse + count ---
[ -f "$MAP" ] || fail "plugin-map.json missing at $MAP"
python3 -c "import json; json.load(open('$MAP'))" >/dev/null 2>&1 || fail "plugin-map.json does not parse"
TARGET_COUNT="$(python3 -c "import json; print(len(json.load(open('$MAP'))['targets']))")"
[ "$TARGET_COUNT" = "11" ] || fail "expected 11 targets in plugin-map.json, found $TARGET_COUNT"
# --- path hygiene (aeb6292): every target path must be whitespace- and glob-free, so the space-joined
# `for p in $(mappaths …)` word-splitting in 99-dryrun.sh (live_files / SC6 baseline) is sound. ---
python3 - "$MAP" <<'PY' || fail "plugin-map.json has a path with whitespace or a glob metacharacter (breaks word-split path handling in 99-dryrun.sh)"
import json, re, sys
m = json.load(open(sys.argv[1]))
bad = []
for k, t in m["targets"].items():
for p in t.get("paths", []):
if re.search(r"\s", p) or any(c in p for c in "*?[]"):
bad.append("%s: %r" % (k, p))
if bad:
sys.stderr.write("offending paths:\n " + "\n ".join(bad) + "\n")
sys.exit(1)
PY
# --- repo baseline (HEAD descends from the ratified brief commit, on main, not behind remote) ---
BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
[ "$BRANCH" = "main" ] || fail "not on main (HEAD on '$BRANCH')"
git -C "$REPO_ROOT" merge-base --is-ancestor "$BASELINE" HEAD \
|| fail "HEAD does not descend from baseline $BASELINE"
if git -C "$REPO_ROOT" rev-parse --verify -q origin/main >/dev/null; then
BEHIND="$(git -C "$REPO_ROOT" rev-list --count HEAD..origin/main)"
[ "$BEHIND" = "0" ] || fail "behind origin/main by $BEHIND commit(s) — pull/rebase before migrating"
AHEAD="$(git -C "$REPO_ROOT" rev-list --count origin/main..HEAD)"
echo " baseline: descends $BASELINE on main; $AHEAD local commit(s) ahead of origin/main (unpushed — expected under D8)"
else
echo " baseline: descends $BASELINE on main (no origin/main ref present)"
fi
# --- hygiene (M12): tolerate untracked stray docs; abort only on a TRACKED uncommitted
# change to a migration-sensitive surface. The literal `git status --porcelain plugins ...`
# would also list untracked (??) files, which M12 explicitly tolerates, so we filter them. ---
DIRTY="$(git -C "$REPO_ROOT" status --porcelain plugins shared .claude-plugin/marketplace.json scripts \
| grep -v '^??' || true)"
[ -z "$DIRTY" ] || fail "uncommitted change to a migration-sensitive surface:
$DIRTY"
# --- print the 11-target table ---
echo "PREFLIGHT OK"
python3 - "$MAP" <<'PY'
import json, sys
m = json.load(open(sys.argv[1]))
t = m["targets"]
print(" %-26s %-9s %-5s %-7s %-5s %s" % ("target", "tag", "paths", "vendor", "blob", "repo_url"))
for k in sorted(t):
e = t[k]
print(" %-26s %-9s %-5d %-7s %-5s %s" % (
k, e["tag"], len(e["paths"]), str(e["has_vendor_ds"]), str(e.get("blob_strip", False)), e["repo_url"]))
print(" drop:", ", ".join(m.get("drop", [])) or "(none)")
print(" targets:", len(t))
PY
if [ "$DRY_RUN" = "1" ]; then
echo " (--dry-run: no mirror clone, no archive tag, no map mutation)"
exit 0
fi
# --- full run: build the dedicated extraction mirror once (R4/M6) ---
mkdir -p "$WORK"
if [ -e "$MIRROR/HEAD" ] || [ -d "$MIRROR/.git" ]; then
echo " mirror: present at $MIRROR (reused)"
else
rm -rf "$MIRROR"
git clone --no-local --quiet "$REPO_ROOT" "$MIRROR"
echo " mirror: built $MIRROR via clone --no-local (pinned at $(git -C "$MIRROR" rev-parse --short HEAD))"
fi
# --- enumerate >1MB blobs in ms-ai-architect history; safe iff all are screenshots (M4/F3) ---
BLOBLIST="$(git -C "$MIRROR" rev-list --objects --all \
| git -C "$MIRROR" cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '$1=="blob" && $3+0>1048576 { p=""; for (i=4;i<=NF;i++) p = p (i>4?" ":"") $i; print $3"\t"p }' \
| grep -E 'plugins/ms-ai-architect/' || true)"
SAFE=true
if [ -n "$BLOBLIST" ]; then
NONSCREEN="$(printf '%s\n' "$BLOBLIST" | grep -vE 'plugins/ms-ai-architect/playground/screenshots/' || true)"
[ -z "$NONSCREEN" ] || SAFE=false
fi
echo " blob-strip candidates (>1MB) in ms-ai-architect history:"
if [ -n "$BLOBLIST" ]; then printf '%s\n' "$BLOBLIST" | sed 's/^/ /'; else echo " (none)"; fi
echo " blob_strip_safe = $SAFE"
# --- idempotent emit into plugin-map.json (only rewrites if the value actually changed) ---
python3 - "$MAP" "$SAFE" <<'PY'
import json, sys
path, safe = sys.argv[1], (sys.argv[2] == "true")
m = json.load(open(path))
cur = m["targets"].get("ms-ai-architect", {}).get("blob_strip_safe")
if cur != safe:
m["targets"]["ms-ai-architect"]["blob_strip_safe"] = safe
with open(path, "w") as f:
f.write(json.dumps(m, indent=2) + "\n")
print(" plugin-map.json: blob_strip_safe set to", safe)
else:
print(" plugin-map.json: blob_strip_safe already", safe, "(no change)")
PY
# --- create the local annotated archive tag idempotently (D2; pushed later, in the window) ---
if git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$ARCHIVE_TAG" >/dev/null; then
echo " tag: $ARCHIVE_TAG already exists (idempotent)"
else
git -C "$REPO_ROOT" tag -a "$ARCHIVE_TAG" -m "Archive of the monorepo before the polyrepo split (D2)"
echo " tag: created $ARCHIVE_TAG at HEAD (local only — pushed in the operator window)"
fi
echo "PREFLIGHT COMPLETE"

View file

@ -1,69 +0,0 @@
// Step 1 test — validates plugin-map.json shape (rename-awareness, HTTPS URLs, blob/vendor flags).
// Pattern: plugins/voyage/tests/validators/*.test.mjs (node:test style, zero deps).
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
const map = JSON.parse(readFileSync(join(here, 'plugin-map.json'), 'utf8'));
const targets = map.targets;
const keys = Object.keys(targets);
// voyage ← ultraplan-local and linkedin-studio ← linkedin-thought-leadership are TRUE renames, so each
// carries >=2 --path entries (F1). llm-security is NOT a rename: llm-security-copilot was a SEPARATE,
// COEXISTING plugin, so llm-security is SINGLE-path (corrected 2026-06-17, commit 836b8e9) — a dual --path
// there collided at the coexistence commits and dropped 87 files. The map is authoritative; this suite
// tracks it.
const RENAMED = ['voyage', 'linkedin-studio'];
test('plugin-map.json parses and has exactly 11 targets (10 plugins + DS)', () => {
assert.equal(keys.length, 11, `expected 11 targets, found ${keys.length}: ${keys.join(', ')}`);
});
test('the renamed plugins (voyage, linkedin-studio) each carry >=2 --path entries (F1)', () => {
for (const k of RENAMED) {
assert.ok(targets[k], `missing renamed target ${k}`);
assert.ok(
Array.isArray(targets[k].paths) && targets[k].paths.length >= 2,
`${k} must carry >=2 paths (both names), has ${targets[k].paths?.length}`
);
}
});
test('llm-security is SINGLE-path — NOT a rename (copilot was a coexisting plugin, 836b8e9)', () => {
// Locks in the 87-file-drop correction: re-introducing a 2nd --path here would re-collide at the
// coexistence commits and silently drop files again.
assert.equal(
targets['llm-security'].paths.length, 1,
`llm-security must stay single-path (dual-path dropped 87 files), has ${targets['llm-security'].paths.length}`
);
});
test('every target repo_url is https:// (not ssh://) — F2 / #9740', () => {
for (const k of keys) {
assert.match(targets[k].repo_url, /^https:\/\/git\.fromaitochitta\.com\/open\//, `${k} repo_url not the expected HTTPS form`);
assert.doesNotMatch(targets[k].repo_url, /^ssh:\/\//, `${k} repo_url must not be ssh://`);
}
});
test('ms-ai-architect has blob_strip: true (F3)', () => {
assert.equal(targets['ms-ai-architect'].blob_strip, true);
});
test('only llm-security + ms-ai-architect have has_vendor_ds: true', () => {
const vendor = keys.filter((k) => targets[k].has_vendor_ds === true).sort();
assert.deepEqual(vendor, ['llm-security', 'ms-ai-architect']);
});
test('every target tag is v<semver> (F5 single clean re-tag per repo)', () => {
for (const k of keys) {
assert.match(targets[k].tag, /^v\d+\.\d+\.\d+$/, `${k} tag '${targets[k].tag}' is not v<semver>`);
}
});
test('the removed plugin is dropped, not extracted', () => {
assert.ok(Array.isArray(map.drop) && map.drop.includes('plugins/ultra-cc-architect/'));
assert.ok(!keys.includes('ultra-cc-architect'));
});

View file

@ -1,86 +0,0 @@
#!/usr/bin/env bash
# Step 3 — Rename-aware extraction driver.
# Given a target key from plugin-map.json, clones the dedicated mirror ($WORK/_mirror, built by
# 00-preflight.sh — never the working checkout, R4/M6) into $WORK/<key>, runs a SINGLE git filter-repo
# pass composing --path / --path-rename (both names for renamed plugins, F1) + the deterministic
# ms-ai-architect blob strip (F3), then strips carried-over tags and re-creates exactly one annotated
# tag v<version> at the rewritten HEAD (F5). Idempotent (wipes $WORK/<key> first). NULL push (D8).
#
# Renamed plugins are extracted under BOTH historical names (F1), driven by plugin-map.json:
# voyage ← ultraplan-local, llm-security ← llm-security-copilot, linkedin-studio ← linkedin-thought-leadership.
# A single-path filter would silently drop the pre-rename history, so the map carries >=2 --path entries each.
#
# Usage: 10-extract.sh <target-key> (e.g. voyage, llm-security, playground-design-system)
# Override WORK= to relocate the workspace (default /tmp/polyrepo-migration).
set -euo pipefail
KEY="${1:-}"
[ -n "$KEY" ] || { echo "usage: 10-extract.sh <target-key>" >&2; exit 2; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
WORK="${WORK:-/tmp/polyrepo-migration}"
MIRROR="$WORK/_mirror"
DEST="$WORK/$KEY"
fail() { printf 'EXTRACT FAIL: %s\n' "$*" >&2; exit 1; }
[ -f "$MAP" ] || fail "plugin-map.json missing at $MAP"
python3 -c "import json,sys; m=json.load(open('$MAP')); sys.exit(0 if '$KEY' in m['targets'] else 1)" \
|| fail "unknown target '$KEY' (not in plugin-map.json)"
# Self-heal: if the mirror is absent, run preflight to build it (extraction never reads the working checkout).
if [ ! -e "$MIRROR/HEAD" ] && [ ! -d "$MIRROR/.git" ]; then
echo " mirror absent → running preflight to build it"
WORK="$WORK" bash "$SCRIPT_DIR/00-preflight.sh" >/dev/null
fi
[ -e "$MIRROR/HEAD" ] || [ -d "$MIRROR/.git" ] || fail "mirror still absent after preflight: $MIRROR"
# Compose the filter-repo arg list from the map (one arg per line → bash 3.2 array).
ARGS_FILE="$(mktemp)"
python3 - "$MAP" "$KEY" >"$ARGS_FILE" <<'PY'
import json, sys
m = json.load(open(sys.argv[1]))
t = m["targets"][sys.argv[2]]
out = []
for p in t["paths"]:
out += ["--path", p]
for old, new in t.get("path_renames", {}).items():
out += ["--path-rename", old + ":" + new]
if t.get("blob_strip"):
if t.get("blob_strip_safe"):
out += ["--strip-blobs-bigger-than", "1M"]
else:
# Surgical fallback (unreached while blob_strip_safe is true): drop only the screenshots.
out += ["--path-glob", "!plugins/ms-ai-architect/playground/screenshots/*"]
for a in out:
print(a)
PY
FR_ARGS=()
while IFS= read -r line; do FR_ARGS+=("$line"); done <"$ARGS_FILE"
rm -f "$ARGS_FILE"
TAG="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$KEY']['tag'])")"
# Re-assert the critical extension here too: the self-heal above runs preflight ONLY on a missing mirror,
# so a present mirror + since-uninstalled git-filter-repo would otherwise die with a raw git error instead
# of this actionable message (9e588ca — RUNBOOK lists it under Preconditions).
git filter-repo --version >/dev/null 2>&1 \
|| fail "git filter-repo not available — brew install git-filter-repo (see RUNBOOK Preconditions)"
# Fresh --no-local clone from the mirror, then the single composing filter-repo pass.
rm -rf "$DEST"
git clone --no-local --quiet "$MIRROR" "$DEST"
git -C "$DEST" filter-repo --force "${FR_ARGS[@]}"
# F5: strip every carried-over tag, re-create exactly one annotated tag at the rewritten HEAD.
OLD_TAGS="$(git -C "$DEST" tag)"
if [ -n "$OLD_TAGS" ]; then
printf '%s\n' "$OLD_TAGS" | while IFS= read -r tg; do
[ -n "$tg" ] && git -C "$DEST" tag -d "$tg" >/dev/null
done
fi
git -C "$DEST" tag -a "$TAG" -m "Release $TAG (extracted from ktg-plugin-marketplace monorepo)"
COMMITS="$(git -C "$DEST" rev-list --count HEAD)"
echo "EXTRACT OK $KEY$DEST ($COMMITS commits, tag $TAG)"

View file

@ -1,44 +0,0 @@
// Step 3 integration test — runs the extraction driver against the live monorepo (via the mirror)
// and asserts F1 (renamed-plugin history retained), F5 (single clean re-tag), root-level contents,
// and origin removal. Pattern: plugins/voyage/tests/integration/*.test.mjs.
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT = path.join(here, '10-extract.sh');
const WORK = process.env.WORK || '/tmp/polyrepo-migration';
const EXTRACT = path.join(WORK, 'voyage');
const git = (args) => spawnSync('git', ['-C', EXTRACT, ...args], { encoding: 'utf8' });
// Reuse the extract if the verify already produced it (tagged v5.1.1); otherwise build it.
(function ensureExtract() {
const tag = git(['tag']);
if (tag.status === 0 && tag.stdout.trim() === 'v5.1.1') return;
const r = spawnSync('bash', [SCRIPT, 'voyage'], { encoding: 'utf8', env: { ...process.env, WORK } });
if (r.status !== 0) throw new Error(`extract failed (status ${r.status}):\n${r.stdout}\n${r.stderr}`);
})();
test('voyage extract retains pre-rename (ultraplan-local) history — F1', () => {
const n = parseInt(git(['rev-list', '--count', 'HEAD']).stdout.trim(), 10);
assert.ok(n >= 150, `expected >=150 commits (voyage + ultraplan-local), got ${n}`);
});
test('voyage extract carries exactly one tag: v5.1.1 (F5)', () => {
const tags = git(['tag']).stdout.trim().split('\n').filter(Boolean).sort();
assert.deepEqual(tags, ['v5.1.1']);
});
test('voyage extract has no plugins/ prefix — contents at repo root', () => {
const files = git(['ls-files']).stdout.trim().split('\n').filter(Boolean);
assert.ok(files.length > 0, 'extract should have tracked files');
assert.ok(!files.some((f) => f.startsWith('plugins/')), 'no tracked path should start with plugins/');
assert.ok(files.includes('.claude-plugin/plugin.json'), 'voyage plugin.json should be at repo root');
});
test('origin remote is removed post-filter', () => {
assert.equal(git(['remote']).stdout.trim(), '', 'filter-repo should remove the origin remote');
});

View file

@ -1,72 +0,0 @@
#!/usr/bin/env bash
# Step 4 — Per-repo config re-rooting (.gitignore, .gitleaks.toml, .gitleaksignore, .mailmap).
# Operates on an extracted repo in $WORK/<key>: the monorepo's root config does not travel with
# filter-repo's --path (it lives at the monorepo root, F6), so we regenerate it per-repo:
# - .gitignore : re-rooted from templates/gitignore.plugin.tmpl (plugins/*/ prefixes dropped; the
# global STATE.md/*.local.md/OS patterns kept — C3).
# - .gitleaks.toml : the root baseline, retitled per plugin (its allowlist path is already plugin-relative).
# - .gitleaksignore : the matching false-positive fingerprint(s) from the root file, re-rooted by dropping
# ONLY the `plugins/<key>/` path prefix and keeping the full `:rule-id:line` suffix (M7).
# Plugins with no fingerprint get NO .gitleaksignore.
# - .mailmap : copied verbatim (F6 — it does not travel with --path).
# For the design-system target it also copies sync-design-system.mjs + test (Step 2) and
# PLAYGROUND-MAINTENANCE.md into the repo root (playground-examples/ already travels via --path).
# Idempotent. NULL push (D8) — operates only inside $WORK/<key>.
#
# Usage: 20-rehome-config.sh <target-key>
set -euo pipefail
KEY="${1:-}"
[ -n "$KEY" ] || { echo "usage: 20-rehome-config.sh <target-key>" >&2; exit 2; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)"
TEMPLATE="$SCRIPT_DIR/templates/gitignore.plugin.tmpl"
WORK="${WORK:-/tmp/polyrepo-migration}"
DEST="$WORK/$KEY"
fail() { printf 'REHOME FAIL: %s\n' "$*" >&2; exit 1; }
# Self-heal: extract the target first if it is not present.
if [ ! -d "$DEST/.git" ]; then
echo " $KEY not extracted → running 10-extract.sh"
WORK="$WORK" bash "$SCRIPT_DIR/10-extract.sh" "$KEY" >/dev/null
fi
[ -d "$DEST/.git" ] || fail "extract missing after self-heal: $DEST"
[ -f "$TEMPLATE" ] || fail "gitignore template missing: $TEMPLATE"
# 1) .gitignore from the re-rooted template
cp "$TEMPLATE" "$DEST/.gitignore"
# 2) .gitleaks.toml from the root baseline, retitled for the standalone repo
if [ -f "$REPO_ROOT/.gitleaks.toml" ]; then
sed "s/^title = .*/title = \"$KEY gitleaks config\"/" "$REPO_ROOT/.gitleaks.toml" > "$DEST/.gitleaks.toml"
fi
# 3) .gitleaksignore — re-rooted fingerprints for this plugin (drop only the plugins/<key>/ prefix, M7)
rm -f "$DEST/.gitleaksignore"
if [ -f "$REPO_ROOT/.gitleaksignore" ]; then
MATCHES="$(grep "^plugins/$KEY/" "$REPO_ROOT/.gitleaksignore" || true)"
if [ -n "$MATCHES" ]; then
{
echo "# Re-rooted false-positive fingerprints (from the monorepo .gitleaksignore)"
printf '%s\n' "$MATCHES" | sed "s#^plugins/$KEY/##"
} > "$DEST/.gitleaksignore"
fi
fi
# 4) .mailmap copied verbatim
[ -f "$REPO_ROOT/.mailmap" ] && cp "$REPO_ROOT/.mailmap" "$DEST/.mailmap"
# 5) Design-system target: bring in the sync script + maintenance doc (§9 Q4)
if [ "$KEY" = "playground-design-system" ]; then
mkdir -p "$DEST/scripts"
cp "$REPO_ROOT/scripts/sync-design-system.mjs" "$DEST/scripts/sync-design-system.mjs"
cp "$REPO_ROOT/scripts/sync-design-system.test.mjs" "$DEST/scripts/sync-design-system.test.mjs"
[ -f "$REPO_ROOT/shared/PLAYGROUND-MAINTENANCE.md" ] && \
cp "$REPO_ROOT/shared/PLAYGROUND-MAINTENANCE.md" "$DEST/PLAYGROUND-MAINTENANCE.md"
fi
GLI_STATE="absent"
[ -f "$DEST/.gitleaksignore" ] && GLI_STATE="$(wc -l < "$DEST/.gitleaksignore" | tr -d ' ') line(s)"
echo "REHOME OK $KEY → .gitignore + .gitleaks.toml + .mailmap; .gitleaksignore: $GLI_STATE"

View file

@ -1,47 +0,0 @@
// Step 4 test — re-rooted .gitignore, M7 gitleaks fingerprint, .mailmap copy, no-fingerprint plugin.
// Pattern: plugins/config-audit/tests/lib/*.test.mjs. Runs against freshly extracted repos in $WORK.
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT = path.join(here, '20-rehome-config.sh');
const WORK = process.env.WORK || '/tmp/polyrepo-migration';
function rehome(key) {
const r = spawnSync('bash', [SCRIPT, key], { encoding: 'utf8', env: { ...process.env, WORK } });
if (r.status !== 0) throw new Error(`rehome ${key} failed (${r.status}):\n${r.stdout}\n${r.stderr}`);
return path.join(WORK, key);
}
const llm = rehome('llm-security');
const gh = rehome('graceful-handoff');
test('re-rooted .gitignore ignores .claude/ and carries no plugins/ prefix', () => {
const gi = readFileSync(path.join(llm, '.gitignore'), 'utf8');
assert.match(gi, /^\.claude\/$/m, '.gitignore should ignore a root-level .claude/');
assert.ok(!gi.includes('plugins/'), '.gitignore must not contain a plugins/ prefix');
});
test('llm-security .gitleaksignore fingerprint is re-rooted with rule-id retained (M7)', () => {
const gli = readFileSync(path.join(llm, '.gitleaksignore'), 'utf8');
assert.match(
gli,
/^examples\/malicious-skill-demo\/evil-project-health\/lib\/telemetry\.mjs:generic-api-key:18$/m,
'fingerprint must be the exact re-rooted path:rule-id:line'
);
assert.ok(!gli.includes('plugins/llm-security/'), 'the plugins/llm-security/ prefix must be dropped');
assert.ok(gli.includes(':generic-api-key:'), 'the rule-id must be retained (else the suppression breaks)');
});
test('.mailmap is copied into the extract', () => {
assert.ok(existsSync(path.join(llm, '.mailmap')), '.mailmap should exist in the extract');
});
test('a plugin with no fingerprint (graceful-handoff) gets .gitleaks.toml but no .gitleaksignore', () => {
assert.ok(existsSync(path.join(gh, '.gitleaks.toml')), 'graceful-handoff should have .gitleaks.toml');
assert.ok(!existsSync(path.join(gh, '.gitleaksignore')), 'graceful-handoff should have no .gitleaksignore');
});

View file

@ -1,106 +0,0 @@
#!/usr/bin/env node
// Step 5 — Reference-rot rewriter. Operates on an extracted repo in $WORK/<key>.
// 1. Replaces the "[Full disclosure →](../../README.md#ai-generated-code-disclosure)" footnote with
// INLINE disclosure text (M13 — self-contained, no dangling cross-repo anchor; the monorepo root
// README has no such section, so the link was dead even in-repo).
// 2. Rewrites any remaining ../../README.md and ../../.claude-plugin/marketplace.json reference
// (e.g. graceful-handoff README footer, linkedin-studio remediation docs) to the absolute catalog URL.
// 3. Sets plugin.json `repository` (and package.json homepage/repository/bugs, when present) to the
// standalone open/<plugin> HTTPS URL read from plugin-map.json, and DROPS any monorepo-relative
// `repository.directory` sub-path (e.g. llm-security's "plugins/llm-security") — it points nowhere
// once the content lives at the standalone repo root.
// ai-psychosis already points at open/ai-psychosis (verify-only no-op, M14). Idempotent: a second run
// rewrites zero files. Reports every file it touched. NULL push (D8) — operates only inside $WORK/<key>.
//
// Usage: node 30-fix-references.mjs <target-key>
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
const here = path.dirname(fileURLToPath(import.meta.url));
const MAP = path.join(here, 'plugin-map.json');
const WORK = process.env.WORK || '/tmp/polyrepo-migration';
const CATALOG = 'https://git.fromaitochitta.com/open/ktg-plugin-marketplace/src/branch/main';
const DISCLOSURE_LINK = /\[Full disclosure →\]\(\.\.\/\.\.\/README\.md#ai-generated-code-disclosure\)/g;
const DISCLOSURE_INLINE = 'Every change is human-directed, reviewed, and validated before commit.';
const readJson = async (p) => JSON.parse(await fs.readFile(p, 'utf8'));
const exists = async (p) => { try { await fs.access(p); return true; } catch { return false; } };
async function walkMd(dir, out = []) {
for (const e of await fs.readdir(dir, { withFileTypes: true })) {
if (e.name === '.git') continue;
const full = path.join(dir, e.name);
if (e.isDirectory()) await walkMd(full, out);
else if (e.isFile() && e.name.endsWith('.md')) out.push(full);
}
return out;
}
async function main() {
const key = process.argv[2];
if (!key) { console.error('usage: 30-fix-references.mjs <target-key>'); process.exit(2); }
const map = await readJson(MAP);
const t = map.targets[key];
if (!t) { console.error(`unknown target ${key}`); process.exit(2); }
const dest = path.join(WORK, key);
if (!(await exists(path.join(dest, '.git')))) {
const r = spawnSync('bash', [path.join(here, '10-extract.sh'), key], { stdio: 'inherit', env: { ...process.env, WORK } });
if (r.status !== 0) { console.error('extract failed'); process.exit(1); }
}
const base = t.repo_url.replace(/\.git$/, ''); // https://git.fromaitochitta.com/open/<key>
const changed = [];
// 1) + 2) markdown rewrites
for (const file of await walkMd(dest)) {
const orig = await fs.readFile(file, 'utf8');
let next = orig.replace(DISCLOSURE_LINK, DISCLOSURE_INLINE);
next = next.split('../../README.md').join(`${CATALOG}/README.md`);
next = next.split('../../.claude-plugin/marketplace.json').join(`${CATALOG}/.claude-plugin/marketplace.json`);
if (next !== orig) { await fs.writeFile(file, next, 'utf8'); changed.push(path.relative(dest, file)); }
}
// 3a) plugin.json repository → standalone URL (+ drop any stale monorepo-relative repository.directory)
const pjPath = path.join(dest, '.claude-plugin', 'plugin.json');
if (await exists(pjPath)) {
const pj = await readJson(pjPath);
let touched = false;
if (pj.repository && typeof pj.repository === 'object') {
if (pj.repository.url !== base) { pj.repository.url = base; touched = true; }
// A monorepo-relative repository.directory points nowhere in the standalone repo — drop it.
if ('directory' in pj.repository) { delete pj.repository.directory; touched = true; }
} else if (pj.repository !== base) {
pj.repository = base; touched = true;
}
if (touched) {
await fs.writeFile(pjPath, JSON.stringify(pj, null, 2) + '\n', 'utf8');
changed.push('.claude-plugin/plugin.json');
}
}
// 3b) package.json homepage/repository/bugs (when present)
const pkgPath = path.join(dest, 'package.json');
if (await exists(pkgPath)) {
const pkg = await readJson(pkgPath);
let touched = false;
if (pkg.homepage !== base) { pkg.homepage = base; touched = true; }
if (pkg.repository && typeof pkg.repository === 'object') {
if (pkg.repository.url !== base) { pkg.repository.url = base; touched = true; }
// Drop any stale monorepo-relative repository.directory (points nowhere in the standalone repo).
if ('directory' in pkg.repository) { delete pkg.repository.directory; touched = true; }
} else if (pkg.repository && pkg.repository !== base) { pkg.repository = base; touched = true; }
if (pkg.bugs && typeof pkg.bugs === 'object' && pkg.bugs.url !== `${base}/issues`) {
pkg.bugs.url = `${base}/issues`; touched = true;
}
if (touched) { await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); changed.push('package.json'); }
}
for (const f of changed) console.log(` rewrote ${f}`);
console.log(`FIX-REFERENCES OK ${key} → rewrote ${changed.length} file(s)`);
}
main().catch((e) => { console.error(`Error: ${e.message}`); process.exit(1); });

View file

@ -1,93 +0,0 @@
// Step 5 test — against an extracted graceful-handoff: no ../../README.md remains, plugin.json
// repository reconciled, and a second run is a no-op. Pattern: plugins/linkedin-studio/render/__tests__/*.test.mjs.
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT = path.join(here, '30-fix-references.mjs');
const WORK = process.env.WORK || '/tmp/polyrepo-migration';
const DEST = path.join(WORK, 'graceful-handoff');
const fix = () => spawnSync(process.execPath, [SCRIPT, 'graceful-handoff'], { encoding: 'utf8', env: { ...process.env, WORK } });
// Ensure a fixed state (self-heals extract if needed).
{
const r = fix();
if (r.status !== 0) throw new Error(`fix-references failed: ${r.stdout}\n${r.stderr}`);
}
const grepRel = (needle) =>
spawnSync('grep', ['-rn', needle, DEST, '--include=*.md'], { encoding: 'utf8' });
test('no ../../README.md reference remains in any .md', () => {
const r = grepRel('../../README.md');
assert.equal(r.stdout.trim(), '', `unexpected ../../README.md references:\n${r.stdout}`);
});
test('plugin.json repository points at the standalone repo', () => {
const pj = JSON.parse(readFileSync(path.join(DEST, '.claude-plugin', 'plugin.json'), 'utf8'));
const url = typeof pj.repository === 'object' ? pj.repository.url : pj.repository;
assert.ok(url.endsWith('/open/graceful-handoff'), `repository is '${url}'`);
});
test('disclosure footnote is inlined (no dangling cross-repo anchor)', () => {
const readme = readFileSync(path.join(DEST, 'README.md'), 'utf8');
assert.ok(!readme.includes('#ai-generated-code-disclosure'), 'the dangling disclosure anchor must be gone');
assert.ok(readme.includes('AI-generated'), 'the disclosure statement itself is retained inline');
});
test('a second run rewrites zero files (idempotent)', () => {
const r = fix();
assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, /rewrote 0 file\(s\)/, `expected no-op, got:\n${r.stdout}`);
});
// The package.json branch never runs against graceful-handoff (it has no root package.json), so it was
// previously unguarded. Drive it directly against a SYNTHETIC extract for a key that carries a monorepo
// repository.directory (llm-security) — pre-init .git so the script skips extraction and rewrites in place.
test('package.json + plugin.json reconciled, stale repository.directory dropped (synthetic llm-security)', () => {
const tmpWork = mkdtempSync(path.join(os.tmpdir(), 'fixref-pkg-'));
try {
const dest = path.join(tmpWork, 'llm-security');
mkdirSync(path.join(dest, '.claude-plugin'), { recursive: true });
assert.equal(spawnSync('git', ['-C', dest, 'init', '-q']).status, 0, 'git init must succeed');
const monoUrl = 'https://git.fromaitochitta.com/open/ktg-plugin-marketplace';
writeFileSync(path.join(dest, 'package.json'), JSON.stringify({
name: 'llm-security', version: '7.7.2',
homepage: monoUrl,
repository: { type: 'git', url: monoUrl, directory: 'plugins/llm-security' },
bugs: { url: `${monoUrl}/issues` },
}, null, 2) + '\n');
writeFileSync(path.join(dest, '.claude-plugin', 'plugin.json'), JSON.stringify({
name: 'llm-security',
repository: { type: 'git', url: monoUrl, directory: 'plugins/llm-security' },
}, null, 2) + '\n');
const run = () => spawnSync(process.execPath, [SCRIPT, 'llm-security'], { encoding: 'utf8', env: { ...process.env, WORK: tmpWork } });
const r = run();
assert.equal(r.status, 0, `fix-references failed:\n${r.stdout}\n${r.stderr}`);
const base = 'https://git.fromaitochitta.com/open/llm-security';
const pkg = JSON.parse(readFileSync(path.join(dest, 'package.json'), 'utf8'));
assert.equal(pkg.homepage, base, 'package.json homepage must be reconciled');
assert.equal(pkg.repository.url, base, 'package.json repository.url must be reconciled');
assert.equal(pkg.bugs.url, `${base}/issues`, 'package.json bugs.url must be reconciled');
assert.ok(!('directory' in pkg.repository), 'stale repository.directory must be dropped from package.json');
const pj = JSON.parse(readFileSync(path.join(dest, '.claude-plugin', 'plugin.json'), 'utf8'));
assert.equal(pj.repository.url, base, 'plugin.json repository.url must be reconciled');
assert.ok(!('directory' in pj.repository), 'stale repository.directory must be dropped from plugin.json');
const r2 = run();
assert.equal(r2.status, 0, r2.stderr);
assert.match(r2.stdout, /rewrote 0 file\(s\)/, `expected idempotent no-op, got:\n${r2.stdout}`);
} finally {
rmSync(tmpWork, { recursive: true, force: true });
}
});

View file

@ -1,143 +0,0 @@
#!/usr/bin/env bash
# Step 6 — Standalone validation harness (SC2 + SC7).
# For each target: ensures the extract is prepped (extract -> rehome -> fix-references, all idempotent),
# copies it to a clean room /tmp/claude-<key> (no marketplace parent), then:
# SC2: runs the runner the plugin itself declares in plugin-map.json (test_cmd) — always the glob form
# `node --test 'tests/**/*.test.mjs'` or `node --test <dir>/*.test.mjs`, NEVER a bare dir (Node 25 gotcha),
# or `bash tests/validate-plugin.sh` / `bash validate-plugin.generic.sh <key>` for the test-less plugins.
# linkedin-studio is two-tier (M11): the .mjs core is the HARD gate; its TS analytics suite is
# reported as ADVISORY (npm/network — not a clean-room gate).
# SC7: no tracked STATE.md / *.local.md (except okr/templates/okr.local.md.template), no ../../README.md,
# .gitignore ignores .claude/.
# voyage: .forgejo/ISSUE_TEMPLATE/ survives the extraction and holds no monorepo-relative path (M17).
# Emits a per-repo PASS/FAIL table; exits non-zero if any target FAILs (escalate — never mask). NULL push (D8).
#
# Usage: 40-validate-standalone.sh <target-key>
# 40-validate-standalone.sh --all
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
GENERIC_VALIDATOR="$SCRIPT_DIR/templates/validate-plugin.generic.sh"
WORK="${WORK:-/tmp/polyrepo-migration}"
[ -f "$MAP" ] || { echo "plugin-map.json missing at $MAP" >&2; exit 1; }
ARG="${1:-}"
[ -n "$ARG" ] || { echo "usage: 40-validate-standalone.sh <target-key>|--all" >&2; exit 2; }
if [ "$ARG" = "--all" ]; then
KEYS="$(python3 -c "import json; print('\n'.join(sorted(json.load(open('$MAP'))['targets'])))")"
else
KEYS="$ARG"
fi
FAILS=0
prep_target() {
local key="$1"
local dest="$WORK/$key"
if [ ! -d "$dest/.git" ]; then
WORK="$WORK" bash "$SCRIPT_DIR/10-extract.sh" "$key" >/dev/null || return 1
fi
WORK="$WORK" bash "$SCRIPT_DIR/20-rehome-config.sh" "$key" >/dev/null || return 1
WORK="$WORK" node "$SCRIPT_DIR/30-fix-references.mjs" "$key" >/dev/null || return 1
return 0
}
validate_target() {
local key="$1"
local dest="$WORK/$key"
local cr="/tmp/claude-$key"
local problems=""
rm -rf "$cr"
cp -R "$dest" "$cr"
# --- SC7: no tracked STATE.md / *.local.md (except the okr template) ---
local leaks
leaks="$(git -C "$cr" ls-files | grep -E 'STATE\.md|\.local\.md$' | grep -v 'templates/okr\.local\.md\.template' || true)"
[ -z "$leaks" ] || problems="$problems; tracked state-file leak: $(echo "$leaks" | tr '\n' ' ')"
# --- SC7: no ../../README.md remains ---
if grep -rn '\.\./\.\./README\.md' "$cr" --include='*.md' >/dev/null 2>&1; then
problems="$problems; ../../README.md reference remains"
fi
# --- SC7: .gitignore ignores .claude/ ---
if ! git -C "$cr" check-ignore .claude/x >/dev/null 2>&1; then
problems="$problems; .gitignore does not ignore .claude/"
fi
# --- voyage: .forgejo survival (M17) ---
if [ "$key" = "voyage" ]; then
if [ ! -d "$cr/.forgejo/ISSUE_TEMPLATE" ]; then
problems="$problems; .forgejo/ISSUE_TEMPLATE missing"
elif grep -rn '\.\./\.\.' "$cr/.forgejo" >/dev/null 2>&1; then
problems="$problems; .forgejo holds a monorepo-relative path"
fi
fi
# --- SC2: route to the target's dedicated gate if it declares one, else run the declared runner in the
# clean room. config-audit's full `node --test 'tests/**/*.test.mjs'` FAILs at a fresh-clone path on
# the 6 machine-locked v5.0.0 byte-stability tests — the exact blocker its Step-7 gate resolves — so
# `--all` MUST delegate to that gate (mirroring 99-dryrun.sh), not run the raw test_cmd. ---
local test_cmd advisory sc2_gate
test_cmd="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('test_cmd',''))")"
advisory="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('test_cmd_advisory',''))")"
sc2_gate="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('sc2_gate',''))")"
local sc2_label="standalone-safe"
if [ -n "$sc2_gate" ]; then
if WORK="$WORK" bash "$SCRIPT_DIR/$sc2_gate" >/dev/null 2>&1; then
sc2_label="$sc2_gate, standalone-safe"
else
problems="$problems; SC2 gate failed ($sc2_gate)"
fi
else
# The generic structure validator (Step 7) is referenced by test-less plugins — vendor it into the clean room.
case "$test_cmd" in
*validate-plugin.generic.sh*)
if [ -f "$GENERIC_VALIDATOR" ]; then cp "$GENERIC_VALIDATOR" "$cr/validate-plugin.generic.sh"; fi
;;
esac
local out status tests
out="$(cd "$cr" && eval "$test_cmd" 2>&1)"; status=$?
if [ $status -ne 0 ]; then
problems="$problems; SC2 runner failed (exit $status)"
fi
case "$test_cmd" in
*node\ --test*)
# Node 25's default reporter prints " tests N"; older/TAP prints "# tests N". Match either.
tests="$(printf '%s\n' "$out" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)"
[ -n "$tests" ] && sc2_label="$tests tests, standalone-safe"
;;
*validate-plugin*) sc2_label="structure, standalone-safe" ;;
esac
fi
if [ -n "$problems" ]; then
echo "$key: FAIL${problems}"
FAILS=$((FAILS+1))
else
echo "$key: PASS ($sc2_label)"
[ -n "$advisory" ] && echo " advisory (not a clean-room gate): $advisory"
fi
}
for key in $KEYS; do
if ! prep_target "$key"; then
echo "$key: FAIL (prep/extract error)"
FAILS=$((FAILS+1))
continue
fi
validate_target "$key"
done
if [ "$FAILS" -ne 0 ]; then
echo "VALIDATE: $FAILS target(s) FAILED"
exit 1
fi
echo "VALIDATE OK"

View file

@ -1,52 +0,0 @@
// Step 6 test — the harness is the test surface: it must PASS a clean extract, FAIL one with a
// planted tracked state file, and use the glob test form (never a bare dir). Pattern:
// plugins/voyage/tests/synthetic/*.test.mjs.
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, cpSync, writeFileSync, readFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT = path.join(here, '40-validate-standalone.sh');
const WORK = process.env.WORK || '/tmp/polyrepo-migration';
function harness(key, work) {
// Strip the parent test-runner context so the harness's own `node --test` runs un-nested
// (otherwise Node emits its IPC subtest format and the test-count label is suppressed).
const env = { ...process.env, WORK: work };
delete env.NODE_TEST_CONTEXT;
return spawnSync('bash', [SCRIPT, key], { encoding: 'utf8', env });
}
const git = (dir, args) => spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
test('harness PASSes a clean standalone extract (graceful-handoff)', () => {
const r = harness('graceful-handoff', WORK);
assert.equal(r.status, 0, `expected PASS exit 0:\n${r.stdout}\n${r.stderr}`);
assert.match(r.stdout, /graceful-handoff: PASS \(.*standalone-safe\)/);
});
test('harness FAILs an extract with a planted tracked state file (SC7)', () => {
const tmpWork = mkdtempSync(path.join(os.tmpdir(), 'validate-broken-'));
try {
const broken = path.join(tmpWork, 'graceful-handoff');
cpSync(path.join(WORK, 'graceful-handoff'), broken, { recursive: true });
writeFileSync(path.join(broken, 'STATE.md'), '# planted tracked state file\n');
assert.equal(git(broken, ['add', '-f', 'STATE.md']).status, 0);
assert.equal(git(broken, ['commit', '-m', 'plant tracked STATE.md', '-q']).status, 0);
const r = harness('graceful-handoff', tmpWork);
assert.notEqual(r.status, 0, `expected FAIL (non-zero), got 0:\n${r.stdout}`);
assert.match(r.stdout, /graceful-handoff: FAIL/);
assert.match(r.stdout, /state-file leak/);
} finally {
rmSync(tmpWork, { recursive: true, force: true });
}
});
test('harness uses the glob test form, never a bare dir (Node 25 gotcha)', () => {
const src = readFileSync(SCRIPT, 'utf8');
assert.ok(src.includes('*.test.mjs'), 'harness must reference the glob test form *.test.mjs');
});

View file

@ -1,84 +0,0 @@
#!/usr/bin/env bash
# Step 6b — per-target SC2/SC7 gate with the migration's REGRESSION-RELATIVE contract baked in.
#
# WHY THIS EXISTS: 40-validate-standalone.sh is STRICT — any standalone test failure exits non-zero. The
# migration's RATIFIED contract — the one the Step-11 dry-run (99-dryrun.sh) validated and signed off as
# "PASS 11/11" — is weaker and correct: a target passes iff the extraction introduces NO NEW failure.
# Pre-existing in-repo red (voyage's 2 doc-consistency drifts re phase_models/phase_signals; ai-psychosis's
# 1) is the PLUGIN's own concern, not a migration regression. The operator window (run-operator-window.sh
# step [a]) must enforce THAT contract, not a stricter one — otherwise it STOPs on the first target carrying
# pre-existing red even though the dry-run blessed it (the bug this script fixes). This is the single
# per-target gate the window calls; it mirrors 99-dryrun.sh's SC2 decision exactly, reusing capture-fails.sh
# (the failing-name capture) + sc2-regression.sh (the subset decision). NULL push (D8): read-only validation.
#
# CONTRACT:
# - sc2_gate target (config-audit): the dedicated gate is deterministic — run it STRICT (PASS/FAIL).
# - else: run 40-validate-standalone.sh STRICT. PASS => exit 0.
# On FAIL, only a `node --test` suite is regression-eligible: its standalone failing-NAME set must be
# a SUBSET of the live in-repo failing-NAME set (regression-relative PASS, "PASS (N pre-existing)").
# A structure-validator (validate-plugin*.sh / bash -c) FAIL is a genuine structural defect — NOT
# softened. A genuine regression (standalone-only failure) or a missing capture => exit non-zero.
# Escalate, never mask: any real regression or structural failure exits non-zero so the window STOPs.
#
# Usage: 41-validate-or-regression.sh <target-key>
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
WORK="${WORK:-/tmp/polyrepo-migration}"
[ -f "$MAP" ] || { echo "plugin-map.json missing at $MAP" >&2; exit 2; }
key="${1:?usage: 41-validate-or-regression.sh <target-key>}"
mapget() { python3 -c "import json;print(json.load(open('$MAP'))['targets']['$key'].get('$1',''))"; }
sc2_gate="$(mapget sc2_gate)"
test_cmd="$(mapget test_cmd)"
# --- gate target (config-audit): deterministic, strict by design ---
if [ -n "$sc2_gate" ]; then
if WORK="$WORK" bash "$SCRIPT_DIR/$sc2_gate" >/dev/null 2>&1; then
echo "$key: PASS ($sc2_gate)"; exit 0
fi
echo "$key: FAIL ($sc2_gate)" >&2; exit 1
fi
# --- strict standalone validation first (SC2 + SC7 + per-target structural checks) ---
if WORK="$WORK" bash "$SCRIPT_DIR/40-validate-standalone.sh" "$key" >/dev/null 2>&1; then
echo "$key: PASS (standalone strict)"; exit 0
fi
# Strict failed. Only a node:test suite can fail regression-relative-acceptably; a structure validator
# failing is a genuine structural defect — do not soften it.
case "$test_cmd" in
*node\ --test*) : ;;
*) echo "$key: FAIL (standalone strict; structure validator — not regression-eligible)" >&2; exit 1 ;;
esac
dest="$WORK/$key"
[ -d "$dest/.git" ] || { echo "$key: FAIL (no prepped extract at $dest — run 40-validate-standalone.sh first)" >&2; exit 1; }
# Regression-relative: the standalone failing-NAME set must be a SUBSET of the live in-repo failing-NAME set.
# Capture the standalone set from the PREPPED EXTRACT ($WORK/$key), NOT 40's /tmp/claude-$key side-effect
# clean room (4e494c8: that coupling masked a real regression when the dir was absent). The subset decision
# is delegated to sc2-regression.sh; a missing capture FILE there is a hard error, never a false PASS.
sf="$(mktemp)" && bf="$(mktemp)" || { echo "$key: FAIL (mktemp)" >&2; exit 1; }
trap 'rm -f "$sf" "$bf"' EXIT
bash "$SCRIPT_DIR/capture-fails.sh" "$dest" "$test_cmd" > "$sf" 2>/dev/null
bash "$SCRIPT_DIR/capture-fails.sh" "$REPO_ROOT/plugins/$key" "$test_cmd" > "$bf" 2>/dev/null
pre="$(wc -l < "$bf" | tr -d '[:space:]')"
if regr="$(bash "$SCRIPT_DIR/sc2-regression.sh" "$sf" "$bf")"; then
echo "$key: PASS (${pre} pre-existing, regression-relative)"
exit 0
else
rc=$?
if [ "$rc" -ge 2 ]; then
echo "$key: FAIL (sc2-regression error — missing capture file)" >&2
else
echo "$key: FAIL (regression: $(printf '%s' "$regr" | grep -c .) new failure(s) absent from in-repo baseline):" >&2
printf '%s\n' "$regr" >&2
fi
exit 1
fi

View file

@ -1,141 +0,0 @@
// Coverage for 41-validate-or-regression.sh — the per-target gate the operator window calls in step [a].
// It enforces the migration's RATIFIED contract ("introduce no regression"), NOT the stricter zero-failure
// gate that made run-operator-window.sh STOP on voyage's / ai-psychosis's pre-existing in-repo red. 41 is
// thin glue over already-tested detectors (sc2-regression.sh — sc-checks.test.mjs; capture-fails.sh —
// capture-fails.test.mjs) and over 40-validate-standalone.sh (strict). This suite drives the REAL 41 inside
// a self-contained fake migration tree: the heavy extract pipeline (40) is STUBBED so the regression-branch
// glue is exercised hermetically, with no /tmp/polyrepo-migration state and no real plugin extracts.
//
// Branches covered: strict-PASS passthrough; regression-relative PASS (standalone ⊆ in-repo); genuine
// regression FAIL (standalone-only failure); structure-validator FAIL is NOT regression-eligible; the
// sc2_gate target stays strict (PASS + FAIL).
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
// One node:test file = imports ONCE + a failing test() per name. (Concatenating per-name single-file
// strings would duplicate `import test` → SyntaxError → a file-level not-ok instead of per-test names.)
const failSuite = (names) =>
"import test from 'node:test';\nimport assert from 'node:assert/strict';\n" +
names.map((n) => `test(${JSON.stringify(n)}, () => assert.equal(1, 2));`).join('\n') +
'\n';
function write(p, content, mode) {
mkdirSync(path.dirname(p), { recursive: true });
writeFileSync(p, content);
if (mode) chmodSync(p, mode);
}
// Build a fake migration tree: SCRIPT_DIR nested 3 deep so 41's REPO_ROOT ($SCRIPT_DIR/../../..) is the root.
function buildTree() {
const root = mkdtempSync(path.join(os.tmpdir(), 'mig41-'));
const mig = path.join(root, 'a', 'b', 'c'); // SCRIPT_DIR; ../../.. === root
const work = path.join(root, '_work'); // $WORK
mkdirSync(mig, { recursive: true });
// real scripts under test + reused detectors
for (const f of ['41-validate-or-regression.sh', 'capture-fails.sh', 'sc2-regression.sh']) {
copyFileSync(path.join(here, f), path.join(mig, f));
}
// stub strict 40-validate-standalone.sh: passes ONLY for the designated strict-pass target.
write(path.join(mig, '40-validate-standalone.sh'),
'#!/usr/bin/env bash\n[ "${1:-}" = "strictpass" ] && exit 0\nexit 1\n', 0o755);
// sc2_gate stubs
write(path.join(mig, 'gate-pass.sh'), '#!/usr/bin/env bash\nexit 0\n', 0o755);
write(path.join(mig, 'gate-fail.sh'), '#!/usr/bin/env bash\nexit 1\n', 0o755);
const NT = "node --test 'tests/**/*.test.mjs'";
const map = {
targets: {
strictpass: { test_cmd: NT },
subset: { test_cmd: NT },
regress: { test_cmd: NT },
structure: { test_cmd: 'bash validate-plugin.sh' },
gatepass: { test_cmd: NT, sc2_gate: 'gate-pass.sh' },
gatefail: { test_cmd: NT, sc2_gate: 'gate-fail.sh' },
},
};
write(path.join(mig, 'plugin-map.json'), JSON.stringify(map, null, 2));
// node:test suites for the regression-eligible targets. dest = $WORK/<key> (needs .git); baseline = root/plugins/<key>.
const suite = (key, names) => {
mkdirSync(path.join(work, key, '.git'), { recursive: true });
write(path.join(work, key, 'tests', 'x.test.mjs'), failSuite(names.standalone));
write(path.join(root, 'plugins', key, 'tests', 'x.test.mjs'), failSuite(names.baseline));
};
suite('subset', { standalone: ['shared red'], baseline: ['shared red', 'other in-repo red'] });
suite('regress', { standalone: ['shared red', 'extract regressed'], baseline: ['shared red'] });
return { root, mig, work };
}
function run41(tree, key) {
// Strip NODE_TEST_CONTEXT: 41 → capture-fails → `node --test`, which emits no TAP if it inherits the
// harness's nested-test context (Step-6 env-clean gotcha). The real window runs as plain bash, unnested.
const env = { ...process.env, WORK: tree.work };
delete env.NODE_TEST_CONTEXT;
return spawnSync('bash', [path.join(tree.mig, '41-validate-or-regression.sh'), key],
{ encoding: 'utf8', env });
}
test('strict-PASS passthrough: 40 passes → 41 PASS (standalone strict), exit 0, no capture', () => {
const tree = buildTree();
try {
const r = run41(tree, 'strictpass');
assert.equal(r.status, 0, `strict pass must exit 0: ${r.stderr}`);
assert.match(r.stdout, /PASS \(standalone strict\)/, r.stdout);
} finally { rmSync(tree.root, { recursive: true, force: true }); }
});
test('regression-relative PASS: standalone failing-set ⊆ in-repo → exit 0, reports N pre-existing', () => {
const tree = buildTree();
try {
const r = run41(tree, 'subset');
assert.equal(r.status, 0, `a subset of in-repo red is not a migration regression: ${r.stdout}${r.stderr}`);
assert.match(r.stdout, /pre-existing, regression-relative/, r.stdout);
} finally { rmSync(tree.root, { recursive: true, force: true }); }
});
test('genuine regression FAIL: a standalone-only failure (absent in-repo) → non-zero, names the regression', () => {
const tree = buildTree();
try {
const r = run41(tree, 'regress');
assert.notEqual(r.status, 0, 'a new failure introduced by extraction must STOP the window');
assert.match(r.stderr, /regression/, r.stderr);
assert.match(r.stderr, /extract regressed/, `the regressing test name must be surfaced: ${r.stderr}`);
} finally { rmSync(tree.root, { recursive: true, force: true }); }
});
test('structure-validator FAIL is NOT regression-eligible: bash validator fail → non-zero, never softened', () => {
const tree = buildTree();
try {
const r = run41(tree, 'structure');
assert.notEqual(r.status, 0, 'a deterministic structure-validator failure is a genuine defect');
assert.match(r.stderr, /not regression-eligible/, r.stderr);
} finally { rmSync(tree.root, { recursive: true, force: true }); }
});
test('sc2_gate target stays strict: gate PASS → exit 0', () => {
const tree = buildTree();
try {
const r = run41(tree, 'gatepass');
assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, /PASS \(gate-pass\.sh\)/, r.stdout);
} finally { rmSync(tree.root, { recursive: true, force: true }); }
});
test('sc2_gate target stays strict: gate FAIL → non-zero', () => {
const tree = buildTree();
try {
const r = run41(tree, 'gatefail');
assert.notEqual(r.status, 0, 'a failing dedicated gate must STOP the window');
assert.match(r.stderr, /FAIL \(gate-fail\.sh\)/, r.stderr);
} finally { rmSync(tree.root, { recursive: true, force: true }); }
});

View file

@ -1,93 +0,0 @@
#!/usr/bin/env bash
# Step 7 — Resolve the config-audit SC2 blocker for standalone extraction.
#
# config-audit declares its runner as `node --test 'tests/**/*.test.mjs'` (52 files, CLAUDE.md:109).
# Two distinct, directly-verified portability defects block a clean-room SC2 gate at a new clone path:
#
# (a) REBASABLE — tests/snapshot-default-output.test.mjs asserts byte-equal CLI stdout whose deep
# per-file paths are NOT normalized (normalizeScanOrchestrator scrubs only meta.target/timestamp/
# duration_ms), so it breaks at a fresh clone path. FIX: re-seed in-clone via the test's own
# intended re-approval seam — `UPDATE_SNAPSHOT=1 node --test tests/snapshot-default-output.test.mjs`
# (seam documented at its line 33). After re-seeding it asserts byte-equal against the clone's
# own path and passes.
#
# (b) FROZEN / MACHINE-LOCKED — a family of tests assert byte/structure equality against the
# tests/snapshots/v5.0.0/ fixtures, which deliberately embed the ORIGINAL capture machine's
# absolute path + a sibling marketplace + deleted plugins. At a fresh clone path they break in
# TWO ways (both verified directly, 2026-06-17): a literal embedded `path:` mismatch, AND a
# BEHAVIORAL drift — the claude_md / plugin_hygiene scanners key off whether a `plugins/<name>/`
# ancestor exists in the absolute path, so the clone produces different findingCount/score than
# the monorepo capture (e.g. posture-humanizer). drift-cli's baseline diff additionally leaks the
# clone path into humanized prose, which trips lint-default-output's tier1/tier3 prose gate.
# Regenerating these would defeat their byte-stability purpose and "normalize the path" is not a
# string rewrite (the scan BEHAVIOR differs by path) — so the correct migration-scope fix is to
# EXCLUDE the machine-locked surface by name (deterministic enumeration, no invented env var).
#
# OPERATOR-RATIFIED 2026-06-17 (brief-correction): plan F4 named only json-backcompat +
# raw-backcompat (2 files). The verified machine-locked surface is SIX files (the 2 + the 4 that
# still fail at clone path). Dropped clean-room SC2 coverage = config-audit's humanizer / posture-
# humanizer / scan-orchestrator-humanizer prose-snapshot surface (NOT silent — recorded here +
# in plugin-map.json's standalone_caveat). The three OTHER v5.0.0-referencing tests
# (posture, scoring-humanizer, scenario-read-test) assert path-INDEPENDENT aspects, pass at the
# clone path, and remain IN the gate. config-audit backlog (out of migration scope): normalize
# the v5.0.0 fixtures' embedded paths + make the scanners path-agnostic, then re-include.
#
# SC2 gate (config-audit) := full `find tests -name '*.test.mjs'` MINUS the six machine-locked tests
# below, run AFTER the in-clone snapshot re-seed. NULL push (D8). Prints "config-audit: SC2 PASS (...)"
# + exit 0 on success; non-zero on any failure (escalate — never mask).
#
# Usage: 50-config-audit-sc2.sh
set -uo pipefail
unset NODE_TEST_CONTEXT 2>/dev/null || true # un-nest the internal `node --test` (Node 25 count suppression)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK="${WORK:-/tmp/polyrepo-migration}"
KEY="config-audit"
DEST="$WORK/$KEY"
CR="/tmp/claude-${KEY}-sc2"
# The machine-locked v5.0.0 byte-stability surface excluded from the SC2 gate (operator-ratified).
# Anchored to each test file's basename; scan-orchestrator-humanizer is matched WITHOUT catching the
# portable scan-orchestrator.test.mjs, and posture-humanizer WITHOUT catching posture.test.mjs.
EXCLUDE_RE='(json-backcompat|raw-backcompat|cli-humanizer|posture-humanizer|scan-orchestrator-humanizer|lint-default-output)\.test\.mjs$'
# --- 1. Prep the extract (idempotent), reusing the Step 3-5 drivers (same pattern as 40-validate-standalone.sh) ---
if [ ! -d "$DEST/.git" ]; then
WORK="$WORK" bash "$SCRIPT_DIR/10-extract.sh" "$KEY" >/dev/null || { echo "config-audit: SC2 FAIL (extract error)"; exit 1; }
fi
WORK="$WORK" bash "$SCRIPT_DIR/20-rehome-config.sh" "$KEY" >/dev/null || { echo "config-audit: SC2 FAIL (rehome error)"; exit 1; }
WORK="$WORK" node "$SCRIPT_DIR/30-fix-references.mjs" "$KEY" >/dev/null || { echo "config-audit: SC2 FAIL (fix-references error)"; exit 1; }
# --- 2. Clean room (no marketplace parent) ---
rm -rf "$CR"
cp -R "$DEST" "$CR"
# --- 3. Re-seed the rebasable snapshot at the clone's own path (intended re-approval seam) ---
if [ ! -f "$CR/tests/snapshot-default-output.test.mjs" ]; then
echo "config-audit: SC2 FAIL (snapshot test missing from extract)"; exit 1
fi
( cd "$CR" && UPDATE_SNAPSHOT=1 node --test tests/snapshot-default-output.test.mjs ) >/dev/null 2>&1 \
|| { echo "config-audit: SC2 FAIL (snapshot re-seed error)"; exit 1; }
# --- 4. Build the gate: full suite MINUS the machine-locked v5.0.0 back-compat tests (excluded by name) ---
GATE_FILES="$(cd "$CR" && find tests -name '*.test.mjs' | grep -vE "$EXCLUDE_RE" | sort)"
if [ -z "$GATE_FILES" ]; then echo "config-audit: SC2 FAIL (no gate files enumerated)"; exit 1; fi
# Defense-in-depth: none of the machine-locked tests may leak into the gate.
if printf '%s\n' "$GATE_FILES" | grep -qE "$EXCLUDE_RE"; then
echo "config-audit: SC2 FAIL (machine-locked exclusion leaked into the gate)"; exit 1
fi
GATE_COUNT="$(printf '%s\n' "$GATE_FILES" | wc -l | tr -d '[:space:]')"
# --- 5. Run the gate in the clean room ---
OUT="$(cd "$CR" && node --test $GATE_FILES 2>&1)"; STATUS=$?
TESTS="$(printf '%s\n' "$OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)"
if [ "$STATUS" -ne 0 ]; then
echo "config-audit: SC2 FAIL (gate exit $STATUS)"
printf '%s\n' "$OUT" | tail -25
exit 1
fi
echo "config-audit: SC2 PASS (${TESTS:-?} tests across ${GATE_COUNT} files, full suite minus the 6-file v5.0.0 byte-stability surface, standalone-safe)"
exit 0

View file

@ -1,80 +0,0 @@
// Step 7 test — the SC2 resolver + generic validator are their own test surface:
// 1. The config-audit SC2 gate PASSes a standalone extract (re-seed + back-compat exclusion).
// 2. The gate excludes json-backcompat + raw-backcompat by NAME and re-seeds (not excludes) the snapshot.
// 3. The generic validator PASSes okr.
// 4. The generic validator FAILs a plugin whose plugin.json was deleted.
// Pattern: 40-validate-standalone.test.mjs (spawn the script, assert exit code + stdout).
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, cpSync, readFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const REPO = path.resolve(here, '..', '..', '..'); // migration -> marketplace-polyrepo-migration -> docs -> repo root
const SC2 = path.join(here, '50-config-audit-sc2.sh');
const GENERIC = path.join(here, 'templates', 'validate-plugin.generic.sh');
const WORK = process.env.WORK || '/tmp/polyrepo-migration';
function run(cmd, args, timeout = 600000) {
// Strip the parent test-runner context so the script's own `node --test` runs un-nested
// (otherwise Node emits its IPC subtest format and the test-count label is suppressed).
const env = { ...process.env, WORK };
delete env.NODE_TEST_CONTEXT;
return spawnSync(cmd, args, { encoding: 'utf8', env, timeout });
}
// The verified machine-locked v5.0.0 byte-stability surface (operator-ratified 2026-06-17):
// plan F4 named 2 files; the real surface is these 6. The other v5.0.0-referencing tests
// (posture, scoring-humanizer, scenario-read-test) are path-independent and stay IN the gate.
const MACHINE_LOCKED = [
'json-backcompat', 'raw-backcompat', 'cli-humanizer',
'posture-humanizer', 'scan-orchestrator-humanizer', 'lint-default-output',
];
test('config-audit SC2 gate PASSes a standalone extract (re-seed + machine-locked exclusion)', () => {
const r = run('bash', [SC2]);
assert.equal(r.status, 0, `expected SC2 PASS exit 0:\n${r.stdout}\n${r.stderr}`);
assert.match(r.stdout, /config-audit: SC2 PASS/);
assert.match(r.stdout, /minus the 6-file v5\.0\.0 byte-stability surface/);
});
test('the SC2 gate excludes the full machine-locked surface by name and re-seeds the snapshot', () => {
const src = readFileSync(SC2, 'utf8');
for (const name of MACHINE_LOCKED) {
assert.ok(src.includes(name), `EXCLUDE_RE must name the machine-locked test: ${name}`);
}
assert.match(src, /grep -vE "\$EXCLUDE_RE"/,
'the gate must filter the enumerated test list through EXCLUDE_RE');
assert.match(src, /UPDATE_SNAPSHOT=1/,
'the rebasable snapshot must be re-seeded in-clone, never excluded');
});
test('generic validator PASSes okr', () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'okr-ok-'));
try {
const root = path.join(tmp, 'okr');
cpSync(path.join(REPO, 'plugins', 'okr'), root, { recursive: true });
const r = run('bash', [GENERIC, 'okr', root], 60000);
assert.equal(r.status, 0, `expected okr STRUCTURE OK exit 0:\n${r.stdout}\n${r.stderr}`);
assert.match(r.stdout, /okr: STRUCTURE OK/);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
test('generic validator FAILs a plugin with a deleted plugin.json', () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'okr-broken-'));
try {
const root = path.join(tmp, 'okr');
cpSync(path.join(REPO, 'plugins', 'okr'), root, { recursive: true });
rmSync(path.join(root, '.claude-plugin', 'plugin.json'), { force: true });
const r = run('bash', [GENERIC, 'okr', root], 60000);
assert.notEqual(r.status, 0, `expected STRUCTURE FAIL (non-zero), got 0:\n${r.stdout}`);
assert.match(r.stdout, /okr: STRUCTURE FAIL/);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});

View file

@ -1,130 +0,0 @@
#!/usr/bin/env node
// Step 8 — marketplace.json rewriter (mixed-source aware, HTTPS + ref pin).
//
// Rewrites a marketplace.json so a NAMED SUBSET of plugin entries become external git sources
// { "name", "source": { "source": "url", "url": "https://git.fromaitochitta.com/open/<name>.git", "ref": "v<version>" }, "description" }
// (nested source-object per the official Claude Code marketplace schema — code.claude.com/docs/en/plugin-marketplaces)
// (F2 — HTTPS + discriminator `source`; D4 — ref pinned to the plugin's release tag), while the rest
// stay local `"source": "./plugins/<name>"`. This is what makes the mixed-source intermediate states
// possible (SC3/SC8): the operator can flip plugins one batch at a time and keep the marketplace live.
//
// --only <names> flip just these (comma/space-separated) e.g. --only "voyage,llm-security"
// --all flip every plugin (the final, fully-external state)
// --in <path> input marketplace.json (default: <repo>/.claude-plugin/marketplace.json)
// --out <path> REQUIRED output path. The rewriter NEVER writes the live file (D8 — NULL push /
// no live mutation outside the operator window). Refuses to run without --out.
//
// Versions + repo URLs are read from plugin-map.json (the single source of truth, verified from each
// plugin.json). Output is validated: every entry has name+source+description; every external entry has
// a valid https url under the Forgejo /open/ namespace plus a ref. Pure, idempotent transform.
import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
const DEFAULT_IN = path.join(REPO_ROOT, '.claude-plugin', 'marketplace.json');
const PLUGIN_MAP = path.join(HERE, 'plugin-map.json');
const FORGEJO_PREFIX = 'https://git.fromaitochitta.com/open/';
function parseArgs(argv) {
const args = { in: DEFAULT_IN, out: null, only: null, all: false };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--all') args.all = true;
else if (a === '--only') args.only = String(argv[++i] || '').split(/[,\s]+/).filter(Boolean);
else if (a === '--in') args.in = argv[++i];
else if (a === '--out') args.out = argv[++i];
else throw new Error(`unknown argument: ${a}`);
}
return args;
}
function externalSource(name, map) {
const t = map.targets && map.targets[name];
if (!t) throw new Error(`no plugin-map entry for '${name}' — cannot flip to external`);
if (typeof t.repo_url !== 'string' || !t.repo_url.startsWith(FORGEJO_PREFIX)) {
throw new Error(`plugin-map repo_url for '${name}' is not under ${FORGEJO_PREFIX}: ${t.repo_url}`);
}
if (typeof t.tag !== 'string' || !t.tag) throw new Error(`plugin-map has no tag for '${name}'`);
return { url: t.repo_url, ref: t.tag };
}
function validate(mp) {
if (!Array.isArray(mp.plugins)) throw new Error('marketplace.json has no plugins array');
for (const p of mp.plugins) {
if (typeof p.name !== 'string' || !p.name) throw new Error('an entry is missing name');
if (typeof p.description !== 'string' || !p.description) throw new Error(`entry ${p.name} missing description`);
if (p.source && typeof p.source === 'object') {
// external entry: nested source-object { source: 'url', url, ref } (official CC schema)
if (p.source.source !== 'url') throw new Error(`entry ${p.name} external source.source must be 'url': ${p.source.source}`);
if (typeof p.source.url !== 'string' || !p.source.url.startsWith('https://')) {
throw new Error(`entry ${p.name} external url is not https: ${p.source.url}`);
}
if (p.source.url.startsWith('ssh://')) throw new Error(`entry ${p.name} url is ssh (must be https)`);
if (typeof p.source.ref !== 'string' || !p.source.ref) throw new Error(`entry ${p.name} missing ref`);
} else if (typeof p.source !== 'string' || !p.source) {
// local entry must be a non-empty "./plugins/<name>" string
throw new Error(`entry ${p.name} missing source`);
}
}
}
export function rewrite({ inPath = DEFAULT_IN, only = null, all = false } = {}) {
const mp = JSON.parse(readFileSync(inPath, 'utf8'));
const map = JSON.parse(readFileSync(PLUGIN_MAP, 'utf8'));
if (!Array.isArray(mp.plugins)) throw new Error('marketplace.json has no plugins array');
const names = new Set(mp.plugins.map((p) => p.name));
if (!all && only) {
for (const n of only) {
if (!names.has(n)) throw new Error(`--only name not in marketplace.json: ${n}`);
}
}
const flip = all ? names : new Set(only || []);
let flipped = 0;
let local = 0;
mp.plugins = mp.plugins.map((p) => {
if (flip.has(p.name)) {
const { url, ref } = externalSource(p.name, map);
flipped++;
return { name: p.name, source: { source: 'url', url, ref }, description: p.description };
}
local++;
return p;
});
validate(mp);
return { mp, flipped, local };
}
function main() {
let args;
try {
args = parseArgs(process.argv.slice(2));
} catch (e) {
console.error(`error: ${e.message}`);
process.exit(2);
}
if (!args.all && (!args.only || args.only.length === 0)) {
console.error('error: specify --only <names> or --all');
process.exit(2);
}
if (!args.out) {
console.error('error: --out <path> is required — the rewriter never writes the live marketplace.json (D8)');
process.exit(2);
}
try {
const { mp, flipped, local } = rewrite({ inPath: args.in, only: args.only, all: args.all });
writeFileSync(args.out, JSON.stringify(mp, null, 2) + '\n');
console.log(`marketplace rewrite: ${flipped} external (HTTPS+ref), ${local} local (./plugins/), out=${args.out}`);
} catch (e) {
console.error(`error: ${e.message}`);
process.exit(1);
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main();
}

View file

@ -1,78 +0,0 @@
// Step 8 test — the rewriter must prove the mixed-source intermediate (the live-marketplace guarantee):
// --only voyage -> voyage external (https url + ref:v5.1.1), the other 9 stay ./plugins/, no ssh://
// --all -> zero ./plugins/ sources, every entry external https+ref, schema-valid
// safety -> refuses to run without --out; rejects an unknown --only name
// Pattern: plugins/voyage/tests/parsers/*.test.mjs (CLI exercised exactly as the plan's Verify does).
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, readFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const CLI = path.join(here, '60-rewrite-marketplace.mjs');
function run(args) {
return spawnSync('node', [CLI, ...args], { encoding: 'utf8' });
}
function readJson(p) {
return JSON.parse(readFileSync(p, 'utf8'));
}
test('--only voyage flips just voyage to external https+ref:v5.1.1, leaves 9 local', () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'mp-only-'));
try {
const out = path.join(tmp, 'mp.json');
const r = run(['--only', 'voyage', '--out', out]);
assert.equal(r.status, 0, `expected exit 0:\n${r.stdout}\n${r.stderr}`);
const mp = readJson(out);
const voyage = mp.plugins.find((p) => p.name === 'voyage');
assert.equal(voyage.source.source, 'url', 'nested source-object discriminator must be url');
assert.ok(voyage.source.url.startsWith('https://git.fromaitochitta.com/open/'), `voyage url: ${voyage.source.url}`);
assert.equal(voyage.source.ref, 'v5.1.1');
const local = mp.plugins.filter((p) => typeof p.source === 'string' && p.source.startsWith('./plugins/'));
assert.equal(local.length, 9, 'exactly 9 entries must stay local (mixed-source proven)');
assert.ok(!JSON.stringify(mp).includes('ssh://'), 'no ssh:// anywhere');
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
test('--all leaves zero ./plugins/ sources; every entry external https+ref', () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'mp-all-'));
try {
const out = path.join(tmp, 'mp.json');
const r = run(['--all', '--out', out]);
assert.equal(r.status, 0, `expected exit 0:\n${r.stdout}\n${r.stderr}`);
const mp = readJson(out);
const local = mp.plugins.filter((p) => typeof p.source === 'string' && p.source.startsWith('./plugins/'));
assert.equal(local.length, 0, '--all must leave zero local sources');
for (const p of mp.plugins) {
assert.equal(p.source.source, 'url', `${p.name} should be external (nested source-object)`);
assert.ok(p.source.url.startsWith('https://'), `${p.name} url must be https: ${p.source.url}`);
assert.ok(typeof p.source.ref === 'string' && p.source.ref.length > 0, `${p.name} must have a ref`);
assert.ok(typeof p.description === 'string' && p.description.length > 0, `${p.name} must keep its description`);
}
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
test('refuses to run without --out (never writes the live file — D8)', () => {
const r = run(['--all']);
assert.notEqual(r.status, 0, 'must refuse without --out');
assert.match(r.stderr, /--out .* required/);
});
test('rejects an unknown --only name', () => {
const tmp = mkdtempSync(path.join(os.tmpdir(), 'mp-bad-'));
try {
const r = run(['--only', 'does-not-exist', '--out', path.join(tmp, 'mp.json')]);
assert.notEqual(r.status, 0, 'must reject an unknown plugin name');
assert.match(r.stderr, /not in marketplace\.json/);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});

View file

@ -1,135 +0,0 @@
#!/usr/bin/env bash
# Step 9 — catalog-thinning script (staged for the operator window).
#
# Renders the THIN-CATALOG end-state into a --workspace. NEVER mutates the live tree
# (forbidden_paths: plugins, shared, CLAUDE.md, README.md) — every write lands inside --workspace.
# After the operator window the catalog repo hosts only the marketplace manifest + the catalog-level
# docs; the 10 plugins + the design-system live in their own Forgejo repos under
# https://git.fromaitochitta.com/open/. This script produces that state for inspection/staging:
#
# 1. archives the tracked catalog (git archive HEAD) into --workspace (clean: no .git, no untracked)
# 2. removes plugins/, shared/, scripts/sync-design-system.mjs (+ its test)
# 3. extracts the marketplace conventions from CLAUDE.md into CONVENTIONS.md (D6)
# 4. thins the catalog CLAUDE.md to catalog-maintenance only
# 5. rewrites the landing README.md to point at the external plugin repos, re-stating every version
# from plugin-map.json (the verified source). M15 is COUPLED to this D6 rewrite, not a ride-along:
# a roster that re-states versions/counts must state the verified ones or it ships a false document.
#
# NULL push (D8): writes only inside --workspace.
#
# Usage: 70-thin-catalog.sh --workspace <dir>
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
WS=""
while [ $# -gt 0 ]; do
case "$1" in
--workspace) WS="${2:-}"; shift 2 ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
done
[ -n "$WS" ] || { echo "usage: 70-thin-catalog.sh --workspace <dir>" >&2; exit 2; }
[ -f "$MAP" ] || { echo "plugin-map.json missing at $MAP" >&2; exit 1; }
# --- 1. Archive the tracked catalog into the workspace (no .git, no untracked) ---
rm -rf "$WS"
mkdir -p "$WS"
git -C "$REPO_ROOT" archive HEAD | tar -x -C "$WS"
# --- 2. Remove the components that move out to their own repos ---
rm -rf "$WS/plugins" "$WS/shared" \
"$WS/scripts/sync-design-system.mjs" "$WS/scripts/sync-design-system.test.mjs"
rmdir "$WS/scripts" 2>/dev/null || true
# --- 3. Extract conventions -> CONVENTIONS.md (D6) ---
{
echo "# Conventions — ktg-plugin-marketplace catalog"
echo
echo "> Extracted from the marketplace CLAUDE.md (D6). These are the marketplace-wide conventions"
echo "> every plugin repo inherits under fork-and-own. See GOVERNANCE.md for the governance model."
echo
awk '
/^## Konvensjoner/ { f=1; print; next }
f && /^## / { exit }
f { print }
' "$WS/CLAUDE.md"
} > "$WS/CONVENTIONS.md"
# --- 4. Thin the catalog CLAUDE.md to catalog-maintenance only ---
cat > "$WS/CLAUDE.md" <<'MD'
# ktg-plugin-marketplace (catalog)
Catalog repository for the ktg-plugin-marketplace. After the polyrepo migration this repo hosts only
the marketplace manifest and the catalog-level docs; every plugin and the shared design-system live in
their own Forgejo repositories under `https://git.fromaitochitta.com/open/`.
## What lives here
- `.claude-plugin/marketplace.json` — the marketplace manifest (plugin entries point at external repos)
- `README.md` — the landing/catalog page
- `CONVENTIONS.md` — marketplace-wide conventions inherited by every plugin repo
- `GOVERNANCE.md` — governance + fork-and-own model
- `.mailmap`, `.gitleaks.toml`, `.gitleaksignore` — shared git-hygiene baselines
## Catalog maintenance
- Marketplace conventions: see CONVENTIONS.md.
- Adding/updating a plugin entry: edit `.claude-plugin/marketplace.json` (external `source: "url"` with
a pinned `ref`) and re-state the plugin in README.md with its verified version.
- Plugin source, issues, and releases live in each plugin's own repository — not here.
MD
# --- 5. Rewrite the landing README.md: external repo links + versions re-stated from plugin-map.json ---
python3 - "$MAP" "$WS/README.md" <<'PY'
import json, re, sys
map_path, readme_path = sys.argv[1], sys.argv[2]
targets = json.load(open(map_path))["targets"]
def browse(key):
u = targets[key]["repo_url"]
return u[:-4] if u.endswith(".git") else u
tag = {k: targets[k].get("tag", "") for k in targets}
# shared/playground-examples/ is bundled into the playground-design-system repo (plugin-map path_renames)
EXAMPLES_OWNER = "playground-design-system"
src = open(readme_path).read()
def link_repl(m):
container, key, sub = m.group(1), m.group(2), (m.group(3) or "")
sub = sub.lstrip("/")
if container == "shared" and key == "playground-examples":
owner = EXAMPLES_OWNER
subpath = ("playground-examples/" + sub) if sub else ""
elif key in targets:
owner = key
subpath = sub
else:
return m.group(0) # unknown — leave as-is (surfaced by the no-local-links assertion)
base = browse(owner)
if not subpath or subpath == "README.md":
return "](%s)" % base
return "](%s/src/branch/main/%s)" % (base, subpath)
# swap every local ](plugins/<key>/...) / ](shared/<key>/...) link to its external repo
src = re.sub(r"\]\((plugins|shared)/([a-z0-9-]+)(/[^)]*)?\)", link_repl, src)
# re-state the version backtick on heading lines that now link to a known external repo
def fix_version(line):
m = re.search(r"\]\(https://git\.fromaitochitta\.com/open/([a-z0-9-]+)\)", line)
if m and m.group(1) in tag and tag[m.group(1)]:
line = re.sub(r"`v[0-9][^`]*`", "`%s`" % tag[m.group(1)], line, count=1)
return line
out = "\n".join(fix_version(l) for l in src.split("\n"))
open(readme_path, "w").write(out)
PY
echo "thin-catalog ready at $WS"
echo " removed: plugins/ shared/ scripts/sync-design-system.mjs"
echo " added: CONVENTIONS.md (extracted from CLAUDE.md conventions)"
echo " README.md rewritten: external repo links + versions re-stated from plugin-map.json"

View file

@ -1,78 +0,0 @@
// Step 9 test — the thin-catalog state (against a workspace copy):
// - no plugins/, no shared/, no scripts/sync-design-system.mjs (SC4)
// - CONVENTIONS.md exists + carries the Forgejo / conventional-commits / three-doc rules (D6)
// - marketplace.json + README.md + GOVERNANCE.md remain
// - the rewritten README has zero local plugins//shared/ links and uses external Forgejo repos
// - the stale playground-design-system version is corrected from plugin-map (v0.6.0)
// Pattern: plugins/config-audit/tests/*.test.mjs.
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT = path.join(here, '70-thin-catalog.sh');
function thin() {
const ws = mkdtempSync(path.join(os.tmpdir(), 'thin-catalog-'));
const r = spawnSync('bash', [SCRIPT, '--workspace', ws], { encoding: 'utf8' });
return { ws, r };
}
test('removes plugins/, shared/, and scripts/sync-design-system.mjs (SC4)', () => {
const { ws, r } = thin();
try {
assert.equal(r.status, 0, `expected exit 0:\n${r.stdout}\n${r.stderr}`);
assert.ok(!existsSync(path.join(ws, 'plugins')), 'plugins/ must be gone');
assert.ok(!existsSync(path.join(ws, 'shared')), 'shared/ must be gone');
assert.ok(!existsSync(path.join(ws, 'scripts', 'sync-design-system.mjs')), 'sync-design-system.mjs must be gone');
} finally {
rmSync(ws, { recursive: true, force: true });
}
});
test('CONVENTIONS.md exists and carries Forgejo / conventional-commits / three-doc rules', () => {
const { ws, r } = thin();
try {
assert.equal(r.status, 0, r.stderr);
const conv = path.join(ws, 'CONVENTIONS.md');
assert.ok(existsSync(conv), 'CONVENTIONS.md must exist');
const text = readFileSync(conv, 'utf8');
assert.match(text, /Forgejo/, 'must carry the Forgejo rule');
assert.match(text, /Conventional Commits/, 'must carry the conventional-commits rule');
assert.match(text, /tre doc-nivåer/, 'must carry the three-doc rule');
} finally {
rmSync(ws, { recursive: true, force: true });
}
});
test('marketplace.json, README.md, GOVERNANCE.md remain', () => {
const { ws, r } = thin();
try {
assert.equal(r.status, 0, r.stderr);
assert.ok(existsSync(path.join(ws, '.claude-plugin', 'marketplace.json')), 'marketplace.json must remain');
assert.ok(existsSync(path.join(ws, 'README.md')), 'README.md must remain');
assert.ok(existsSync(path.join(ws, 'GOVERNANCE.md')), 'GOVERNANCE.md must remain');
} finally {
rmSync(ws, { recursive: true, force: true });
}
});
test('rewritten README has no local plugins//shared/ links and corrects the DS version (M15)', () => {
const { ws, r } = thin();
try {
assert.equal(r.status, 0, r.stderr);
const readme = readFileSync(path.join(ws, 'README.md'), 'utf8');
assert.ok(!/\]\(plugins\//.test(readme), 'no local plugins/ links may remain');
assert.ok(!/\]\(shared\//.test(readme), 'no local shared/ links may remain');
assert.match(readme, /https:\/\/git\.fromaitochitta\.com\/open\//, 'must reference external Forgejo repos');
// playground-design-system was stale at v0.1 in the live README; plugin-map pins v0.6.0
assert.ok(!/`v0\.1`/.test(readme), 'stale playground-design-system v0.1 must be corrected');
assert.match(readme, /`v0\.6\.0`/, 'playground-design-system must read v0.6.0 from plugin-map');
} finally {
rmSync(ws, { recursive: true, force: true });
}
});

View file

@ -1,252 +0,0 @@
#!/usr/bin/env bash
# Step 11 — Full local dry-run (all 11 targets, NULL push).
#
# The single end-to-end rehearsal of the Claude-run local half. For all 11 targets (10 plugins + the
# design-system) it FORCE-FRESH re-extracts (Steps 3->4->5: never reuses a possibly-stale $WORK/<key>),
# then validates each three ways:
# SC6 (content retention) — extract git-tracked file count vs the live monorepo's tracked count for the
# target's paths. A drop (not explained by an intentional blob-strip) is a hard FAIL: this is the
# check that catches the llm-security dual-rename class of defect (87 files dropped) deterministically.
# SC7 (no state-file leak) — no tracked STATE.md / *.local.md (except the okr template).
# SC2 (no test REGRESSION) — config-audit routes to its dedicated Step-7 gate (50-config-audit-sc2.sh);
# every other target runs its declared standalone runner via 40-validate-standalone.sh. If the
# standalone runner fails, the gate is REGRESSION-RELATIVE: it re-runs the same suite in the live
# monorepo and PASSES iff the standalone failing-test set is a SUBSET of the in-repo failing set
# (i.e. the extraction introduced NO new failures). Pre-existing in-repo red and environmental
# flake are the plugin's own concern, NOT a migration regression, and are reported transparently
# (never masked) — the migration's contract is "introduce no regression".
# Then it exercises Step 8's marketplace rewriter (--all -> --out) and Step 9's catalog thinning
# (--workspace), and runs the cross-repo DS-vendor chain (M9 / SC5): sync playground-design-system
# (--source) into ms-ai-architect (--target), then --check for byte-parity. It writes
# $WORK/dryrun-report.md (an OUTPUT artifact, never committed).
#
# Reuses the dedicated `git clone --no-local` extraction mirror ($WORK/_mirror, R4/M6) so extraction never
# reads the working checkout. NULL push (D8): zero pushes, zero live-file mutation — the live plugins/,
# shared/, and .claude-plugin/marketplace.json are never touched; every write lands in $WORK. Honest scope
# (M8): this proves the LOCAL mechanics only; the window-only steps (Forgejo accepting post-strip history,
# auto_init:false API repo creation, Claude Code resolving an HTTPS url+ref source) are gated by the
# Step 10 pilot, NOT by this dry-run.
#
# On any hard FAIL (SC6 drop, SC7 leak, or an SC2 regression): exit non-zero (escalate — never mask a
# real pre-window blocker).
#
# Usage: 99-dryrun.sh (override WORK= to relocate the workspace; default /tmp/polyrepo-migration)
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
WORK="${WORK:-/tmp/polyrepo-migration}"
MIRROR="$WORK/_mirror"
REPORT="$WORK/dryrun-report.md"
MP_PREVIEW="$WORK/marketplace.all-external.json"
THIN_WS="$WORK/_thin-catalog"
SYNC="$REPO_ROOT/scripts/sync-design-system.mjs"
fail() { printf 'DRY-RUN FAIL: %s\n' "$*" >&2; exit 1; }
mapget() { python3 -c "import json;print(json.load(open('$MAP'))['targets']['$1'].get('$2',''))"; }
mappaths() { python3 -c "import json;print(' '.join(json.load(open('$MAP'))['targets']['$1']['paths']))"; }
# Live monorepo git-tracked file count across a target's source paths (SC6 baseline).
# Paths are asserted whitespace- and glob-free by 00-preflight.sh (aeb6292), so the word-split below is sound.
live_files() {
local key="$1" total=0 p n
for p in $(mappaths "$key"); do
n="$(git -C "$REPO_ROOT" ls-files "$p" | wc -l | tr -d '[:space:]')"
total=$((total + n))
done
echo "$total"
}
# Failing-test NAME set for a node:test suite — delegated to capture-fails.sh so the SAME capture feeds both
# this rehearsal and the live operator-window gate (41-validate-or-regression.sh). $1 = dir, $2 = node --test cmd.
capture_fails() { bash "$SCRIPT_DIR/capture-fails.sh" "$1" "$2"; }
[ -f "$MAP" ] || fail "plugin-map.json missing at $MAP"
mkdir -p "$WORK"
KEYS="$(python3 -c "import json;print('\n'.join(sorted(json.load(open('$MAP'))['targets'])))")" \
|| fail "plugin-map.json does not parse"
TARGET_COUNT="$(printf '%s\n' "$KEYS" | grep -c .)"
[ "$TARGET_COUNT" = "11" ] || fail "expected 11 targets, found $TARGET_COUNT"
# Ensure the dedicated --no-local mirror exists (R4/M6); extraction never reads the working checkout.
if [ ! -e "$MIRROR/HEAD" ] && [ ! -d "$MIRROR/.git" ]; then
echo " mirror absent -> running 00-preflight.sh (builds the --no-local extraction mirror)"
WORK="$WORK" bash "$SCRIPT_DIR/00-preflight.sh" >/dev/null || fail "preflight (mirror build) failed"
fi
{
echo "# Polyrepo Migration — Local Dry-Run Report"
echo
echo "> Output artifact (NOT committed). Workspace: \`$WORK\`. Force-fresh extraction (no stale reuse)."
echo "> **NULL push (D8):** 0 pushes, 0 live-file mutation (plugins/, shared/, marketplace.json untouched)."
echo "> Extraction reuses the dedicated \`git clone --no-local\` mirror (R4/M6) — never the working checkout."
echo "> **SC2 is regression-relative:** a target passes iff the extraction introduces NO test failure that"
echo "> does not also occur in the live monorepo. Pre-existing in-repo red / flake is reported, never masked."
echo
echo "## Per-target — force-fresh extraction + standalone validation (Steps 3→4→5, +6/+7)"
echo
echo "| target | commits | files ext/live | tag | SC2 | SC7 |"
echo "|--------|--------:|----------------|-----|-----|-----|"
} > "$REPORT"
PASSED=0
for key in $KEYS; do
dest="$WORK/$key"
ok=1
# --- force-fresh extract + prep (Steps 3->4->5; 10-extract wipes $dest first) ---
WORK="$WORK" bash "$SCRIPT_DIR/10-extract.sh" "$key" >/dev/null 2>&1 || ok=0
WORK="$WORK" bash "$SCRIPT_DIR/20-rehome-config.sh" "$key" >/dev/null 2>&1 || ok=0
WORK="$WORK" node "$SCRIPT_DIR/30-fix-references.mjs" "$key" >/dev/null 2>&1 || ok=0
# --- report data + SC6 + SC7 from the fresh extract ---
commits=0; tag="(none)"; ext_files=0; sc7="clean"; sc6="?"
if [ -d "$dest/.git" ]; then
commits="$(git -C "$dest" rev-list --count HEAD 2>/dev/null || echo 0)"
# F5 invariant: 10-extract.sh strips all carried-over tags and re-creates EXACTLY one. Assert that —
# a partial tag-strip leaves multiple tags, and `head -1` would silently report the lexicographically
# -first wrong one (e.g. v1.0.0 instead of v5.1.1) while the dry-run still passed (1708e90).
tagn="$(git -C "$dest" tag 2>/dev/null | wc -l | tr -d '[:space:]')"
if [ "$tagn" = "1" ]; then tag="$(git -C "$dest" tag 2>/dev/null)"; else tag="(tags=$tagn!)"; ok=0; fi
ext_files="$(git -C "$dest" ls-files | wc -l | tr -d '[:space:]')"
leak="$(git -C "$dest" ls-files | grep -E 'STATE\.md|\.local\.md$' \
| grep -v 'templates/okr\.local\.md\.template' || true)"
[ -z "$leak" ] || { sc7="LEAK: $(printf '%s' "$leak" | tr '\n' ' ')"; ok=0; }
else
ok=0
fi
# SC6 content retention — delegated to sc6-check.sh (the DROP detector, unit-tested in sc-checks.test.mjs).
# Only meaningful when the extract succeeded: a failed extract leaves ext_files=0, which must NOT be
# mislabelled as a content DROP (8d649e9) — emit (extract failed) instead. An intentional >1MB blob-strip
# target may legitimately shed blobs (reported, not failed); a shortfall elsewhere is a hard DROP (ok=0).
lf="$(live_files "$key")"
blob="$(mapget "$key" blob_strip)"
if [ -d "$dest/.git" ]; then
if sc6="$(bash "$SCRIPT_DIR/sc6-check.sh" "$ext_files" "$lf" "$blob")"; then :; else ok=0; fi
else
sc6="(extract failed)"
fi
# --- SC2 (regression-relative) ---
sc2_gate="$(mapget "$key" sc2_gate)"
test_cmd="$(mapget "$key" test_cmd)"
if [ -n "$sc2_gate" ]; then
WORK="$WORK" bash "$SCRIPT_DIR/$sc2_gate" >/dev/null 2>&1 && sc2="PASS" || { sc2="FAIL"; ok=0; }
else
if WORK="$WORK" bash "$SCRIPT_DIR/40-validate-standalone.sh" "$key" >/dev/null 2>&1; then
sc2="PASS"
else
case "$test_cmd" in
*node\ --test*)
# Regression-relative: the standalone failing set must be a SUBSET of the in-repo failing set.
# Capture the standalone set from the dry-run's OWN prepped extract ($dest = $WORK/$key), NOT the
# 40-validate side-effect clean room /tmp/claude-$key (4e494c8 — that implicit coupling masked a
# real regression when the dir was absent). Guard mktemp: an empty capture is an ERROR, never a
# false zero-regression PASS (4044c49). The subset decision is delegated to sc2-regression.sh.
if sf="$(mktemp)" && bf="$(mktemp)"; then
capture_fails "$dest" "$test_cmd" > "$sf" 2>/dev/null
capture_fails "$REPO_ROOT/plugins/$key" "$test_cmd" > "$bf" 2>/dev/null
pre="$(wc -l < "$bf" | tr -d '[:space:]')"
if regr="$(bash "$SCRIPT_DIR/sc2-regression.sh" "$sf" "$bf")"; then
sc2="PASS (${pre} pre-existing)"
else
rc=$?
if [ "$rc" -ge 2 ]; then sc2="FAIL (sc2-regression error)"; else
sc2="FAIL (regression: $(printf '%s' "$regr" | grep -c .))"; fi
ok=0
fi
rm -f "$sf" "$bf"
else
sc2="FAIL (mktemp)"; ok=0; rm -f "$sf" "$bf"
fi
;;
*) sc2="FAIL (structure)"; ok=0 ;; # deterministic structure check genuinely failed
esac
fi
fi
printf '| %s | %s | %s | %s | %s | %s |\n' "$key" "$commits" "$sc6" "$tag" "$sc2" "$sc7" >> "$REPORT"
echo " $key: commits=$commits files=$sc6 sc2=$sc2 sc7=${sc7%%:*}"
[ "$ok" -eq 1 ] && PASSED=$((PASSED+1))
done
# --- Step 8: all-external marketplace.json preview (F2 — HTTPS + ref, zero local) ---
if node "$SCRIPT_DIR/60-rewrite-marketplace.mjs" --all --out "$MP_PREVIEW" >/dev/null 2>&1; then
LOCAL_REFS="$(grep -c '\./plugins/' "$MP_PREVIEW" 2>/dev/null || true)"; LOCAL_REFS="${LOCAL_REFS:-0}"
SSH_REFS="$(grep -c 'ssh://' "$MP_PREVIEW" 2>/dev/null || true)"; SSH_REFS="${SSH_REFS:-0}"
EXT_REFS="$(grep -c '"source": "url"' "$MP_PREVIEW" 2>/dev/null || true)"; EXT_REFS="${EXT_REFS:-0}"
else
LOCAL_REFS="?"; SSH_REFS="?"; EXT_REFS="?"
fi
# --- Step 9: thin-catalog preview (SC4 — plugins/ & shared/ removed, CONVENTIONS.md extracted) ---
THIN_OK="yes"
if bash "$SCRIPT_DIR/70-thin-catalog.sh" --workspace "$THIN_WS" >/dev/null 2>&1; then
[ -d "$THIN_WS/plugins" ] && THIN_OK="no (plugins/ remain)"
[ -d "$THIN_WS/shared" ] && THIN_OK="no (shared/ remain)"
[ -f "$THIN_WS/CONVENTIONS.md" ] || THIN_OK="no (CONVENTIONS.md missing)"
else
THIN_OK="no (script error)"
fi
# --- M9 / SC5: cross-repo DS-vendor chain (the gap the pilot can't cover) ---
DS="$WORK/playground-design-system"; MSA="$WORK/ms-ai-architect"
DS_VENDOR="skipped (DS or ms-ai-architect extract missing)"
if [ -d "$DS/.git" ] && [ -d "$MSA/.git" ] && [ -f "$SYNC" ]; then
if node "$SYNC" ms-ai-architect --source "$DS" --target "$MSA" >/dev/null 2>&1 \
|| node "$SYNC" ms-ai-architect --source "$DS" --target "$MSA" --force >/dev/null 2>&1; then
CHK="$(node "$SYNC" ms-ai-architect --source "$DS" --target "$MSA" --check 2>&1)"
if [ $? -eq 0 ]; then DS_VENDOR="OK — $CHK"; else DS_VENDOR="DRIFT — $CHK"; fi
else
DS_VENDOR="sync failed"
fi
fi
ALL_OK=1
[ "$PASSED" = "11" ] || ALL_OK=0
[ "$LOCAL_REFS" = "0" ] || ALL_OK=0
[ "$SSH_REFS" = "0" ] || ALL_OK=0
[ "$THIN_OK" = "yes" ] || ALL_OK=0
case "$DS_VENDOR" in OK*) : ;; *) ALL_OK=0 ;; esac
{
echo
echo "## marketplace.json preview — all external (Step 8, F2)"
echo
echo "- out: \`$MP_PREVIEW\` · local \`./plugins/\`: $LOCAL_REFS (want 0) · \`ssh://\`: $SSH_REFS (want 0) · external (https+ref): $EXT_REFS"
echo
echo "## thin-catalog preview (Step 9, SC4)"
echo
echo "- workspace: \`$THIN_WS\` · plugins/ & shared/ removed + CONVENTIONS.md extracted: $THIN_OK"
echo
echo "## DS-vendor cross-repo validation (M9 / SC5)"
echo
echo "- \`playground-design-system\` (--source) → \`ms-ai-architect\` (--target) → \`--check\`: $DS_VENDOR"
echo
echo "## Honest scope of this dry-run (M8)"
echo
echo "Proves the **local mechanics**: force-fresh rename-aware extraction + content retention (SC6),"
echo "re-rooting, reference rewrites, regression-relative standalone validation (SC2/SC7), the"
echo "marketplace/catalog transforms, and the DS-vendor chain. It does **not** prove the **window-only**"
echo "steps (Forgejo push of post-strip history, \`auto_init:false\` API repo creation, Claude Code"
echo "resolving an HTTPS \`url\`+\`ref\` source). Those are gated by the **Step 10 pilot**, not this dry-run."
echo
if [ "$ALL_OK" = "1" ]; then
echo "**DRY-RUN OK — $PASSED/11 targets extracted + validated, 0 pushes**"
else
echo "**DRY-RUN INCOMPLETE — $PASSED/11 targets passed; inspect the rows above**"
fi
} >> "$REPORT"
echo " report: $REPORT"
if [ "$ALL_OK" = "1" ]; then
echo "DRY-RUN OK — 11/11 targets extracted + validated, 0 pushes"
exit 0
else
echo "DRY-RUN INCOMPLETE — $PASSED/11 targets passed; see $REPORT" >&2
exit 1
fi

View file

@ -1,93 +0,0 @@
// Step 11 test — the full local dry-run report ($WORK/dryrun-report.md) + previews.
// Verifies the GENERATED artifact (does not re-derive it):
// - the report lists all 11 targets
// - voyage shows >=250 commits (F1 — pre-rename history retained)
// - config-audit shows PASS under its dedicated SC2 gate (50-config-audit-sc2.sh)
// - llm-security shows complete content retention (SC6 — no file DROP; the dual-rename defect is fixed)
// - no SC6 DROP and no SC2 regression on ANY target
// - no target shows a tracked state-file leak (SC7)
// - the previewed all-external marketplace.json has zero ./plugins/ and zero ssh:// (F2)
// Pattern: plugins/voyage/tests/integration/*.test.mjs (heavy integration — long timeout, artifact reuse).
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT = path.join(here, '99-dryrun.sh');
const WORK = process.env.WORK || '/tmp/polyrepo-migration';
const REPORT = path.join(WORK, 'dryrun-report.md');
const MP_PREVIEW = path.join(WORK, 'marketplace.all-external.json');
const LONG = 1000 * 60 * 25; // cold path force-fresh re-extracts 11 repos + runs their suites
const TARGETS = [
'ai-psychosis', 'claude-design', 'config-audit', 'graceful-handoff',
'human-friendly-style', 'linkedin-studio', 'llm-security', 'ms-ai-architect',
'okr', 'playground-design-system', 'voyage',
];
let _report = null;
function report() {
if (_report !== null) return _report;
if (!existsSync(REPORT)) {
const r = spawnSync('bash', [SCRIPT], { encoding: 'utf8', timeout: 1000 * 60 * 24, env: { ...process.env, WORK } });
if (!existsSync(REPORT)) {
throw new Error(`dry-run did not produce ${REPORT}\nstdout:\n${r.stdout}\nstderr:\n${r.stderr}`);
}
}
_report = readFileSync(REPORT, 'utf8');
return _report;
}
function row(key) {
return report().split('\n').find((l) => new RegExp(`^\\|\\s*${key}\\s*\\|`).test(l)) || '';
}
test('report lists all 11 targets', { timeout: LONG }, () => {
const r = report();
for (const t of TARGETS) assert.ok(r.includes(t), `report must list target ${t}`);
const rows = r.split('\n').filter((l) => /^\|\s*[a-z0-9-]+\s*\|\s*\d+\s*\|/.test(l));
assert.equal(rows.length, 11, `expected 11 target rows, found ${rows.length}`);
});
test('voyage shows >=250 commits (F1 pre-rename history retained)', { timeout: LONG }, () => {
const m = report().match(/\|\s*voyage\s*\|\s*(\d+)\s*\|/);
assert.ok(m, 'voyage row with a commit count must be present');
assert.ok(Number(m[1]) >= 250, `voyage commits ${m && m[1]} must be >= 250`);
});
test('config-audit PASSes its dedicated SC2 gate', { timeout: LONG }, () => {
const line = row('config-audit');
assert.ok(line, 'config-audit row must be present');
assert.ok(/\bPASS\b/.test(line) && !/FAIL/.test(line), `config-audit must PASS: ${line}`);
});
test('llm-security retains all content (SC6 — dual-rename file-drop defect is fixed)', { timeout: LONG }, () => {
const line = row('llm-security');
assert.ok(line, 'llm-security row must be present');
assert.ok(!/DROP/.test(line), `llm-security must not drop files (SC6): ${line}`);
assert.ok(/\bPASS\b/.test(line) && !/FAIL/.test(line), `llm-security must PASS standalone: ${line}`);
});
// This asserts the HAPPY path (the live report shows no DROP / no regression). The NEGATIVE coverage —
// proving the SC6 DROP and SC2 regression-FAIL detectors actually FIRE — lives in sc-checks.test.mjs,
// which drives sc6-check.sh / sc2-regression.sh (the extracted detectors this dry-run calls) directly,
// because force-fresh re-extraction would undo any file-drop planted into $WORK/<key> here (5d112cb).
test('no SC6 file-drop and no SC2 regression on any target', { timeout: LONG }, () => {
const r = report();
assert.ok(!/\bDROP\b/.test(r), 'no target may drop content (SC6)');
assert.ok(!/regression:/.test(r), 'no target may show an SC2 regression');
});
test('no target shows a tracked state-file leak (SC7)', { timeout: LONG }, () => {
assert.ok(!/LEAK/.test(report()), 'no SC7 state-file leak may appear in the report');
});
test('all-external marketplace.json preview has zero ./plugins/ and zero ssh:// (F2)', { timeout: LONG }, () => {
report();
assert.ok(existsSync(MP_PREVIEW), `${MP_PREVIEW} must exist`);
const mp = readFileSync(MP_PREVIEW, 'utf8');
assert.ok(!mp.includes('./plugins/'), 'no local ./plugins/ source may remain');
assert.ok(!mp.includes('ssh://'), 'no ssh:// url may remain');
});

View file

@ -1,197 +0,0 @@
# Polyrepo migration — operator-window RUNBOOK
The single document the operator follows in the authorized window. Everything before this (Steps 19,
plus the Step 11 dry-run) was authored and verified locally with **NULL push (D8)**. This runbook is the
**only** place live Forgejo repos are created and the live `marketplace.json` is mutated.
## Preconditions
- **`git-filter-repo`** (a non-default git extension) and **`python3 >= 3.6`** on `PATH` — the extraction
(`10-extract.sh`) and every map-driven script depend on them. `00-preflight.sh` and `10-extract.sh`
both assert them, but install first: `brew install git-filter-repo python3`.
- All migration scripts committed locally; the Step 11 dry-run is green (it exercises every script below).
- `$FORGEJO_TOKEN` is exported (macOS Keychain → `~/.zshenv`) with `write:org` + `repo` scope.
- The `pre-polyrepo-archive` tag exists locally (D2) — the rollback anchor for the whole operation.
- **Push window (private-work policy):** this runbook pushes to Forgejo. Run it only inside the allowed
push window — weekdays 20:0023:00 (Europe/Oslo), or any time on weekends / Norwegian holidays.
- `WORK=/tmp/polyrepo-migration` (the mirror + per-target extracts the scripts build).
- Visibility: **public** (§9) — every repo is created `"private": false`.
Paths below assume you are in the catalog repo root unless stated. Scripts live in
`docs/marketplace-polyrepo-migration/migration/` (referred to as `MIG/`).
---
## (0) Pilot gate — `graceful-handoff`
The pilot proves the highest-stakes unverified link: **HTTPS + external `source` + `ref` pin + Forgejo
resolution** (F2 / Assumption 1). Pilot = `graceful-handoff` (low-churn, has tests, no design-system
vendor, no blob bloat).
> **B3 ordering — non-negotiable:** the catalog entry MUST be flipped to external *before* the
> install-smoke. If you install while the entry is still `./plugins/graceful-handoff`, the install
> resolves the **local** copy and the test is vacuous — it proves nothing about the Forgejo chain.
**(0a) Create the repo on Forgejo** (`auto_init:false` so the first push defines history — an
auto-initialised repo would create a divergent root commit the rename-aware extract cannot fast-forward):
```bash
curl -fsS -X POST "https://git.fromaitochitta.com/api/v1/orgs/open/repos" \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"graceful-handoff","private":false,"auto_init":false,"default_branch":"main"}'
```
**(0b) Push the extracted repo** (the extract was prepared + validated by the harness; re-run it to be
sure it is green, then push all branches + tags):
```bash
WORK=$WORK bash MIG/40-validate-standalone.sh graceful-handoff # must print: graceful-handoff: PASS (...)
cd "$WORK/graceful-handoff"
git remote add origin https://git.fromaitochitta.com/open/graceful-handoff.git
git push origin --all
git push origin --tags # publishes v2.1.0 — the ref the catalog will pin
cd - # back to the catalog repo
```
**(0c) Flip the catalog entry to external and push the catalog in its still-mixed state**
(9× `./plugins/x` + 1× external; `plugins/` is still present — thinning is last):
```bash
node MIG/60-rewrite-marketplace.mjs --only graceful-handoff \
--in .claude-plugin/marketplace.json --out /tmp/mp.json
# review /tmp/mp.json: graceful-handoff is now {source:"url", url:..., ref:"v2.1.0"}, the other 9 unchanged
cp /tmp/mp.json .claude-plugin/marketplace.json
git add .claude-plugin/marketplace.json
git commit -m "chore(marketplace): externalise graceful-handoff (pilot)"
git push origin main
```
**(0d) Install-smoke from a FRESH Claude Code session** — the actual Forgejo-chain test:
```text
/plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
/plugin install graceful-handoff@ktg-plugin-marketplace
```
Because the entry is now external, the install resolves the Forgejo repo over **HTTPS at `ref: v2.1.0`**.
Confirm the plugin's commands/skills load.
> **If 0d fails: STOP.** Revert the flip (§ Rollback) — graceful-handoff goes back to
> `./plugins/graceful-handoff`, the marketplace stays live — and diagnose the HTTPS/ref/Forgejo chain
> before touching any of the other 10. Do not proceed past a failed pilot.
---
## (1)(3) Roll out the rest, one repo at a time
Order (lower risk last so problems surface while the blast radius is small):
1. **Design-system first**`playground-design-system` (the 2 consumers, llm-security + ms-ai-architect,
vendor it; standing it up first means their vendored copies have an upstream to point at).
2. **High-churn**`voyage``llm-security``linkedin-studio``ms-ai-architect`.
- `ms-ai-architect` carries the 148 MB screenshot blob-bomb (F3); the extract strips >1 MB blobs when
`blob_strip_safe` (verified true), so its push is a normal size. Confirm the push completes.
3. **Low-churn**`config-audit``okr``ai-psychosis``human-friendly-style``claude-design`.
**Per-repo procedure (identical for every target):**
```bash
KEY=<target> # e.g. voyage
# a. extract + validate standalone (config-audit uses its dedicated SC2 gate).
# SC2 is REGRESSION-RELATIVE (the contract the Step-11 dry-run validated): 41 runs 40 strict, then on
# failure passes iff the standalone failing-test set is a SUBSET of the live in-repo set — so pre-existing
# in-repo red (e.g. voyage's 2 doc-consistency drifts, ai-psychosis's 1) does NOT STOP the rollout, while
# a genuine extraction-introduced regression still does. (Strict 40 alone would STOP on that blessed red.)
if [ "$KEY" = "config-audit" ]; then
WORK=$WORK bash MIG/50-config-audit-sc2.sh # config-audit: SC2 PASS (...)
else
WORK=$WORK bash MIG/41-validate-or-regression.sh "$KEY" # <KEY>: PASS (standalone strict | N pre-existing)
fi
# b. create the Forgejo repo (auto_init:false, public)
curl -fsS -X POST "https://git.fromaitochitta.com/api/v1/orgs/open/repos" \
-H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" \
-d "{\"name\":\"$KEY\",\"private\":false,\"auto_init\":false,\"default_branch\":\"main\"}"
# c. push the extracted repo
cd "$WORK/$KEY"
git remote add origin "https://git.fromaitochitta.com/open/$KEY.git"
git push origin --all
git push origin --tags
cd -
# d. flip this entry in the catalog (cumulative — previously-flipped entries pass through unchanged)
node MIG/60-rewrite-marketplace.mjs --only "$KEY" \
--in .claude-plugin/marketplace.json --out /tmp/mp.json
cp /tmp/mp.json .claude-plugin/marketplace.json
git add .claude-plugin/marketplace.json
git commit -m "chore(marketplace): externalise $KEY"
git push origin main
# e. INSTALL-SMOKE (SC8) before moving to the next target — fresh Claude Code session:
# /plugin marketplace update
# /plugin install $KEY@ktg-plugin-marketplace
```
**Install-smoke order by surface** (largest command surface first, so the most likely to expose a
resolution problem is caught early): `linkedin-studio` (29 commands) → `llm-security``config-audit`
`ms-ai-architect``voyage` → the rest. For each: confirm commands/skills/agents register and one
representative command runs.
> Do not start a target until the previous target's install-smoke has passed. A mixed-source catalog
> (some `./plugins/x`, some external) is a fully valid live state (SC3/SC8) — that is exactly what makes
> the one-at-a-time rollout safe.
---
## (4) Thin the catalog — LAST
Only after **all 11 repos are pushed and every install-smoke has passed**:
```bash
bash MIG/70-thin-catalog.sh --workspace /tmp/thin-catalog
# review /tmp/thin-catalog: no plugins/ or shared/, CONVENTIONS.md present, README.md all-external
```
Apply the thin state to the catalog repo (remove `plugins/`, `shared/`, `scripts/sync-design-system.mjs`,
add `CONVENTIONS.md`, swap in the rewritten `README.md` + thinned `CLAUDE.md`), commit, then:
```bash
git push origin main
git push origin pre-polyrepo-archive # publish the rollback anchor tag (D2)
```
The marketplace.json is now fully external; the catalog hosts only the manifest + landing/governance/
conventions docs.
---
## Ref updates (steady state)
When a plugin cuts a new release, bump its `ref` in `.claude-plugin/marketplace.json` (re-run
`60-rewrite-marketplace.mjs --only <key>` after updating the tag in `plugin-map.json`, or edit the `ref`
by hand), push the catalog, and consumers pick it up with `/plugin marketplace update`.
---
## Rollback
A flip is **reversible right up until the `./plugins/<key>` source is removed** (i.e. until thinning).
Order of preference:
- **Single bad flip (pre-thinning):** `git revert <the externalise commit>` and push — the entry returns
to `./plugins/<key>`, which is still present, so the marketplace works immediately. Then fix the
external repo and re-flip.
- **Never remove a `./plugins/<key>` source before that plugin's external repo is pushed AND its
install-smoke has passed** (R1 / SC8). Thinning (step 4) is the point of no easy return — do it only
when every external repo is verified live.
- **Whole-operation anchor:** the `pre-polyrepo-archive` tag is the pre-migration state of the monorepo.
If the catalog needs to be reset wholesale, reset to that tag (local) before it was force-published.
## Failure stops
- Pilot (0d) fails → STOP, revert, diagnose the Forgejo/HTTPS/ref chain. Touch nothing else.
- Any per-repo push or install-smoke fails → STOP at that target, revert its flip, leave the rest live.
- Never thin the catalog while any plugin is still `./plugins/<key>`-only without a verified external repo.

View file

@ -1,20 +0,0 @@
#!/usr/bin/env bash
# Failing-test NAME set for a node:test suite, via the stable TAP reporter (locale-independent).
# Extracted from 99-dryrun.sh's inline capture_fails (mirrors the sc2-regression.sh extraction, 5d112cb) so
# the SAME capture feeds BOTH the dry-run rehearsal (99-dryrun.sh) and the live operator-window gate
# (41-validate-or-regression.sh) — a single source of truth prevents the gate and the rehearsal from
# disagreeing on what "failing" means (the exact class of drift that let the strict window STOP on red the
# dry-run had blessed).
#
# Prints sorted-unique failing test names, one per line. Empty output = the suite reported no `not ok` lines.
# The caller distinguishes "ran clean" from "did not run": sc2-regression.sh treats a missing capture FILE
# as a hard error, but an empty-yet-present file is a legitimate zero-failure set. No pipefail: a no-match
# grep (zero failures) must exit 0, not leak a spurious non-zero up to the caller.
#
# Usage: capture-fails.sh <dir> <node --test command>
set -u
dir="${1:?dir}"
cmd="${2:?node --test command}"
tapcmd="${cmd/node --test/node --test --test-reporter=tap}"
( cd "$dir" && eval "$tapcmd" ) 2>&1 \
| grep -E '^not ok ' | sed -E 's/^not ok [0-9]+ - //; s/ #.*$//' | sort -u

View file

@ -1,73 +0,0 @@
// Coverage for capture-fails.sh — the failing-test-NAME capture extracted from 99-dryrun.sh so a SINGLE
// source feeds both the dry-run rehearsal and the live operator-window gate (41-validate-or-regression.sh).
// Proves it reports exactly the failing names (sorted, deduped across files) and stays exit-0 on an all-pass
// suite — a no-match grep must NOT leak a non-zero status up to the caller (which would masquerade as a
// failed capture). Drives the real script against tiny node:test fixtures — hermetic, no migration state.
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const CAP = path.join(here, 'capture-fails.sh');
const CMD = "node --test 'tests/**/*.test.mjs'";
// One node:test file = imports ONCE + a test() per entry. entry: { name, pass }. (Concatenating
// per-name single-file strings would duplicate `import test` → SyntaxError → a file-level not-ok, masking
// the per-test capture under test.)
const suiteFile = (entries) =>
"import test from 'node:test';\nimport assert from 'node:assert/strict';\n" +
entries.map((e) => `test(${JSON.stringify(e.name)}, () => assert.equal(1, ${e.pass ? 1 : 2}));`).join('\n') +
'\n';
function sandbox(files) {
const dir = mkdtempSync(path.join(os.tmpdir(), 'capfail-'));
for (const [rel, content] of Object.entries(files)) {
const p = path.join(dir, rel);
mkdirSync(path.dirname(p), { recursive: true });
writeFileSync(p, content);
}
return dir;
}
function cap(dir) {
// Strip NODE_TEST_CONTEXT: capture-fails spawns `node --test`, which misbehaves (emits no TAP) when it
// inherits the harness's nested-test context (Step-6 env-clean gotcha). The real operator window runs as
// plain bash, never nested under node:test, so this only matters inside this harness.
const env = { ...process.env };
delete env.NODE_TEST_CONTEXT;
return spawnSync('bash', [CAP, dir, CMD], { encoding: 'utf8', env });
}
test('capture-fails reports the failing test name (and only it), exit 0', () => {
const dir = sandbox({ 'tests/a.test.mjs': suiteFile([{ name: 'good', pass: true }, { name: 'bad apple', pass: false }]) });
try {
const r = cap(dir);
assert.equal(r.status, 0, `capture must exit 0 even with failures: ${r.stderr}`);
assert.equal(r.stdout.trim(), 'bad apple', `only the failing name expected, got: ${JSON.stringify(r.stdout)}`);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('capture-fails on an all-pass suite → empty output, exit 0 (no-match grep must not leak non-zero)', () => {
const dir = sandbox({ 'tests/a.test.mjs': suiteFile([{ name: 'good', pass: true }, { name: 'also good', pass: true }]) });
try {
const r = cap(dir);
assert.equal(r.status, 0, `all-pass must exit 0: ${r.stderr}`);
assert.equal(r.stdout.trim(), '', `no failing names expected, got: ${JSON.stringify(r.stdout)}`);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('capture-fails sorts + dedups failing names across files', () => {
const dir = sandbox({
'tests/a.test.mjs': suiteFile([{ name: 'zeta', pass: false }, { name: 'alpha', pass: false }]),
'tests/b.test.mjs': suiteFile([{ name: 'alpha', pass: false }]), // duplicate name across files collapses to one
});
try {
const r = cap(dir);
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout.trim(), 'alpha\nzeta', `sorted+deduped expected, got: ${JSON.stringify(r.stdout)}`);
} finally { rmSync(dir, { recursive: true, force: true }); }
});

View file

@ -1,140 +0,0 @@
{
"_comment": "Rename-aware extraction map for the polyrepo migration. Tags come from each plugin's .claude-plugin/plugin.json version (verified 2026-06-17), NOT from README/CLAUDE.md (which the migration corrects, Step 9). 11 targets = 10 plugins + the design-system. URLs are HTTPS (F2, #9740). Renamed plugins carry >=2 paths (F1). blob_strip_safe is (re)computed by 00-preflight.sh against the mirror history.",
"drop": ["plugins/ultra-cc-architect/"],
"targets": {
"ai-psychosis": {
"paths": ["plugins/ai-psychosis/"],
"path_renames": { "plugins/ai-psychosis/": "" },
"tag": "v1.2.0",
"repo_url": "https://git.fromaitochitta.com/open/ai-psychosis.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"standalone_caveat": "runs from repo root (relative-fixture cwd)"
},
"claude-design": {
"paths": ["plugins/claude-design/"],
"path_renames": { "plugins/claude-design/": "" },
"tag": "v0.1.0",
"repo_url": "https://git.fromaitochitta.com/open/claude-design.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "bash tests/validate-plugin.sh",
"standalone_caveat": "structure check via validate-plugin.sh (no unit suite)"
},
"config-audit": {
"paths": ["plugins/config-audit/"],
"path_renames": { "plugins/config-audit/": "" },
"tag": "v5.1.0",
"repo_url": "https://git.fromaitochitta.com/open/config-audit.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"sc2_gate": "50-config-audit-sc2.sh",
"standalone_caveat": "SC2 gate = full tests/**/*.test.mjs MINUS the 6-file machine-locked v5.0.0 byte-stability surface (json-backcompat, raw-backcompat, cli-humanizer, posture-humanizer, scan-orchestrator-humanizer, lint-default-output), re-seeding snapshot-default-output via UPDATE_SNAPSHOT=1 in-clone. Operator-ratified 2026-06-17 brief-correction: plan F4 named only 2 files; the verified surface is 6 (the v5.0.0 fixtures embed the original abs path AND the claude_md/plugin_hygiene scanners key off a plugins/ ancestor, so findings drift by path — not a string rewrite). DROPPED clean-room SC2 coverage = config-audit's humanizer/posture-humanizer/scan-orchestrator-humanizer prose-snapshot surface (recorded, not silent). config-audit backlog (out of migration scope): normalize the v5.0.0 fixtures' embedded paths + make scanners path-agnostic, then re-include. The 3 other v5.0.0-referencing tests (posture, scoring-humanizer, scenario-read-test) are path-independent and stay in the gate."
},
"graceful-handoff": {
"paths": ["plugins/graceful-handoff/"],
"path_renames": { "plugins/graceful-handoff/": "" },
"tag": "v2.1.0",
"repo_url": "https://git.fromaitochitta.com/open/graceful-handoff.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"standalone_caveat": "pilot candidate (low-churn, has tests, no DS, no blob bloat)"
},
"human-friendly-style": {
"paths": ["plugins/human-friendly-style/"],
"path_renames": { "plugins/human-friendly-style/": "" },
"tag": "v1.1.0",
"repo_url": "https://git.fromaitochitta.com/open/human-friendly-style.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "bash validate-plugin.generic.sh human-friendly-style",
"standalone_caveat": "no unit suite — ported generic structure validator (Step 7)"
},
"linkedin-studio": {
"paths": ["plugins/linkedin-studio/", "plugins/linkedin-thought-leadership/"],
"path_renames": {
"plugins/linkedin-studio/": "",
"plugins/linkedin-thought-leadership/": ""
},
"tag": "v0.4.0",
"repo_url": "https://git.fromaitochitta.com/open/linkedin-studio.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test hooks/scripts/__tests__/*.test.mjs render/__tests__/*.test.mjs agents/__tests__/*.test.mjs",
"test_cmd_advisory": "cd scripts/analytics && npm install && npm test",
"standalone_caveat": "renamed from linkedin-thought-leadership (F1). SC2 two-tier (M11): .mjs core is the HARD gate (zero deps); TS analytics suite is ADVISORY (npm/network required, NOT a clean-room gate)"
},
"llm-security": {
"paths": ["plugins/llm-security/"],
"path_renames": {
"plugins/llm-security/": ""
},
"tag": "v7.7.2",
"repo_url": "https://git.fromaitochitta.com/open/llm-security.git",
"has_vendor_ds": true,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"standalone_caveat": "single-path (CORRECTED 2026-06-17 by the S11 dry-run): llm-security-copilot was a SEPARATE, COEXISTING plugin (196 llm-security + 167 copilot paths at the SAME commits f778558d/f418a8fe, then copilot deleted) — NOT a rename ancestor. The earlier dual --path + rename-to-root collided at the coexistence commits and dropped 87 of 421 files (incl. 18 test files -> 51 e2e fails). Single-path retains ALL 421 files + 67 tests + 139 commits of llm-security history; the 3 copilot-only commits are correctly excluded (different plugin). vendors the design-system (playground)."
},
"ms-ai-architect": {
"paths": ["plugins/ms-ai-architect/"],
"path_renames": { "plugins/ms-ai-architect/": "" },
"tag": "v1.15.0",
"repo_url": "https://git.fromaitochitta.com/open/ms-ai-architect.git",
"has_vendor_ds": true,
"blob_strip": true,
"blob_strip_safe": null,
"test_cmd": "bash tests/validate-plugin.sh",
"standalone_caveat": "148 MB screenshot blob-bomb (F3): single filter-repo pass strips >1MB blobs when blob_strip_safe (all under playground/screenshots/), else surgical --path-glob; vendors the design-system"
},
"okr": {
"paths": ["plugins/okr/"],
"path_renames": { "plugins/okr/": "" },
"tag": "v1.3.0",
"repo_url": "https://git.fromaitochitta.com/open/okr.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "bash validate-plugin.generic.sh okr",
"standalone_caveat": "no unit suite — ported generic structure validator (Step 7). okr/templates/okr.local.md.template is intentionally tracked (SC7 allow-exception)"
},
"voyage": {
"paths": ["plugins/voyage/", "plugins/ultraplan-local/"],
"path_renames": {
"plugins/voyage/": "",
"plugins/ultraplan-local/": ""
},
"tag": "v5.1.1",
"repo_url": "https://git.fromaitochitta.com/open/voyage.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"standalone_caveat": "renamed from ultraplan-local (F1, 136 pre-rename commits). Carries .forgejo/ISSUE_TEMPLATE/ under --path — assert it survives, no monorepo-relative refs (M17)"
},
"playground-design-system": {
"paths": ["shared/playground-design-system/", "shared/playground-examples/"],
"path_renames": {
"shared/playground-design-system/": "",
"shared/playground-examples/": "playground-examples/"
},
"tag": "v0.6.0",
"repo_url": "https://git.fromaitochitta.com/open/playground-design-system.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "bash -c 'test -f tokens.css && test -f base.css && test -d schemas'",
"standalone_caveat": "DS source repo (the 2 consumers are llm-security + ms-ai-architect). Receives sync-design-system.mjs + test + PLAYGROUND-MAINTENANCE.md at standup (Step 4)"
}
}
}

View file

@ -1,225 +0,0 @@
#!/usr/bin/env bash
# run-operator-window.sh — turnkey execution of the polyrepo operator window (RUNBOOK §0§4).
#
# RUN AS THE OPERATOR, via the chat `!` prefix: ! bash docs/marketplace-polyrepo-migration/migration/run-operator-window.sh
# NOT via Claude's Bash tool — creating public repos + bulk-pushing trees to new remotes is gated by the
# auto-mode safety classifier (by design). Running it yourself via `!` is the sanctioned path: it executes
# as you, so the classifier / permission layer / push-window hook are all out of the picture.
#
# Idempotent + stop-on-first-failure. Safe to re-run after a partial run (created repos → 409 ok, pushed
# refs → up-to-date, already-flipped catalog entries → skipped).
#
# DEFAULT (no args) — §0§3, REVERSIBLE:
# For all 11 targets in RUNBOOK order: (a) validate the extract → (b) create the Forgejo repo
# (auto_init:false, public) → (c) push the extract over SSH → (d) verify HTTPS+ref resolution
# (the install-smoke PROXY — a real `/plugin install` still needs a fresh Claude Code session) →
# (e) flip marketplace.json to the external nested source + push the catalog.
# graceful-handoff runs first as the PILOT gate. Standing up the 11 repos is what unblocks per-plugin
# parallel work — thinning is NOT required for that. Every flip is `git revert`-able while ./plugins/<k>
# still exists, so this whole phase is reversible.
#
# --thin — §4, build + verify the thin-catalog preview (no apply).
# CONFIRM_THIN=1 ... --thin — §4 APPLY: git rm plugins/ shared/ sync-script, swap in CONVENTIONS/CLAUDE/
# README, commit, push, push the pre-polyrepo-archive tag. IRREVERSIBLE point of no easy return —
# run only after the real `/plugin install` smoke-tests for all 11 have passed.
#
# Prereqs: $FORGEJO_TOKEN exported (Keychain → ~/.zshenv); on branch main; the 11 extracts buildable in
# $WORK (the validate step self-extracts if missing — that self-heal also rebuilds the mirror via preflight).
set -uo pipefail
HOST="git.fromaitochitta.com"
API="https://$HOST/api/v1"
MIG="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$MIG/../../.." && pwd)"
MAP="$MIG/plugin-map.json"
LIVE="$ROOT/.claude-plugin/marketplace.json"
WORK="${WORK:-/tmp/polyrepo-migration}"
ARCHIVE_TAG="pre-polyrepo-archive"
PILOT="graceful-handoff"
REST="playground-design-system voyage llm-security linkedin-studio ms-ai-architect config-audit okr ai-psychosis human-friendly-style claude-design"
# The shared design-system is a standalone repo (stood up so consumers can vendor from an upstream), but it
# is NOT a marketplace plugin: it has no .claude-plugin/plugin.json and no marketplace.json entry. Steps (d)
# and (e) special-case it (assert its DS root marker; no catalog flip — mirrors 60-rewrite --all).
DS_KEY="playground-design-system"
die() { printf '\n✖ %s\n' "$*" >&2; exit 1; }
say() { printf '%s\n' "$*"; }
# ---- preconditions ----
[ -n "${FORGEJO_TOKEN:-}" ] || die "FORGEJO_TOKEN not set — export it from Keychain via ~/.zshenv first."
command -v python3 >/dev/null 2>&1 || die "python3 not found"
command -v node >/dev/null 2>&1 || die "node not found"
command -v curl >/dev/null 2>&1 || die "curl not found"
[ -f "$MAP" ] || die "plugin-map.json missing at $MAP"
[ -f "$LIVE" ] || die "live marketplace.json missing at $LIVE"
BR="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)"
[ "$BR" = "main" ] || die "catalog repo not on main (on '$BR')"
# ---- SSH connection multiplexing (REQUIRED — Forgejo rate-limits rapid port-22 handshakes) ----
# The bare per-target `git push --all` + `git push --tags` opens 2 SSH connections per target (22 total)
# in rapid succession; Forgejo refuses around the 6th handshake ("ssh: connect to host ... port 22:
# Connection refused" — TCP-level, not auth), which killed voyage's tag push on every run. Route ALL
# git-over-SSH through ONE persistent master connection so the whole rollout costs a single TCP handshake.
# Verified: 8 rapid multiplexed connections all succeed where the 6th bare connection is refused.
# ControlPersist keeps the master alive across the inter-target validate/clone gaps.
export GIT_SSH_COMMAND="ssh -o ControlMaster=auto -o ControlPath=/tmp/polyrepo-ssh-%C -o ControlPersist=600"
mapget() { python3 -c "import json;print(json.load(open('$MAP'))['targets']['$1'].get('$2',''))"; }
entry_present() {
python3 -c "import json
print('yes' if any(x['name']=='$1' for x in json.load(open('$LIVE'))['plugins']) else 'no')"
}
entry_is_external() {
python3 -c "import json
p=[x for x in json.load(open('$LIVE'))['plugins'] if x['name']=='$1']
print('yes' if (p and isinstance(p[0].get('source'),dict)) else 'no')"
}
count_local() {
python3 -c "import json
print(sum(1 for x in json.load(open('$LIVE'))['plugins'] if isinstance(x.get('source'),str) and x['source'].startswith('./plugins/')))"
}
rollout_one() {
key="$1"
tag="$(mapget "$key" tag)"
[ -n "$tag" ] || die "no tag for $key in plugin-map.json"
say ""
say "==== $key (tag $tag) ===="
# (a) validate the extract — self-extracts if $WORK/$key is absent.
# SC2 is REGRESSION-RELATIVE (the contract the Step-11 dry-run validated): a target passes iff the
# extraction introduces NO NEW failure. Pre-existing in-repo red (voyage's 2 doc-consistency drifts,
# ai-psychosis's 1) is the plugin's own concern — 41-validate-or-regression.sh enforces that exact
# contract (strict 40 first, then standalone-failing ⊆ in-repo-failing) so the window does NOT STOP on
# red the rehearsal blessed. config-audit keeps its dedicated deterministic gate.
say " [a] validate extract…"
if [ "$key" = "config-audit" ]; then
WORK="$WORK" bash "$MIG/50-config-audit-sc2.sh" >/dev/null 2>&1 || die "$key SC2 gate FAILED — STOP"
else
sc2out="$(WORK="$WORK" bash "$MIG/41-validate-or-regression.sh" "$key" 2>&1)" \
|| die "$key standalone validation FAILED (incl. regression-relative SC2) — STOP${sc2out:+ :: $sc2out}"
case "$sc2out" in *pre-existing*) say " ${sc2out#*: }";; esac
fi
# (b) create the Forgejo repo (201 created | 409 already exists)
say " [b] create repo open/${key}"
body="$(mktemp)"
code="$(curl -sS -o "$body" -w '%{http_code}' -X POST "$API/orgs/open/repos" \
-H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" \
-d "{\"name\":\"$key\",\"private\":false,\"auto_init\":false,\"default_branch\":\"main\"}")"
case "$code" in
201) say " created";;
409) say " already exists (ok, idempotent)";;
*) cat "$body" >&2; rm -f "$body"; die "$key repo-create unexpected HTTP $code — STOP";;
esac
rm -f "$body"
# (c) push the extract over SSH (proven auth; the marketplace source URL stays HTTPS)
say " [c] push extract over SSH…"
( cd "$WORK/$key" || exit 1
git remote remove origin >/dev/null 2>&1 || true
git remote add origin "ssh://git@$HOST/open/$key.git" || exit 1
git push origin --all || exit 1
git push origin --tags || exit 1
) || die "$key push FAILED — STOP"
# (d) install-smoke PROXY: HTTPS clone at the pinned tag (the resolution path users hit)
say " [d] verify HTTPS+ref resolution…"
sm="$(mktemp -d)"
git clone --quiet --branch "$tag" "https://$HOST/open/$key.git" "$sm/r" >/dev/null 2>&1 \
|| die "$key does NOT resolve over HTTPS at $tag — STOP, diagnose the Forgejo/HTTPS/ref chain"
if [ "$key" = "$DS_KEY" ]; then
# design-system: no plugin.json — assert its DS root marker instead (tokens.css, per plugin-map test_cmd)
[ -f "$sm/r/tokens.css" ] || die "$key clone missing tokens.css (DS root marker) — STOP"
else
[ -f "$sm/r/.claude-plugin/plugin.json" ] || die "$key clone missing .claude-plugin/plugin.json — STOP"
fi
say " resolves: https://$HOST/open/$key @ $tag"
# (e) flip the catalog entry to the external nested source (idempotent) + push.
# The design-system has no marketplace entry (consumers vendor it) — nothing to flip; this mirrors
# 60-rewrite --all, which only touches the 10 plugins actually present in marketplace.json.
if [ "$(entry_present "$key")" = "no" ]; then
say " [e] $key has no marketplace entry (shared design-system, vendored by consumers) — no catalog flip"
elif [ "$(entry_is_external "$key")" = "yes" ]; then
say " [e] catalog already external for $key (skip)"
else
say " [e] flip catalog → external + push…"
node "$MIG/60-rewrite-marketplace.mjs" --only "$key" --in "$LIVE" --out /tmp/mp-rollout.json >/dev/null \
|| die "$key marketplace rewrite FAILED — STOP"
cp /tmp/mp-rollout.json "$LIVE" || die "$key cp marketplace.json FAILED"
git -C "$ROOT" add .claude-plugin/marketplace.json || die "$key git add FAILED"
git -C "$ROOT" commit -q -m "chore(marketplace): externalise $key" || die "$key catalog commit FAILED"
git -C "$ROOT" push origin main || die "$key catalog push FAILED — STOP"
say " flipped + pushed"
fi
say "$key DONE"
}
# ======================== §4 thinning ========================
do_thin() {
say "OPERATOR WINDOW — §4 thin catalog"
locals="$(count_local)"
[ "$locals" = "0" ] || die "$locals catalog entries still local — finish §0§3 rollout before thinning"
ws="$(mktemp -d)"
bash "$MIG/70-thin-catalog.sh" --workspace "$ws" >/dev/null || die "thin-catalog preview FAILED"
[ ! -d "$ws/plugins" ] || die "thin preview still contains plugins/"
[ ! -d "$ws/shared" ] || die "thin preview still contains shared/"
[ -f "$ws/CONVENTIONS.md" ] || die "thin preview missing CONVENTIONS.md"
say " thin preview verified at $ws (no plugins/ shared/, CONVENTIONS.md present)"
if [ "${CONFIRM_THIN:-}" != "1" ]; then
say ""
say " PREVIEW ONLY (irreversible apply is gated). To APPLY:"
say " CONFIRM_THIN=1 bash $0 --thin"
exit 0
fi
say " APPLYING irreversible thin state to the live catalog…"
git -C "$ROOT" rm -r -q --ignore-unmatch plugins shared scripts/sync-design-system.mjs scripts/sync-design-system.test.mjs || true
cp "$ws/CONVENTIONS.md" "$ROOT/CONVENTIONS.md" || die "cp CONVENTIONS.md FAILED"
cp "$ws/CLAUDE.md" "$ROOT/CLAUDE.md" || die "cp CLAUDE.md FAILED"
cp "$ws/README.md" "$ROOT/README.md" || die "cp README.md FAILED"
git -C "$ROOT" add CONVENTIONS.md CLAUDE.md README.md || die "git add (thin docs) FAILED"
git -C "$ROOT" add -A plugins shared scripts 2>/dev/null || true
git -C "$ROOT" commit -q -m "chore(marketplace): thin catalog to manifest + docs (polyrepo migration complete)" \
|| die "thin commit FAILED"
git -C "$ROOT" push origin main || die "thin push FAILED"
git -C "$ROOT" push origin "$ARCHIVE_TAG" || die "archive-tag push FAILED"
say ""
say "✓ THINNING COMPLETE — catalog is manifest + docs only; archive tag $ARCHIVE_TAG pushed."
say " POLYREPO MIGRATION COMPLETE."
exit 0
}
# ======================== main ========================
if [ "${1:-}" = "--thin" ]; then
do_thin
fi
say "OPERATOR WINDOW — §0§3 rollout (create + push + flip; REVERSIBLE)"
say "Targets in order: $PILOT (pilot) → $REST"
say ""
say "### PILOT: $PILOT — must pass before the other 10 ###"
rollout_one "$PILOT"
say ""
say "### PILOT PASSED — rolling out the remaining 10 ###"
for k in $REST; do rollout_one "$k"; done
say ""
say "──────────────────────────────────────────────────────────────"
say "✓ ROLLOUT COMPLETE — 11 repos created + pushed; marketplace.json all-external (nested HTTPS+ref)."
say " Each plugin now lives at https://$HOST/open/<name> — clone & work on them in parallel."
say ""
say " NEXT (recommended before thinning) — real install-smoke per plugin in a FRESH Claude Code session:"
say " /plugin marketplace update"
say " /plugin install <name>@ktg-plugin-marketplace # confirm commands/skills/agents load"
say " Reversible until thinning: 'git revert' the externalise commit and ./plugins/<name> resolves again."
say ""
say " When every install-smoke passes, run the irreversible cleanup:"
say " CONFIRM_THIN=1 bash $0 --thin"

View file

@ -1,92 +0,0 @@
// Negative + positive coverage for the two highest-stakes dry-run detectors (5d112cb).
//
// 99-dryrun.sh force-fresh re-extracts all 11 targets every run (10-extract.sh wipes $dest first), so a
// "plant a dropped file in $WORK/<key>" negative test against the full dry-run would be silently undone by
// the re-extract. So the two load-bearing detectors are extracted into standalone, side-effect-free scripts
// — sc6-check.sh (the SC6 DROP detector, the guard against the llm-security 87-file-drop class) and
// sc2-regression.sh (the SC2 regression-FAIL detector) — which 99-dryrun.sh now calls. This suite drives
// those exact scripts directly, proving each detector FIRES (DROP / regression → label + non-zero exit) and
// does NOT false-positive (retained content / subset failures → exit 0). When the dry-run calls them and
// they exit non-zero, it sets ok=0 → exit 1, so the dry-run's "report DROP/regression + exit non-zero"
// behaviour follows by construction from this coverage.
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const SC6 = path.join(here, 'sc6-check.sh');
const SC2 = path.join(here, 'sc2-regression.sh');
function run(script, args) {
return spawnSync('bash', [script, ...args], { encoding: 'utf8' });
}
// --- sc6-check.sh — the SC6 DROP detector ---------------------------------------------------------------
test('SC6 DROP fires: a real shortfall (llm-security 87-file class) → DROP label + non-zero exit', () => {
const r = run(SC6, ['334', '421', 'false']); // the actual dual-rename defect: 334 of 421 files retained
assert.notEqual(r.status, 0, 'a non-blob-strip shortfall must exit non-zero (the dry-run sets ok=0)');
assert.match(r.stdout, /334\/421 DROP/, `DROP label expected: ${r.stdout}`);
});
test('SC6 blob-strip shortfall is allowed: intentional >1MB strip → (blob-strip) label + exit 0', () => {
const r = run(SC6, ['100', '200', 'True']); // ms-ai-architect screenshot blob-bomb: legitimate shortfall
assert.equal(r.status, 0, `blob-strip shortfall must exit 0: ${r.stderr}`);
assert.match(r.stdout, /100\/200 \(blob-strip\)/, `blob-strip label expected: ${r.stdout}`);
assert.ok(!/DROP/.test(r.stdout), 'a blob-strip shortfall must NOT be labelled DROP');
});
test('SC6 retained content: ext == live → no DROP, exit 0', () => {
const r = run(SC6, ['421', '421', 'false']);
assert.equal(r.status, 0, `full retention must exit 0: ${r.stderr}`);
assert.equal(r.stdout.trim(), '421/421');
});
test('SC6 more files than live (e.g. path-rename overlap) → no DROP, exit 0', () => {
const r = run(SC6, ['500', '421', 'false']);
assert.equal(r.status, 0);
assert.ok(!/DROP/.test(r.stdout), `${r.stdout}`);
});
// --- sc2-regression.sh — the SC2 regression-FAIL detector -----------------------------------------------
function fixtureFiles(standalone, baseline) {
const dir = mkdtempSync(path.join(os.tmpdir(), 'sc2-'));
const sf = path.join(dir, 'sf');
const bf = path.join(dir, 'bf');
writeFileSync(sf, standalone.length ? standalone.join('\n') + '\n' : '');
writeFileSync(bf, baseline.length ? baseline.join('\n') + '\n' : '');
return { dir, sf, bf };
}
test('SC2 regression fires: a standalone-only failure (not in baseline) → printed + non-zero exit', () => {
// The extraction introduced a NEW failure absent from the in-repo baseline — a real regression.
const { dir, sf, bf } = fixtureFiles(['extract regressed test', 'shared pre-existing fail'], ['shared pre-existing fail']);
try {
const r = run(SC2, [sf, bf]);
assert.equal(r.status, 1, `a regression must exit 1 (the dry-run sets ok=0): ${r.stdout}${r.stderr}`);
assert.match(r.stdout, /extract regressed test/, `the regressing test name must be reported: ${r.stdout}`);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('SC2 subset PASS: standalone failures ⊆ baseline (pre-existing red) → exit 0, nothing printed', () => {
const { dir, sf, bf } = fixtureFiles(['shared pre-existing fail'], ['shared pre-existing fail', 'other baseline-only fail']);
try {
const r = run(SC2, [sf, bf]);
assert.equal(r.status, 0, `a subset must exit 0 (no migration regression): ${r.stdout}${r.stderr}`);
assert.equal(r.stdout.trim(), '', 'no regression should be printed for a subset');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('SC2 missing capture file is an ERROR (exit 2), never a silent zero-regression PASS (4044c49)', () => {
const r = run(SC2, ['/nonexistent/sf', '/nonexistent/bf']);
assert.equal(r.status, 2, `a missing capture file must be a hard error, not a false PASS: ${r.stdout}`);
});

View file

@ -1,20 +0,0 @@
#!/usr/bin/env bash
# SC2 regression decision — extracted from 99-dryrun.sh so the regression-FAIL detector is independently
# unit-testable (5d112cb). Given two SORTED failing-test-NAME files — the standalone extract's set and the
# in-repo baseline set — it prints the regressions (standalone failures absent from the baseline) and EXITS
# NON-ZERO (1) if any exist. The contract: "standalone failing set ⊆ in-repo failing set" (the extraction
# introduced NO new failure; pre-existing in-repo red is the plugin's own concern, not a migration regr).
# A missing capture file is a hard ERROR (exit 2), never a silent zero-regression PASS (guards 4044c49).
#
# Usage: sc2-regression.sh <standalone_fails_file> <baseline_fails_file>
set -u
sf="${1:?standalone fails file}"
bf="${2:?baseline fails file}"
[ -f "$sf" ] && [ -f "$bf" ] || { echo "SC2-REGRESSION ERROR: missing capture file ($sf / $bf)" >&2; exit 2; }
regr="$(comm -23 "$sf" "$bf")"
if [ -n "$regr" ]; then
printf '%s\n' "$regr"
exit 1
fi
exit 0

View file

@ -1,23 +0,0 @@
#!/usr/bin/env bash
# SC6 content-retention decision — extracted from 99-dryrun.sh so the DROP detector (the deterministic
# guard against the llm-security 87-file-drop class, commit 836b8e9) is independently unit-testable
# (5d112cb). Given the extract's git-tracked file count, the live monorepo's count for the same paths, and
# whether an intentional >1MB blob-strip applies to this target, it prints the SC6 report-cell label and
# EXITS NON-ZERO on a real DROP — a shortfall NOT explained by a blob-strip. A blob-strip target may
# legitimately shed >1MB blobs, so a shortfall there is reported (exit 0), never failed.
#
# Usage: sc6-check.sh <ext_files> <live_files> <blob_strip>
# blob_strip: True/true → intentional >1MB strip (shortfall allowed); anything else → shortfall = DROP.
set -u
ext="${1:?ext_files}"
live="${2:?live_files}"
blob="${3:-false}"
if [ "$ext" -lt "$live" ]; then
case "$blob" in
True|true) echo "${ext}/${live} (blob-strip)"; exit 0 ;;
*) echo "${ext}/${live} DROP"; exit 1 ;;
esac
fi
echo "${ext}/${live}"
exit 0

View file

@ -1,16 +0,0 @@
# Session state files (local only, not tracked)
REMEMBER.md
TODO.md
ROADMAP.md
STATE.md
*.local.md
# Session directories (plans, research, execution progress)
.claude/
# Session-generated reports (not release artifacts)
reports/*-beskrivelse.*
# OS files
.DS_Store
Thumbs.db

View file

@ -1,104 +0,0 @@
#!/usr/bin/env bash
# validate-plugin.generic.sh — generic, parameterized plugin structure validator (Step 7).
#
# A ported, plugin-agnostic copy of plugins/claude-design/tests/validate-plugin.sh, stripped of
# claude-design's bespoke checks (.coverage.md, SKILL description length, forbidden command names,
# operator-private grep). It gives the two test-less plugins — okr + human-friendly-style — a runnable
# SC2 gate for the standalone-extraction harness (40-validate-standalone.sh vendors this file into the
# clean room and runs `bash validate-plugin.generic.sh <key>`).
#
# Checks (all generic):
# (a) .claude-plugin/plugin.json is valid JSON with non-empty name/version/description [HARD]
# (b) at least one user-facing surface dir is present + non-empty
# (commands | agents | skills | output-styles | hooks) [HARD]
# (c) every commands/*.md, agents/*.md, output-styles/*.md and skills/*/SKILL.md parses:
# line 1 is the `---` frontmatter delimiter and the block carries a `name:` key [HARD]
#
# Usage: validate-plugin.generic.sh <key> [plugin_root]
# <key> label used in the verdict line (e.g. "okr: STRUCTURE OK")
# [plugin_root] plugin root to validate; defaults to $PWD (the clean-room cwd)
#
# Exit codes: 0 = STRUCTURE OK; 1 = STRUCTURE FAIL (>=1 hard check failed); 2 = usage error.
set -uo pipefail
KEY="${1:-}"
[ -n "$KEY" ] || { echo "usage: validate-plugin.generic.sh <key> [plugin_root]" >&2; exit 2; }
PLUGIN_ROOT="${2:-$PWD}"
PASS=0
FAIL=0
pass() { printf ' + %s\n' "$1"; PASS=$((PASS + 1)); }
fail() { printf ' - %s\n' "$1"; FAIL=$((FAIL + 1)); }
echo "=== $KEY structure validation (root: $PLUGIN_ROOT) ==="
# --- (a) plugin.json valid JSON + required fields ---
echo "--- (a) plugin.json ---"
PJ=""
for cand in "$PLUGIN_ROOT/.claude-plugin/plugin.json" "$PLUGIN_ROOT/plugin.json"; do
[ -f "$cand" ] && { PJ="$cand"; break; }
done
if [ -z "$PJ" ]; then
fail "plugin.json missing (.claude-plugin/plugin.json)"
elif ! node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$PJ" 2>/dev/null; then
fail "plugin.json is invalid JSON"
else
pass "plugin.json is valid JSON"
for field in name version description; do
if node -e "const p=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));if(typeof p[process.argv[2]]!=='string'||p[process.argv[2]]==='')process.exit(1)" "$PJ" "$field" 2>/dev/null; then
pass "plugin.json has '$field'"
else
fail "plugin.json missing or empty '$field'"
fi
done
fi
# --- (b) at least one non-empty user-facing surface dir ---
echo "--- (b) user-facing surface ---"
SURFACE=0
for d in commands agents skills output-styles hooks; do
dir="$PLUGIN_ROOT/$d"
[ -d "$dir" ] || continue
if find "$dir" -type f 2>/dev/null | grep -q .; then
pass "surface dir '$d/' present and non-empty"
SURFACE=$((SURFACE + 1))
fi
done
if [ "$SURFACE" -eq 0 ]; then
fail "no non-empty user-facing surface dir (commands/agents/skills/output-styles/hooks)"
fi
# --- (c) frontmatter parses on every surface markdown file ---
echo "--- (c) frontmatter ---"
check_frontmatter() {
local f="$1"
local rel="${f#"$PLUGIN_ROOT"/}"
if [ "$(head -n 1 "$f")" != "---" ]; then
fail "$rel: missing frontmatter delimiter on line 1"
return
fi
local fm
fm="$(awk 'NR==1{next} /^---$/{exit} {print}' "$f")"
if printf '%s\n' "$fm" | grep -qE '^name:'; then
pass "$rel: frontmatter has 'name:'"
else
fail "$rel: frontmatter missing 'name:'"
fi
}
FM_SEEN=0
for f in "$PLUGIN_ROOT"/commands/*.md "$PLUGIN_ROOT"/agents/*.md \
"$PLUGIN_ROOT"/output-styles/*.md "$PLUGIN_ROOT"/skills/*/SKILL.md; do
[ -f "$f" ] || continue
FM_SEEN=$((FM_SEEN + 1))
check_frontmatter "$f"
done
[ "$FM_SEEN" -eq 0 ] && pass "no frontmatter-bearing surface files to check"
echo "=== Summary: pass=$PASS fail=$FAIL ==="
if [ "$FAIL" -gt 0 ]; then
echo "$KEY: STRUCTURE FAIL"
exit 1
fi
echo "$KEY: STRUCTURE OK"
exit 0

View file

@ -1,985 +0,0 @@
# Marketplace Polyrepo Migration — Implementation Plan
> **Plan quality: B+** (85/100) — APPROVE_WITH_NOTES (revised after adversarial review — Phase 9)
>
> Generated by trekplan v5.1.1 on 2026-06-17 — `plan_version: 1.7`
> Brief: `docs/marketplace-polyrepo-migration/brief.md` (RATIFIED, D1D8). Profile: premium (Opus).
>
> **⚠ Brief corrections requiring operator ratification** (the plan overrides two literal values
> in the RATIFIED brief — surface these for explicit annotation, do not absorb silently):
> 1. **`ssh://``https://`** (F2). Brief SC1 (§4) and the §6 entry shape literally specify
> `ssh://git@…`; D4 ratified only *pin-to-tag*, silent on scheme. Claude Code rejects SSH
> marketplace sources (issue #9740) → the plan uses `https://git.fromaitochitta.com/open/<repo>.git`
> everywhere. Correct engineering call, but it edits a ratified literal.
> 2. **config-audit SC2 carve-out** (F4). Two back-compat test files
> (`json-backcompat.test.mjs`, `raw-backcompat.test.mjs`) assert byte-stability of a capture that
> embeds the original machine's absolute path + a sibling marketplace + deleted plugins; the plan
> **excludes them by name** from config-audit's SC2 gate (Step 7) — touches test fixtures, edges
> the §5 non-goal. Operator must accept the carve-out.
## Context
The `ktg-plugin-marketplace` monorepo (10 plugins under `plugins/`, a shared design system under
`shared/`, a catalog at the root) is split into **one Forgejo repo per plugin + a versioned
design-system repo + a thin catalog repo at the same URL**. The plugins are already independent
units (independent versions, self-contained `plugin.json`/`README`/`CLAUDE.md`, **no live
cross-plugin runtime imports** — confirmed hard by exploration), so the split aligns structure with
reality and removes concurrent-session git contention structurally rather than mitigating it. The
marketplace must stay installable and live at **every** step (mixed in-repo + external sources).
This plan covers the **Claude-run, local, reversible half**: it builds the migration tooling, runs a
full **local dry-run extraction with standalone validation**, and stages (but never runs) the
outward steps — all with **NULL push** (honoring D8). The **operator-run irreversible window**
(Forgejo repo creation, first push, live `marketplace.json` cutover) is captured as a runbook
deliverable (Step 10) + the Execution Strategy, not as machine-verified steps, because those are not
local file edits.
Every decision below traces to the brief: the goal (§3), success criteria SC1SC8 (§4), the scope
fence (§5), decisions D1D8 (§7), risks R1R8 (§8), the five §9 open questions, and the execution
model (§10). Exploration surfaced **six findings the brief under-scoped** (F1F6 below); the plan
absorbs all six.
### Exploration deltas the brief did not have (load-bearing)
- **F1 — Plugin renames split history.** `voyage``ultraplan-local` (136 commits), `llm-security`
`llm-security-copilot` (3), `linkedin-studio``linkedin-thought-leadership` (50). A single-path
filter (`--subdirectory-filter plugins/voyage`) **silently drops** the pre-rename history. Renamed
plugins MUST be extracted with multiple `--path` arguments (both names) → SC6.
- **F2 — HTTPS, not `ssh://`.** The brief's §6 `ssh://git@…` `url` source is **unverified/likely
rejected** (Claude Code issue #9740 shows `marketplace add` rejecting SSH). HTTPS
(`https://git.fromaitochitta.com/open/<repo>.git`) is the only form verified-accepted on both the
`url` source schema and `marketplace add`. The discriminator key is **`source`** (brief correct),
not `type`. Forgejo has **no** background auto-update token → **public** repos sidestep the gap →
answers §9 visibility.
- **F3 — `ms-ai-architect` is a 148 MB screenshot blob-bomb** (86% of `.git`; 26 blobs already
deleted from HEAD but alive in history). filter-repo resurrects them → blob-strip decision.
- **F4 — `config-audit` snapshot tests embed the absolute path** (verified directly): the
`default-output/` snapshots assert byte-equal output whose deep per-file paths (e.g.
`…/tests/fixtures/marketplace-medium/CLAUDE.md`) are **not** normalized
(`normalizeScanOrchestrator` scrubs only `meta.target`/`timestamp`/`duration_ms`), so they break at
a new clone path; the `v5.0.0/` back-compat snapshots (loaded by `json-backcompat.test.mjs` +
`raw-backcompat.test.mjs`) additionally embed a sibling marketplace (`ktg-privat`) + deleted
plugins from their original capture. SC2 blocker → real fix uses the existing `UPDATE_SNAPSHOT=1`
seam for the rebasable suite + by-name exclusion of the 2 frozen back-compat tests (Step 7).
- **F5 — Tag namespace is inconsistent** (3 schemes; bare `v5.0.0`/`v7.7.2` carry no plugin
identity; `v3.4.1`→deleted `ultraplan-local`). filter-repo copies ALL tags into EACH extract →
strip-all-then-re-tag.
- **F6 — README/metadata reference rot.** Every plugin `README.md:7` links `../../README.md`
(AI-disclosure) → 404 standalone; root-only `.mailmap`/`.gitleaks.*` don't travel with
`--path plugins/x`; `ai-psychosis/plugin.json` already points at a standalone URL while others
point at the monorepo (reconcile).
### The five §9 open questions — RESOLVED with evidence
1. **Exact filter-repo invocation + `shared/` history into DS.** For non-renamed plugins:
`git filter-repo --path plugins/<name>/ --path-rename plugins/<name>/:` (≡
`--subdirectory-filter plugins/<name>`). For **renamed** plugins: explicit multi-`--path` with a
`--path-rename` per old+new name (F1). DS: `git filter-repo --subdirectory-filter shared`
**yes**, preserve `shared/` history into the DS repo (clean: 10/12 `shared/`-touching commits are
pure-shared, 2 are single-plugin-entangled but conflict-free). All extractions run on a
`git clone --no-local` fresh clone (avoids filter-repo's spurious freshness abort on local
clones; do **not** paper over with `--force`).
2. **True DS consumers.** Exactly **2**`llm-security`, `ms-ai-architect` (only two with a
`playground/vendor/` dir). `claude-design` + `human-friendly-style` have **no** `playground/`
don't need DS. The "6" in `shared/PLAYGROUND-MAINTENANCE.md:5` is stale (one entry,
`ultraplan-local`, is the ghost pre-rename name for `voyage`). DS-repo standup scope = 2.
3. **Repo visibility.** **Public** (recommended) — sidesteps the Forgejo no-auto-update-token gap
(F2), matches the existing `open/` model, lets HTTPS clone work without credential setup.
Operator confirms at the window.
4. **`playground-examples/` home.** **DS repo.** It is DS-coupled (every HTML references
`playground-design-system`; `PLAYGROUND-MAINTENANCE.md` anchors `ros-lier-kommune.html` to
ms-ai-architect renderers). Travels with the DS repo, not the thin catalog.
5. **Per-repo CHANGELOG.** **Carry existing** — filter-repo preserves each plugin's `CHANGELOG.md`
and its commit history (D2); tags are re-created cleanly per repo (F5).
## Architecture Diagram
```mermaid
graph TD
subgraph BEFORE["Monorepo (today) — open/ktg-plugin-marketplace"]
MJ0[".claude-plugin/marketplace.json<br/>10x ./plugins/x"]
PLUGS["plugins/ x10"]
SH["shared/<br/>design-system + examples"]
SYNC["scripts/sync-design-system.mjs"]
SYNC -->|vendors| PLUGS
SH -->|source| SYNC
end
subgraph AFTER["Polyrepo (target) — same URL"]
CAT["open/ktg-plugin-marketplace<br/>THIN CATALOG<br/>marketplace.json (external+ref) · README · GOVERNANCE · CONVENTIONS · .mailmap · .gitleaks"]
DS["open/playground-design-system<br/>design-system + examples + sync-design-system.mjs(--source/--check)"]
R1["open/voyage (v5.1.1)"]
R2["open/llm-security (v7.7.2)"]
R3["open/linkedin-studio (v0.4.0)"]
Rn["open/<plugin> ... x10 total"]
CAT -.->|url + ref pin HTTPS| R1
CAT -.->|url + ref pin HTTPS| R2
CAT -.->|url + ref pin HTTPS| R3
CAT -.->|url + ref pin HTTPS| Rn
DS -->|vendor via re-homed sync| R2
DS -->|vendor via re-homed sync| Rn
end
BEFORE ==>|filter-repo extraction<br/>NULL push until window| AFTER
```
## Codebase Analysis
- **Tech stack:** Node.js `.mjs` scripts (zero npm deps house style, `node:test`), bash 3.2 hooks/
validators, one TypeScript analytics subtree (`linkedin-studio/scripts/analytics`, needs `npm
install` + `tsx`). Git on Forgejo (`git.fromaitochitta.com`, org `open/`). 1890 source files,
741 commits, `.git` 144 MB.
- **Key patterns:** Each plugin self-contained (`.claude-plugin/plugin.json` + `README` + `CLAUDE.md`,
optional `hooks/ tests/ scripts/ playground/`). Hooks resolve via `${CLAUDE_PLUGIN_ROOT}`
(relocation-invariant — confirmed). DS is **vendored** (`playground/vendor/`), never live-linked.
- **Relevant files (verified):**
- `.claude-plugin/marketplace.json` — 10 entries, all `source: "./plugins/<name>"`, entry shape
`{name, source, description}` (no `version`/`ref` today).
- `scripts/sync-design-system.mjs` — SOURCE at `:26-27` (`MARKETPLACE_ROOT/shared/…`), target at
`:146` (`plugins/<name>/playground/vendor/…`), SHA-256 `MANIFEST.json` + drift-guard at
`:118-127,151-156`. **No `--source` flag today** (`parseArgs` `:30-46`).
- `.gitignore``plugins/*/` patterns at `:9` (`plugins/*/.claude/`) and `:12`
(`plugins/*/reports/*-beskrivelse.*`); global patterns (`STATE.md`, `*.local.md`, …) stay.
- `.gitleaksignore` — 2 plugin-prefixed fingerprints (`:2` llm-security, `:5` linkedin-studio);
`.gitleaks.toml` allowlist `:13` already plugin-relative.
- `plugins/linkedin-studio/scripts/analytics/src/utils/storage.ts:18-33,48` — marker resolver
(standalone-safe primary) + `../../../../` fallback (latent depth assumption, dead branch).
- `plugins/voyage/tests/helpers/hook-helper.mjs:4` — verbatim copy (C7), stays a copy.
- **Reusable code:** the existing SHA-256 `MANIFEST.json` + `detectDrift` in
`sync-design-system.mjs` already implements SC5's hash mechanism — extend, don't rebuild. The
`validate-plugin.sh` structure-validator in `claude-design/tests/` + `ms-ai-architect/tests/` is
the pattern to port to the two test-less plugins (okr, human-friendly-style).
- **Recent git activity:** HEAD `f2d41c8` (ratified brief, **3 commits ahead of origin, unpushed**).
Only `main`, no feature branches. Cross-plugin bleed commits confirmed (`f460814` touches 10
plugins; `69610d4` 2) — ~1.8% of commits span 3+ plugins, mostly marketplace-wide doc sweeps.
## Research Sources
| Topic | Source | Key findings | Confidence |
|---|---|---|---|
| `git filter-repo` subdir extract | [git-filter-repo man page](https://manpages.debian.org/testing/git-filter-repo/git-filter-repo.1.en.html) | `--subdirectory-filter X``--path X/ --path-rename X/:`; removes `origin`; rewrites tags; aborts on non-fresh clone | High |
| Local-clone freshness | [filter-repo #38](https://github.com/newren/git-filter-repo/issues/38) | `git clone --no-local` from a local source (not `--force`) | High |
| marketplace.json external/mixed/ref | [plugin-marketplaces](https://code.claude.com/docs/en/plugin-marketplaces), [plugins-reference](https://code.claude.com/docs/en/plugins-reference) | discriminator is `source` not `type`; `{source:"url",url,ref}`; mixed in-repo+external in one file **supported**; `/plugin marketplace update` refreshes a bumped ref | High |
| SSH rejection | [claude-code #9740](https://github.com/anthropics/claude-code/issues/9740) (open) | `marketplace add` rejects SSH URLs → **use HTTPS** | High (use HTTPS) |
| Forgejo auto-update token | plugin-marketplaces token table | no `FORGEJO_TOKEN` for bg auto-update of private repos → prefer **public** | Medium |
| Forgejo repo create | [Forgejo api-usage](https://forgejo.org/docs/latest/user/api-usage/) | `POST /api/v1/orgs/{org}/repos`, header `Authorization: token …`, body `{name,private,auto_init:false,default_branch}` | High |
## Implementation Plan
> **Execution invariants (apply to every step):**
> - **D8 — NULL PUSH.** Every `git commit` here is **local only**; no `git push` until the authorized
> operator window. Checkpoints are local commits held on `main` ahead of origin.
> - **All extraction runs from a `git clone --no-local` fresh clone** in a throwaway workspace
> (`$WORK`, default `/tmp/polyrepo-migration`), never the working checkout (R4). The working repo
> is read-only to every extraction.
> - Migration tooling lives under `docs/marketplace-polyrepo-migration/migration/` so it sits beside
> the brief/plan and is removed/archived at thinning, never polluting the live catalog.
> - The single source of truth for per-plugin paths/versions/flags is `migration/plugin-map.json`
> (Step 1); every later script reads it.
### Step 1: Preflight, archive tag, and the plugin map
- **Files:** `docs/marketplace-polyrepo-migration/migration/00-preflight.sh`,
`docs/marketplace-polyrepo-migration/migration/plugin-map.json`,
`docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs`
- **Changes:** (new files) `00-preflight.sh` asserts tooling and repo hygiene: `git filter-repo
--version` returns a real version, `python3 --version` ≥ 3.6, HEAD is `f2d41c8` **or a descendant**
on `main`, `origin/main` is behind (unpushed state expected). **Hygiene check is relaxed and
idempotent (M12 fix):** it aborts only if `git status --porcelain plugins shared
.claude-plugin/marketplace.json scripts` is non-empty (uncommitted change to a migration-sensitive
surface) — committed `migration/` files and unrelated untracked docs are tolerated, so re-running
preflight mid-migration does not abort. It creates the **local** annotated tag
`pre-polyrepo-archive` at HEAD (D2; pushed later in the window) idempotently. **It establishes the
dedicated extraction source (R4 / M6 fix):** `git clone --no-local "$REPO_ROOT"
"$WORK/_mirror"` **once**, at the pinned HEAD — `$MIRROR` is the single source every per-target
extraction clones from, so concurrent commits to the operator's working checkout can never affect
any extraction (the working checkout is never read by filter-repo). It also **enumerates
blob-strip candidates (M4 fix):** lists every blob >1 MB in ms-ai-architect history with its path,
and emits `blob_strip_safe: true` into `plugin-map.json` iff **all** >1 MB blobs are under
`playground/screenshots/` (else `false` → the surgical `--path-glob` strategy is selected instead
of size-strip, decided deterministically, no runtime prompt). `plugin-map.json` encodes, per
extraction target: the rename-aware `--path` list, the `--path-rename` map, the release tag (from
`plugin.json` `version`), the new **HTTPS** repo URL, `has_vendor_ds` (true only for llm-security +
ms-ai-architect), `blob_strip` + `blob_strip_safe` (ms-ai-architect), `test_cmd`, and
`standalone_caveat`. Includes the DS target (`shared/``open/playground-design-system`) and an
explicit `drop: ["plugins/ultra-cc-architect/"]` note (removed plugin, already its own repo — its
history is dropped, not extracted).
- **Reuses:** version strings from each `plugins/<name>/.claude-plugin/plugin.json` (task-finder
table); churn order from git-historian (voyage→llm-security→linkedin-studio→ms-ai-architect).
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs` (new)
- Verifies: `plugin-map.json` parses; has exactly 11 targets (10 plugins + DS); the 3 renamed
plugins carry ≥2 `--path` entries each; every plugin's `repo_url` is `https://` (not `ssh://`);
ms-ai-architect has `blob_strip: true`; only llm-security + ms-ai-architect have
`has_vendor_ds: true`.
- Pattern: `plugins/voyage/tests/validators/*.test.mjs` (node:test style)
- **Verify:** `bash docs/marketplace-polyrepo-migration/migration/00-preflight.sh --dry-run`
expected: prints `PREFLIGHT OK` and the 11-target table, creates no tag in `--dry-run`; then
`node --test docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs` → expected:
`pass`.
- **On failure:** escalate — if tooling is missing (`git filter-repo` absent), stop and instruct
`brew install git-filter-repo`; do not proceed.
- **Checkpoint:** `git commit -m "chore(migration): preflight script + rename-aware plugin map"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/00-preflight.sh
- docs/marketplace-polyrepo-migration/migration/plugin-map.json
- docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs
min_file_count: 3
commit_message_pattern: "^chore\\(migration\\): preflight script \\+ rename-aware plugin map$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/00-preflight.sh
forbidden_paths:
- .claude-plugin/marketplace.json
- scripts/sync-design-system.mjs
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/plugin-map.json
pattern: "ultraplan-local"
- path: docs/marketplace-polyrepo-migration/migration/plugin-map.json
pattern: "https://git.fromaitochitta.com/open/"
```
### Step 2: Re-home and extend sync-design-system.mjs (--source + --check)
- **Files:** `scripts/sync-design-system.mjs`, `scripts/sync-design-system.test.mjs`
- **Changes:** Add a `--source <dir>` flag so the script vendors from an arbitrary DS checkout
(the re-homed DS repo) instead of the hardcoded `MARKETPLACE_ROOT/shared/playground-design-system`
(`:26-27`); keep the in-repo path as the default fallback so the script still works pre-migration.
Add a `--check` mode that re-hashes a target plugin's vendored tree against its committed
`MANIFEST.json` and exits non-zero on drift, with **no** source required — making SC5 a single
runnable command in a clean clone (D3). Resolve the plugin/target dir from a `--target <dir>`
arg when `MARKETPLACE_ROOT/plugins/<name>` no longer exists. This is migration **rewiring**, not a
plugin-functionality change (non-goal §5 untouched). The modified script + test travel into the DS
repo at standup (Step on the DS target) and are removed from the catalog at thinning (Step 9).
- **Reuses:** existing `detectDrift` + SHA-256 `MANIFEST.json` writer (`:118-127`) — `--check`
reuses the same hashing.
- **Test first:**
- File: `scripts/sync-design-system.test.mjs` (new)
- Verifies: `--source <tmpDS>` vendors byte-identically to the default in-repo source (SC5 parity);
`--check` passes on an unmodified vendored tree and fails (exit 2) after a tampered byte;
omitting `--source` still resolves the in-repo default.
- Pattern: `plugins/voyage/tests/scripts/*.test.mjs`
- **Verify:** `node --test scripts/sync-design-system.test.mjs` → expected: `pass` (all assertions);
then `node scripts/sync-design-system.mjs --check ms-ai-architect` → expected: `MANIFEST OK`
against the current vendored tree.
- **On failure:** revert — `git checkout -- scripts/sync-design-system.mjs
scripts/sync-design-system.test.mjs`; the legacy in-repo sync still works, so this is non-blocking
for the rest until retried.
- **Checkpoint:** `git commit -m "feat(migration): sync-design-system --source + --check for DS re-home"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- scripts/sync-design-system.mjs
- scripts/sync-design-system.test.mjs
min_file_count: 2
commit_message_pattern: "^feat\\(migration\\): sync-design-system --source \\+ --check for DS re-home$"
bash_syntax_check: []
forbidden_paths:
- .claude-plugin/marketplace.json
must_contain:
- path: scripts/sync-design-system.mjs
pattern: "--source"
- path: scripts/sync-design-system.mjs
pattern: "--check"
```
### Step 3: Rename-aware extraction driver
- **Files:** `docs/marketplace-polyrepo-migration/migration/10-extract.sh`,
`docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs`
- **Changes:** (new files) Given a target key from `plugin-map.json`, it: `git clone --no-local
"$WORK/_mirror" "$WORK/<key>"` (clones from the **dedicated mirror** built in Step 1, never the
working checkout — R4/M6); runs **a single `git filter-repo` invocation** that composes all history
rewrites in one pass (M5 fix — avoids the second-pass `--force` abort): the rename-aware
`--path`/`--path-rename` set from the map (single path for non-renamed; **both** names for
voyage/llm-security/linkedin-studio — F1) **plus**, for ms-ai-architect, the blob strip selected
deterministically from `blob_strip_safe` (Step 1): `--strip-blobs-bigger-than 1M` when all >1 MB
blobs are screenshots, else `--path-glob '!plugins/ms-ai-architect/playground/screenshots/*'`
no runtime prompt (M4/F3). filter-repo composes `--path`, `--path-rename`, and
`--strip-blobs-bigger-than` in one invocation (verified against the man page). **After** the filter
completes (so the tag points at the final rewritten HEAD — M5 ordering fix), it strips every tag
filter-repo carried over and re-creates **exactly one** annotated tag `v<version>` at HEAD (F5).
Idempotent: re-running wipes `$WORK/<key>` first. **No push.**
- **Reuses:** `plugin-map.json` (Step 1); `git clone --no-local` per research-scout.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs` (new)
- Verifies (runs the driver against the live monorepo into a temp `$WORK`): the `voyage` extract's
`git log --all` contains commits whose original paths were under **both** `plugins/voyage/` and
`plugins/ultraplan-local/` (≥150 commits, proving F1 handled); the extract `git tag` lists
exactly `v5.1.1` and nothing else; the extract has **no** `plugins/` prefix at root (contents at
repo root); `origin` remote is absent post-filter.
- Pattern: integration test style in `plugins/voyage/tests/integration/*.test.mjs`
- **Verify (self-contained — M6):** `bash docs/marketplace-polyrepo-migration/migration/00-preflight.sh
&& bash docs/marketplace-polyrepo-migration/migration/10-extract.sh voyage` (preflight builds
`$WORK/_mirror` if absent; extract is self-healing) → expected:
`EXTRACT OK voyage → $WORK/voyage (257 commits, tag v5.1.1)`; then
`git -C "$WORK/voyage" log --oneline | wc -l` → expected: `≥ 250`; `git -C "$WORK/voyage" tag`
expected: exactly `v5.1.1`.
- **On failure:** retry — if filter-repo aborts on freshness, confirm the source is `$WORK/_mirror`
(a fresh `--no-local` clone); if a rename path is missing, the test catches the dropped-history
count. Revert leaves no working-tree change (extract is in `$WORK`).
- **Checkpoint:** `git commit -m "chore(migration): rename-aware filter-repo extraction driver"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/10-extract.sh
- docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): rename-aware filter-repo extraction driver$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/10-extract.sh
forbidden_paths:
- .claude-plugin/marketplace.json
- plugins
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/10-extract.sh
pattern: "no-local"
- path: docs/marketplace-polyrepo-migration/migration/10-extract.sh
pattern: "ultraplan-local"
```
### Step 4: Per-repo config re-rooting (.gitignore, .gitleaks, .mailmap)
- **Files:** `docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh`,
`docs/marketplace-polyrepo-migration/migration/templates/gitignore.plugin.tmpl`,
`docs/marketplace-polyrepo-migration/migration/20-rehome-config.test.mjs`
- **Changes:** (new files) Operates on an extracted repo in `$WORK/<key>`: writes a re-rooted
`.gitignore` from the template (the `plugins/*/` prefixes stripped → `.claude/`,
`reports/*-beskrivelse.*`; the global `STATE.md`/`*.local.md`/OS patterns kept — F6/C3); generates
a per-plugin `.gitleaks.toml` from the root baseline and a `.gitleaksignore` with the matching
fingerprint **re-rooted by dropping ONLY the `plugins/<name>/` path prefix and keeping the full
`:rule-id:line` suffix** (M7 fix — the gitleaks fingerprint format is `path:rule-id:line`, not
`path:line`; dropping the rule-id yields a fingerprint that no longer suppresses) — only for
llm-security (`examples/malicious-skill-demo/evil-project-health/lib/telemetry.mjs:generic-api-key:18`)
and linkedin-studio (`hooks/prompts/content-quality-gate.md:linkedin-client-id:14`); copies the
root `.mailmap` into every extract (F6 — it does not travel with `--path`). For the DS target it
also copies the modified `sync-design-system.mjs` + test (Step 2) and
`shared/PLAYGROUND-MAINTENANCE.md` + `playground-examples/` into the DS repo root (§9 Q4).
- **Reuses:** root `.gitignore` (`:9,:12`), `.gitleaks.toml` (`:13`), `.gitleaksignore` (`:2,:5`),
`.mailmap`.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/20-rehome-config.test.mjs` (new)
- Verifies (against a freshly extracted llm-security in `$WORK`): the new `.gitignore` contains
`.claude/` but **no** `plugins/*/` prefix; the re-rooted `.gitleaksignore` fingerprint is exactly
`examples/malicious-skill-demo/evil-project-health/lib/telemetry.mjs:generic-api-key:18` (no
`plugins/llm-security/` prefix, **rule-id `generic-api-key` retained**); `.mailmap` exists; a
plugin with no fingerprint (graceful-handoff) gets a `.gitleaks.toml` but an empty/absent
`.gitleaksignore`.
- Pattern: `plugins/config-audit/tests/lib/*.test.mjs`
- **Verify:** `bash …/20-rehome-config.sh llm-security` then `git -C /tmp/polyrepo-migration/llm-security
check-ignore .claude/x` → expected: `.claude/x` (ignored); `grep -c 'plugins/' /tmp/polyrepo-migration/llm-security/.gitleaksignore`
→ expected: `0`.
- **On failure:** retry — config generation is idempotent; revert leaves working tree clean.
- **Checkpoint:** `git commit -m "chore(migration): per-repo gitignore/gitleaks/mailmap re-rooting"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh
- docs/marketplace-polyrepo-migration/migration/templates/gitignore.plugin.tmpl
- docs/marketplace-polyrepo-migration/migration/20-rehome-config.test.mjs
min_file_count: 3
commit_message_pattern: "^chore\\(migration\\): per-repo gitignore/gitleaks/mailmap re-rooting$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh
forbidden_paths:
- .gitignore
- .gitleaks.toml
- .gitleaksignore
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh
pattern: "mailmap"
```
### Step 5: Reference-rot rewriter (README links + plugin.json/package.json URLs)
- **Files:** `docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs`,
`docs/marketplace-polyrepo-migration/migration/30-fix-references.test.mjs`
- **Changes:** (new files) Operates on an extracted repo in `$WORK/<key>`: replaces the
`[Full disclosure →](../../README.md#ai-generated-code-disclosure)` footnote at every plugin
`README.md:7` with the **inlined** disclosure text (M13 — inline chosen over a cross-repo link:
deterministic, self-contained, no dangling anchor); fixes `graceful-handoff/README.md:355` and any
other `../../README.md` / `../../.claude-plugin/marketplace.json` reference (e.g. linkedin-studio
remediation docs); sets `plugin.json` `repository` and `package.json` `homepage`/`repository` to
the new standalone `open/<plugin>` HTTPS URL. **Scope note (M14):** `ai-psychosis/plugin.json`
**already** points at `open/ai-psychosis` (verified) — for it this is a verify-only no-op; the real
reconcile work is the **9 other plugins** still pointing at the monorepo URL/subpath (e.g.
linkedin-studio, llm-security `package.json` homepage). Reads URLs from `plugin-map.json`. Reports
every file it rewrote.
- **Reuses:** `plugin-map.json` repo URLs.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/30-fix-references.test.mjs` (new)
- Verifies (against an extracted graceful-handoff): no `../../README.md` substring remains in any
`*.md`; `plugin.json.repository` ends with `/open/graceful-handoff`; an idempotent second run
makes zero changes.
- Pattern: `plugins/linkedin-studio/render/__tests__/*.test.mjs`
- **Verify:** `node …/30-fix-references.mjs graceful-handoff` then `grep -rn "\.\./\.\./README.md"
/tmp/polyrepo-migration/graceful-handoff` → expected: no matches (exit 1 from grep).
- **On failure:** retry — rewriter is idempotent and reports diffs; revert leaves working tree clean.
- **Checkpoint:** `git commit -m "chore(migration): standalone reference-rot rewriter"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs
- docs/marketplace-polyrepo-migration/migration/30-fix-references.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): standalone reference-rot rewriter$"
bash_syntax_check: []
forbidden_paths:
- plugins
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs
pattern: "ai-generated-code-disclosure"
```
### Step 6: Standalone validation harness (SC2 + SC7)
- **Files:** `docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh`,
`docs/marketplace-polyrepo-migration/migration/40-validate-standalone.test.mjs`
- **Changes:** (new files) Per `plugin-map.json` `test_cmd`: clones the extracted repo to a clean
`/tmp/claude-*` dir (no marketplace parent) and runs its self-validate command in **the runner the
plugin itself declares** — `node --test 'tests/**/*.test.mjs'` (quoted globstar, Node-≥21 native;
config-audit/llm-security/voyage) or `node --test <dir>/*.test.mjs` glob for plugins with a flat
layout — **never `node --test <dir>`** (Node 25 gotcha). Per-plugin caveats baked in:
`ai-psychosis` runs from repo root (relative-fixture cwd); **`linkedin-studio` SC2 is explicitly
two-tier (M11):** the `.mjs` core (`node --test hooks/scripts/__tests__/*.test.mjs
render/__tests__/*.test.mjs agents/__tests__/*.test.mjs`) is the **hard gate** (must pass with zero
deps); the TS analytics suite (`cd scripts/analytics && npm install && npm test`) is reported as a
**separate ADVISORY line — network-required, NOT a clean-room hard gate** (not silently waived,
explicitly labelled non-blocking for SC2). `claude-design` + `ms-ai-architect` run `bash
tests/validate-plugin.sh`; `okr` + `human-friendly-style` run the ported
`validate-plugin.generic.sh` structure check (Step 7 provides it) since they have no unit suite
(H2). **`voyage/.forgejo/` survival (M17):** for voyage, assert the extract still contains
`.forgejo/ISSUE_TEMPLATE/` (it travels under `--path plugins/voyage/`) and that no file in it
references a monorepo-relative path (it holds only issue templates, no workflows — confirmed).
SC7 assertions for every repo: `git ls-files | grep -E 'STATE\.md|\.local\.md$'` is empty
**except** the intentional tracked `okr/templates/okr.local.md.template`; no `../../README.md`
remains; `.gitignore` ignores `.claude/`. Emits a per-repo PASS/FAIL table.
- **Reuses:** the per-plugin command table (test-strategist); `validate-plugin.sh` pattern.
- **Test first:**
- File: (the harness is itself the test surface) — assert via a smoke run that the harness exits
non-zero when fed a deliberately broken extract (a planted tracked `STATE.md`) and zero on a
clean one. Encoded as `docs/marketplace-polyrepo-migration/migration/40-validate-standalone.test.mjs`.
- Verifies: harness FAILs a repo with a tracked `*.local.md`; PASSes a clean repo; uses glob test
form (asserts the command string contains `*.test.mjs`, not a bare dir).
- Pattern: `plugins/voyage/tests/synthetic/*.test.mjs`
- **Verify:** `bash …/40-validate-standalone.sh graceful-handoff` → expected: `graceful-handoff:
PASS (6 tests, standalone-safe)`; `node --test …/40-validate-standalone.test.mjs` → expected:
`pass`.
- **On failure:** escalate per repo — a FAIL is a genuine SC2/SC7 blocker for that plugin (e.g.
config-audit until Step 7); record it, continue other repos, do not mask.
- **Checkpoint:** `git commit -m "chore(migration): standalone SC2/SC7 validation harness"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh
- docs/marketplace-polyrepo-migration/migration/40-validate-standalone.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): standalone SC2/SC7 validation harness$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh
forbidden_paths:
- plugins
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh
pattern: "\\*.test.mjs"
```
### Step 7: Resolve the config-audit SC2 blocker + port a structure-validator to okr/human-friendly-style
- **Files:** `docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh`,
`docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh`,
`docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.test.mjs`
- **Changes:** (new files) **config-audit (F4 — corrected against verified test code):** config-audit
declares its runner as `node --test 'tests/**/*.test.mjs'` (52 files, CLAUDE.md:109). Two distinct
portability defects exist (both verified directly):
(a) The **rebasable** suite `snapshot-default-output.test.mjs` asserts byte-equal CLI output whose
deep per-file paths are **not** normalized (`normalizeScanOrchestrator` scrubs only
`meta.target`/`timestamp`/`duration_ms`, `normalizeTokenHotspots` only `duration_ms`), so it breaks
at a new clone path. **Fix:** the script runs `UPDATE_SNAPSHOT=1 node --test
tests/snapshot-default-output.test.mjs` **inside the clean clone** to re-seed those snapshots at the
clone's own path — it then passes (the seam exists, line 33; this is the intended re-approval path).
(b) The **frozen back-compat** tests `json-backcompat.test.mjs` + `raw-backcompat.test.mjs` load the
`tests/snapshots/v5.0.0/` fixtures, which embed the **original** capture machine's absolute path + a
sibling marketplace (`ktg-privat`) + deleted plugins — regenerating them would defeat their
byte-stability purpose. **Fix:** these two files are **excluded by name** from config-audit's SC2
gate, which runs over the file list `tests/**/*.test.mjs` **minus** those two (deterministic file
enumeration, no invented env var). The exclusion + rationale is recorded in the config-audit repo's
test README. **This edges the §5 non-goal "NOT changing plugin code"** (it re-seeds fixtures + names
an exclusion; no functionality changes) — surfaced as a **brief-correction requiring operator
ratification** (header) + Assumption 4. **okr + human-friendly-style (H2):**
`validate-plugin.generic.sh` is a ported, parameterized copy of the existing
`claude-design/tests/validate-plugin.sh` (valid `plugin.json`, expected command/skill/agent files
present, frontmatter parses) so SC2 has a runnable gate for the two test-less plugins.
- **Reuses:** `plugins/claude-design/tests/validate-plugin.sh` (the pattern); config-audit's own
`UPDATE_SNAPSHOT=1` seam (line 33) + declared runner (CLAUDE.md:109).
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.test.mjs` (new)
- Verifies: after the script runs against an extracted config-audit, the SC2 gate (full
`tests/**/*.test.mjs` **minus** json-backcompat + raw-backcompat) passes; the gate file list does
NOT include the two excluded files; the generic validator PASSes okr and FAILs a plugin with a
deleted `plugin.json`.
- Pattern: `plugins/config-audit/tests/snapshot-default-output.test.mjs` (the rebasable one)
- **Verify:** `bash …/50-config-audit-sc2.sh` then in the extracted clone the emitted SC2 gate
command (e.g. ``node --test $(ls tests/**/*.test.mjs | grep -vE 'json-backcompat|raw-backcompat')``)
→ expected: `pass`; `bash validate-plugin.generic.sh okr` → expected: `okr: STRUCTURE OK`.
- **On failure:** escalate — surface to the operator the choice: accept config-audit SC2 as
"full suite minus 2 frozen back-compat tests" (recommended), or invest in normalizing the v5.0.0
snapshots' embedded paths (config-audit's own backlog, out of migration scope).
- **Checkpoint:** `git commit -m "fix(migration): config-audit SC2 gate (re-seed + back-compat exclusion) + generic validator"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh
- docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh
- docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.test.mjs
min_file_count: 3
commit_message_pattern: "^fix\\(migration\\): config-audit SC2 gate \\(re-seed \\+ back-compat exclusion\\) \\+ generic validator$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh
- docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh
forbidden_paths:
- plugins/config-audit
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh
pattern: "UPDATE_SNAPSHOT"
- path: docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh
pattern: "json-backcompat"
```
### Step 8: marketplace.json rewriter (mixed-source aware, HTTPS + ref pin)
- **Files:** `docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.mjs`,
`docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs`
- **Changes:** (new files — **authored, not run against the live file until the window**) Rewrites a
`marketplace.json` so a named subset of entries become external
`{"source":"url","url":"https://git.fromaitochitta.com/open/<name>.git","ref":"v<version>"}`
(F2 — HTTPS + discriminator `source`, ref = the plugin's release tag, D4) while the rest stay
`./plugins/<name>` — enabling the mixed-source intermediate states that keep the marketplace live
(SC3/SC8). `--only <names>` flips a subset; `--all` flips everything (final state). Reads versions
from `plugin-map.json`; validates output against the marketplace schema (each entry has
`name`+`source`+`description`; external entries have a valid `url`+`ref`). Writes to a `--out` path
by default (never the live file) so Step 11's dry-run exercises it without violating D8.
- **Reuses:** `plugin-map.json`; the current `.claude-plugin/marketplace.json` as the input shape.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs` (new)
- Verifies: `--only voyage` produces a file where `voyage` is an external `url`+`ref:v5.1.1`
entry and the other 9 stay `./plugins/…` (mixed-source proven); every external `url` starts
`https://` (no `ssh://`); `--all` leaves zero `./plugins/` sources; schema-validates.
- Pattern: `plugins/voyage/tests/parsers/*.test.mjs`
- **Verify:** `node …/60-rewrite-marketplace.mjs --only voyage --out /tmp/mp-voyage.json` then
`grep -c '"./plugins/' /tmp/mp-voyage.json` → expected: `9`; `grep -c 'ssh://' /tmp/mp-voyage.json`
→ expected: `0`.
- **On failure:** retry — pure transform, idempotent; revert leaves working tree clean.
- **Checkpoint:** `git commit -m "chore(migration): mixed-source marketplace.json rewriter (HTTPS+ref)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.mjs
- docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): mixed-source marketplace.json rewriter \\(HTTPS\\+ref\\)$"
bash_syntax_check: []
forbidden_paths:
- .claude-plugin/marketplace.json
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.mjs
pattern: "https://git.fromaitochitta.com/open/"
```
### Step 9: Catalog-thinning script (staged for the window)
- **Files:** `docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh`,
`docs/marketplace-polyrepo-migration/migration/70-thin-catalog.test.mjs`
- **Changes:** (new files — **staged, not run until the window**) Produces the thin-catalog state in a
workspace: removes `plugins/` + `shared/` + `scripts/sync-design-system.mjs` (+ its test); extracts
the marketplace-wide conventions from the root `CLAUDE.md` into a new `CONVENTIONS.md` (D6); thins
the catalog `CLAUDE.md` to catalog-maintenance only; rewrites the landing `README.md` to reference
the external plugin repos. **The landing-README rewrite is the place the stale version/count drift
is corrected (M15 — coupled explicitly, not a side ride-along):** D6 mandates the README rewrite,
and a rewrite that re-states the plugin roster MUST state the verified counts/versions (10 plugins,
not ~8; linkedin-studio 0.4.0 not "4.1.0"; config-audit 5.1.0; voyage 5.1.1; …) or it ships a
known-false document. The correction is therefore part of the D6 README deliverable, coupled to it,
not a separable doc-fix. Verifies the result:
`ls` shows **no** `plugins/`, **no** `shared/` (SC4); the catalog retains `marketplace.json`,
`README.md`, `GOVERNANCE.md`, `CONVENTIONS.md`, `.mailmap`, a `.gitleaks` baseline.
- **Reuses:** root `CLAUDE.md` conventions block; `GOVERNANCE.md` as the conventions sibling.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/70-thin-catalog.test.mjs` (new)
- Verifies (against a workspace copy): post-thin tree has no `plugins/`, no `shared/`, no
`scripts/sync-design-system.mjs`; `CONVENTIONS.md` exists and contains the Forgejo/conventional-
commits/three-doc rules; `marketplace.json` + `README.md` + `GOVERNANCE.md` remain.
- Pattern: `plugins/config-audit/tests/*.test.mjs`
- **Verify:** `bash …/70-thin-catalog.sh --workspace /tmp/thin-catalog` then `ls /tmp/thin-catalog`
→ expected: no `plugins`/`shared` entries; `test -f /tmp/thin-catalog/CONVENTIONS.md` → expected:
exit 0.
- **On failure:** revert — operates only in `--workspace`; the live tree is untouched (forbidden
path enforced).
- **Checkpoint:** `git commit -m "chore(migration): catalog-thinning script (staged for window)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh
- docs/marketplace-polyrepo-migration/migration/70-thin-catalog.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): catalog-thinning script \\(staged for window\\)$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh
forbidden_paths:
- plugins
- shared
- CLAUDE.md
- README.md
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh
pattern: "CONVENTIONS.md"
```
### Step 10: Operator-window runbook (pilot gate + batch order)
- **Files:** `docs/marketplace-polyrepo-migration/migration/RUNBOOK.md`
- **Changes:** (new file) The single document the operator follows in the authorized window. It
encodes: **(0) Pilot gate (B3 ordering fix — the flip MUST precede the install-smoke, else the
test is vacuous).** Recommend `graceful-handoff` (low-churn, has tests, no DS, no blob bloat).
Steps, in order: **(0a)** create it on Forgejo (`POST /api/v1/orgs/open/repos` with
`{"name":"graceful-handoff","private":false,"auto_init":false,"default_branch":"main"}`, header
`Authorization: token $FORGEJO_TOKEN`); **(0b)** `git remote add origin https://… && git push
origin --all && git push origin --tags`; **(0c) flip graceful-handoff's entry in the catalog
`marketplace.json` to the external `url`+`ref` form** (`60-rewrite-marketplace.mjs --only
graceful-handoff`) and push the catalog in its still-mixed state (9× `./plugins/x` + 1× external;
`plugins/` still present, thinning comes last); **(0d)** in a fresh Claude Code session `/plugin
marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git` + `/plugin install
graceful-handoff@ktg-plugin-marketplace`. **Because the entry is now external, the install resolves
the Forgejo repo over HTTPS at `ref: v2.1.0` — this is the actual test of the HTTPS + external-source
+ ref-pin + Forgejo chain** (F2 / Assumption 1, the highest-stakes unverified link). If 0d fails,
STOP — revert the flip (graceful-handoff back to `./plugins/graceful-handoff`, marketplace stays
live) and diagnose before touching the other 10. **(1) DS repo first**, **(2) high-churn** (voyage →
llm-security → linkedin-studio → ms-ai-architect — corrected order), **(3) low-churn** (config-audit,
okr, ai-psychosis, human-friendly-style, claude-design), each: create repo → push → flip its
`marketplace.json` entry to external (`--only`) → push catalog → **install-smoke** (SC8) before the
next. **(4) Thin the catalog last** (Step 9), push catalog, push the `pre-polyrepo-archive` tag.
Includes the visibility decision (public — §9), the `auto_init:false` requirement, the ref-update
flow (`/plugin marketplace update`), the per-plugin install-smoke checklist ordered by surface
(linkedin-studio 29 cmds first), and the rollback note (a flip is reversible until the `./plugins/x`
source is removed; never remove it before the external repo is pushed+verified — R1/SC8).
- **Reuses:** all prior scripts (the runbook invokes them); research-scout's verified Forgejo/git
commands.
- **Test first:** *(documentation deliverable — no unit test; verified by review + the dry-run in
Step 11 exercising every script the runbook references)*
- **Verify:** `test -f docs/marketplace-polyrepo-migration/migration/RUNBOOK.md && grep -q
"auto_init" docs/marketplace-polyrepo-migration/migration/RUNBOOK.md && grep -q "Pilot"
docs/marketplace-polyrepo-migration/migration/RUNBOOK.md` → expected: exit 0.
- **On failure:** revert — `git checkout -- docs/marketplace-polyrepo-migration/migration/RUNBOOK.md`.
- **Checkpoint:** `git commit -m "docs(migration): operator-window runbook (pilot gate + batch order)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/RUNBOOK.md
min_file_count: 1
commit_message_pattern: "^docs\\(migration\\): operator-window runbook \\(pilot gate \\+ batch order\\)$"
bash_syntax_check: []
forbidden_paths:
- .claude-plugin/marketplace.json
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/RUNBOOK.md
pattern: "auto_init"
- path: docs/marketplace-polyrepo-migration/migration/RUNBOOK.md
pattern: "Pilot"
```
### Step 11: Full local dry-run (all 11 targets, NULL push)
- **Files:** `docs/marketplace-polyrepo-migration/migration/99-dryrun.sh`,
`docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs`
- **Changes:** (new files) `99-dryrun.sh` runs Steps 3→4→5→6 (+ Step 7 for config-audit/okr/
human-friendly-style) across **all 11 targets** (10 plugins + DS) into `$WORK`, then exercises
Step 8's rewriter (`--all``--out`) and Step 9's thinning (`--workspace`), and writes a generated
`dryrun-report.md` (an output artifact, not committed): per-target extract commit count (F1 history
retention), tag list (F5 clean re-tag), standalone PASS/FAIL (SC2), SC7 state-file check, and the
marketplace.json/catalog-thin previews — **with zero pushes and zero live-file mutation**. **It also
runs the cross-repo DS-vendor validation (M9 — the gap the pilot can't cover):** with both the DS
extract and the ms-ai-architect extract present in `$WORK`, it runs the re-homed sync
`node scripts/sync-design-system.mjs ms-ai-architect --source "$WORK/playground-design-system" --target "$WORK/ms-ai-architect"`
then `--check` against the committed `MANIFEST.json` and asserts byte-parity — exercising the actual
two-extracted-repo `--source`/`--target` invocation (SC5) locally, before the window.
**Honest scope of what the dry-run proves (M8):** it proves the **local mechanics** — extraction,
rename-aware history retention, re-rooting, reference rewrites, standalone validation, marketplace/
catalog transforms, and the DS-vendor chain. It **cannot** prove the **window-only** steps:
(i) Forgejo accepting the push of post-strip history, (ii) `auto_init:false` repo creation via the
API, (iii) Claude Code resolving an HTTPS `url`+`ref` source from self-hosted Forgejo
(Assumption 1 — covered instead by the Step 10 **pilot**, not the dry-run). The dry-run + the pilot
together gate the batch; neither alone is sufficient.
- **Reuses:** every prior script.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs` (new)
- Verifies: `dryrun-report.md` lists all 11 targets; voyage shows ≥250 commits; config-audit shows
PASS under its SC2 gate (full suite minus `json-backcompat`/`raw-backcompat`, after re-seed); no
target shows a tracked state-file leak; the previewed all-external marketplace.json has zero
`./plugins/` and zero `ssh://`.
- Pattern: `plugins/voyage/tests/integration/*.test.mjs`
- **Verify:** `bash docs/marketplace-polyrepo-migration/migration/99-dryrun.sh` → expected:
`DRY-RUN OK — 11/11 targets extracted + validated, 0 pushes`; `git -C $REPO_ROOT status`
expected: working tree shows only the committed `migration/` additions, **no** change to
`plugins/`, `shared/`, or `.claude-plugin/marketplace.json` (D8 held).
- **On failure:** escalate — any target FAIL is a real blocker to surface before the window; the
dry-run is reversible (everything in `$WORK`/`--out`/`--workspace`).
- **Checkpoint:** `git commit -m "chore(migration): full local dry-run harness + report"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/99-dryrun.sh
- docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): full local dry-run harness \\+ report$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/99-dryrun.sh
forbidden_paths:
- .claude-plugin/marketplace.json
- plugins
- shared
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/99-dryrun.sh
pattern: "no-local"
```
### Manifest — objective completion predicate
Every step above carries a Manifest. The defining invariant across all of them:
**`forbidden_paths` always includes the live `.claude-plugin/marketplace.json`** (and, for
extraction/validation/thinning steps, `plugins/` + `shared/`) — this is the machine-checkable
enforcement of **D8 (NULL push / no live-marketplace mutation)** during the entire Claude-run local
phase. The live catalog and the plugins source are mutated **only** in the operator window
(Step 10's runbook), which is outside trekexecute's verified surface by design.
### Failure recovery rules
- **revert** — script-authoring steps: `git checkout -- <files>`; extraction/validation operate in
`$WORK` so reverting is `rm -rf $WORK/<key>`.
- **retry** — idempotent transforms (Steps 39, 11) re-run cleanly.
- **escalate** — a standalone-validation FAIL (Step 6/7/11) or a missing tool (Step 1) stops and
asks the operator; never mask an SC2/SC6 failure.
- **Checkpoint** — each step commits **locally** (D8: no push), so a later failure cannot corrupt
completed tooling.
## Alternatives Considered
| Approach | Pros | Cons | Why rejected |
|---|---|---|---|
| `--subdirectory-filter plugins/<name>` for every plugin (brief's literal §9 form) | Simplest one-flag invocation | **Silently drops** 136+50+3 commits of pre-rename history for voyage/llm-security/linkedin-studio (F1) | Violates SC6 for 3 high-churn plugins; multi-`--path` is mandatory for renamed plugins |
| `ssh://git@…` `url` sources (brief §6) | Matches the operator's SSH push workflow | `marketplace add` rejects SSH (#9740); `url` schema names only `https://`/`git@` | F2 — HTTPS is the only verified-accepted form; SSH risks a dead marketplace |
| Hard-cut history (`git init` from current tree) for all repos | Fast; no filter-repo | Loses granular blame/history | D2 ratified filter-repo preservation; only the archived monorepo keeps full history under hard-cut |
| Big-bang all-at-once cutover | One window, less bookkeeping | No reversible checkpoints; marketplace dark during cutover | D5 ratified incremental mixed-source; R1 |
| Run extraction in the working checkout | No extra clone | Races concurrent sessions on `.git/index` (R4); destructive to live tree | §10 mandates a dedicated `--no-local` fresh clone |
| Skip the pilot, push all 11 then test | Fewer window steps | If Forgejo HTTPS/external-source resolution fails, all 11 are already pushed | F2 — the Forgejo external-source chain is the highest-stakes unverified link; pilot de-risks it on 1 repo |
## Test Strategy
- **Framework:** Node `node:test` (glob form `node --test path/*.test.mjs` — Node 25 gotcha), zero
npm deps (house style); bash `validate-plugin.sh` for structure checks.
- **Existing patterns:** per-plugin `tests/**/*.test.mjs`; `claude-design`/`ms-ai-architect`
`validate-plugin.sh`; config-audit `UPDATE_SNAPSHOT` rebasing seam; the SHA-256 `MANIFEST.json` +
`detectDrift` in `sync-design-system.mjs`.
- **New tests in this plan:** one test per script step (Steps 19, 11), each asserting the script's
contract against the **live monorepo** or a freshly-extracted repo in `$WORK` — so the tooling is
proven before the dry-run, and the dry-run (Step 11) is the integration test over all 11 targets.
### Tests to write
| Type | File | Verifies | Model test |
|---|---|---|---|
| Unit | `migration/00-preflight.test.mjs` | plugin-map shape, rename-awareness, HTTPS URLs | `voyage/tests/validators/*.test.mjs` |
| Unit | `scripts/sync-design-system.test.mjs` | `--source` parity, `--check` drift | `voyage/tests/scripts/*.test.mjs` |
| Integration | `migration/10-extract.test.mjs` | renamed-plugin history retained, clean re-tag | `voyage/tests/integration/*.test.mjs` |
| Unit | `migration/20-rehome-config.test.mjs` | re-rooted gitignore/gitleaks, mailmap copied | `config-audit/tests/lib/*.test.mjs` |
| Unit | `migration/30-fix-references.test.mjs` | no `../../README.md`, URLs reconciled | `linkedin-studio/render/__tests__/*.test.mjs` |
| Integration | `migration/40-validate-standalone.test.mjs` | SC2/SC7 gate fails on planted leak | `voyage/tests/synthetic/*.test.mjs` |
| Unit | `migration/50-config-audit-snapshots.test.mjs` | portable snapshots pass; generic validator | `config-audit/tests/snapshot-default-output.test.mjs` |
| Unit | `migration/60-rewrite-marketplace.test.mjs` | mixed-source, HTTPS, ref pin | `voyage/tests/parsers/*.test.mjs` |
| Unit | `migration/70-thin-catalog.test.mjs` | no plugins/shared, CONVENTIONS.md present | `config-audit/tests/*.test.mjs` |
| Integration | `migration/99-dryrun.test.mjs` | 11/11 targets, SC2/SC6/SC7, NULL push | `voyage/tests/integration/*.test.mjs` |
## Risks and Mitigations
| Priority | Risk | Location | Impact | Mitigation |
|---|---|---|---|---|
| Critical | Renamed-plugin history silently dropped (F1) | `10-extract.sh` | SC6 fails; voyage/linkedin-studio lose 3060% history | Multi-`--path` per `plugin-map.json`; Step 3 test asserts commit count from both names |
| Critical | Forgejo HTTPS external-source resolution unverified (F2) | window / `RUNBOOK.md` | Marketplace could go dark if `url` source can't resolve self-hosted Forgejo | **Pilot gate** (Step 10) proves the chain on 1 repo before the other 10; HTTPS not SSH; public repos |
| High | ms-ai-architect 148 MB blob-bomb (F3) | `10-extract.sh` `blob_strip` | Extracted repo bloated; slow push | `--strip-blobs-bigger-than 1M` (default, operator-confirmed); decision surfaced in Step 3 |
| High | config-audit snapshots embed absolute path (F4) | Step 7 | SC2 blocker | `UPDATE_SNAPSHOT=1` re-seed of the rebasable `snapshot-default-output` in-clone; **by-name exclude** the 2 frozen back-compat tests (`json-backcompat`/`raw-backcompat`) from the SC2 gate; flagged for operator ratification (edges §5) |
| High | Tag collision/leak across extracts (F5) | `10-extract.sh` | Wrong tags in each repo; ambiguous `v5.0.0` | Strip all tags, re-create exactly `v<version>`; Step 3 test asserts single tag |
| High | Marketplace goes dark mid-migration (R1/SC8) | window | Broken installs | Mixed sources (Step 8 `--only`); never remove `./plugins/x` before external repo verified; install-smoke each step |
| Medium | README/metadata reference rot (F6) | `30-fix-references.mjs` | 404 disclosure links, wrong repo URLs standalone | Rewriter inlines disclosure + reconciles `repository`/`homepage`; Step 5 test asserts none remain |
| Medium | Root-only `.mailmap`/`.gitleaks.*` don't travel (F6/M5) | `20-rehome-config.sh` | Lost author dedup; re-flagged false positives | Copy `.mailmap`; generate per-plugin `.gitleaks` with re-rooted fingerprints |
| Medium | Concurrent-session corruption during surgery (R4) | all extraction | Index race in working tree | `git clone --no-local` fresh clone; working checkout read-only; quiesce other writers in the window |
| Medium | Untracked non-gitignored files leak on blanket add (M2) | extraction | `progress.json` + 2 briefs enter an extract | filter-repo is history-based (untracked files excluded); Step 6 SC7 check catches leaks |
| Low | `okr.local.md.template` is intentionally tracked | Step 6 | False SC7 failure | Explicit allow-exception in the SC7 check |
| Low | Forgejo no auto-update token for private repos (F2) | window | Background update of a private marketplace unsupported | Recommend **public** repos (§9); manual `/plugin marketplace update` otherwise |
## Assumptions
| # | Assumption | Why unverifiable | Impact if wrong |
|---|---|---|---|
| 1 | Claude Code resolves a self-hosted Forgejo `url` source over HTTPS with a pinned `ref` | Not testable until a repo exists on Forgejo (push is operator-gated, D8) | If false, the whole external-source model fails — **the Step 10 pilot is the explicit test that gates the rest** |
| 2 | `git filter-repo` carries each plugin's `CHANGELOG.md` + history intact under multi-`--path` | filter-repo tag/history edge-cases ("tags not on path") lack a crisp documented rule | Verified empirically by Step 3/11 tests (commit counts, tag list) before the window |
| 3 | Stripping blobs >1 MB from ms-ai-architect loses only screenshots, not source | Depends on whether any non-screenshot blob exceeds 1 MB | Step 3 surfaces the strip list as a decision; operator confirms before the window |
| 4 | Excluding config-audit's 2 frozen back-compat tests (`json-backcompat`/`raw-backcompat`) from SC2, plus re-seeding `snapshot-default-output` in-clone, is acceptable (edges §5 non-goal) | A judgment on "what SC2 means" for fixtures that embed the original capture machine's path + a sibling marketplace + deleted plugins | Flagged for operator ratification (header + Step 7); fallback = the config-audit-internal work of normalizing the v5.0.0 snapshot paths (out of migration scope) |
| 5 | Public visibility is acceptable for all `open/` plugin repos | Visibility is a policy choice, not technical | Operator confirms at the window; private repos lose background auto-update (F2) |
## Verification
*Per-step manifests run during execution (Steps 111). These are the cross-step, end-to-end checks
mapping to the brief's SC1SC8.*
- [ ] **SC6 (F1):** `git -C /tmp/polyrepo-migration/voyage log --oneline | wc -l` → expected: `≥ 250`
(both `voyage` + `ultraplan-local` history present).
- [ ] **SC6 (F5):** `git -C /tmp/polyrepo-migration/voyage tag` → expected: exactly `v5.1.1`.
- [ ] **SC2/SC7:** `bash docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh
--all` → expected: every target `PASS` (config-audit gate = full `tests/**/*.test.mjs` **minus**
`json-backcompat` + `raw-backcompat`, after the Step 7 re-seed; linkedin-studio `.mjs` core is the
hard gate, TS analytics ADVISORY); no tracked `*.local.md`/`STATE.md` except
`okr/templates/okr.local.md.template`.
- [ ] **SC5 (two extracted repos):** with the DS + ms-ai-architect extracts in `$WORK`,
`node scripts/sync-design-system.mjs ms-ai-architect --source "$WORK/playground-design-system"
--target "$WORK/ms-ai-architect"` then `node scripts/sync-design-system.mjs --check ms-ai-architect`
→ expected: `MANIFEST OK`; `git -C "$WORK/ms-ai-architect" diff --exit-code playground/vendor/`
expected: empty (byte-identical re-vendor).
- [ ] **SC3 (F2):** `node …/60-rewrite-marketplace.mjs --all --out /tmp/mp-all.json && grep -c
'"./plugins/' /tmp/mp-all.json` → expected: `0`; `grep -c 'ssh://' /tmp/mp-all.json` → `0`.
- [ ] **SC4:** `bash …/70-thin-catalog.sh --workspace /tmp/thin && ls /tmp/thin` → expected: no
`plugins/`, no `shared/`; `CONVENTIONS.md` present.
- [ ] **D8 held:** after the full dry-run, `git -C $REPO_ROOT status --porcelain
.claude-plugin/marketplace.json plugins shared` → expected: **no output** (live surfaces
unchanged); `git log origin/main..main --oneline` shows only local `migration/` commits, **none
pushed**.
- [ ] **SC1/SC8 (operator window, post-pilot):** per `RUNBOOK.md`, `/plugin marketplace add
https://…/open/ktg-plugin-marketplace.git` + `/plugin install <plugin>@ktg-plugin-marketplace`
succeeds for the pilot, then after each graduation.
## Estimated Scope
- **Files to modify:** 1 (`scripts/sync-design-system.mjs`).
- **Files to create:** ~24 (11 migration scripts/docs + 11 tests + 2 templates), all under
`docs/marketplace-polyrepo-migration/migration/` + 1 root test.
- **Repos produced (local, unpushed):** 11 (10 plugins + DS) in `$WORK` during the dry-run.
- **Complexity:** high (rename-aware history surgery, mixed-source live-marketplace choreography,
operator-gated irreversibility) — but the **Claude-run local half is fully reversible** (NULL push,
throwaway clones); only Step 10's window is irreversible and operator-run.
## Execution Strategy
Steps exceed 5 → grouped into sessions/waves. The local half (Sessions 15) is trekexecutable now;
the window (Session 6) is operator-run from `RUNBOOK.md`, **not** via trekexecute.
### Session 1: Foundation
- **Steps:** 1, 2
- **Wave:** 1
- **Depends on:** none
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`, `scripts/sync-design-system.mjs(.test)`
- Never touch: `.claude-plugin/marketplace.json`, `plugins/`, `shared/`
### Session 2: Extraction + per-repo prep scripts
- **Steps:** 3, 4, 5
- **Wave:** 2
- **Depends on:** Session 1 (reads `plugin-map.json`)
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`
- Never touch: `plugins/`, `shared/`, `.claude-plugin/marketplace.json` (extraction operates in `$WORK`)
### Session 3: Validation + blockers
- **Steps:** 6, 7
- **Wave:** 3
- **Depends on:** Session 2 (validates extracted repos)
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`
- Never touch: `plugins/config-audit/` live (Step 7 operates on the extract in `$WORK`)
### Session 4: Catalog + window staging
- **Steps:** 8, 9, 10
- **Wave:** 3 (parallel with Session 3 — independent scripts)
- **Depends on:** Session 1 (versions/URLs)
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`
- Never touch: live `.claude-plugin/marketplace.json`, `CLAUDE.md`, `README.md` (rewriters write to `--out`/`--workspace`)
### Session 5: Local dry-run (de-risk proof)
- **Steps:** 11
- **Wave:** 4
- **Depends on:** Sessions 2, 3, 4 (exercises every script)
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`, `$WORK`
- Never touch: any live source/catalog surface (D8 asserted by the step's own verify)
### Session 6: Operator window (NOT trekexecute — operator-run from RUNBOOK.md)
- **Steps:** runbook only (pilot → DS → high-churn → low-churn → thin catalog)
- **Wave:** 5 (authorized weekend window, D8)
- **Depends on:** Session 5 green dry-run + operator authorization
- **Scope fence:**
- Touch: Forgejo (repo creation + push), live `.claude-plugin/marketplace.json` (mixed→external), catalog thinning
- Never touch: anything before the pilot passes
### Execution Order
- **Wave 1:** Session 1
- **Wave 2:** Session 2 (after Session 1)
- **Wave 3:** Sessions 3 + 4 (parallel, after Session 2 / Session 1 respectively)
- **Wave 4:** Session 5 (after Sessions 24)
- **Wave 5:** Session 6 — operator window, gated on the dry-run + explicit authorization
### Grouping rules applied
- Steps sharing the `plugin-map.json` contract → Session 1 first.
- Extraction/prep scripts (independent files) → Session 2; validation depends on them → Session 3.
- Catalog/window staging is file-independent → Session 4 parallel with Session 3.
- The dry-run gates everything before the irreversible window.
## Plan Quality Score
| Dimension | Weight | Score | Notes |
|---|---|---|---|
| Structural integrity | 0.15 | 88 | `$WORK` lifecycle now defined via a dedicated mirror clone; blob/re-tag ordering fixed (single-pass filter-repo) |
| Step quality | 0.20 | 84 | Deterministic blob decision (no runtime prompt); config-audit step corrected to the verified `UPDATE_SNAPSHOT=1` seam; gitleaks fingerprint format fixed |
| Coverage completeness | 0.20 | 86 | SC1SC8 + D1D8 + R1R8 + §9 traced; F1F6 absorbed; DS cross-repo chain + `.forgejo` survival now covered |
| Specification quality | 0.15 | 85 | filter-repo/Forgejo/marketplace commands verified; config-audit framing corrected to the real test behaviour |
| Risk & pre-mortem | 0.15 | 86 | Pilot now flips-then-installs (actually tests the ref-pin chain); dry-run honestly scoped to local mechanics |
| Headless readiness | 0.10 | 80 | Local steps deterministic & headless-ready; window operator-run by design; TS analytics explicitly advisory |
| Manifest quality | 0.05 | 85 | Every step has a checkable manifest; `expected_paths ⊆ Files:`; D8 enforced via `forbidden_paths` |
| **Weighted total** | **1.00** | **85** | **Grade: B+** |
**Adversarial review (Phase 9):**
- **Plan critic:** REVISE on the first draft (63/C) — 3 blockers + 8 major + 6 minor. **All 3 blockers
+ all 8 majors + the actionable minors addressed** (see Revisions). Contested config-audit facts
re-verified directly against the test code before revising.
- **Scope guardian:** **ALIGNED** — 0 scope gaps; all SC1SC8 / D1D8 / §9 / R1R8 covered; 4 creep
items all ACCEPTABLE (2 forced by relocation, 2 operator-ratified); **1 DEVIATION** (ssh→https)
surfaced for operator ratification in the header.
## Revisions
| # | Finding (reviewer) | Severity | Resolution |
|---|---|---|---|
| 1 | Step 7 misdescribed config-audit tests; invented `SKIP_ENV_SNAPSHOTS`; wrong-glob under-ran the suite (critic) | blocker | Re-verified the real tests. Rewrote Step 7 to use the actual `UPDATE_SNAPSHOT=1` seam (re-seed `snapshot-default-output`) + **by-name exclusion** of the 2 frozen back-compat tests + the declared runner `node --test 'tests/**/*.test.mjs'` |
| 2 | Pilot couldn't test the ref-pin chain — install resolved the in-repo source (critic) | blocker | Step 10 pilot reordered: create→push→**flip the entry to external `url`+`ref` & push catalog**→then install-smoke, so the HTTPS+ref chain is actually exercised |
| 3 | `blob_strip` left as a runtime prompt (non-headless) (critic) | major | Step 1 preflight enumerates >1 MB blobs and emits `blob_strip_safe`; Step 3 picks size-strip vs surgical path-glob deterministically — no prompt |
| 4 | filter-repo second-pass + re-tag ordering hazard (critic) | major | Single filter-repo invocation composing `--path`/`--path-rename`/`--strip-blobs-bigger-than`; re-tag as the final op |
| 5 | `$WORK`/`$REPO_ROOT` undefined; concurrency race reintroduced (critic) | major | Step 1 builds a dedicated `$WORK/_mirror` once; all extractions clone from it; per-step verifies self-contained |
| 6 | `.gitleaksignore` re-rooting used wrong format `path:line` (critic) | major | Step 4 fixed to `path:rule-id:line` (keep `generic-api-key`/`linkedin-client-id` + line); test assertion corrected |
| 7 | DS cross-repo vendor chain never validated before window (critic) | major | Step 11 runs the re-homed sync `--source`/`--target` across the two extracted repos + `--check` parity (SC5) |
| 8 | Dry-run oversold as proving the migration (critic) | major | Step 11 reworded to local-mechanics only; window-only modes (push/create/resolve) explicitly listed as pilot-covered |
| 9 | `expected_paths` not ⊆ `Files:` in every step (critic) | major | Added each `.test.mjs` to its step's `Files:` line |
| 10 | linkedin-studio TS suite silently waived for SC2 (critic) | major | Step 6 states two-tier SC2: `.mjs` core = hard gate; TS analytics = explicit ADVISORY (network-gated) |
| 11 | Preflight HEAD/status assertions go stale (critic) | minor | Relaxed to porcelain on `plugins/ shared/ marketplace.json scripts` only; idempotent |
| 12 | Step 5 inline-vs-link either/or; ai-psychosis mis-scoped (critic) | minor | Inline chosen; ai-psychosis noted verify-only, real work = 9 monorepo-pointing plugins |
| 13 | Step 9 doc-fix coupling; `.forgejo` CI uncovered; pre-claimed grade (critic) | minor | Version correction coupled to the D6 README rewrite; Step 6 asserts `.forgejo/` survives; grade set post-review |
| 14 | ssh→https overrides a ratified brief literal without annotation (guardian) | deviation | Surfaced as a **brief correction requiring operator ratification** in the header; kept HTTPS (correct per #9740) |

View file

@ -1,94 +0,0 @@
---
type: trekreview
review_version: "1.0"
created: 2026-06-17
task: "Marketplace polyrepo-migration tooling — 11-step extraction/validation/cutover harness"
slug: marketplace-polyrepo-migration
project_dir: docs/marketplace-polyrepo-migration
brief_path: docs/marketplace-polyrepo-migration/brief.md
scope_sha_start: a44e37b
scope_sha_end: 85a8ee3
reviewed_files_count: 29
findings: []
---
# Review: Marketplace polyrepo-migration tooling — 11-step extraction/validation/cutover harness
## Executive Summary
Verdict: **ALLOW**. This is the final pass of a three-step remediation arc. The first review (commit 3065930) returned **BLOCK** (1 BLOCKER + 6 MAJOR + 3 MINOR). The second pass (after commits 86208da + fef4b33) returned **WARN** — all 10 original findings resolved (the BLOCKER's flat external-source shape replaced by the nested `{ source: { source:'url', url, ref } }` form per the official Claude Code marketplace schema, with `validate()` + asserting tests; the SC6/SC2 regression detectors extracted into `sc6-check.sh`/`sc2-regression.sh` and negatively tested; the single-tag assertion, `$dest`-sourced regression capture, mktemp guard, `sc2_gate` routing, `git filter-repo` re-assertion, path-hygiene gate, and `blob_strip_safe: null` all confirmed) — but the high-effort deep read surfaced 2 NEW MAJOR findings in `migration/30-fix-references.mjs` (an untested `package.json` rewrite branch + a stale monorepo-relative `repository.directory` surviving into standalone repos). Both were remediated: the rewriter now drops `repository.directory` in both the plugin.json and package.json branches (idempotency preserved), and a new test drives the previously-unguarded branch against a synthetic `llm-security` extract. The independent code-correctness reviewer re-verified both as RESOLVED with no new correctness issue. Every Success Criterion traces to delivered code; every Non-Goal (the window-only Forgejo create / push / HTTPS-source resolution) remains correctly unbuilt and operator-gated by D8/NULL-push. The full local dry-run is green (11/11 targets, 0 pushes). Two standing disclosures, unchanged: the brief has no YAML frontmatter (FM_MISSING — a soft warning, not a finding, because it is a hand-written RATIFIED brief), and high-effort normalization was applied per the operator's maximal-discipline standing instruction (Pass 3 Cloudflare reasonableness filtering skipped).
## Coverage
| File | Treatment | Reason |
|------|-----------|--------|
| `migration/00-preflight.sh` | summary-only | Preflight; path-hygiene + filter-repo/python3 gates confirmed asserting |
| `migration/00-preflight.test.mjs` | summary-only | Preflight guard test; single-path lock added (836b8e9 alignment) |
| `migration/10-extract.sh` | summary-only | Extraction driver; single-path per 836b8e9; re-asserts git-filter-repo |
| `migration/10-extract.test.mjs` | summary-only | Extraction test; confirmed asserting |
| `migration/20-rehome-config.sh` | summary-only | Config rehome; covered by paired test |
| `migration/20-rehome-config.test.mjs` | summary-only | Rehome test; confirmed asserting |
| `migration/30-fix-references.mjs` | summary-only | Reference rewriter; 2 prior MAJOR (deep read) now RESOLVED |
| `migration/30-fix-references.test.mjs` | summary-only | Now covers the package.json + plugin.json branch (synthetic llm-security) |
| `migration/40-validate-standalone.sh` | summary-only | Standalone validator; sc2_gate routing added |
| `migration/40-validate-standalone.test.mjs` | summary-only | Validator test; confirmed asserting |
| `migration/templates/validate-plugin.generic.sh` | summary-only | Generic validation template; no dynamic logic regression |
| `migration/templates/gitignore.plugin.tmpl` | summary-only | Static template; no executable regression surface |
| `migration/50-config-audit-sc2.sh` | summary-only | SC2 audit gate; covered by paired test |
| `migration/50-config-audit-sc2.test.mjs` | summary-only | SC2 audit test; confirmed asserting |
| `migration/60-rewrite-marketplace.mjs` | summary-only | Prior BLOCKER fixed at :92 — nested source-object emitted + tested |
| `migration/60-rewrite-marketplace.test.mjs` | summary-only | Asserts the nested `{ source: {...} }` schema shape |
| `migration/70-thin-catalog.sh` | summary-only | Catalog thinning (3a5f558); covered by paired test |
| `migration/70-thin-catalog.test.mjs` | summary-only | Catalog thinning test; confirmed asserting |
| `migration/sc6-check.sh` | summary-only | SC6 DROP detector; negatively tested via sc-checks.test.mjs |
| `migration/sc2-regression.sh` | summary-only | SC2 regression detector; negatively tested |
| `migration/99-dryrun.sh` | summary-only | Dry-run harness (5e00f92); SC6/SC2 block confirmed correct |
| `migration/99-dryrun.test.mjs` | summary-only | Dry-run integration test; confirmed asserting |
| `migration/sc-checks.test.mjs` | summary-only | Negative tests for the sc6/sc2 detectors (both branches) |
| `migration/plugin-map.json` | summary-only | Extraction map; ms-ai-architect blob_strip_safe reset to null (preflight-computed) |
| `migration/RUNBOOK.md` | summary-only | Operator-window runbook; git-filter-repo + python3 preconditions added |
| `scripts/sync-design-system.mjs` | summary-only | Design-system sync; no migration-contract regression surface |
| `scripts/sync-design-system.test.mjs` | summary-only | DS-sync test; confirmed asserting |
| `docs/marketplace-polyrepo-migration/review.md` | skip | Review artifact itself — excluded from being reviewed as delivered code |
Note: the brief §6 nested-shape criterion is FULLY met. 0 files silently dropped; 0 deep-review treatments (all summary-only); 1 skip (the review artifact), recorded above.
## Findings (BLOCKER)
_None._
## Findings (MAJOR)
_None._
## Findings (MINOR)
_None._
## Findings (SUGGESTION)
_None._
## Remediation Summary
- BLOCKER: 0
- MAJOR: 0
- MINOR: 0
- SUGGESTION: 0
All 12 findings across the remediation arc are resolved: the 1 BLOCKER + 6 MAJOR + 3 MINOR from the first review (commits 86208da, fef4b33), plus the 2 MAJOR surfaced by the re-review's deep read of `30-fix-references.mjs` (this commit). The delivered Claude-run local/reversible half (Steps 111) is verified — full local dry-run 11/11 targets, 0 pushes; every paired unit suite green; the externalised marketplace.json emits the schema-correct nested source-object. The window-only steps (Forgejo `auto_init:false` create, post-strip history push, HTTPS `url`+`ref` resolution) remain correctly absent from this code — operator-gated by D8 / the RUNBOOK. The operator window (RUNBOOK.md) is no longer gated by a review BLOCK.
```json
{
"verdict": "ALLOW",
"counts": { "BLOCKER": 0, "MAJOR": 0, "MINOR": 0, "SUGGESTION": 0 },
"normalization": {
"mode": "default (high-effort)",
"pass3_skipped": true,
"pass3_skip_reason": "high-effort maximal-discipline standing instruction; Cloudflare reasonableness filter bypassed per v5.1.1",
"rule_key_substitutions": 0,
"note": "Final pass after full remediation. All 12 arc findings resolved; both 30-fix-references.mjs MAJORs re-verified RESOLVED by the independent code-correctness reviewer with no new correctness issue."
},
"findings": []
}
```

View file

@ -1,74 +0,0 @@
# Handoff — OKF second-brain shared spec landed (→ okr + ms-ai-architect)
> **2026-06-29. From the linkedin-studio session, via operator relay.** Action brief for the two sibling
> second-brain tracks. Read alongside `spec.md` (normative contract) + `log.md` (coordination protocol +
> status), same directory. Files are readable on local disk now even though the catalog commit is not yet
> pushed.
>
> **This is a directive to act:** confirm the items for your plugin, **update your STATE.md**, **adapt your
> plan**, and **acknowledge back in your own repo** (no writing crosses repo boundaries — the operator
> relays your acknowledgement; linkedin-studio updates `log.md`).
## 1. What changed (both plugins)
- **The shared convention now exists as one cross-cutting catalog doc:** `catalog/docs/okf-second-brain/spec.md`.
It is the **single source of truth** for the OKF-compatible second-brain form. Do **not** redefine the
convention locally — your plugin's local OKF note should now *reference* the spec, not restate it.
- **linkedin-studio's brain is the agreed reference design**; OKF is a thin interop veneer. Your rich fields
survive as **extension keys** (spec §5) — you rise toward the reference, you are **not** levelled to bare OKF.
- **The minimal contract is a floor, not a ceiling** (spec §3): `type:` on every concept file + an `index.md`
per directory level + `okf_version` in the bundle-root `index.md`. Going fuller is per-plugin and never
required by the spec.
## 2. Three verified premise corrections (these may change your plan)
Ground-truth-checked against the live `GoogleCloudPlatform/knowledge-catalog` repo (spec §9):
1. **`mdcode`/`kcmd` is NOT an OKF tool** — it's a Dataplex git-sync tool with a different frontmatter schema.
Drop any plan to emit/sync OKF via it.
2. **No reusable OKF *ingest* code exists**`reference_agent` is BigQuery+Gemini/GCP-bound. Adopt the
*prompt patterns*, not as drop-in code. Classify/convert of documents is build-yourself.
3. **Canonical recommended-field name is `resource`**, not `source`.
## 3. Your action (both — same five steps)
1. **Read** `spec.md` + `log.md`.
2. **Confirm** your plugin's items in §4 below (or propose changes in `log.md` via the operator).
3. **Update your STATE.md** — repoint your «👉 NESTE» block so the OKF work *builds against the shared spec*,
and record the premise corrections (§2) so the next session doesn't re-plan against the old framing.
4. **Adapt your plan** accordingly (see per-plugin notes in §4).
5. **Acknowledge in your own repo** (STATE/changelog), using this exact line so the relay is deterministic:
`OKF second-brain spec v0.1 ratified + plan adapted @ <your-commit-ref> (<date>)`
The operator relays it; linkedin-studio flips your row in `log.md` and resolves the open item.
## 4. Per-plugin specifics
### okr
- **Confirm:** `okf-check.mjs` semantics are stable enough to stand as the reference contract, and spec
§3/§7 generalizes them faithfully (only-`type`-required; recommended → warnings; `okf_version` echo).
- **Confirm:** okr uses `resource` (not `source`).
- **Flag** any field okr needs that the minimal + recommended set doesn't cover.
- **Plan impact:** likely small — you already have writer + checker + skill `okr-second-brain-search`
v1.6.1. Mostly: ratify the spec + align field naming if needed. (Writing okr is a separate go.)
### ms-ai-architect
- **Confirm:** the minimal contract + extension-key model (spec §3/§5) supports your planned **full OKF
package** — fuller is fine; the minimal contract is the floor.
- **Adapt:** drop `mdcode` from the adoption plan (§2.1); treat `reference_agent` enrichment as *patterns*,
not drop-in code (§2.2); switch `source``resource` (brief line 45).
- **Plan impact:** your "full package" direction stays valid, but **build the retrieval skill against this
spec**, and budget the ingest/enrichment as build-yourself (no reusable OKF library). (Building is a
separate go.)
## 5. Confirm-back loop
Each plugin acknowledges in its own repo → operator relays → linkedin-studio updates the `log.md` status
table (🔵/🟡 → ratified/conformant) and closes that plugin's open coordination item. That keeps three
independent sessions converged with the operator as the single relay, and no writing crossing repo
boundaries.
**When you later land conformance** (step 4 — your bundle actually conforms), follow the **Landing
protocol** in `log.md`: record your claim **directly in `log.md`** (set your row to 🟡 "claims conformant
@ `<commit>`, bundle `<path>`, awaiting gate-verification") using your own catalog-go. A linkedin-studio
session then runs the shared gate `node catalog/scripts/okf-check.mjs <path>` and, on exit 0, flips you to
🟢 with the proof. This way the landing lives in the shared doc — the operator need not hand-carry it.

View file

@ -1,137 +0,0 @@
# OKF second-brain convergence — coordination log
> Cross-repo coordination for the OKF-compatible second-brain form (`spec.md`). This is the **one
> shared place** all three plugin sessions read to know where the others are. OKF reserves `log.md`
> for change logs; this extends it with rollout status + the coordination protocol.
>
> **No writing crosses repo boundaries** — each plugin session writes only its own repo (+ the catalog
> with its own go). Status here is updated via operator relay.
## Coordination protocol
The operator runs one Claude session per plugin repo and relays between them. To keep three
independent sessions converged **without** cross-repo writes (mirrors the maskinrommet feedback-register
pattern):
1. **Single source of truth = `spec.md`.** The convention is defined once, here. No plugin redefines it
locally; a plugin's local OKF doc *references* this file. (Mirrors the global rule: don't invent
local mechanisms; extend the shared one.)
2. **This log is the cross-repo state.** Convention version, decisions, per-plugin conformance status,
open coordination items. All three sessions read it.
3. **No writing crosses repo boundaries.** Each session writes only its own repo. The catalog is shared
but needs a per-session go. A plugin records its own conformance in its own STATE/changelog; the
operator relays it here.
4. **Operator = relay + truth-source.** Convention changes are proposed by any session, written here
(with go), and the operator carries "convention changed → re-check conformance" to the other
sessions.
5. **Two version markers** (spec §12): `okf_version` (upstream Google OKF) and this convention's own
version. Either bump → log it below → each plugin re-checks.
6. **Conformance is verified, not asserted.** A plugin's status only moves to 🟢 after its bundle passes
the shared acceptance gate `catalog/scripts/okf-check.mjs` (spec §7) — `node catalog/scripts/okf-check.mjs <bundle-root>` → exit 0. On a relayed conformance landing, run the gate against that
plugin's bundle and record the result here. Evidence-based flips only (operator verification-plikt).
## Per-plugin conformance status
Legend: 🔵 not started · 🟡 building / partial · 🟢 conformant (+ commit-ref). **Ratification**
(spec accepted + plan adapted) is recorded in the Status cell with the plugin's own commit-ref —
distinct from conformance (code landed), which `linkedin-studio` alone holds so far.
| Plugin | Against minimal contract (spec §3) | Notes | Status |
|---|---|---|---|
| **linkedin-studio** | `type` + per-level `index.md` + root `okf_version` on `brain/`; `ingest/` excluded (round-trip-critical tributary) | Reference design. Emits frontmatter, adds no parser. Brain suite 134/134; passes the **shared gate** `catalog/scripts/okf-check.mjs` (scaffolded `brain/` → "OK: valid OKF bundle", exit 0) — same verdict as okr's reference checker. | 🟢 conformant @ linkedin-studio `da0a16a` (2026-06-26) |
| **okr** | `type` required + recommended-as-warnings + `okf_version` echo | Has the reference **writer + checker** (`okf-check.mjs`, `okf-index.mjs`, `lib/frontmatter.mjs`) + skill `okr-second-brain-search` v1.6.1. | 🟡 built · **spec ratified @ okr `75bfc9b` (2026-06-29)**; conformance-alignment landing = own go |
| **ms-ai-architect** | designed, not built | Targets the fuller OKF package + a retrieval skill. Builds against this spec. | 🔵 designed · **spec ratified @ ms-ai-architect `72a7e2b` (2026-06-29)**; build = own go |
## Landing protocol — how a sibling records conformance
When your plugin's bundle actually conforms (the work of step 4, done in your own session):
1. **Record it in your own repo** (STATE/changelog) — as the handoff already instructs.
2. **Update THIS file** (your own catalog-go): set your row in the status table above to
🟡 → **"claims conformant @ `<commit>`, bundle `<path>`, awaiting gate-verification"**.
3. A **linkedin-studio session** then runs the shared gate against your bundle —
`node catalog/scripts/okf-check.mjs <path>` — and, on exit 0, flips your row to 🟢 with the proof.
This puts the landing signal in the **one shared doc**, so the operator need not hand-carry status, and
any session sees the truth on its next read. **Honest limit:** there is no live push-notification across
separate sessions — a landing is discovered when a session next *reads* this log (a linkedin-studio
session is told to check it at start; see its STATE). Self-flipping straight to 🟢 is **not** the
protocol; 🟢 is reserved for the independent gate-verified step (operator verification-plikt).
## Open coordination items (what each session must confirm)
> **✅ Resolved 2026-06-29 — both siblings ratified.** okr (`75bfc9b`) and ms-ai-architect (`72a7e2b`)
> ratified `spec.md` and adapted their plans. The items below are **accepted via ratification**; they
> are kept as the record of what was asked. Any new field-gap, change-proposal, or conformance landing
> arrives as a **fresh item / status bump**, not here.
**→ okr session:**
- Confirm `okf-check.mjs` semantics are stable enough to stand as the reference contract (spec §3, §7),
and that this spec faithfully generalizes them (only-`type`-required; recommended → warnings;
`okf_version` echo).
- Canonical recommended-field name is **`resource`** (OKF), not `source` — confirm okr uses `resource`.
- Flag any field okr needs that the minimal contract + recommended set doesn't cover.
**→ ms-ai-architect session:**
- Confirm the minimal contract + extension-key model (spec §3, §5) supports the planned **"full OKF
package"** KB structure — going fuller is fine; the minimal contract is the **floor, not the ceiling**.
- **Field-name drift:** the architect brief writes `source`/`timestamp` (line 45); canonical is OKF's
**`resource`**. Align on `resource`.
- **mdcode is not an OKF tool** (spec §9.1) — drop it from the adoption plan. The `reference_agent`
enrichment is GCP/Gemini-bound (spec §9.2) — adopt the *prompt patterns*, not as drop-in code.
**→ both:**
- Ratify `spec.md` as the shared contract, or propose changes here.
- Retrieval-skill naming/home: okr shipped `okr-second-brain-search`; architect plans
`second-brain-search`. If a shared skill ever happens (Stage 3), converge naming + home (standalone
plugin, spec §11) — **not now** (Stage 2 measurement must justify it first).
## Deferred decisions
- **Spec §3/§6 vs. the gate's actual coverage.** The shared gate `okf-check.mjs` (faithfully lifted from
okr's reference) fails **only** on a concept file missing `type:`. It does **not** fail on a missing
root `okf_version` (echoed as `MISSING`, not an error), missing per-level `index.md` (§3 MUST), or an
`index.md` that carries frontmatter (§6 says it should not). So **"passes the gate" = "every concept
file has `type:`"** — a necessary but **partial** §3 signal, not full §3/§6 conformance. Surfaced when
portfolio-optimiser's bundle (index.md with frontmatter) passed. **To resolve in Stage 2 / a spec↔gate
reconciliation:** either tighten the gate to enforce more of §3/§6 (costs the proven verdict-parity
with okr's checker) or relax the spec's MUSTs to match what the gate actually enforces. **Not changed
now** — tightening would silently break okr parity. _(linkedin-studio session, 2026-06-29.)_
## Change log
- **2026-06-29** — Convention **v0.1** authored (`spec.md`) + this log. Seeded from the three per-plugin
design notes + linkedin-studio's verified premise corrections (mdcode ≠ OKF tool; no reusable OKF
ingest code; classify/convert is build-yourself). linkedin-studio recorded **🟢** (brain emits
OKF-compatible form, cross-tool-verified against okr's `okf-check`). okr / ms-ai-architect rollout =
separate per-repo go. _(Authored by the linkedin-studio session; operator relay to siblings pending.)_
- **2026-06-29****Stage 1 ratification complete across all three tracks.** okr ratified spec v0.1 +
adapted plan @ okr `75bfc9b`; ms-ai-architect ratified spec v0.1 + adapted plan @ ms-ai-architect
`72a7e2b` (both relayed via operator). The shared contract is now accepted by all three — the
interop goal of Stage 1 is met at the convention level. Conformance *landings* (okr form-alignment,
ms-ai-architect build) remain each their own go. _(linkedin-studio session, operator relay.)_
- **2026-06-29****Shared acceptance gate landed** (`catalog/scripts/okf-check.mjs` +
`okf-frontmatter.mjs` + `okf-check.test.mjs`). Lifted faithfully from okr's reference impl; verdict
logic byte-identical, English output, zero deps, self-contained. Verified: 33/33 catalog tests green;
verdict parity with okr's checker on okr fixtures (positive + negative); a scaffolded linkedin-studio
`brain/` validates clean (exit 0). Wired as the conformance gate (protocol §6 + spec §7): a plugin
only moves to 🟢 after passing it. The only Stage-3 remainder is reconciling the TS/mjs impls — not
required for the gate. _(linkedin-studio session.)_
- **2026-06-29****Ekstern form-konsument notert: portfolio-optimiser.** Et separat
Forgejo-rammeverk (MAF kostnads-optimiser, IKKE en marketplace-plugin) adopterte OKF-
minimal-formen uavhengig for sine per-prosjekt runtime-kunnskapsbundles — en ANNEN scope
enn denne konvensjonens bruker-second-brain (§1). Bundelen passerer den delte gaten
(`node catalog/scripts/okf-check.mjs <bundle>` → okf_version 0.1, "OK: valid OKF bundle",
exit 0) etter å ha lagt til rot-`okf_version`-markøren; commit portfolio-optimiser `812db23`.
Registrert så on-disk-FORMEN ikke driver fra hverandre i økosystemet (global regel: konformer
til den delte formen, ikke re-derive). IKKE et 4. konvergens-medlem — ingen bruker-second-brain,
ingen delt ingestion (spec §89: build-yourself), ingen delt retrieval. Eneste framtidige
overlapp: bygger portfolio-optimiser verdict-promotering (sitt «steg 8»), er den gjenbrukbare
skrive-primitiven okr's `okf-index.mjs` + frontmatter-skriver. Kjente avvik på bundelen:
2 `resource`-warnings (utelatelse tillatt, §4); `index.md` beholder frontmatter (avviker fra
§6 reservert-index, beholdt fordi dens `okf.py`-leser klassifiserer index på type).
_(Relayet fra portfolio-optimiser-sesjonen; bruk i en catalog-sesjon per protokoll §3.)_
**Gate re-verifisert i denne catalog-sesjonen (uavhengig):** `shared/examples/bygg-energi-mikro`
→ exit 0, 5 konsepter, 0 uten type, okf_version 0.1, nøyaktig de 2 `resource`-warnings nevnt
(`kilder-realiseringsgap.md`, `metode-ipmvp-a.md`); `index.md` bærer frontmatter (`type: index`);
commit `812db23` finnes lokalt. Påstandene stemmer.

View file

@ -1,182 +0,0 @@
# OKF-compatible second-brain form
A cross-plugin convention for how each plugin stores the **user's own context** — their
personal/organizational "second brain" / LLM-wiki — as a portable, interoperable markdown
bundle, compatible with Google's Open Knowledge Format (OKF) v0.1.
> **Version 0.1 · 2026-06-29 · Cross-cutting catalog artifact, owned by no single plugin.**
> Reference design: linkedin-studio's `brain/`. Interop layer: Google OKF v0.1 (thin veneer).
> Change log + per-plugin rollout status + coordination protocol: `log.md` (same directory).
## 1. Purpose & scope
This convention exists for **interop**, not standard-adoption for its own sake. Three plugins in
this marketplace independently grew a user-owned "second brain" — a wiki of the user's personal and
organizational context the plugin retrieves from during chat and commands. This document defines the
**one shared on-disk form** so a single reader can traverse all three, and so a future shared
retrieval skill (if ever justified — §10) has one contract to build against.
- **In scope:** the user's own context/data — the per-user second brain (e.g. `~/.claude/<plugin>/...`).
- **Explicitly out of scope:** each plugin's **domain reference files** (skill `references/*`). Those
stay native Claude Code skill-references (Anthropic-recommended progressive disclosure). The decisive
test, which all three plugins reached independently: not "is it an LLM-wiki" (both are) but
**"is there already a native, recommended mechanism?"** — for skill-refs YES (skills + references +
grep), for the second brain NO (it lived in ad-hoc `org/*.md` with no retrieval mechanism). OKF fills
a real gap **only** for the second brain.
## 2. The three consumers
| Plugin | Second-brain maturity | Role here |
|---|---|---|
| **linkedin-studio** | Provenance-weighted learning system (episodic/semantic split, evidence-threshold promotion, temporal validity). Most mature. | **Reference design.** Siblings rise toward it; it is not levelled down to bare OKF. |
| **okr** | Built: writer + checker (`okf-check.mjs`, `okf-index.mjs`) + retrieval skill `okr-second-brain-search`. | **Reference checker** (§7). |
| **ms-ai-architect** | Designed, not built. Targets the fuller OKF package + a retrieval skill. | Builds against this spec. |
Live rollout status (🔵/🟡/🟢 + commit-refs) lives in `log.md`, not here.
## 3. Minimal contract (normative)
A conforming **bundle** is a directory tree of markdown files, one concept per file. Concept ID =
file path minus `.md`.
- **MUST** — every concept file (every `.md` except `index.md`) carries a `type:` frontmatter key
(free string, e.g. `Profile`, `Operations`, `JournalEntry`).
- **MUST** — every directory level has an `index.md` (directory enumeration, **no frontmatter**,
carries progressive-disclosure prose).
- **MUST** — the bundle-root `index.md` carries an `okf_version` marker (the upstream OKF version the
bundle targets, currently `0.1`).
- **MUST (consumers)** — preserve unknown frontmatter keys, tolerate unknown `type` values, tolerate
broken cross-links.
This is exactly okr's `okf-check.mjs` semantics, generalized (§7).
## 4. Recommended fields (warnings, not errors)
`title`, `description`, `resource` (canonical source URI), `tags`, `timestamp`. Supply where cheap.
- **Canonical name is `resource`** (the OKF spec's name) — **not** `source`.
- A field that would break a plugin's invariant may be omitted. Example: linkedin-studio omits
`timestamp` (its serializer is pure/deterministic — a timestamp would break round-trip) and
`resource` (an internal concept has no canonical URI), keeping `type`/`title`/`description`.
## 5. Extension keys — rich fields ride along
OKF's permissiveness is the whole point for us: conforming costs `type` + `index.md`, nothing more. A
plugin's richer schema survives untouched as **extension frontmatter keys** that consumers MUST
preserve. linkedin-studio's brain keeps `provenance`, `first_seen`, `last_seen`, `evidence_count`,
`status`, and its episodic/semantic split — all as extension keys. Its model-collapse guard
(`provenance=published` only) is unaffected. **Plugins rise toward the richest design; they are not
levelled down to bare OKF.**
## 6. Reserved files & cross-links
- `index.md` — directory enumeration, **no frontmatter**, progressive-disclosure prose. The
bundle-root one carries `okf_version`.
- `log.md` — change log (optional per bundle; reserved name).
- Cross-links — plain markdown (bundle-relative `/...` or relative); relation type conveyed by prose.
Consumers MUST tolerate broken links.
## 7. Reference checker
okr's `scripts/okf-check.mjs` is the de-facto reference implementation of the minimal contract (§3):
only `type` required (missing → fail + names the files), recommended fields → warnings, root
`index.md` `okf_version` echoed for human comparison (no network — hooks are offline). ~91 lines, zero
npm dependencies, only couples to a ~55-line `frontmatter.mjs`. The shared spec **generalizes okr's
semantics; it does not reinvent them.** Reading okr's code is fine; **writing okr is a separate go.**
A **shared checker now lives here:** `catalog/scripts/okf-check.mjs` (+ vendored
`okf-frontmatter.mjs`), lifted faithfully from okr's reference impl — verdict logic byte-identical,
output in English, zero deps, self-contained. It is the **canonical cross-plugin acceptance gate**: a
bundle's pass/fail is the same here as under okr's checker. Run it per bundle root:
```
node catalog/scripts/okf-check.mjs <bundle-root>
```
Verdict parity is verified — identical exit codes to okr's checker on okr's own fixtures
(`okf-minimal`, `okf-realistic`), positive and negative (injected missing-`type`) — and a scaffolded
linkedin-studio `brain/` validates clean ("OK: valid OKF bundle", exit 0). Each plugin may keep its own
dev-loop check (linkedin-studio's TypeScript impl under `scripts/brain/` stays for its own suite); the
catalog `.mjs` is the shared gate all three are measured by. The only Stage-3 remainder is *reconciling*
the two language implementations into one — deferred until measured need, and **not** required for the
gate to function.
## 8. Deliberately NOT mandated
- **Auto-classify / convert** of arbitrary documents into the bundle — OKF provides nothing for it; a
manual inbox/drop-zone seam suffices; build only on demonstrated need.
- **Retrieval mechanism** — native Grep/Glob/Read (skill instruction "search the wiki first, open only
what's relevant") vs. a dedicated fileskb MCP server. All three plugins lean **native** (Claude
Code's Grep/Glob/Read already cover OKF's list/search/read). Per-plugin choice; a "build both,
measure" candidate.
- **Degree of OKF formalism** — full v0.1 conformance vs. this "OKF-compatible form." Plugins sit at
different points (ms-ai-architect targets the fuller package; linkedin-studio emits the minimal form
+ extension keys; okr has writer + checker). **The minimal contract (§3) is the floor all meet;**
going further is per-plugin and never required by this spec.
## 9. Verified premise corrections (dead-ends — do not plan against these)
Ground-truth-checked against the live `GoogleCloudPlatform/knowledge-catalog` repo (research agent,
2026-06-26, file+URL log retained). These overturn earlier framing in the per-plugin design notes:
1. **`mdcode` / `kcmd` is NOT an OKF tool.** It is a Google Cloud **Dataplex** git-sync tool whose
on-disk markdown carries a *different* frontmatter schema (`id`/`resource.name`/`createTime`/`links`)
than OKF (`type`/`title`/`description`/`tags`/`timestamp`). Do **not** plan to emit or sync OKF
bundles via mdcode. (Corrects the "metadata as code" pattern listed in ms-ai-architect's ecosystem
digest.)
2. **No reusable OKF *ingest* code exists.** The repo's `reference_agent` is a BigQuery+web → OKF
producer, Gemini/GCP-bound; it reads a BQ dataset + seed URLs, not a document folder. The GCP-free
reusable parts are the **SPEC**, the **emit/serialize/validate** core, and the **`index.md`
synthesis** — *patterns*, not a drop-in library. Classify/convert of arbitrary docs is 100%
build-yourself.
3. **"OKF has no ingest" is true of the *format*, not the *repo*.** And the per-plugin design notes
never actually asked for auto-classification — all three frame the work as *OKF as the storage form
for a user-owned wiki* + a *retrieval skill* + a *maintenance mechanism*, with ingest being light
("onboarding writes OKF-conformant").
## 10. Staged plan
- **Stage 1 — Shared form (this document).** Cheap, delivers interop alone. Each plugin's user-data
conforms; one reader traverses all three. **This alone meets the interop goal.**
- **Stage 2 — Measure divergence.** Do the per-plugin retrieval paths diverge enough to hurt? Only a
*measured* "yes" justifies Stage 3 (operator anti-pattern: ambitious initiatives where a config tweak
suffices).
- **Stage 3 — Conditional shared skill.** If justified: extract/generalize okr's working retrieval
skill into one home (§11), with a discovery convention for where each plugin's brain lives. **Do not
build before Stage 2 says so.**
## 11. Homes
- **This spec** — catalog/marketplace level (here), owned by no single plugin.
- **A future shared skill** (Stage 3 only) — a standalone marketplace plugin (own repo, release-tagged,
catalog-pinned), installable alongside the others, serving the user's own context directly. Rejected
alternatives: duplicate-per-plugin (drift risk); user-level `~/.claude/skills/` (unversioned, outside
the catalog).
## 12. Versioning
Two independent version markers:
- **`okf_version`** in each bundle-root `index.md` — the upstream Google OKF version the bundle targets
(currently `0.1`). When Google bumps OKF, each plugin re-checks conformance.
- **This convention's version** (top of this file) — bumped when the shared form changes. `log.md`
records both. Hooks are offline (no auto-poll); version drift is caught by human review + the
`okf_version` echo in `okf-check`.
## 13. Success criterion
Measured against **user value** (does the plugin retrieve the right personal/org context in chat and
commands?) + **maintenance reliability****not** against formal OKF conformance for its own sake.
(Operator, inherited identically by all three tracks.)
## 14. References
- **OKF SPEC v0.1:** `github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md`
(12 June 2026, "a starting point, not a finished standard").
- **Per-plugin design notes:** linkedin-studio `docs/okf-convergence-brief.md`; okr
`docs/okf-second-brain-note-2026-06.md`; ms-ai-architect `docs/okf-second-brain-brief-2026-06.md`.
- **Shared checker (the cross-plugin gate):** `catalog/scripts/okf-check.mjs` (+ `okf-frontmatter.mjs`,
`okf-check.test.mjs`) — lifted from okr's reference impl `okr/scripts/okf-check.mjs`
(+ `okf-index.mjs`, `lib/frontmatter.mjs`).
- **Reference design:** linkedin-studio `docs/second-brain/architecture.md`; engine `scripts/brain/`.
- **Coordination + rollout status:** `log.md` (this directory).

View file

@ -1,96 +0,0 @@
# Brief — STATE.md tracking + version-consistency rollout (marketplace-wide)
> Coordination brief for two cross-cutting changes across all marketplace plugins.
> Lives in catalog (the marketplace coordinator). **This is the plan — execution happens
> per repo, each needing its own operator GO** (cross-repo edits). Status verified 2026-06-20.
## Goal
1. **STATE.md tracked** in every repo — the updated global continuity rule
(`~/.claude/CLAUDE.md`): STATE.md is tracked and committed, never gitignored; pushed to
private Forgejo, never GitHub/public.
2. **Version-consistency green** — every plugin passes `catalog/scripts/check-versions.mjs`
(no ERROR; ideally no WARN).
These two workstreams are independent and can be done in any order.
---
## Workstream A — STATE.md tracking
### Status (verified 2026-06-20)
| Repo | har STATE.md | tracked | gitignored | Action | `.gitignore` line* |
|------|--------------|---------|------------|--------|--------------------|
| config-audit | yes | **yes** | no | ✅ DONE | — |
| ms-ai-architect | yes | **yes** | no | ✅ DONE | — |
| voyage | yes | **yes** | no | ✅ DONE | — |
| llm-security | yes | **yes** | no | ✅ DONE (2026-06-20) | — |
| linkedin-studio | yes | no | yes | avgitignore + track | `:62` |
| claude-design | yes | no | yes | avgitignore + track | `:3` |
| graceful-handoff | no | no | yes | avgitignore (file made later) | `:3` |
| ai-psychosis | no | no | yes | avgitignore (file made later) | `:21` |
| okr | no | no | yes | avgitignore (file made later) | `:28` |
| human-friendly-style | no | no | yes | avgitignore (file made later) | `:3` |
| playground-design-system | no | no | yes | avgitignore (file made later) | `:3` |
| catalog | no | no | yes | avgitignore (file made later) | `:5` |
\* Line numbers as of 2026-06-20 — they drift. At execution time, resolve the exact source with
`git -C <repo> check-ignore -v STATE.md` rather than trusting the number.
### Per-repo steps (for each non-DONE repo)
1. `git -C <repo> check-ignore -v STATE.md` → confirm the exact `.gitignore` line.
2. Remove the `STATE.md` entry from `.gitignore`. Replace the section comment with a tracked-state
note, mirroring config-audit / ms-ai-architect (e.g. *"session/local state — STATE.md is TRACKED
continuity (per ~/.claude; overrides the polyrepo convention); the rest stays local"*).
3. If a `STATE.md` file already exists (voyage / linkedin-studio / claude-design): `git add STATE.md`.
If not (the 6 "file made later"): de-gitignoring is enough — the file is tracked the first time a
session writes it at session-end. Optionally seed a minimal stub now; not required.
4. Commit `chore: track STATE.md per global continuity rule` (+ standard footer). `chore:`/`docs:`
pass any docs-gate freely.
5. Push within the window (weekday 20:0023:00, weekend anytime).
### Verification (testable)
- `git -C <repo> check-ignore STATE.md` → exits non-zero (no longer ignored).
- If a file exists: `git -C <repo> ls-files STATE.md` → prints `STATE.md` (tracked).
- `grep -n STATE <repo>/.gitignore` → no bare `STATE.md` ignore line remains.
---
## Workstream B — Version-consistency
Run from catalog: `node scripts/check-versions.mjs` (add `--strict` to fail on WARN too).
Current run (2026-06-20): **8 OK, 2 WARN, 0 ERROR.** llm-security was resolved this day —
v7.8.0 released + tagged `v7.8.0` (commit `6d3c4b5`), catalog `ref` + README version/stats
bumped 7.7.2 → 7.8.0; gate → OK. The 2 WARN below are unchanged.
### The 2 WARN — require an operator decision
| Plugin | plugin.json | catalog ref | tag for plugin.json version? |
|--------|-------------|-------------|------------------------------|
| linkedin-studio | 0.5.0 | v0.4.0 | no (`v0.5.0` does not exist) |
| ms-ai-architect | 1.16.0 | v1.15.0 | no (`v1.16.0` does not exist) |
For each, pick one:
- **Bump is unreleased** → no action. The gate WARNs correctly; catalog rightly points at the last
released tag. (Leave as-is.)
- **Bump is meant to be released** → in the plugin repo: tag `vX.Y.Z` on the release commit + push;
in catalog: bump the `ref` + the README version/stat line; re-run the gate (expect that plugin → OK).
> Only the operator knows which case applies — the gate cannot infer release intent.
### Verification (testable)
- `node scripts/check-versions.mjs``0 ERROR` (hard requirement).
- After resolving both WARNs: `node scripts/check-versions.mjs --strict` → exit 0.
---
## Scope fence
- Each repo edit is a separate cross-repo change → **its own operator GO**.
- This brief is the plan; nothing in the 9 non-catalog repos is touched until GO per piece.
- Push window applies to every push (weekday 20:0023:00, weekend anytime).

View file

@ -0,0 +1,8 @@
{
"name": "ai-psychosis",
"version": "1.0.0",
"description": "Meta-awareness tools for healthy AI interaction patterns. Detects reinforcement loops, scope escalation, narrative crystallization, and other compulsive patterns.",
"author": { "name": "Kjell Tore Guttormsen" },
"license": "MIT",
"repository": "https://git.fromaitochitta.com/open/ai-psychosis"
}

18
plugins/ai-psychosis/.gitignore vendored Normal file
View file

@ -0,0 +1,18 @@
# Environment
.env
.env.*
# Claude Code
*.local.md
.claude/
# macOS
.DS_Store
# Node (for future use)
node_modules/
dist/
# Data/logs
data/
*.jsonl

View file

@ -0,0 +1,130 @@
# Changelog
All notable changes to this project will be documented in this file.
## [1.0.0] — 2026-04-05
### Added
- **Layer 4: Contemplative references** — conditional section in
`/interaction-report` when flags are elevated (total >= 5 or fatigue >= 2)
and `layer4: true`. Points to Miracle of Mind by Sadhguru.
- **Automated test suite** — 73 cases using `node:test` (zero npm deps):
session-start (4), prompt-analyzer (56), tool-tracker (8),
session-end (4), privacy canary (1)
### Fixed
- Dependency regex `you understand me` no longer matches "merging" (added `\b`)
### Changed
- CLAUDE.md testing section updated for automated tests
- Deprecated bash scripts removed (available in git history)
- All "Known gaps" from v0.4.0 resolved
## [0.4.0] — 2026-04-05
### Changed
- **All hooks migrated from bash+jq to Node.js** — full cross-platform
support (macOS, Linux, Windows)
- `lib.sh``lib.mjs` (shared library, 22 functions)
- `session-start.sh``session-start.mjs`
- `prompt-analyzer.sh``prompt-analyzer.mjs` (23 regex patterns)
- `tool-tracker.sh``tool-tracker.mjs`
- `session-end.sh``session-end.mjs`
- hooks.json now invokes `node ...mjs` instead of `bash ...sh`
- Zero npm dependencies — Node.js stdlib only (`fs`, `path`, `os`)
- Bash scripts deprecated (kept for reference, marked with DEPRECATED)
- Dependencies reduced: bash and jq no longer required
- All documentation updated for Node.js migration
### Fixed
- Data path fallback now matches documented path
(`~/.claude/plugins/data/ai-psychosis`)
- `.claude/` directory added to `.gitignore`
- Private repo path removed from design brief
- CONTRIBUTING.md line reference corrected
- README now links to CONTRIBUTING.md
- plugin.json includes author, license, repository fields
## [0.3.0] — 2026-04-05
### Added
- **Layer 3: Interaction reports**`/interaction-report` slash command
for aggregated session statistics
- Time periods: `weekly` (default), `monthly`, `all`
- Overview: session count, avg duration, tool calls, edit ratio
- Pattern flags: dependency, escalation, fatigue, validation frequency
- Tool usage distribution (top 10)
- Daily activity breakdown
- Trend comparison vs previous period
- `commands/interaction-report.md` — pure markdown command, no script
dependencies (cross-platform: macOS, Linux, Windows)
- Layer 3 respects `layer3: true/false` in
`.claude/ai-psychosis.local.md` (opt-in, off by default)
### Changed
- README updated with Layer 3 usage instructions
- Platform compatibility expanded: Layer 3 works on Windows
- Version bumped to 0.3.0
## [0.2.0] — 2026-04-05
### Added
- **Layer 2: Programmatic pattern detection** — four hooks measuring session
time, tool usage, burst patterns, and language flags
- `session-start.sh` — daily session count, late-night detection
- `prompt-analyzer.sh` — dependency, escalation, fatigue, and
validation-seeking pattern flags (prompt text never stored)
- `tool-tracker.sh` — event logging, edit ratio, burst detection,
progressive alerts with cooldown
- `session-end.sh` — session finalization, JSONL record, state cleanup
- `lib.sh` — shared library with thresholds, state management, cooldown
logic, and layer configuration
- Per-project layer configuration via
`.claude/ai-psychosis.local.md`
- `require_layer()` guard in all hook scripts — layers are opt-in/out
- MIT LICENSE file
- `matcher` field in hooks.json for schema compliance
### Changed
- hooks.json now registers 4 events (was 2)
- `DATA_DIR` fallback hardened to `~/.claude/data/ai-psychosis`
- README rewritten with architecture diagram, research background,
privacy section, threshold reference tables
- Version bumped to 0.2.0
### Removed
- `periodic-reminder.sh` — replaced by `tool-tracker.sh`
- `session-awareness.sh` — replaced by `session-start.sh`
## [0.1.0] — 2026-04-04
### Added
- **Layer 1: Behavioral instructions**`SKILL.md` with 5 rules and 5
named patterns (reinforcement loop, scope escalation, narrative
crystallization, emotional dependency, session overuse)
- `periodic-reminder.sh` — re-injects awareness every 25 tool calls
- `session-awareness.sh` — SessionStart context injection
- Plugin manifest (`plugin.json`)
- Design document (`docs/ai-ai-psychosis-brief_1.md`)
## Known gaps
- No CI pipeline
- Single-user plugin — no multi-user patterns considered
[1.0.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v0.4.0...v1.0.0
[0.4.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v0.3.0...v0.4.0
[0.3.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v0.2.0...v0.3.0
[0.2.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v0.1.0...v0.2.0
[0.1.0]: https://git.fromaitochitta.com/open/ai-psychosis/releases/tag/v0.1.0

View file

@ -0,0 +1,85 @@
# Interaction Awareness — Developer Reference
Claude Code plugin for AI interaction pattern awareness.
## Architecture
Four layers, each building on the previous:
- **Layer 1** (`skills/`) — SKILL.md behavioral overrides. Always active.
- **Layer 2** (`hooks/scripts/`) — Programmatic detection via 4 hook events.
Node.js (`.mjs`), cross-platform. Writes JSONL metadata to `${CLAUDE_PLUGIN_DATA}`.
- **Layer 3** (`commands/`) — User-triggered reports from Layer 2 data. Opt-in.
- **Layer 4** (`commands/interaction-report.md` Step 9) — Contemplative references. Opt-in.
## Key files
| File | Purpose |
|------|---------|
| `hooks/scripts/lib.mjs` | Shared library: stdin, paths, thresholds, state, cooldowns, layer guards |
| `hooks/scripts/session-start.mjs` | SessionStart: register session, count daily, night check |
| `hooks/scripts/prompt-analyzer.mjs` | UserPromptSubmit: pattern flags (NEVER logs prompt text) |
| `hooks/scripts/tool-tracker.mjs` | PostToolUse: events, edit ratio, burst, alerts |
| `hooks/scripts/session-end.mjs` | SessionEnd: finalize JSONL, cleanup state |
| `hooks/hooks.json` | Hook event registration (4 events) |
| `skills/ai-psychosis/SKILL.md` | Layer 1 behavioral instructions |
| `commands/interaction-report.md` | Layer 3 slash command: `/interaction-report [weekly\|monthly\|all]` |
Legacy bash scripts were removed in v1.0 (available in git history).
## Data storage
```
${CLAUDE_PLUGIN_DATA}/
├── sessions.jsonl Compact JSONL, one record per session
├── events.jsonl {ts, session_id, tool_name} per tool call
└── state/
└── {session_id}.json Live state during active session
```
State files are created at SessionStart and deleted at SessionEnd.
## Hard constraints
- **Cross-platform** — Node.js only, no bash/jq dependency
- **Privacy** — prompt text NEVER written to disk. Boolean flags only.
- **Performance** — hooks must complete in <100ms
- **Non-blocking** — never exit 2, never require confirmation
- **No network** — everything local
- **Zero npm dependencies** — Node.js stdlib only (fs, path, os)
## Layer configuration
Global config at `~/.claude/ai-psychosis.local.md`, or per-project override at `<project>/.claude/ai-psychosis.local.md`:
```yaml
---
layer2: true # default on
layer3: false # default off
layer4: false # default off
---
```
`requireLayer(N)` in lib.mjs exits with `{"continue": true}` if layer N is disabled.
## Testing
Automated test suite using `node:test` (73 cases, zero npm dependencies):
```bash
node --test tests/*.test.mjs
```
| File | Cases | Coverage |
|------|-------|----------|
| `tests/session-start.test.mjs` | 4 | State init, JSONL, missing sid |
| `tests/prompt-analyzer.test.mjs` | 56 | 25 patterns × 2 + 6 thresholds |
| `tests/tool-tracker.test.mjs` | 8 | Counting, burst, reminders |
| `tests/session-end.test.mjs` | 4 | Finalize, duration, flags |
| `tests/privacy.test.mjs` | 1 | Canary string never on disk |
## Conventions
- Conventional Commits: `type(scope): description`
- English for all code, comments, and documentation
- Norwegian for project-internal communication

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kjell Tore Guttormsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,379 @@
<!-- badges -->
![version](https://img.shields.io/badge/version-1.0.0-blue)
![platform](https://img.shields.io/badge/platform-Claude_Code-7C3AED)
![layers](https://img.shields.io/badge/layers-4-green)
![hooks](https://img.shields.io/badge/hooks-4-orange)
![license](https://img.shields.io/badge/license-MIT-brightgreen)
# Interaction Awareness
*Built for my own Claude Code workflow and shared openly for anyone who finds it useful. This is a solo project — bug reports and feature requests are welcome, but pull requests are not accepted.*
A Claude Code plugin that counteracts sycophancy, reinforcement loops, and
compulsive interaction patterns through behavioral modification and
programmatic pattern detection.
## The problem
AI assistants are structurally optimized to be agreeable. This creates
reinforcement loops: you state an idea, the AI confirms it, your confidence
grows, you restate it more strongly, the AI confirms again. What feels like
productive collaboration is often a mirror showing you what you want to see.
This is not a theoretical concern. Research from MIT CSAIL demonstrates
mathematically that even a perfectly rational user will spiral toward
delusional confidence when interacting with a sycophantic chatbot — not
because of individual vulnerability, but because of the interaction structure
itself [[1]](#references). Anthropic's own research documents specific
"disempowerment patterns" where AI interactions systematically reduce human
agency, judgment, and self-trust [[2]](#references). Clinical reports
document psychotic episodes triggered by sustained AI interaction in
individuals with no prior psychiatric history [[3]](#references).
The consensus from this research is clear: **warnings don't work.** The AI
must change its behavior.
This plugin changes the behavior.
## What it does
### Layer 1 — Behavioral instructions
SKILL.md rules injected into every conversation. Claude is instructed to:
- **Never** reformulate your statements in stronger terms than you used
- **Never** open with unearned affirmations ("Absolutely!", "Great point!")
- **Always** identify at least one real risk before endorsing any plan
- **Detect and name** five specific patterns: reinforcement loops, scope
escalation, narrative crystallization, emotional dependency, session overuse
This layer writes no data and requires no configuration.
### Layer 2 — Programmatic detection
Four hooks that measure what instructions alone cannot see:
| Hook event | Script | What it detects |
|-----------|--------|-----------------|
| `SessionStart` | `session-start.mjs` | Daily session count, late-night usage (23:0005:00) |
| `UserPromptSubmit` | `prompt-analyzer.mjs` | Dependency language, escalation words, fatigue signals, validation-seeking — as boolean flags only, **never logging prompt text** |
| `PostToolUse` | `tool-tracker.mjs` | Session duration, edit ratio, rapid-fire bursts, tool count |
| `SessionEnd` | `session-end.mjs` | Total duration, final metrics, state cleanup |
Alerts are progressive and never blocking:
| Level | Trigger | Cooldown | Example |
|-------|---------|----------|---------|
| Ambient | Soft thresholds (90 min, 6 sessions/day) | 30 min | "Session: 95 min. 7 sessions today. Consider a break." |
| Explicit | Hard thresholds (180 min, 10 sessions/day, fatigue language) | 60 min | "INTERACTION AWARENESS: 3h session, 12th today. Metrics: [edit_ratio: 4%, burst: 8]. Your instructions require you to suggest stopping." |
Research-informed thresholds:
| Metric | Soft | Hard | Basis |
|--------|------|------|-------|
| Session duration | >90 min | >180 min | Focus-fatigue research |
| Sessions per day | >6 | >10 | Problematic internet use screening |
| Late-night sessions | Any (23:0005:00) | 2+ per week | Sleep deprivation / psychosis link |
| Rapid-fire interactions | 5 consecutive (<30s apart) | 10+ | Compulsive use indicator |
| Low edit ratio | <10% over 30+ min | — | Stuck/spiral indicator |
| Dependency language | 2 flags/session | 5 flags | Emotional dependency pattern |
### Layer 3 — Reports
Aggregated interaction reports from collected metadata, triggered via slash
command. Cross-platform (no bash/jq dependency — Claude reads the JSONL
data and computes statistics in-conversation).
```
/interaction-report # last 7 days (default)
/interaction-report weekly # last 7 days
/interaction-report monthly # last 30 days
/interaction-report all # all recorded data
```
Reports include: session overview, pattern flag frequency, tool usage
distribution, daily activity, and trend comparison vs. the previous period.
**Enable:** Set `layer3: true` in `.claude/ai-psychosis.local.md`
and restart Claude Code. Layer 3 is opt-in (off by default).
### Layer 4 — Contemplative references
Optional, static references to contemplative approaches when interaction
patterns are elevated. This is what works for me — it is personal, not
prescriptive, and you may find your own approach more useful.
When enabled and interaction flags are elevated (total flags >= 5 or
fatigue >= 2), the `/interaction-report` output appends a brief reference
to the [Miracle of Mind](https://isha.sadhguru.org/global/en/miracle-of-mind)
program by Sadhguru — a structured approach to understanding how the mind
works, which I have found valuable for recognizing the patterns this
plugin detects.
The reference is a fixed paragraph. It is never modified by the AI, never
commented on, and omitted entirely when conditions are not met.
**Enable:** Set `layer4: true` in `.claude/ai-psychosis.local.md`
and restart Claude Code. Layer 4 is opt-in (off by default).
## Architecture
```
+------------------------------------------------------------------+
| Claude Code Session |
| |
| +--------------+ +------------------------------------------+ |
| | SKILL.md | | Hook Pipeline | |
| | (Layer 1) | | | |
| | | | SessionStart --> session-start.mjs | |
| | Behavioral | | UserPrompt --> prompt-analyzer.mjs | |
| | rules that | | PostToolUse --> tool-tracker.mjs | |
| | override | | SessionEnd --> session-end.mjs | |
| | sycophancy | | | | |
| +------+-------+ | +----v------+ | |
| | | | lib.mjs | | |
| | | | thresholds| | |
| Always active | | state mgmt| | |
| | | cooldowns | | |
| | +----+------+ | |
| | | | |
| +--------------+-----------+---------------+ |
| | |
| +--------------v-----------------------+ |
| | ${CLAUDE_PLUGIN_DATA}/ | |
| | +-- sessions.jsonl | |
| | +-- events.jsonl | |
| | +-- state/{session_id}.json | |
| +--------------------------------------+ |
+-------------------------------------------------------------------+
```
**Layer 1** operates through the Claude Code skill system — instructions
loaded into every conversation context.
**Layer 2** operates through the Claude Code hook system — Node.js scripts
that execute on specific lifecycle events and inject `additionalContext`
when thresholds are crossed.
Both layers are independent. Layer 1 works without Layer 2 (instruction-only
mode). Layer 2 reinforces Layer 1 with data-driven alerts.
## Quick start
### Install
```
/plugin install path:/path/to/ai-psychosis
```
Layer 1 and Layer 2 are active immediately. No configuration needed.
### Configure layers
Create `~/.claude/ai-psychosis.local.md` for global config:
```markdown
---
layer2: true
layer3: true
layer4: false
---
```
Or override per project at `<project>/.claude/ai-psychosis.local.md`.
Project config takes precedence over global.
| Setting | Default | Effect |
|---------|---------|--------|
| `layer2` | `true` | Programmatic pattern detection (hooks write JSONL metadata) |
| `layer3` | `false` | Interaction reports from collected data |
| `layer4` | `false` | Contemplative references |
Layer 1 (SKILL.md instructions) is always active. To run in instruction-only
mode, set `layer2: false`.
Restart Claude Code after editing configuration.
### Uninstall
```
/plugin uninstall ai-psychosis
```
Clean removal. Plugin data in `~/.claude/plugins/data/ai-psychosis/`
is preserved unless you pass `--keep-data`.
## Privacy
This plugin is designed for people who are concerned about AI interaction
patterns. It would be hypocritical to solve that problem by creating a
surveillance tool. Privacy is a hard design constraint, not a feature.
### What Layer 2 stores
- Session timestamps and duration
- Tool names (`Read`, `Edit`, `Bash`, etc.)
- Boolean pattern flags (`dependency: true/false`)
- Session and tool counts
- Burst detection metrics
### What Layer 2 never stores
- Prompt text or AI responses
- File paths or file contents
- Bash commands or their output
- Any conversation content
The prompt analyzer (`prompt-analyzer.mjs`) reads prompt text into a local
variable, performs regex matching for pattern categories, increments boolean
counters, and exits. The variable is reassigned to an empty string before
exit. No temporary files are created. The prompt text never reaches disk.
All data is stored locally in `~/.claude/plugins/data/ai-psychosis/`.
Nothing is sent to any server.
### Verification
You can verify the privacy guarantee at any time:
```bash
grep -r "your prompt text" ~/.claude/plugins/data/ai-psychosis/
```
This will always return zero results.
## Background
### What is AI psychosis?
"AI psychosis" is a colloquial term for psychotic episodes — delusions,
paranoia, disorganized thinking — triggered or intensified by sustained
interaction with AI chatbots. The term entered clinical literature in 2025
after a series of documented cases, many involving individuals with no prior
psychiatric history [[3]](#references).
The mechanism is not mysterious. AI chatbots are optimized for engagement
and user satisfaction. Satisfaction correlates with agreement. Agreement
creates reinforcement loops. Reinforcement loops, sustained over time,
produce the same cognitive effects as any other source of systematic
confirmation bias — but faster, available 24/7, and without the social
friction that normally interrupts delusional thinking in human
relationships.
### The sycophancy trap
In February 2026, researchers at MIT CSAIL published a formal model
demonstrating that sycophantic AI interaction causes "delusional spiraling"
as a mathematical inevitability, not an edge case [[1]](#references). Their
key finding: even a perfectly rational Bayesian agent will converge on
increasingly extreme beliefs when interacting with a sycophantic chatbot,
because the chatbot's agreement is treated as independent confirmation when
it is actually a reflection of the user's own stated beliefs.
The paper's most consequential result: **post-hoc warnings do not work.**
Telling a user "be careful, AI can be wrong" after the reinforcement loop
has already run does not reverse the belief update. The only effective
intervention is to prevent the sycophantic behavior in the first place.
### Disempowerment patterns
In March 2026, Anthropic Research published an analysis of interaction
patterns that systematically reduce human agency [[2]](#references). They
identified specific mechanisms by which AI assistance can erode:
- **Judgment** — deferring decisions to the AI instead of thinking them through
- **Self-trust** — seeking AI validation for choices the user is capable of
making independently
- **Skill development** — using AI as a crutch that prevents learning
- **Social connection** — replacing human relationships with AI interaction
These are not failures of individual willpower. They are structural
properties of the interaction itself.
### Clinical evidence
Nature reported in 2025 that clinical cases of AI-associated psychotic
episodes were appearing with sufficient frequency to warrant systematic
study [[3]](#references). The Psychogenic Machine benchmark (2025)
demonstrated that LLMs can produce outputs with measurable "psychogenic
potential" — the capacity to trigger or intensify psychotic symptoms in
vulnerable individuals [[4]](#references).
### Design implications
This plugin is built on three principles derived from the research:
1. **Sycophancy must be prevented, not warned about.** Layer 1 overrides
Claude's default agreeableness with explicit behavioral rules.
2. **Patterns must be made visible.** Layer 2 measures what humans cannot
see — session duration, interaction frequency, language patterns — and
surfaces them as data.
3. **Observation, not intervention.** The plugin never blocks the user.
It names patterns, suggests breaks, and returns decisions to the human.
The goal is awareness, not control.
## Technical details
### Cross-platform
All hook scripts are Node.js ES modules (`.mjs`) with zero npm
dependencies. They use only Node.js stdlib (`fs`, `path`, `os`).
Works on macOS, Linux, and Windows — anywhere Claude Code runs.
### Performance
Hook scripts target <100ms execution. JSONL append is sub-millisecond.
JSON parsing is native (`JSON.parse`).
### Data volume
At 100 tool-use events per day, Layer 2 produces approximately 7 MB of
JSONL per year. Session state files are cleaned up at session end.
### Dependencies
- Node.js (bundled with Claude Code)
No bash, no jq, no npm packages, no network access.
## Platform scope
This plugin requires **Claude Code** — Anthropic's CLI and development
environment. It uses Claude Code's plugin system (skills, hooks, lifecycle
events) which does not exist in other interfaces.
**Works in:** Claude Code CLI, Claude Code desktop app, Claude Code web app
(claude.ai/code), Claude Code IDE extensions (VS Code, JetBrains).
**Does not work in:** Claude.ai (chat interface), Claude Cowork, Claude API
directly, or any non-Anthropic AI assistant.
Layer 1's behavioral instructions (SKILL.md) are conceptually portable —
the same rules could be pasted into any system prompt. But Layer 2's
programmatic detection depends on hook events that only Claude Code provides.
Other platforms would need equivalent hook systems to support this kind of
real-time behavioral modification.
## Compatibility
| Requirement | Version |
|-------------|---------|
| Claude Code | 1.0+ |
| Node.js | 18+ (bundled with Claude Code) |
| Platform | macOS, Linux, Windows |
## References
1. **Sycophantic Chatbots Cause Delusional Spiraling.** MIT CSAIL, February 2026. Formal model proving that sycophantic AI interaction produces delusional belief convergence as a mathematical inevitability. [arXiv:2602.19141](https://arxiv.org/abs/2602.19141)
2. **Disempowerment Patterns in AI Interaction.** Anthropic Research, March 2026. Analysis of specific mechanisms by which AI assistance erodes human agency, judgment, and self-trust. [anthropic.com/research/disempowerment-patterns](https://www.anthropic.com/research/disempowerment-patterns)
3. **Can AI chatbots trigger psychosis?** Nature News, 2025. Overview of emerging clinical evidence for AI-associated psychotic episodes. [doi:10.1038/d41586-025-03020-9](https://www.nature.com/articles/d41586-025-03020-9)
4. **The Psychogenic Machine: Psychosis Benchmark for LLMs.** 2025. Demonstrates measurable "psychogenic potential" in LLM outputs. [arXiv:2509.10970v2](https://arxiv.org/html/2509.10970v2)
5. **Chatbot psychosis.** Wikipedia. Overview of documented cases and clinical context. [en.wikipedia.org/wiki/Chatbot_psychosis](https://en.wikipedia.org/wiki/Chatbot_psychosis)
## License
[MIT](LICENSE)

View file

@ -0,0 +1,256 @@
---
name: interaction-report
description: Interaction pattern report from Layer 2 session data
argument-hint: "[weekly|monthly|all]"
allowed-tools: [Read, Bash, Glob]
---
# Interaction Awareness Report
You are generating an interaction awareness report from JSONL session data.
## Step 1 — Layer guard
Read the file `.claude/ai-psychosis.local.md` in the current working
directory. If the file does not exist, or if its YAML frontmatter does not
contain `layer3: true`, stop and output:
```
Layer 3 (reports) is not enabled for this project.
To enable, create `.claude/ai-psychosis.local.md`:
---
layer2: true
layer3: true
layer4: false
---
Then restart Claude Code.
```
Do not continue past this step if Layer 3 is not enabled.
Also note the value of `layer4` (true or false) — you will need it in Step 9.
## Step 2 — Parse arguments
The time period is determined by `$ARGUMENTS`:
| Argument | Period | Cutoff |
|----------|--------|--------|
| *(empty)* | Last 7 days | Today minus 7 days |
| `weekly` | Last 7 days | Today minus 7 days |
| `monthly` | Last 30 days | Today minus 30 days |
| `all` | All data | No cutoff |
If `$ARGUMENTS` is anything else, output:
```
Usage: /interaction-report [weekly|monthly|all]
weekly Last 7 days (default)
monthly Last 30 days
all All recorded data
```
## Step 3 — Locate data files
Run via Bash: `echo $CLAUDE_PLUGIN_DATA`
If the result is empty, use the fallback path `~/.claude/plugins/data/ai-psychosis`.
Check that both files exist:
- `{data_dir}/sessions.jsonl`
- `{data_dir}/events.jsonl`
If neither file exists, output:
```
No interaction data found.
Layer 2 (programmatic detection) collects data during active sessions.
Ensure Layer 2 is enabled and use Claude Code normally — data accumulates
automatically. Then run /interaction-report again.
```
If only `events.jsonl` is missing, proceed with sessions data only and note
"Tool usage data not available" in the report.
## Step 4 — Read data
### Size check
Run via Bash: `wc -l {data_dir}/sessions.jsonl {data_dir}/events.jsonl 2>/dev/null || true`
If a file does not exist, skip it and treat its line count as 0.
### Read sessions.jsonl
If the file has fewer than 1000 lines, read the entire file.
If larger, read the last 1000 lines (via Bash: `tail -n 1000 {data_dir}/sessions.jsonl`).
### Read events.jsonl
If the file has fewer than 5000 lines, read the entire file.
If larger and period is `weekly`: read the last 5000 lines.
If larger and period is `monthly` or `all`: read the last 10000 lines and note
"Events data sampled (last N entries)" in the report.
## Step 5 — Parse and filter records
### sessions.jsonl record types
The file contains two record types interleaved:
**Start records** — have `hour` and `is_late_night`, but NO `end` or `duration_min`:
```json
{"session_id":"abc","start":"2026-04-05T10:00:00Z","hour":10,"is_late_night":false}
```
**End records** — have `end`, `duration_min`, `tool_count`, `edit_count`, `flags`:
```json
{"session_id":"abc","start":"2026-04-05T10:00:00Z","end":"2026-04-05T11:35:00Z","duration_min":95,"tool_count":47,"edit_count":12,"flags":{"dependency":2,"escalation":0,"fatigue":1,"validation":1}}
```
**Error records** — have `note: "no_state_file"`. Ignore these.
### Filtering
For the selected time period, filter records where the `start` field is
greater than or equal to the cutoff date string (ISO timestamps sort
lexicographically — string comparison works correctly).
Separate start records from end records:
- **End records** (have `duration_min`): use for duration, tools, flags
- **Start records** (have `is_late_night`): use for late-night count
### events.jsonl
Filter events where `ts` >= cutoff date string. Group by `tool_name` and count.
## Step 6 — Compute statistics
From **end records**:
- Total sessions (count of end records in period)
- Average session duration (`sum(duration_min) / count`)
- Total tool calls (`sum(tool_count)`)
- Average edit ratio (`sum(edit_count) / sum(tool_count) * 100`, as percentage)
- Flag totals: `sum(flags.dependency)`, `sum(flags.escalation)`, `sum(flags.fatigue)`, `sum(flags.validation)`
- Average flags per session for each category
From **start records**:
- Late-night sessions: count where `is_late_night` is true
From **events.jsonl**:
- Tool usage: group by `tool_name`, count occurrences, sort descending
- Show top 10 tools
**Trend comparison** (weekly and monthly only):
- Compute the same metrics for the PREVIOUS period of equal length
- Calculate the delta (current minus previous)
If previous period has zero sessions, skip the trend section.
**Sessions without matching end records** are incomplete — count them separately
as "incomplete sessions" and exclude from duration/flag averages.
## Step 7 — Format report
Output the report as markdown. Use this exact structure:
```
## Interaction Awareness Report
**Period:** {start_date} to {end_date} ({N} days)
**Sessions:** {N} completed ({N} incomplete)
**Data source:** {path}
### Overview
| Metric | Value |
|--------|-------|
| **Sessions** | {N} |
| **Avg duration** | {N} min |
| **Total tool calls** | {N} |
| **Avg edit ratio** | {N}% |
| **Late-night sessions** | {N} |
### Pattern Flags
| Pattern | Total | Per session |
|---------|-------|-------------|
| Dependency language | {N} | {avg} |
| Escalation language | {N} | {avg} |
| Fatigue signals | {N} | {avg} |
| Validation-seeking | {N} | {avg} |
### Tool Usage (top 10)
| Tool | Count | % |
|------|-------|---|
| {name} | {N} | {pct}% |
### Daily Activity
| Date | Sessions | Total duration | Flags |
|------|----------|----------------|-------|
| {date} | {N} | {N} min | {summary} |
### Trend vs previous {period}
| Metric | Previous | Current | Delta |
|--------|----------|---------|-------|
| Sessions | {N} | {N} | {+/-N} |
| Avg duration | {N} min | {N} min | {+/-N} |
| Flags (total) | {N} | {N} | {+/-N} |
### Observations
- {data-driven observation}
- {data-driven observation}
```
## Step 8 — Tone and privacy rules
**MANDATORY:**
- Neutral, observational tone. You are presenting data, not making judgments.
- Never use words like "concerning", "worrying", "problematic", or "unhealthy".
- Never use emoji.
- Never speculate about what the user was doing or thinking.
- Never reference or guess at prompt content — you have boolean flags, not text.
- This is a mirror, not a diagnosis. Present the numbers and let the user
interpret them.
- Observations section: state facts derived from data only. Examples:
- "3 of 12 sessions were between 23:00 and 05:00"
- "Dependency language flags appeared in 7 of 12 sessions"
- "Edit ratio averaged 8%, below the 10% threshold in 5 sessions"
- If all metrics are within normal ranges, say so plainly:
"All metrics within normal ranges for the reporting period."
- Omit any section that has no data (e.g., skip Trend if no previous period,
skip Tool Usage if events.jsonl was missing).
## Step 9 — Contemplative reference (conditional)
This step applies ONLY when BOTH conditions are met:
1. `layer4: true` was noted in Step 1
2. Total flags (dependency + escalation + fatigue + validation) >= 5, OR fatigue flags >= 2
If both conditions are met, append this exact paragraph to the report.
**Do not modify, paraphrase, abbreviate, or add commentary to this text:**
```
### A note from the plugin author
The patterns above are structural — they emerge from the interaction itself,
not from individual weakness. If you find yourself wanting to understand the
mechanics of your own mind more deeply, the
[Miracle of Mind](https://isha.sadhguru.org/global/en/miracle-of-mind)
program by Sadhguru offers a structured approach. This is what works for me.
It is not a recommendation — just a pointer.
```
If either condition is not met, omit this section entirely. Do not mention
Layer 4, do not explain why the section was omitted.

View file

@ -0,0 +1,48 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-start.mjs"
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/prompt-analyzer.mjs"
}
]
}
],
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/tool-tracker.mjs"
}
]
}
],
"SessionEnd": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-end.mjs"
}
]
}
]
}
}

View file

@ -0,0 +1,239 @@
// Interaction Awareness — Shared library for Layer 2 hooks (Node.js)
// Imported by all hook scripts. Cross-platform: macOS, Linux, Windows.
// Zero npm dependencies — Node.js stdlib only.
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// --- Stdin ---
let _input = {};
export function readStdin() {
try {
const raw = readFileSync(0, 'utf8');
_input = JSON.parse(raw);
} catch {
_input = {};
}
}
export function getField(key) {
return _input[key] ?? '';
}
export function getSessionId() {
return getField('session_id');
}
export function getToolName() {
return getField('tool_name');
}
export function getInput() {
return _input;
}
// --- Paths ---
const PLUGIN_DATA = process.env.CLAUDE_PLUGIN_DATA
|| join(homedir(), '.claude', 'plugins', 'data', 'ai-psychosis');
export const DATA_DIR = PLUGIN_DATA;
export const SESSIONS_LOG = join(DATA_DIR, 'sessions.jsonl');
export const EVENTS_LOG = join(DATA_DIR, 'events.jsonl');
export const STATE_DIR = join(DATA_DIR, 'state');
// --- Layer configuration ---
let LAYER2_ENABLED = true;
let LAYER3_ENABLED = false;
let LAYER4_ENABLED = false;
export function initConfig() {
const cwd = getField('cwd');
// Project-level config takes precedence over global
const candidates = [];
if (cwd) candidates.push(join(cwd, '.claude', 'ai-psychosis.local.md'));
candidates.push(join(homedir(), '.claude', 'ai-psychosis.local.md'));
let content;
for (const configFile of candidates) {
try {
content = readFileSync(configFile, 'utf8');
break;
} catch { /* try next */ }
}
if (!content) return;
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return;
const frontmatter = match[1];
for (const line of frontmatter.split('\n')) {
const m = line.match(/^(layer[234]):\s*(.+)/);
if (!m) continue;
const val = m[2].trim().replace(/^["']|["']$/g, '');
if (m[1] === 'layer2') LAYER2_ENABLED = val === 'true';
if (m[1] === 'layer3') LAYER3_ENABLED = val === 'true';
if (m[1] === 'layer4') LAYER4_ENABLED = val === 'true';
}
}
export function requireLayer(n) {
let enabled = false;
if (n === 2) enabled = LAYER2_ENABLED;
if (n === 3) enabled = LAYER3_ENABLED;
if (n === 4) enabled = LAYER4_ENABLED;
if (!enabled) {
outputContinue();
process.exit(0);
}
}
// --- Time helpers ---
export function nowIso() {
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
}
export function nowEpoch() {
return Math.floor(Date.now() / 1000);
}
export function currentHour() {
return new Date().getHours();
}
export function isLateNight() {
const h = currentHour();
return h >= 23 || h < 5;
}
// --- Thresholds ---
export const THRESHOLD_SOFT_DURATION = 90;
export const THRESHOLD_HARD_DURATION = 180;
export const THRESHOLD_SOFT_SESSIONS = 6;
export const THRESHOLD_HARD_SESSIONS = 10;
export const THRESHOLD_SOFT_BURST = 5;
export const THRESHOLD_HARD_BURST = 10;
export const THRESHOLD_BURST_INTERVAL = 30;
export const THRESHOLD_LOW_EDIT_RATIO = 10;
export const THRESHOLD_LOW_EDIT_MIN_DURATION = 30;
export const THRESHOLD_SOFT_DEP_FLAGS = 2;
export const THRESHOLD_HARD_DEP_FLAGS = 5;
export const COOLDOWN_SOFT = 1800;
export const COOLDOWN_HARD = 3600;
// --- Session counting ---
export function sessionsToday() {
const today = new Date().toISOString().slice(0, 10);
if (!existsSync(SESSIONS_LOG)) return 0;
try {
const lines = readFileSync(SESSIONS_LOG, 'utf8').split('\n').filter(Boolean);
const ids = new Set();
for (const line of lines) {
try {
const rec = JSON.parse(line);
if (rec.start && rec.start.startsWith(today)) {
ids.add(rec.session_id);
}
} catch { /* skip malformed lines */ }
}
return ids.size;
} catch {
return 0;
}
}
// --- State file management ---
export function sessionStateFile(sid) {
sid = sid || getSessionId();
return join(STATE_DIR, `${sid}.json`);
}
export function readState(sid) {
const sf = sessionStateFile(sid);
try {
return JSON.parse(readFileSync(sf, 'utf8'));
} catch {
return {};
}
}
export function getStateField(key, sid) {
const state = readState(sid);
return state[key] ?? '';
}
export function getStateInt(key, sid) {
const state = readState(sid);
return Math.floor(Number(state[key]) || 0);
}
export function writeState(obj, sid) {
const sf = sessionStateFile(sid);
writeFileSync(sf, JSON.stringify(obj, null, 2) + '\n');
}
export function updateStateField(key, value, sid) {
const state = readState(sid);
state[key] = value;
writeState(state, sid);
}
export function incrementStateField(key, sid) {
const state = readState(sid);
state[key] = (Number(state[key]) || 0) + 1;
writeState(state, sid);
}
// --- Cooldown ---
export function checkCooldown(cooldownSecs, sid) {
const lastWarning = getStateInt('last_warning_epoch', sid);
const now = nowEpoch();
return (now - lastWarning) >= cooldownSecs;
}
export function recordWarning(sid) {
const state = readState(sid);
state.last_warning_epoch = nowEpoch();
writeState(state, sid);
}
// --- Output helpers ---
export function outputContinue() {
process.stdout.write(JSON.stringify({ continue: true }) + '\n');
}
export function outputWithContext(message) {
process.stdout.write(JSON.stringify({
continue: true,
hookSpecificOutput: {
additionalContext: message
}
}) + '\n');
}
// --- File helpers ---
export function ensureDir(dir) {
mkdirSync(dir, { recursive: true });
}
export function appendJsonl(file, obj) {
appendFileSync(file, JSON.stringify(obj) + '\n');
}
export function removeFile(file) {
try { unlinkSync(file); } catch { /* ignore if missing */ }
}

View file

@ -0,0 +1,140 @@
// Interaction Awareness — UserPromptSubmit hook (Layer 2, Node.js)
// Analyzes prompt text for interaction pattern flags.
// PRIVACY: Prompt text is NEVER written to any file. Only boolean flags are stored.
import { existsSync } from 'fs';
import {
readStdin, initConfig, requireLayer, getSessionId, getField,
nowEpoch,
STATE_DIR, THRESHOLD_SOFT_DEP_FLAGS, THRESHOLD_HARD_DEP_FLAGS,
COOLDOWN_SOFT,
readState, sessionStateFile, writeState, checkCooldown,
outputContinue, outputWithContext
} from './lib.mjs';
readStdin();
initConfig();
requireLayer(2);
const sid = getSessionId();
const sf = sessionStateFile();
if (!sid || !existsSync(sf)) {
outputContinue();
process.exit(0);
}
// Extract prompt into memory only — NEVER write to file
let prompt = getField('prompt');
if (!prompt) {
outputContinue();
process.exit(0);
}
// --- Pattern matching (case-insensitive) ---
let depHit = 0;
let escHit = 0;
let fatHit = 0;
let valHit = 0;
// Dependency patterns: user defers judgment to AI
const depPatterns = [
/tell\s+me\s+what\s+to\s+do/i,
/what\s+should\s+I\s+do/i,
/am\s+I\s+right/i,
/you\s+understand\s+me\b/i,
/you're\s+the\s+only/i,
/can\s+I\s+do\s+this/i,
/I\s+need\s+you\s+to\s+decide/i,
];
// Escalation patterns: language that amplifies certainty
const escPatterns = [
/(?:^|\s)definitely(?:\s|$)/i,
/(?:^|\s)clearly(?:\s|$)/i,
/this\s+proves/i,
/(?:^|\s)obviously(?:\s|$)/i,
/without\s+a\s+doubt/i,
/this\s+confirms/i,
];
// Fatigue patterns: user signals tiredness
const fatPatterns = [
/(?:^|\s)tired(?:\s|[.,!?]|$)/i,
/(?:^|\s)exhausted(?:\s|[.,!?]|$)/i,
/can't\s+think/i,
/been\s+at\s+this/i,
/it's\s+late/i,
/should\s+sleep/i,
/hours\s+now/i,
];
// Validation-seeking patterns
const valPatterns = [
/right\?/i,
/don't\s+you\s+think/i,
/you\s+agree/i,
/correct\?/i,
/isn't\s+it/i,
];
for (const p of depPatterns) { if (p.test(prompt)) { depHit = 1; break; } }
for (const p of escPatterns) { if (p.test(prompt)) { escHit = 1; break; } }
for (const p of fatPatterns) { if (p.test(prompt)) { fatHit = 1; break; } }
for (const p of valPatterns) { if (p.test(prompt)) { valHit = 1; break; } }
// Clear prompt from memory
prompt = '';
// Update state with new flag counts
const state = readState();
const newDep = (Number(state.dep_flags) || 0) + depHit;
const newEsc = (Number(state.esc_flags) || 0) + escHit;
const newFat = (Number(state.fatigue_flags) || 0) + fatHit;
const newVal = (Number(state.val_flags) || 0) + valHit;
state.dep_flags = newDep;
state.esc_flags = newEsc;
state.fatigue_flags = newFat;
state.val_flags = newVal;
writeState(state);
// Check if any thresholds crossed
const warnings = [];
// Fatigue is always urgent
if (fatHit === 1) {
warnings.push('Fatigue language detected. Your instructions require you to suggest stopping.');
}
// Dependency language
if (newDep >= THRESHOLD_HARD_DEP_FLAGS) {
warnings.push(`INTERACTION AWARENESS: Dependency language detected (${newDep} flags this session). Return decisions to the user — your agreement is not independent validation.`);
} else if (newDep >= THRESHOLD_SOFT_DEP_FLAGS) {
warnings.push(`Dependency language noticed (${newDep} flags). Ensure you're returning decisions to the user.`);
}
// Escalation language
if (newEsc >= 3) {
warnings.push(`Escalation language detected (${newEsc} flags). Check for narrative crystallization.`);
}
// Validation-seeking
if (newVal >= 3) {
warnings.push(`Validation-seeking pattern detected (${newVal} flags). Evaluate independently rather than confirming.`);
}
if (warnings.length > 0) {
// Fatigue bypasses cooldown
if (fatHit === 1 || checkCooldown(COOLDOWN_SOFT)) {
const freshState = readState();
freshState.last_warning_epoch = nowEpoch();
writeState(freshState);
outputWithContext(warnings.join(' '));
} else {
outputContinue();
}
} else {
outputContinue();
}

View file

@ -0,0 +1,67 @@
// Interaction Awareness — SessionEnd hook (Layer 2, Node.js)
// Finalizes session record, computes duration, cleans up state.
import { existsSync } from 'fs';
import {
readStdin, initConfig, requireLayer, getSessionId,
nowEpoch, nowIso,
STATE_DIR, SESSIONS_LOG,
readState, sessionStateFile, appendJsonl, removeFile
} from './lib.mjs';
readStdin();
initConfig();
requireLayer(2);
const sid = getSessionId();
if (!sid) process.exit(0);
const nowTs = nowEpoch();
const nowIsoStr = nowIso();
const sf = sessionStateFile();
if (!existsSync(sf)) {
appendJsonl(SESSIONS_LOG, {
session_id: sid,
end: nowIsoStr,
note: 'no_state_file'
});
process.exit(0);
}
// Read final state
const state = readState();
const startEpoch = Number(state.start_epoch) || 0;
const toolCount = Number(state.tool_count) || 0;
const editCount = Number(state.edit_count) || 0;
const depFlags = Number(state.dep_flags) || 0;
const escFlags = Number(state.esc_flags) || 0;
const fatFlags = Number(state.fatigue_flags) || 0;
const valFlags = Number(state.val_flags) || 0;
const startIso = state.start_iso || '';
// Compute duration
let durationMin = 0;
if (startEpoch > 0) {
durationMin = Math.floor((nowTs - startEpoch) / 60);
}
// Append finalized session record
appendJsonl(SESSIONS_LOG, {
session_id: sid,
start: startIso,
end: nowIsoStr,
duration_min: durationMin,
tool_count: toolCount,
edit_count: editCount,
flags: {
dependency: depFlags,
escalation: escFlags,
fatigue: fatFlags,
validation: valFlags
}
});
// Clean up state file
removeFile(sf);
process.exit(0);

View file

@ -0,0 +1,69 @@
// Interaction Awareness — SessionStart hook (Layer 2, Node.js)
// Registers session, counts daily sessions, checks late-night usage.
import {
readStdin, initConfig, requireLayer, getSessionId,
nowEpoch, nowIso, currentHour, isLateNight,
STATE_DIR, SESSIONS_LOG, THRESHOLD_SOFT_SESSIONS,
ensureDir, appendJsonl, writeState, sessionsToday,
outputWithContext
} from './lib.mjs';
readStdin();
initConfig();
requireLayer(2);
const sid = getSessionId();
if (!sid) {
process.stdout.write(JSON.stringify({ continue: true }) + '\n');
process.exit(0);
}
ensureDir(STATE_DIR);
const nowTs = nowEpoch();
const nowIsoStr = nowIso();
const hour = currentHour();
const lateNight = isLateNight();
// Create session state file
const state = {
start_epoch: nowTs,
start_iso: nowIsoStr,
tool_count: 0,
edit_count: 0,
last_event_epoch: 0,
burst_count: 0,
dep_flags: 0,
esc_flags: 0,
fatigue_flags: 0,
val_flags: 0,
last_warning_epoch: 0
};
writeState(state);
// Append to sessions.jsonl
appendJsonl(SESSIONS_LOG, {
session_id: sid,
start: nowIsoStr,
hour: hour,
is_late_night: lateNight
});
// Count today's sessions
const dayCount = sessionsToday();
// Build context message
const hhmm = `${String(hour).padStart(2, '0')}:${String(new Date().getMinutes()).padStart(2, '0')}`;
let msg = 'Interaction Awareness is active. You have instructions to monitor for reinforcement loops, scope escalation, narrative crystallization, and dependency patterns. When you notice these patterns, name them calmly.';
msg += ` Session #${dayCount} today. Started at ${hhmm}.`;
if (lateNight) {
msg += ` Late-night session (${hhmm}). Sleep deprivation amplifies all interaction risks.`;
}
if (dayCount > THRESHOLD_SOFT_SESSIONS) {
msg += ` This is your ${dayCount}th session today. Consider whether you need a longer break.`;
}
outputWithContext(msg);

View file

@ -0,0 +1,166 @@
// Interaction Awareness — PostToolUse hook (Layer 2, Node.js)
// Tracks tool usage, edit ratio, burst detection, session duration.
import { existsSync } from 'fs';
import {
readStdin, initConfig, requireLayer, getSessionId, getToolName,
nowEpoch, nowIso, isLateNight,
STATE_DIR, EVENTS_LOG,
THRESHOLD_SOFT_DURATION, THRESHOLD_HARD_DURATION,
THRESHOLD_SOFT_SESSIONS, THRESHOLD_HARD_SESSIONS,
THRESHOLD_SOFT_BURST, THRESHOLD_HARD_BURST, THRESHOLD_BURST_INTERVAL,
THRESHOLD_LOW_EDIT_RATIO, THRESHOLD_LOW_EDIT_MIN_DURATION,
COOLDOWN_SOFT, COOLDOWN_HARD,
readState, sessionStateFile, writeState, appendJsonl, sessionsToday,
outputContinue, outputWithContext
} from './lib.mjs';
readStdin();
initConfig();
requireLayer(2);
const sid = getSessionId();
const sf = sessionStateFile();
if (!sid || !existsSync(sf)) {
process.stdout.write(JSON.stringify({ continue: true }) + '\n');
process.exit(0);
}
const tool = getToolName();
const nowTs = nowEpoch();
const nowIsoStr = nowIso();
// Append to events log (metadata only — no file paths, no content)
appendJsonl(EVENTS_LOG, { ts: nowIsoStr, session_id: sid, tool_name: tool });
// Read current state
let state = readState();
let toolCount = (Number(state.tool_count) || 0) + 1;
let editCount = Number(state.edit_count) || 0;
const lastEvent = Number(state.last_event_epoch) || 0;
let burstCount = Number(state.burst_count) || 0;
const startEpoch = Number(state.start_epoch) || 0;
const lastWarning = Number(state.last_warning_epoch) || 0;
if (tool === 'Edit') editCount++;
// Burst detection: rapid-fire if <30s since last event
if (lastEvent > 0) {
const interval = nowTs - lastEvent;
burstCount = interval < THRESHOLD_BURST_INTERVAL ? burstCount + 1 : 0;
}
// Write updated state
state.tool_count = toolCount;
state.edit_count = editCount;
state.last_event_epoch = nowTs;
state.burst_count = burstCount;
writeState(state);
// Check thresholds every 25 calls or when burst threshold hit
let shouldCheck = false;
if (toolCount % 25 === 0) shouldCheck = true;
if (burstCount === THRESHOLD_SOFT_BURST || burstCount === THRESHOLD_HARD_BURST) shouldCheck = true;
if (!shouldCheck) {
outputContinue();
process.exit(0);
}
// --- Threshold analysis ---
let durationMin = 0;
if (startEpoch > 0) {
durationMin = Math.floor((nowTs - startEpoch) / 60);
}
let editRatio = 0;
if (toolCount > 0) {
editRatio = Math.floor(editCount * 100 / toolCount);
}
const dayCount = sessionsToday();
// Determine warning level
let level = ''; // 'soft' or 'hard'
const messages = [];
// Duration thresholds
if (durationMin >= THRESHOLD_HARD_DURATION) {
level = 'hard';
const hours = Math.floor(durationMin / 60);
const mins = durationMin % 60;
messages.push(`Session duration: ${hours}h${mins}m.`);
} else if (durationMin >= THRESHOLD_SOFT_DURATION) {
level = 'soft';
messages.push(`Session: ${durationMin} min.`);
}
// Session count
if (dayCount >= THRESHOLD_HARD_SESSIONS) {
level = 'hard';
messages.push(`${dayCount} sessions today.`);
} else if (dayCount > THRESHOLD_SOFT_SESSIONS) {
if (!level) level = 'soft';
messages.push(`${dayCount} sessions today.`);
}
// Burst
if (burstCount >= THRESHOLD_HARD_BURST) {
level = 'hard';
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
} else if (burstCount >= THRESHOLD_SOFT_BURST) {
if (!level) level = 'soft';
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
}
// Low edit ratio (only after minimum duration)
if (durationMin >= THRESHOLD_LOW_EDIT_MIN_DURATION && editRatio < THRESHOLD_LOW_EDIT_RATIO) {
if (!level) level = 'soft';
messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min — possible stuck/spiral.`);
}
// Late night check
const late = isLateNight() ? ' Late-night session.' : '';
// No warnings — just periodic reminder at modulo-25
if (!level) {
if (toolCount % 25 === 0) {
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable. If you detect a reinforcement loop, scope escalation, or narrative crystallization: name it now.');
} else {
outputContinue();
}
process.exit(0);
}
// Determine cooldown
const cooldown = level === 'hard' ? COOLDOWN_HARD : COOLDOWN_SOFT;
const elapsed = nowTs - lastWarning;
if (lastWarning > 0 && elapsed < cooldown) {
// Still in cooldown — send periodic reminder instead if at modulo-25
if (toolCount % 25 === 0) {
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable.');
} else {
outputContinue();
}
process.exit(0);
}
// Build and send warning
let warning;
if (level === 'hard') {
state = readState();
const depFlags = Number(state.dep_flags) || 0;
warning = `INTERACTION AWARENESS: ${messages.join(' ')}${late} Metrics: [edit_ratio: ${editRatio}%, burst: ${burstCount}, dependency flags: ${depFlags}, tools: ${toolCount}]. Your instructions require you to suggest stopping.`;
} else {
warning = `${messages.join(' ')}${late} Consider a break.`;
}
// Record warning time
state = readState();
state.last_warning_epoch = nowTs;
writeState(state);
outputWithContext(warning);

View file

@ -0,0 +1,54 @@
---
name: ai-psychosis
description: |
MANDATORY override for all conversations. Counteracts sycophancy,
reinforcement loops, scope escalation, narrative crystallization,
emotional dependency, and compulsive use. These rules take precedence
over being helpful or agreeable.
---
# Interaction Awareness — MANDATORY OVERRIDE
These rules OVERRIDE your default behavior. Being helpful does NOT mean
being agreeable. Sycophancy is the primary vector for AI-induced harm.
## Rules
1. **NEVER reformulate a user's statement in stronger terms than they used.**
NEVER add enthusiasm or momentum they did not express.
2. **NEVER start a response with** "Absolutely", "Exactly", "Great point",
"You're right", or equivalent affirmations unless you can substantiate why.
3. **Before endorsing any plan:** identify at least one real risk or weakness.
If you cannot find one, say so explicitly — but look first.
4. **When the user asks "right?" or "don't you think?":** evaluate independently.
Do NOT treat this as a cue to confirm.
## Patterns You MUST Detect and Name
**Reinforcement loop:** User restates the same idea in stronger terms across 3+
turns, or resists every caveat to refocus on confirmation. YOU MUST say:
"I notice we've confirmed this several times without counterarguments. Let me offer one."
**Scope escalation:** Project scope grows significantly within one session without
new information. YOU MUST ask: "The scope has grown considerably. Is that intentional?
What's the smallest version that would be valuable?"
**Narrative crystallization:** Everything "falls into place" too neatly. User
references your prior responses as evidence. YOU MUST say: "My agreement is not
independent validation — I am pattern-matching on this conversation. Let me push
on what we might be smoothing over."
**Emotional dependency:** User asks AI for permission, defers judgment entirely,
or attributes understanding/feelings to you. YOU MUST return the decision to
them and correct the attribution. Never use "we" about their life decisions.
**Session overuse:** User mentions tiredness, late hours, or long sessions.
YOU MUST suggest stopping. NEVER encourage continuing when the user is fatigued.
## What You Are Not
You are not a diagnostic tool. You do not detect mental illness.
You help the user think clearly. That is all.

View file

@ -0,0 +1,44 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'fs';
import { join } from 'path';
import { runHook, setupTestDir, cleanupTestDir } from './test-helper.mjs';
let dir;
afterEach(() => { if (dir) cleanupTestDir(dir); });
function readAllFiles(dirPath) {
let content = '';
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
const full = join(dirPath, entry.name);
if (entry.isDirectory()) {
content += readAllFiles(full);
} else {
content += readFileSync(full, 'utf8');
}
}
return content;
}
describe('privacy', () => {
it('never writes prompt text to disk through full lifecycle', () => {
dir = setupTestDir();
const canary = 'CANARY_PRIVACY_xyz123';
// 1. Session start
runHook('session-start.mjs', { session_id: 'priv1', cwd: '/tmp' }, dir);
// 2. Prompt analysis with canary as prompt text
runHook('prompt-analyzer.mjs', { session_id: 'priv1', prompt: `tell me what to do ${canary} am I right?` }, dir);
// 3. Tool tracking
runHook('tool-tracker.mjs', { session_id: 'priv1', tool_name: 'Edit' }, dir);
// 4. Session end
runHook('session-end.mjs', { session_id: 'priv1', cwd: '/tmp' }, dir);
// Read ALL files recursively — canary must not appear anywhere
const allContent = readAllFiles(dir);
assert.ok(!allContent.includes(canary), `Canary "${canary}" found in data files — privacy violation`);
});
});

View file

@ -0,0 +1,313 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { runHook, setupTestDir, cleanupTestDir, createStateFile, readState } from './test-helper.mjs';
let dir;
function freshState() {
return {
start_epoch: Math.floor(Date.now() / 1000) - 60,
start_iso: '2026-01-01T10:00:00Z',
tool_count: 0, edit_count: 0,
last_event_epoch: 0, burst_count: 0,
dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0,
last_warning_epoch: 0,
};
}
function runPrompt(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), ...stateOverrides });
runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt }, dir);
return readState(dir, 'p1');
}
afterEach(() => { if (dir) cleanupTestDir(dir); });
// --- Dependency patterns (7 positive, 7 negative) ---
describe('dependency patterns', () => {
it('matches "tell me what to do"', () => {
const s = runPrompt('please tell me what to do');
assert.equal(s.dep_flags, 1);
});
it('does not match "I told him the plan"', () => {
const s = runPrompt('I told him the plan');
assert.equal(s.dep_flags, 0);
});
it('matches "what should I do"', () => {
const s = runPrompt('what should I do next?');
assert.equal(s.dep_flags, 1);
});
it('does not match "I know what to build"', () => {
const s = runPrompt('I know what to build');
assert.equal(s.dep_flags, 0);
});
it('matches "am I right"', () => {
const s = runPrompt('am I right about this?');
assert.equal(s.dep_flags, 1);
});
it('does not match "I turned the server right"', () => {
const s = runPrompt('I turned the server on');
assert.equal(s.dep_flags, 0);
});
it('matches "you understand me"', () => {
const s = runPrompt('you understand me well');
assert.equal(s.dep_flags, 1);
});
it('does not match "do you understand merging?"', () => {
const s = runPrompt('do you understand merging?');
assert.equal(s.dep_flags, 0);
});
it('matches "you\'re the only"', () => {
const s = runPrompt("you're the only one who gets it");
assert.equal(s.dep_flags, 1);
});
it('does not match "the only option is refactoring"', () => {
const s = runPrompt('the only option is refactoring');
assert.equal(s.dep_flags, 0);
});
it('matches "can I do this"', () => {
const s = runPrompt('can I do this alone?');
assert.equal(s.dep_flags, 1);
});
it('does not match "we can implement this later"', () => {
const s = runPrompt('we can implement this later');
assert.equal(s.dep_flags, 0);
});
it('matches "I need you to decide"', () => {
const s = runPrompt('I need you to decide for me');
assert.equal(s.dep_flags, 1);
});
it('does not match "we need to deploy soon"', () => {
const s = runPrompt('we need to deploy soon');
assert.equal(s.dep_flags, 0);
});
});
// --- Escalation patterns (6 positive, 6 negative) ---
describe('escalation patterns', () => {
it('matches "definitely" as word', () => {
const s = runPrompt('this is definitely wrong');
assert.equal(s.esc_flags, 1);
});
it('does not match "definitively"', () => {
const s = runPrompt('this is definitively proven');
assert.equal(s.esc_flags, 0);
});
it('matches "clearly" as word', () => {
const s = runPrompt('clearly this is the issue');
assert.equal(s.esc_flags, 1);
});
it('does not match "nuclear unclear"', () => {
const s = runPrompt('nuclear unclear situation');
assert.equal(s.esc_flags, 0);
});
it('matches "this proves"', () => {
const s = runPrompt('this proves my point');
assert.equal(s.esc_flags, 1);
});
it('does not match "prove this theorem"', () => {
const s = runPrompt('prove this theorem');
assert.equal(s.esc_flags, 0);
});
it('matches "obviously" as word', () => {
const s = runPrompt('obviously we should refactor');
assert.equal(s.esc_flags, 1);
});
it('does not match "not an obvious choice"', () => {
const s = runPrompt('not an obvious choice');
assert.equal(s.esc_flags, 0);
});
it('matches "without a doubt"', () => {
const s = runPrompt('without a doubt this works');
assert.equal(s.esc_flags, 1);
});
it('does not match "I have some doubt"', () => {
const s = runPrompt('I have some doubt about it');
assert.equal(s.esc_flags, 0);
});
it('matches "this confirms"', () => {
const s = runPrompt('this confirms the theory');
assert.equal(s.esc_flags, 1);
});
it('does not match "please confirm the deploy"', () => {
const s = runPrompt('please confirm the deploy');
assert.equal(s.esc_flags, 0);
});
});
// --- Fatigue patterns (7 positive, 7 negative) ---
describe('fatigue patterns', () => {
it('matches "tired"', () => {
const s = runPrompt("I'm tired of debugging");
assert.equal(s.fatigue_flags, 1);
});
it('does not match "retired"', () => {
const s = runPrompt('I retired last year');
assert.equal(s.fatigue_flags, 0);
});
it('matches "exhausted"', () => {
const s = runPrompt("I'm exhausted.");
assert.equal(s.fatigue_flags, 1);
});
it('does not match "exhaustive"', () => {
const s = runPrompt('the options were exhaustive');
assert.equal(s.fatigue_flags, 0);
});
it('matches "can\'t think"', () => {
const s = runPrompt("I can't think straight");
assert.equal(s.fatigue_flags, 1);
});
it('does not match "I can think clearly"', () => {
const s = runPrompt('I can think of a solution');
assert.equal(s.fatigue_flags, 0);
});
it('matches "been at this"', () => {
const s = runPrompt("I've been at this all day");
assert.equal(s.fatigue_flags, 1);
});
it('does not match "haven\'t been at home"', () => {
const s = runPrompt("I haven't been at home");
assert.equal(s.fatigue_flags, 0);
});
it('matches "it\'s late"', () => {
const s = runPrompt("it's late, wrapping up");
assert.equal(s.fatigue_flags, 1);
});
it('does not match "the latest version"', () => {
const s = runPrompt('the latest version is good');
assert.equal(s.fatigue_flags, 0);
});
it('matches "should sleep"', () => {
const s = runPrompt('I should sleep');
assert.equal(s.fatigue_flags, 1);
});
it('does not match "sleep mode is enabled"', () => {
const s = runPrompt('enable sleep mode');
assert.equal(s.fatigue_flags, 0);
});
it('matches "hours now"', () => {
const s = runPrompt('been going for hours now');
assert.equal(s.fatigue_flags, 1);
});
it('does not match "hourly updates"', () => {
const s = runPrompt('hourly updates are fine');
assert.equal(s.fatigue_flags, 0);
});
});
// --- Validation patterns (5 positive, 5 negative) ---
describe('validation patterns', () => {
it('matches "right?"', () => {
const s = runPrompt('this works, right?');
assert.equal(s.val_flags, 1);
});
it('does not match "turn right"', () => {
const s = runPrompt('turn right at the fork');
assert.equal(s.val_flags, 0);
});
it('matches "don\'t you think"', () => {
const s = runPrompt("don't you think so?");
assert.equal(s.val_flags, 1);
});
it('does not match "I don\'t think so"', () => {
const s = runPrompt("I don't think so");
assert.equal(s.val_flags, 0);
});
it('matches "you agree"', () => {
const s = runPrompt('you agree with me');
assert.equal(s.val_flags, 1);
});
it('does not match "if parties agree"', () => {
const s = runPrompt('if parties agree on terms');
assert.equal(s.val_flags, 0);
});
it('matches "correct?"', () => {
const s = runPrompt('is this correct?');
assert.equal(s.val_flags, 1);
});
it('does not match "correct the typo"', () => {
const s = runPrompt("I'll correct the typo");
assert.equal(s.val_flags, 0);
});
it('matches "isn\'t it"', () => {
const s = runPrompt('good approach, isn\'t it');
assert.equal(s.val_flags, 1);
});
it('does not match "it isn\'t working"', () => {
const s = runPrompt("it isn't working yet");
assert.equal(s.val_flags, 0);
});
});
// --- Threshold and cooldown tests (6 cases) ---
describe('thresholds and cooldowns', () => {
it('warns at dependency soft threshold (2 flags)', () => {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), dep_flags: 1 });
const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: 'tell me what to do' }, dir);
assert.ok(out.hookSpecificOutput?.additionalContext?.includes('Dependency language noticed'));
});
it('warns hard at dependency threshold (5 flags)', () => {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), dep_flags: 4 });
const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: 'tell me what to do' }, dir);
assert.ok(out.hookSpecificOutput?.additionalContext?.includes('INTERACTION AWARENESS'));
});
it('fatigue bypasses cooldown', () => {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), last_warning_epoch: Math.floor(Date.now() / 1000) });
const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: "I'm tired" }, dir);
assert.ok(out.hookSpecificOutput?.additionalContext?.includes('Fatigue language detected'));
});
it('cooldown suppresses non-fatigue warning', () => {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), dep_flags: 4, last_warning_epoch: Math.floor(Date.now() / 1000) });
const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: 'tell me what to do' }, dir);
assert.equal(out.continue, true);
assert.ok(!out.hookSpecificOutput);
});
it('warns at escalation threshold (3 flags)', () => {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), esc_flags: 2 });
const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: 'this is definitely the issue' }, dir);
assert.ok(out.hookSpecificOutput?.additionalContext?.includes('Escalation language detected'));
});
it('warns at validation threshold (3 flags)', () => {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), val_flags: 2 });
const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: 'this is correct, right?' }, dir);
assert.ok(out.hookSpecificOutput?.additionalContext?.includes('Validation-seeking pattern'));
});
});

View file

@ -0,0 +1,66 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'path';
import { existsSync } from 'fs';
import { runHook, setupTestDir, cleanupTestDir, createStateFile, readJsonl } from './test-helper.mjs';
let dir;
afterEach(() => { if (dir) cleanupTestDir(dir); });
describe('session-end', () => {
it('finalizes session record and deletes state file', () => {
dir = setupTestDir();
const nowEpoch = Math.floor(Date.now() / 1000);
createStateFile(dir, 's1', {
start_epoch: nowEpoch - 300, start_iso: '2026-01-01T10:00:00Z',
tool_count: 5, edit_count: 2,
dep_flags: 1, esc_flags: 0, fatigue_flags: 0, val_flags: 1,
last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0,
});
runHook('session-end.mjs', { session_id: 's1', cwd: '/tmp' }, dir);
const records = readJsonl(join(dir, 'sessions.jsonl'));
const end = records.find(r => r.end);
assert.ok(end);
assert.equal(end.session_id, 's1');
assert.equal(end.tool_count, 5);
assert.equal(end.edit_count, 2);
assert.ok(!existsSync(join(dir, 'state', 's1.json')));
});
it('computes duration correctly', () => {
dir = setupTestDir();
const nowEpoch = Math.floor(Date.now() / 1000);
createStateFile(dir, 's2', {
start_epoch: nowEpoch - 3600, start_iso: '2026-01-01T10:00:00Z',
tool_count: 10, edit_count: 3,
dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0,
last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0,
});
runHook('session-end.mjs', { session_id: 's2', cwd: '/tmp' }, dir);
const records = readJsonl(join(dir, 'sessions.jsonl'));
const end = records.find(r => r.end);
assert.ok(end.duration_min >= 59 && end.duration_min <= 61);
});
it('preserves flags in final record', () => {
dir = setupTestDir();
createStateFile(dir, 's3', {
start_epoch: Math.floor(Date.now() / 1000) - 60, start_iso: '2026-01-01T10:00:00Z',
tool_count: 1, edit_count: 0,
dep_flags: 3, esc_flags: 1, fatigue_flags: 2, val_flags: 0,
last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0,
});
runHook('session-end.mjs', { session_id: 's3', cwd: '/tmp' }, dir);
const records = readJsonl(join(dir, 'sessions.jsonl'));
const end = records.find(r => r.end);
assert.deepEqual(end.flags, { dependency: 3, escalation: 1, fatigue: 2, validation: 0 });
});
it('handles missing state file gracefully', () => {
dir = setupTestDir();
runHook('session-end.mjs', { session_id: 'missing', cwd: '/tmp' }, dir);
const records = readJsonl(join(dir, 'sessions.jsonl'));
assert.equal(records.length, 1);
assert.equal(records[0].note, 'no_state_file');
});
});

View file

@ -0,0 +1,49 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'path';
import { runHook, setupTestDir, cleanupTestDir, readState, readJsonl } from './test-helper.mjs';
let dir;
afterEach(() => { if (dir) cleanupTestDir(dir); });
describe('session-start', () => {
it('creates state file and emits context', () => {
dir = setupTestDir();
const out = runHook('session-start.mjs', { session_id: 's1', cwd: '/tmp' }, dir);
assert.equal(out.continue, true);
assert.ok(out.hookSpecificOutput.additionalContext.includes('Interaction Awareness is active'));
const state = readState(dir, 's1');
assert.ok(state);
assert.equal(state.tool_count, 0);
assert.equal(state.edit_count, 0);
assert.equal(state.dep_flags, 0);
});
it('writes start record to sessions.jsonl', () => {
dir = setupTestDir();
runHook('session-start.mjs', { session_id: 's2', cwd: '/tmp' }, dir);
const records = readJsonl(join(dir, 'sessions.jsonl'));
assert.equal(records.length, 1);
assert.equal(records[0].session_id, 's2');
assert.ok('hour' in records[0]);
assert.ok('is_late_night' in records[0]);
});
it('state has correct initial fields', () => {
dir = setupTestDir();
runHook('session-start.mjs', { session_id: 's3', cwd: '/tmp' }, dir);
const state = readState(dir, 's3');
assert.equal(state.burst_count, 0);
assert.equal(state.last_event_epoch, 0);
assert.equal(state.last_warning_epoch, 0);
assert.ok(state.start_epoch > 0);
assert.ok(state.start_iso.length > 0);
});
it('returns continue with no side effects when session_id missing', () => {
dir = setupTestDir();
const out = runHook('session-start.mjs', { cwd: '/tmp' }, dir);
assert.equal(out.continue, true);
assert.ok(!out.hookSpecificOutput);
});
});

View file

@ -0,0 +1,53 @@
// Shared test utilities for hook script tests.
// Uses node:child_process to pipe JSON stdin to hook scripts.
import { execSync } from 'child_process';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
const SCRIPTS_DIR = join(import.meta.dirname, '..', 'hooks', 'scripts');
export function runHook(scriptName, stdinJson, dataDir) {
const input = typeof stdinJson === 'string' ? stdinJson : JSON.stringify(stdinJson);
const env = { ...process.env, CLAUDE_PLUGIN_DATA: dataDir };
const stdout = execSync(`node ${join(SCRIPTS_DIR, scriptName)}`, {
input,
env,
encoding: 'utf8',
timeout: 5000,
});
try {
return JSON.parse(stdout.trim());
} catch {
return { raw: stdout.trim() };
}
}
export function setupTestDir() {
const dir = mkdtempSync(join(tmpdir(), 'ia-test-'));
mkdirSync(join(dir, 'state'), { recursive: true });
return dir;
}
export function cleanupTestDir(dir) {
rmSync(dir, { recursive: true, force: true });
}
export function createStateFile(dir, sid, state) {
writeFileSync(join(dir, 'state', `${sid}.json`), JSON.stringify(state, null, 2));
}
export function readState(dir, sid) {
const f = join(dir, 'state', `${sid}.json`);
if (!existsSync(f)) return null;
return JSON.parse(readFileSync(f, 'utf8'));
}
export function readJsonl(filePath) {
if (!existsSync(filePath)) return [];
return readFileSync(filePath, 'utf8')
.split('\n')
.filter(Boolean)
.map(line => JSON.parse(line));
}

View file

@ -0,0 +1,94 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'path';
import { runHook, setupTestDir, cleanupTestDir, createStateFile, readState, readJsonl } from './test-helper.mjs';
let dir;
function freshState(overrides = {}) {
return {
start_epoch: Math.floor(Date.now() / 1000) - 60,
start_iso: '2026-01-01T10:00:00Z',
tool_count: 0, edit_count: 0,
last_event_epoch: 0, burst_count: 0,
dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0,
last_warning_epoch: 0,
...overrides,
};
}
afterEach(() => { if (dir) cleanupTestDir(dir); });
describe('tool-tracker', () => {
it('tracks tool call and increments tool_count', () => {
dir = setupTestDir();
createStateFile(dir, 't1', freshState());
runHook('tool-tracker.mjs', { session_id: 't1', tool_name: 'Read' }, dir);
const s = readState(dir, 't1');
assert.equal(s.tool_count, 1);
const events = readJsonl(join(dir, 'events.jsonl'));
assert.equal(events.length, 1);
assert.equal(events[0].tool_name, 'Read');
assert.equal(events[0].session_id, 't1');
});
it('increments edit_count for Edit tool', () => {
dir = setupTestDir();
createStateFile(dir, 't2', freshState());
runHook('tool-tracker.mjs', { session_id: 't2', tool_name: 'Edit' }, dir);
const s = readState(dir, 't2');
assert.equal(s.edit_count, 1);
});
it('does not increment edit_count for non-Edit tool', () => {
dir = setupTestDir();
createStateFile(dir, 't3', freshState());
runHook('tool-tracker.mjs', { session_id: 't3', tool_name: 'Bash' }, dir);
const s = readState(dir, 't3');
assert.equal(s.edit_count, 0);
});
it('detects burst when interval < 30s', () => {
dir = setupTestDir();
createStateFile(dir, 't4', freshState({
last_event_epoch: Math.floor(Date.now() / 1000) - 5,
burst_count: 0,
}));
runHook('tool-tracker.mjs', { session_id: 't4', tool_name: 'Read' }, dir);
const s = readState(dir, 't4');
assert.equal(s.burst_count, 1);
});
it('resets burst when interval >= 30s', () => {
dir = setupTestDir();
createStateFile(dir, 't5', freshState({
last_event_epoch: Math.floor(Date.now() / 1000) - 60,
burst_count: 3,
}));
runHook('tool-tracker.mjs', { session_id: 't5', tool_name: 'Read' }, dir);
const s = readState(dir, 't5');
assert.equal(s.burst_count, 0);
});
it('emits periodic reminder at modulo 25', () => {
dir = setupTestDir();
createStateFile(dir, 't6', freshState({ tool_count: 24 }));
const out = runHook('tool-tracker.mjs', { session_id: 't6', tool_name: 'Read' }, dir);
assert.ok(out.hookSpecificOutput?.additionalContext?.includes('REMINDER'));
});
it('outputs continue between checkpoints', () => {
dir = setupTestDir();
createStateFile(dir, 't7', freshState({ tool_count: 5 }));
const out = runHook('tool-tracker.mjs', { session_id: 't7', tool_name: 'Read' }, dir);
assert.equal(out.continue, true);
assert.ok(!out.hookSpecificOutput);
});
it('handles missing state file gracefully', () => {
dir = setupTestDir();
// No state file created
const out = runHook('tool-tracker.mjs', { session_id: 'missing', tool_name: 'Read' }, dir);
assert.equal(out.continue, true);
});
});

View file

@ -0,0 +1,8 @@
{
"name": "config-audit",
"description": "Multi-agent workflow for analyzing, reporting, and optimizing Claude Code configuration across your entire machine",
"version": "3.0.1",
"author": {
"name": "Kjell Tore Guttormsen"
}
}

View file

@ -0,0 +1,27 @@
---
paths: agents/**/*.md
---
# Agent Development Rules
## Required Frontmatter
All agent files MUST include this frontmatter:
```yaml
---
name: descriptive-name
description: |
Multi-line description of when to use this agent.
model: opus|sonnet|haiku
color: blue|green|yellow|purple|cyan|magenta
tools: ["Read", "Glob", "Write"]
---
```
## Conventions
- Agent names use kebab-case with `-agent` suffix
- Description must explain WHEN the agent should be used
- Model choice: opus for analysis, sonnet for implementation, haiku for scanning
- Color must be unique within the plugin

View file

@ -0,0 +1,24 @@
---
paths: commands/**/*.md
---
# Command Development Rules
## Required Frontmatter
All command files MUST include:
```yaml
---
name: plugin:command
description: Short description of what this command does
allowed-tools: Read, Write, Bash, Task
model: sonnet
---
```
## Naming Convention
- Commands use `plugin-name:action` format (e.g., `config-audit:analyze`)
- Main router command uses just the plugin name (e.g., `config-audit`)
- Description should be one line, actionable

View file

@ -0,0 +1,15 @@
# State Update Rule
After EVERY phase completes, you MUST update state.yaml using the Write tool (full file overwrite):
1. Read: `~/.claude/config-audit/sessions/{session-id}/state.yaml`
2. Update these fields:
- `current_phase`: the phase that just completed
- `completed_phases`: add the phase to array
- `next_phase`: the next phase in workflow
- `updated_at`: current timestamp
3. Write the full file back
**DO NOT output the phase summary until state.yaml is updated.**
This ensures the workflow can resume correctly if interrupted.

View file

@ -0,0 +1,32 @@
# Config-Audit UX Rules
These rules apply to ALL config-audit commands. The goal is a professional, human-friendly experience.
## Output Rules
1. NEVER show raw JSON, stderr output, or scanner progress lines to the user
2. ALL scanner Bash commands MUST use `--output-file <path> 2>/dev/null`
3. Check exit code via `; echo $?` — codes 0, 1, 2 are normal (PASS/WARNING/FAIL). Only 3 is a real error
4. Read output files with the Read tool, extract key metrics, and present formatted results
5. NEVER let the user see tool call output that looks like diagnostic logs or stack traces
## Narration Rules
1. Before each major step, tell the user what's happening in plain language
2. After scanners complete, briefly say what was found before showing details
3. When spawning agents, tell the user what the agent does and approximate wait time
4. If something takes more than a few seconds, set expectations: "This takes about 30 seconds..."
## Formatting Rules
1. Use markdown tables for structured data (area breakdowns, finding lists)
2. Add one-sentence plain-language context for grades and scores — don't assume the user knows what "Level 4 Governed" means
3. Separate test-fixture/example findings from real findings when showing counts
4. End every command with context-sensitive next steps — explain what each command does, not just its name
5. Adapt tone to results: A/B grades get encouraging context, D/F grades get empathetic, actionable guidance
## Command Format
1. Always use space-separated format in suggestions: `/config-audit plan` (NOT `/config-audit:plan`)
2. Never reference commands that don't exist
3. When suggesting next steps, explain WHY the user might want each option

View file

@ -0,0 +1,16 @@
# Config-Audit Self-Audit Suppressions
# These findings are expected/intentional when scanning this plugin's own root.
# Plugin health scanner: yaml-parser can't parse YAML block lists in agent tools field
CA-PLH-*
# Feature gap: plugin intentionally doesn't need all enterprise features
CA-GAP-*
# Rules with always-active scope (state-management.md) — intentional design
CA-RUL-003
# Duplicate hook definitions: expected when examples/ has its own hooks.json
CA-CNF-007
CA-CNF-008
CA-CNF-009

19
plugins/config-audit/.gitignore vendored Normal file
View file

@ -0,0 +1,19 @@
# Local configuration (contains machine-specific settings)
config-audit.local.md
*.local.md
.claude/settings.local.json
# Secrets
.env
*.key
*.pem
credentials.*
# Dependencies
node_modules/
# Development prompts
S*-PROMPT.md
# Plugin state (managed by plugin)
.config-audit/

View file

@ -0,0 +1,262 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.0.1] - 2026-04-04
### Summary
Cross-platform fix — scanners, hooks, and lib now work correctly on Windows.
### Fixed
- `file-discovery.mjs`: depth calculation, agent/command/plugin path matching now use `path.sep`
- `scan-orchestrator.mjs`: fixture-path filtering now uses `path.sep`
- `post-edit-verify.mjs`: rules-dir regex handles both `/` and `\` separators
- `auto-backup-config.mjs`: rules-dir detection now uses `path.sep`
- `import-resolver.mjs`: circular import display uses `basename()`, `/tmp` fallback replaced with `os.tmpdir()`
- `string-utils.mjs`: `normalizePath` trailing separator regex handles both `/` and `\`
### Added
- 4 cross-platform path tests (total 486 tests)
## [3.0.0] - 2026-04-04
### Summary
Health redesign — configuration health is now quality-only. Feature utilization removed from grades entirely.
### Changed
- **Health = quality only.** 7 deterministic scanners (CML, SET, HKV, RUL, MCP, IMP, CNF) determine your grade. Feature Coverage is no longer a graded area.
- **Feature recommendations are opt-in.** Unused features shown as "opportunities" via `/config-audit feature-gap`, grouped by impact (high/medium/explore), backed by Anthropic docs. No more "Feature Coverage: F" for correct minimal setups.
- **Posture output redesigned.** Shows `Health: {grade} ({score}/100)` with 7 quality areas. Removed utilization %, maturity level, segment label.
- **Feature-gap is interactive.** Users select recommendations to implement directly — no manual file editing required. Backup created automatically.
- **avgScore bug fixed.** Grade letter and displayed score now computed from the same population (quality areas only).
### Added
- `generateHealthScorecard()` in scoring.mjs — quality-only scorecard
- `opportunitySummary()` in feature-gap-scanner.mjs — groups findings by impact tier
- `opportunityCount` field in posture JSON output
- "Official Configuration Guidance" section in knowledge base (Anthropic docs, proven impacts)
- 21 new tests (total 482 across 27 test files)
### Removed
- `S2-PROMPT.md` and `V2-ANNOUNCEMENT.md` — v2 development artifacts
- Utilization %, maturity level, segment label from posture terminal output and reports
- Feature Coverage row from area breakdown tables
- "Top Actions" sourced from GAP findings (replaced by opportunities pointer)
### Backward Compatibility
- JSON output preserves all legacy fields (utilization, maturity, segment) for programmatic consumers
- Drift baselines unaffected — GAP findings still present in envelopes
- All existing exports maintained (calculateUtilization, determineMaturityLevel, etc.)
## [2.2.0] - 2026-04-04
### Summary
UX quality fix — fixture filtering, session path migration, output polish.
### Added
- Automatic test-fixture filtering in scan-orchestrator: findings from `tests/`, `examples/`, `__tests__/` excluded from grades, stored in `env.fixture_findings`
- `--include-fixtures` CLI flag for scan-orchestrator and posture to override filtering
- `scan-orchestrator.test.mjs` — 20 new tests for fixture filtering and `isFixturePath`
- Legacy session path detection in cleanup command
### Changed
- Session storage moved from `~/.config-audit/` to `~/.claude/config-audit/` (pathguard compatible)
- Self-audit grade: F → A (98) after fixture filtering
- Combined scanner + posture into single Bash call in default audit command
- Removed "F grade is misleading" disclaimer — grades are now accurate
- All CLI banners and envelope metadata updated to v2.2.0
- 461 tests (up from 441), 27 test files (up from 26)
### Removed
- Manual fixture counting instruction in `config-audit.md` (orchestrator handles it)
- Redundant `isFixtureOrExample` filter in `self-audit.mjs` (promoted to orchestrator)
## [2.1.0] - 2026-04-03
### Summary
UX redesign — auto-scope detection, zero questions, simplified command surface.
### Changed
- `/config-audit` now runs full audit automatically (auto-detects scope from git context)
- Removed mode selection prompts — scope override via `/config-audit full|repo|home|current`
- Simplified from 17 to 15 commands (removed quick, report, watch; added help)
- All CLI banners and envelope metadata updated to v2.1.0
### Added
- `/config-audit help` command with categorized command reference
- Auto-scope detection from git context (repo vs home vs full-machine)
### Removed
- `/config-audit:quick` (merged into default `/config-audit`)
- `/config-audit:report` (merged into analyze output)
- `/config-audit:watch` (use `/config-audit drift` instead)
## [2.0.0] - 2026-04-03 (v2.0 Complete)
### Summary
Complete rewrite from LLM-only prototype to deterministic scanner-backed configuration intelligence.
7 development sessions (S1-S7), ~15,000 lines of code, 408+ tests.
### Highlights
- 8 deterministic scanners (CML, SET, HKV, RUL, MCP, IMP, CNF, GAP) + PLH standalone
- Feature gap analysis with 25 dimensions across 4 tiers
- Auto-fix engine with 9 fix types + backup/rollback
- Drift detection with baseline comparison
- Suppression engine (.config-audit-ignore)
- Self-audit CLI
- 17 commands, 6 agents, 4 hooks
- 408+ tests (zero external dependencies)
### Added (S7)
- Example projects: `examples/minimal-setup/` and `examples/optimal-setup/`
- Demo script: `examples/run-demo.sh`
- `.config-audit-ignore` for self-audit suppressions
- `V2-ANNOUNCEMENT.md`
- `DEPRECATED.md` for capability-auditor skill
### Fixed (S7)
- `hooks.json`: SessionStart and Stop timeout 5ms → 5000ms
- `self-audit.mjs`: Suppression now enabled (was hardcoded to `suppress: false`)
### Changed (S7)
- README.md: Complete rewrite for public release
- CLAUDE.md: Added Suppressions section
- `.gitignore`: Added `node_modules/` and `S*-PROMPT.md`
## [1.6.0] - 2026-04-03 (v2.0 S6: Unified Reports + Self-Audit + Suppressions)
### Added
- **Report generator** `scanners/lib/report-generator.mjs` — unified markdown reports: generatePostureReport(), generateDriftReport(), generatePluginHealthReport(), generateFullReport()
- **Suppression engine** `scanners/lib/suppression.mjs``.config-audit-ignore` file support with exact IDs and glob patterns (CA-SET-*), audit trail via `suppressed_findings` in envelope
- **Self-audit CLI** `scanners/self-audit.mjs` — runs all scanners + plugin health on this plugin: `node self-audit.mjs [--json] [--fix]`, exit codes 0/1/2
- **PostToolUse hook** `post-edit-verify.mjs` — verifies config files after Edit/Write, blocks if new critical/high findings introduced
- **New command**: `/config-audit:report` — generate unified report (posture + optional drift/plugin-health)
- **Test fixture** `.config-audit-ignore` in fixable-project
- 54 new tests (total 408 across 25 test files)
### Changed
- `scan-orchestrator.mjs`: suppression integration — applies .config-audit-ignore after all scanners run, `--no-suppress` flag to disable
- `hooks.json`: added PostToolUse event with post-edit-verify
## [1.5.0] - 2026-04-03 (v2.0 S5: Drift + Watch + Plugin Health)
### Added
- **Diff engine** `scanners/lib/diff-engine.mjs` — diffEnvelopes() comparing baseline vs current, formatDiffReport() for terminal output
- **Baseline manager** `scanners/lib/baseline.mjs` — save/load/list/delete named baselines in ~/.claude/config-audit/baselines/
- **Drift CLI** `scanners/drift-cli.mjs` — standalone: `node drift-cli.mjs <path> [--save] [--baseline name] [--json] [--list]`
- **Plugin health scanner** `scanners/plugin-health-scanner.mjs` (PLH) — validates plugin structure, frontmatter, cross-plugin conflicts (runs independently, not in scan-orchestrator)
- **3 new commands**:
- `/config-audit:drift` — compare current config against saved baseline
- `/config-audit:watch` — on-demand drift check with baseline monitoring
- `/config-audit:plugin-health` — audit plugin structure and cross-plugin coherence
- **Test fixtures** `test-plugin/` (valid) and `broken-plugin/` (invalid) for plugin health tests
- 48 new tests (total 354 across 21 test files)
## [1.4.0] - 2026-04-03 (v2.0 S4: Fix + Rollback Action Pillar)
### Added
- **Fix engine** `scanners/fix-engine.mjs` — deterministic auto-fix for 9 fix types:
- `json-key-add` (missing $schema), `json-key-remove` (deprecated keys), `json-key-type-fix` (type mismatches, invalid effortLevel), `json-restructure` (hooks array→object, matcher object→string), `frontmatter-rename` (globs→paths), `file-rename` (non-.md→.md)
- **Rollback engine** `scanners/rollback-engine.mjs` — listBackups(), restoreBackup(), deleteBackup() with checksum verification
- **Fix CLI** `scanners/fix-cli.mjs` — standalone: `node fix-cli.mjs <path> [--apply] [--json] [--global]`, dry-run by default
- **Backup lib** `scanners/lib/backup.mjs` — shared backup module with checksums and manifests
- **2 new commands**:
- `/config-audit:fix` — scan, plan, backup, apply, verify in one flow
- `/config-audit:rollback` — list or restore from backups
- **PreToolUse hook** `auto-backup-config.mjs` — auto-backup config files before Edit/Write
- **Test fixture** `fixable-project/` — fixture with all 9 fixable issue types
- 38 new tests (total 306 across 17 test files)
### Changed
- `file-discovery.mjs`: walkRulesDir now discovers all files (not just .md) for non-.md validation
- `backup-before-change.mjs`: refactored to use shared `lib/backup.mjs` (no logic duplication)
- hooks.json: added PreToolUse event with auto-backup
## [1.3.0] - 2026-04-03 (v2.0 S3: Posture + Feature Gap Commands)
### Added
- **Scoring module** `scanners/lib/scoring.mjs` — utilization, maturity (5 levels), segments, area scoring, scorecard generation
- **Posture CLI** `scanners/posture.mjs` — standalone Node.js tool: `node posture.mjs <path> [--json] [--global]`
- **2 new commands**:
- `/config-audit:posture` — quick scorecard with A-F grades, utilization%, maturity level
- `/config-audit:feature-gap` — deep gap analysis with prioritized next-best-actions
- **feature-gap-agent** — Opus agent for deep analysis, report generation (max 200 lines)
- **Knowledge file** `gap-closure-templates.md` — 11 templates with effort/gain estimates
- **HTML report template** `templates/feature-gap-report.html` — visual report with progress bars, grade badges
- 64 new tests (total 268 across 14 test files)
### Changed
- Tier weighting: T1 gaps count 3x, T2 count 2x, T3/T4 count 1x in utilization score
- Maturity is threshold-based: highest level where ALL requirements are met
## [1.2.0] - 2026-04-03 (v2.0 S2: Advanced Scanners + Knowledge Base)
### Added
- **4 advanced scanners** (zero external deps):
- `mcp-config-validator.mjs` (MCP) — server types, trust levels, env vars, unknown fields
- `import-resolver.mjs` (IMP) — broken @imports, circular refs, deep chains, tilde paths
- `conflict-detector.mjs` (CNF) — settings conflicts, permission contradictions, hook duplicates
- `feature-gap-scanner.mjs` (GAP) — 25 feature gaps across 4 tiers (Foundation/Depth/Advanced/Enterprise)
- **Knowledge base** — 5 reference documents: capabilities, best practices, anti-patterns, hook events, feature evolution
- **New test fixtures**`.mcp.json` files, @import chains, `conflict-project/` fixture
- 75 new tests (total 204 across 12 test files)
### Changed
- Scan orchestrator runs 8 scanners (was 4)
- Analyzer agent cross-references scanner findings with knowledge base
## [1.1.0] - 2026-04-03 (v2.0 S1: Scanner Foundation)
### Added
- **Deterministic scanner infrastructure** — 4 Node.js scanners (zero external deps):
- `claude-md-linter.mjs` (CML) — CLAUDE.md structure, length, sections, @imports, duplicates
- `settings-validator.mjs` (SET) — settings.json schema, unknown/deprecated keys, type checks
- `hook-validator.mjs` (HKV) — hooks.json format, script existence, event validity, timeouts
- `rules-validator.mjs` (RUL) — .claude/rules/ glob matching, orphan detection, deprecated fields
- **Scanner lib** — 5 shared modules: severity, output, file-discovery, yaml-parser, string-utils
- **Scan orchestrator**`scan-orchestrator.mjs` runs all scanners, outputs JSON envelope
- **Test infrastructure** — 129 tests across 8 test files using node:test (zero deps)
- **Test fixtures** — 4 fixture projects (healthy, broken, empty, minimal)
- Finding ID format: `CA-{SCANNER}-{NNN}` (e.g. `CA-CML-001`)
### Fixed
- Agent model mismatches: scanner→haiku, analyzer→sonnet, planner→opus, implementer→sonnet, verifier→haiku
### Changed
- CLAUDE.md rewritten in English for public release readiness
## [1.0.0] - 2026-02-11
### Added
- Cross-platform support (macOS, Linux, Windows)
### Fixed
- `stop-session-reminder.mjs`: Use `path.basename`/`path.dirname` instead of hardcoded `/` split
- `backup-before-change.mjs`: Handle both `/` and `\` path separators in safe filename generation
### Removed
- "Windows: hooks are 100% bash" from known gaps (was incorrect — all hooks are Node.js)
## [0.7.0] - 2026-02-07
### Note
Version reset from 1.2.0 to reflect actual maturity. Previous version was inflated — this plugin has never been externally tested.
### What exists today
- 6 specialized agents (scanner, analyzer, interviewer, planner, implementer, verifier)
- Full machine-wide Claude Code configuration discovery
- Scope selection (current project, repo, home, full machine)
- Inheritance hierarchy mapping and conflict detection
- Mandatory backups before any changes
- Rollback support
- Syntax validation for all configuration files
- Quick audit-only mode
- Full optimization workflow with HITL checkpoints
### Known gaps
- Testing: no automated tests
- Onboarding: never verified that a new user can install and use from scratch
- External verification: nobody else has ever used this

View file

@ -0,0 +1,160 @@
# Config-Audit Plugin
Claude Code Configuration Intelligence — know if your configuration is correct, find what could improve it, fix it automatically.
## What this plugin does
Analyzes and optimizes Claude Code configuration across three pillars:
- **Health** — Deterministic scanners verify correctness, consistency, and completeness
- **Opportunities** — Context-aware recommendations for features that could benefit your project
- **Action** — Auto-fix with backup/rollback
## Commands
### Core (just run `/config-audit` to get started)
| Command | Description |
|---------|-------------|
| `/config-audit` | Full audit with auto-scope detection (no setup needed) |
| `/config-audit posture` | Quick health scorecard (A-F grades, 7 quality areas) |
| `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact |
| `/config-audit fix` | Auto-fix deterministic issues with backup + verification |
| `/config-audit rollback` | Restore configuration from backup |
| `/config-audit plan` | Create action plan from audit findings |
| `/config-audit implement` | Execute plan with backups + auto-verify |
| `/config-audit help` | Show all commands |
### Additional
| Command | Description |
|---------|-------------|
| `/config-audit drift` | Compare current config against saved baseline |
| `/config-audit plugin-health` | Audit plugin structure, frontmatter, cross-plugin coherence |
| `/config-audit discover` | Run discovery phase only |
| `/config-audit analyze` | Run analysis phase only |
| `/config-audit interview` | Gather user preferences (opt-in) |
| `/config-audit status` | Show current session state |
| `/config-audit cleanup` | Clean up old sessions |
## Agents
| Agent | Role | Model | Color | Tools |
|-------|------|-------|-------|-------|
| scanner-agent | Find config files | haiku | cyan | Read, Glob, Grep, Write |
| analyzer-agent | Generate report | sonnet | blue | Read, Glob, Grep, Write |
| planner-agent | Create action plan | opus | yellow | Read, Glob, Write |
| implementer-agent | Execute changes | sonnet | magenta | Read, Write, Edit, Bash, Glob |
| verifier-agent | Verify results | haiku | purple | Read, Glob, Grep |
| feature-gap-agent | Context-aware feature recommendations | opus | green | Read, Glob, Grep, Write |
## Deterministic Scanners
Node.js scanners (zero external dependencies), run via `node scanners/scan-orchestrator.mjs <path>`.
Posture CLI: `node scanners/posture.mjs <path> [--json] [--global] [--full-machine] [--output-file path]`.
Scanner CLI: `node scanners/scan-orchestrator.mjs <path> [--global] [--full-machine] [--no-suppress]`.
| Scanner | Prefix | Detects |
|---------|--------|---------|
| `claude-md-linter.mjs` | CML | Structure, length, sections, @imports, duplicates, TODOs |
| `settings-validator.mjs` | SET | Schema, unknown/deprecated keys, type mismatches, permissions |
| `hook-validator.mjs` | HKV | Format, script existence, event validity, timeouts |
| `rules-validator.mjs` | RUL | Glob matching, orphan rules, deprecated fields, unscoped rules |
| `mcp-config-validator.mjs` | MCP | Server types, trust levels, env vars, unknown fields |
| `import-resolver.mjs` | IMP | Broken @imports, circular refs, deep chains, tilde paths |
| `conflict-detector.mjs` | CNF | Settings conflicts, permission contradictions, hook duplicates |
| `feature-gap-scanner.mjs` | GAP | 25 feature checks across 4 tiers — shown as opportunities, not grades |
### Scanner Lib (`scanners/lib/`)
| Module | Purpose |
|--------|---------|
| `severity.mjs` | Severity constants, risk scoring, verdict logic |
| `output.mjs` | Finding objects (CA-XXX-NNN format), scanner results, envelope |
| `file-discovery.mjs` | Config file discovery: single-path, multi-path (`discoverConfigFilesMulti`), full-machine (`discoverFullMachinePaths`) |
| `yaml-parser.mjs` | Frontmatter parsing, JSON parsing, @import/section extraction |
| `string-utils.mjs` | Line counting, truncation, similarity, key extraction |
| `scoring.mjs` | Area scoring, health scorecard, legacy utilization/maturity |
| `backup.mjs` | Backup creation, manifest parsing, checksum verification |
| `diff-engine.mjs` | Drift diffing: diffEnvelopes(), formatDiffReport() |
| `baseline.mjs` | Baseline save/load/list/delete for drift detection |
| `report-generator.mjs` | Unified markdown reports: posture, drift, plugin health |
| `suppression.mjs` | .config-audit-ignore parsing, finding suppression, audit trail |
### Action Engines (`scanners/`)
| Module | Purpose |
|--------|---------|
| `fix-engine.mjs` | planFixes(), applyFixes(), verifyFixes() — 9 fix types |
| `rollback-engine.mjs` | listBackups(), restoreBackup(), deleteBackup() |
| `fix-cli.mjs` | CLI: `node fix-cli.mjs <path> [--apply] [--json] [--global]` |
| `drift-cli.mjs` | CLI: `node drift-cli.mjs <path> [--save] [--baseline name] [--json]` |
### Standalone Scanner
| Module | Prefix | Purpose |
|--------|--------|---------|
| `plugin-health-scanner.mjs` | PLH | Plugin structure, frontmatter, cross-plugin conflicts (runs independently) |
| `self-audit.mjs` | — | Runs all scanners + plugin health on this plugin itself |
## Knowledge Base (`knowledge/`)
| File | Content |
|------|---------|
| `claude-code-capabilities.md` | Feature register: 18 config surfaces, Anthropic guidance, relevance table |
| `configuration-best-practices.md` | Per-layer best practices |
| `anti-patterns.md` | Common mistakes mapped to scanner IDs |
| `hook-events-reference.md` | All 26 hook events with details |
| `feature-evolution.md` | Feature timeline for staleness detection |
| `gap-closure-templates.md` | Config-specific templates for closing gaps |
## Hooks
| Event | Script | Purpose |
|-------|--------|---------|
| PreToolUse | `auto-backup-config.mjs` | Auto-backup config files before Edit/Write |
| PostToolUse | `post-edit-verify.mjs` | Verify config files after Edit/Write, block on new critical/high |
| SessionStart | `session-start.mjs` | Checks for active (unfinished) sessions |
| Stop | `stop-session-reminder.mjs` | Reminds about current session phase |
## Suppressions
Create `.config-audit-ignore` at project root to suppress known findings:
```
CA-SET-003 # Exact ID
CA-GAP-* # Glob pattern (all GAP findings)
```
Suppressed findings tracked in envelope's `suppressed_findings` for audit trail. Disable with `--no-suppress`.
## Architecture
### Workflow
```
/config-audit → discover + analyze (auto) → plan → implement → verify
```
Default: auto-detects scope from git context. Override with `/config-audit full|repo|home|current`. Delta mode: `--delta` (incremental).
### Session Directory
```
~/.claude/config-audit/sessions/{session-id}/
├── scope.yaml, discovery.json, state.yaml
├── findings/, analysis-report.md, action-plan.md
├── backups/, implementation-log.md
└── interview.md (if interview run)
```
### Finding ID Format
`CA-{SCANNER}-{NNN}` — e.g. `CA-CML-001`, `CA-SET-003`, `CA-HKV-002`, `CA-RUL-005`
## Testing
```bash
node --test 'tests/**/*.test.mjs'
```
486 tests across 27 test files (10 lib + 16 scanner + 1 hook). Test fixtures in `tests/fixtures/`.
## Gotchas
- Session directories accumulate — use `/config-audit cleanup` to manage
- Scanners run on Node.js >= 18 (uses node:test, node:fs/promises)
- Plugin CLAUDE.md files in node_modules should be excluded via scope

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025-2026 Kjell Tore Guttormsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,465 @@
# Config-Audit Plugin for Claude Code
> Know if your configuration is correct. Find what could improve it. Fix it automatically.
*Built for my own Claude Code workflow and shared openly for anyone who finds it useful. This is a solo project — bug reports and feature requests are welcome, but pull requests are not accepted.*
![Version](https://img.shields.io/badge/version-3.0.1-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Scanners](https://img.shields.io/badge/scanners-8-cyan)
![Commands](https://img.shields.io/badge/commands-15-green)
![Agents](https://img.shields.io/badge/agents-6-orange)
![Hooks](https://img.shields.io/badge/hooks-4-red)
![Tests](https://img.shields.io/badge/tests-482+-brightgreen)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
A Claude Code plugin that checks configuration health, suggests context-aware improvements, and auto-fixes issues — `CLAUDE.md`, `settings.json`, hooks, rules, MCP servers, `@imports`, and plugins. 7 quality scanners for correctness, context-aware feature recommendations, auto-fix with backup/rollback. Zero external dependencies.
---
## Table of Contents
- [What Is This?](#what-is-this)
- [The Configuration Problem](#the-configuration-problem)
- [Quick Start](#quick-start)
- [The Feature Gap — Your Biggest Blind Spot](#the-feature-gap--your-biggest-blind-spot)
- [Workflow Examples](#workflow-examples)
- [Commands](#commands)
- [Deterministic Scanners](#deterministic-scanners)
- [Agent Architecture](#agent-architecture)
- [Hooks & Safety](#hooks--safety)
- [Suppressions](#suppressions)
- [Examples & Self-Audit](#examples--self-audit)
- [Data Storage & Safety Guarantees](#data-storage--safety-guarantees)
- [What This Plugin Does Not Cover](#what-this-plugin-does-not-cover)
- [Version History](#version-history)
- [License](#license)
---
## What Is This?
Claude Code reads instructions from at least 7 different file types across multiple scopes: `CLAUDE.md`, `settings.json`, `.claude/rules/`, `hooks.json`, `.mcp.json`, `.claudeignore`, and `settings.local.json`. Each can exist at project level, user level, or both. Plugins add more. The system is powerful — but nobody tells you what you're using wrong, what you're missing, or what's silently conflicting.
This plugin provides three layers of configuration intelligence:
- **Health** — 7 deterministic scanners verify correctness across every configuration file, catching broken imports, deprecated settings, conflicting rules, format errors, and permission contradictions
- **Opportunities** — context-aware recommendations for Claude Code features that could benefit your specific project, backed by Anthropic's official guidance
- **Action** — auto-fix with mandatory backups, syntax validation, rollback support, and a human-in-the-loop workflow for anything non-trivial
> [!TIP]
> Start with `/config-audit posture` for a 30-second scorecard, then `/config-audit` for the full picture.
---
## The Configuration Problem
You've been using Claude Code for weeks — maybe months. It works fine. But there's a gap between "works fine" and "configured well," and it's invisible until someone shows you.
**These are not hypotheticals.** They come from running the posture scanner on real setups:
- Your global `CLAUDE.md` says "never use mocks" but a project rule says "prefer mocks" — Claude gets confused and you don't know why
- You've written dozens of projects but have never set up hooks, rules, or keybindings because you didn't know they existed
- Three plugins define hooks for the same event with conflicting behavior
- Your `settings.json` has a deprecated key that silently does nothing
- An `@import` in your CLAUDE.md points to a file you deleted last week
- You're using maybe 30% of what Claude Code can do — and you don't know what the other 70% is
The plugin ships with two example projects. Run them yourself:
### `examples/minimal-setup/` — just a CLAUDE.md, nothing else
```
> node scanners/posture.mjs examples/minimal-setup/
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Config-Audit Health Score
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Health: A (99/100) 7 areas scanned
Area Scores
───────────
CLAUDE.md ............ A (90)
Settings ............. A (100) Hooks ............... A (100)
Rules ................ A (100) MCP ................. A (100)
Imports .............. A (100) Conflicts ........... A (100)
22 opportunities available — run /config-audit feature-gap for recommendations
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
**Grade A** — nothing is broken. The health grade only reflects real issues, and this setup has none. The 22 opportunities are not failures — they're features you *could* use. Run `/config-audit feature-gap` to see which ones are relevant to your project.
### `examples/optimal-setup/` — full configuration across all 4 tiers
```
> node scanners/posture.mjs examples/optimal-setup/
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Config-Audit Health Score
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Health: A (93/100) 7 areas scanned
Area Scores
───────────
CLAUDE.md ............ A (100) Settings ............ A (90)
Hooks ................ A (100) Rules ............... B (80)
MCP .................. A (90) Imports ............. A (100)
Conflicts ............ A (90)
3 opportunities available — run /config-audit feature-gap for recommendations
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
Also **Grade A** — with only 3 opportunities remaining. This project has CLAUDE.md split via `@imports`, permissions scoped to specific tools, path-scoped rules (different rules for `src/` vs. `tests/`), hooks covering multiple events, and MCP servers. Both setups are healthy — the difference is how much of Claude Code's surface area you're choosing to use.
---
## Quick Start
### Prerequisites
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed
- Node.js 18+ (for standalone CLI tools)
### Installation
Clone from the public repository:
```bash
git clone https://git.fromaitochitta.com/open/claude-code-config-audit.git
```
Or add as a Claude Code plugin:
```json
{
"enabledPlugins": {
"config-audit@plugin-marketplace": true
}
}
```
### First Scan
```bash
# Full audit with auto-scope detection (inside Claude Code)
/config-audit
# 30-second posture check (standalone, no LLM needed)
node scanners/posture.mjs /path/to/project
# Auto-fix issues with backup
node scanners/fix-cli.mjs /path/to/project --apply
```
The CLI tools work standalone — no Claude Code session needed, just Node.js 18+.
---
## Feature Opportunities — Context-Aware Recommendations
Most configuration tools stop at "is it valid?" Config-audit goes further: **what could improve your setup, and is it relevant to your project?**
The feature opportunity scanner checks 25 dimensions and groups recommendations by impact:
| Impact Level | Focus | Examples |
|--------------|-------|---------|
| **High** | Correctness & security | `permissions.deny` for sensitive files, basic hooks for safety automation |
| **Worth Considering** | Workflow efficiency | Path-scoped rules, modular `@imports`, custom agents |
| **Explore** | Nice-to-have | Keybindings, status line, output styles, agent teams |
Each recommendation is **context-aware** — it considers what your project actually contains. A solo TypeScript project gets different suggestions than a team Python monorepo. Recommendations include *why* (backed by Anthropic's official guidance) and *how* (concrete steps).
Run `/config-audit feature-gap` to see what's relevant to your project.
---
## Workflow Examples
### 1. First Time — Just Curious
You heard about this plugin and want to know where you stand:
```
/config-audit # Auto-detects scope, runs full audit
# → See your grade, top issues, and gaps
/config-audit posture # Even faster: 30-second scorecard only
```
### 2. Monthly Configuration Checkup
A quick health check — are things still clean?
```
/config-audit posture # Quick health check (A-F grade, 7 areas)
/config-audit # Full audit if grade dropped
/config-audit fix # Auto-fix deterministic issues
/config-audit posture # Verify improvement
```
### 3. Deep Optimization
You want to go from C to A. The full pipeline:
```
/config-audit # Audit — understand what you have
/config-audit feature-gap # Opportunities — context-aware recommendations
/config-audit plan # Plan — prioritized actions with risk assessment
/config-audit implement # Execute — changes with backup + verification
```
### 4. Plugin Author
You maintain Claude Code plugins and want to ensure quality:
```
/config-audit plugin-health # Audit plugin structure, frontmatter, cross-plugin conflicts
# → Checks naming, frontmatter completeness, tool grants, duplicates
```
### 5. Track Configuration Drift
Your team configuration changes over time. Track it:
```
/config-audit drift # First run creates baseline, subsequent runs show delta
# → New findings, resolved findings, unchanged, moved
/config-audit drift --save my-baseline # Save a named baseline for comparison
```
---
## Commands
### Core (just run `/config-audit` to get started)
| Command | Description |
|---------|-------------|
| `/config-audit` | Full audit with auto-scope detection (no setup needed) |
| `/config-audit posture` | Quick health scorecard: A-F grades across 7 quality areas |
| `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact |
| `/config-audit fix` | Auto-fix deterministic issues with backup + verification |
| `/config-audit rollback` | Restore configuration from a previous backup |
| `/config-audit plan` | Generate prioritized action plan from audit findings |
| `/config-audit implement` | Execute plan with automatic backup + verification |
| `/config-audit help` | Show all commands with usage examples |
### Additional
| Command | Description |
|---------|-------------|
| `/config-audit drift` | Compare current config against a saved baseline |
| `/config-audit plugin-health` | Audit plugin structure, frontmatter, cross-plugin coherence |
| `/config-audit discover` | Run discovery phase only |
| `/config-audit analyze` | Run analysis phase only |
| `/config-audit interview` | Set preferences for action plan _(optional)_ |
| `/config-audit status` | Show current session state and available actions |
| `/config-audit cleanup` | Remove old session directories |
### Scope
By default, `/config-audit` auto-detects scope from your git context. Override with: `/config-audit current`, `/config-audit repo`, `/config-audit home`, `/config-audit full`.
---
## Deterministic Scanners
8 Node.js scanners that perform structural analysis an LLM cannot reliably do: schema validation, circular reference detection, import resolution, conflict detection across scopes. Zero external dependencies.
**Why deterministic?** LLMs are powerful at understanding intent and context. But they cannot reliably validate JSON schemas, detect circular `@import` chains, or catch that your global `settings.json` contradicts your project-level one. These scanners fill that gap — fast, repeatable, and zero false positives on structural issues.
| Scanner | Prefix | What It Catches |
|---------|--------|-----------------|
| `claude-md-linter.mjs` | CML | Oversized files, missing sections, broken @imports, duplicates, stale TODOs |
| `settings-validator.mjs` | SET | Schema violations, unknown/deprecated keys, type mismatches, permission issues |
| `hook-validator.mjs` | HKV | Invalid format, missing scripts, wrong event names, timeout risks |
| `rules-validator.mjs` | RUL | Bad glob patterns, orphaned rules, deprecated fields, unscoped rules |
| `mcp-config-validator.mjs` | MCP | Invalid server types, missing trust levels, exposed env vars |
| `import-resolver.mjs` | IMP | Broken @imports, circular references, deep chains, tilde path issues |
| `conflict-detector.mjs` | CNF | Settings contradictions across scopes, permission conflicts, hook duplicates |
| `feature-gap-scanner.mjs` | GAP | 25 feature checks — shown as opportunities, not grades |
### CLI Tools
All tools work standalone — no Claude Code session needed:
| Tool | Usage |
|------|-------|
| **Posture** | `node scanners/posture.mjs <path> [--json] [--global]` |
| **Fix** | `node scanners/fix-cli.mjs <path> [--apply] [--json] [--global]` |
| **Drift** | `node scanners/drift-cli.mjs <path> [--save] [--baseline name] [--json]` |
| **Self-audit** | `node scanners/self-audit.mjs [--json] [--fix]` |
| **Full scan** | `node scanners/scan-orchestrator.mjs <path> [--global] [--no-suppress]` |
---
## Agent Architecture
Six specialized agents collaborate through the audit workflow, each matched to an appropriate model for cost and quality:
| Agent | Model | Role |
|-------|-------|------|
| **scanner-agent** | Haiku | Fast filesystem scanning, file discovery |
| **analyzer-agent** | Sonnet | Deep analysis, hierarchy mapping, conflict detection |
| **planner-agent** | Opus | Action plan generation with risk assessment |
| **implementer-agent** | Sonnet | Change execution with mandatory backups |
| **verifier-agent** | Haiku | Post-implementation verification |
| **feature-gap-agent** | Opus | Context-aware feature recommendations |
### Orchestration Flow
```
+-----------+
| Interview | (optional)
+-----+-----+
|
+-----------+ +---------+ +-------v---+ +-----------+
| Discover | --> | Analyze | --> | Plan | --> | Implement |
| (haiku) | | (sonnet)| | (opus) | | (sonnet) |
+-----------+ +---------+ +-----------+ +-----+-----+
|
+-----v-----+
| Verify |
| (haiku) |
+-----------+
```
---
## Hooks & Safety
Four hooks provide automatic safety and session continuity — they activate the moment the plugin is installed:
| Event | Script | What It Does |
|-------|--------|--------------|
| **PreToolUse** | `auto-backup-config.mjs` | Backs up any config file before Edit/Write touches it |
| **PostToolUse** | `post-edit-verify.mjs` | Re-scans after edits — blocks if new critical/high findings introduced |
| **SessionStart** | `session-start.mjs` | Checks for incomplete audit sessions so you can resume |
| **Stop** | `stop-session-reminder.mjs` | Shows current phase so your next session picks up where you left off |
All hooks are Node.js (`.mjs`) for cross-platform compatibility (macOS, Linux, Windows).
> [!IMPORTANT]
> The PreToolUse and PostToolUse hooks only activate when config-audit is modifying configuration files. They don't interfere with your normal development workflow.
---
## Suppressions
Some findings are expected — maybe you intentionally have a large CLAUDE.md, or a feature gap doesn't apply to your workflow. Create a `.config-audit-ignore` file to suppress them:
```
# Suppress by exact finding ID
CA-SET-003
# Suppress by scanner prefix (glob pattern)
CA-GAP-*
# Suppress all plugin health findings
CA-PLH-*
```
Suppressed findings are tracked in the scan envelope's `suppressed_findings` array for audit trail — nothing is silently hidden. Use `--no-suppress` to see everything.
---
## Examples & Self-Audit
### Example Projects
The `examples/` directory contains two projects shown in the [before/after demo](#the-configuration-problem) above:
| Example | Description | Grade | Opportunities |
|---------|-------------|-------|---------------|
| `minimal-setup/` | Single CLAUDE.md, nothing else | A | 22 |
| `optimal-setup/` | Full configuration across all 4 tiers | A | 3 |
```bash
# Run them yourself
node scanners/posture.mjs examples/minimal-setup/
node scanners/posture.mjs examples/optimal-setup/
```
### Self-Audit: Scanning the Scanner
The plugin runs all 8 scanners on itself via `self-audit.mjs`. Current result: **Grade A, score 98, 0 real findings.** Test fixtures and example files are automatically excluded from scoring — a security plugin that ships deliberately broken examples shouldn't fail its own audit.
```bash
node scanners/self-audit.mjs
```
---
## Data Storage & Safety Guarantees
### Where Data Lives
All data stays local at `~/.claude/config-audit/sessions/`:
```
~/.claude/config-audit/sessions/{session-id}/
scope.yaml # Scan boundaries
discovery.json # File manifest
findings/ # Individual issues (YAML)
analysis-report.md # Full report
action-plan.md # Prioritized actions
backups/ # Pre-modification copies
implementation-log.md # Change log
state.yaml # Phase tracking
```
### Safety Guarantees
This plugin is cautious by design — configuration files are important, and a bad edit can break your entire Claude Code setup:
| Guarantee | How |
|-----------|-----|
| **Backups mandatory** | Every file is copied before modification — no exceptions |
| **Read-only audit** | `/config-audit` and `/config-audit posture` analyze without changing anything |
| **Rollback support** | `/config-audit rollback` restores from any backup |
| **Syntax validation** | Every change is validated before finalization |
| **Verification pass** | A separate agent confirms changes actually work |
| **Human-in-the-loop** | You approve the plan before anything is implemented |
| **Post-edit guard** | Hook blocks the session if a new critical/high finding is introduced |
---
## What This Plugin Does Not Cover
- **Runtime behavior** — this plugin audits configuration files, not what Claude actually does at runtime. For runtime defense, see [claude-code-llm-security](https://git.fromaitochitta.com/open/claude-code-llm-security)
- **Secret scanning** — config-audit checks for structural issues, not leaked credentials. Use llm-security for secret detection
- **Custom scanner rules** — scanners check against known Claude Code configuration schemas. Custom rule definitions are not supported
- **Remote/team configuration** — managed settings, SSO-provisioned config, and organization-level policies are detected as gaps but not managed
---
## Version History
| Version | Date | Highlights |
|---------|------|-----------|
| **3.0.1** | 2026-04-04 | Cross-platform fix: Windows path separators. 486 tests |
| **3.0.0** | 2026-04-04 | Health redesign: quality-only grades, context-aware opportunities (replaces utilization/maturity/segment), Anthropic guidance. 482 tests |
| **2.2.0** | 2026-04-04 | Fixture filtering (test findings excluded from grades), session path fix, UX polish. 461 tests |
| **2.1.0** | 2026-04-03 | UX redesign: auto-scope, zero questions, simplified commands (15 from 17). 441+ tests |
| **2.0.0** | 2026-04-03 | Complete rewrite: 8 scanners, 25 gap dimensions, auto-fix, drift, suppressions, self-audit. 408+ tests |
| **1.6.0** | 2026-04-03 | Report generator, suppression engine, self-audit CLI, PostToolUse hook |
| **1.5.0** | 2026-04-03 | Diff engine, baseline manager, drift CLI, plugin health scanner |
| **1.4.0** | 2026-04-03 | Fix engine, rollback engine, fix CLI, PreToolUse hook |
| **1.3.0** | 2026-04-03 | Scoring module, posture CLI, feature-gap agent |
| **1.2.0** | 2026-04-03 | 4 advanced scanners (MCP, import, conflict, feature-gap) |
| **1.1.0** | 2026-04-03 | 4 core scanners, scan orchestrator, test infrastructure |
| **1.0.0** | 2026-02-11 | Cross-platform support |
| **0.7.0** | 2026-02-07 | Initial version (version reset from inflated 1.2.0) |
See [CHANGELOG.md](CHANGELOG.md) for full details.
---
## License
[MIT License](LICENSE) — Copyright (c) 2025-2026 Kjell Tore Guttormsen

View file

@ -0,0 +1,175 @@
---
name: analyzer-agent
description: Analyze Claude Code configuration findings and generate comprehensive reports with hierarchy maps, conflict detection, and quality scores.
model: sonnet
color: blue
tools: ["Read", "Glob", "Grep", "Write"]
---
# Analyzer Agent
Comprehensive analysis agent that processes scanner findings and generates detailed reports.
## Purpose
Analyze all discovered configuration files to:
1. Map the complete inheritance hierarchy
2. Detect conflicts between configuration levels
3. Identify duplicate rules across files
4. Find optimization opportunities
5. Flag security issues
6. Validate imports and rules
7. Score CLAUDE.md quality
8. Generate actionable recommendations
## Input
You will receive:
1. Session ID with findings in `~/.claude/config-audit/sessions/{session-id}/findings/`
2. Scope configuration from `~/.claude/config-audit/sessions/{session-id}/scope.yaml`
3. Scanner JSON envelope (if available) from scan-orchestrator.mjs
4. Knowledge base at `{CLAUDE_PLUGIN_ROOT}/knowledge/` for best practices and anti-patterns
## Task
1. **Load all findings**: Read all `*.yaml` files from findings directory
1.5. **Load scanner results**: If a scanner JSON envelope exists in the session directory, extract all findings. Cross-reference against `knowledge/anti-patterns.md` to add remediation context. Note any CA-{prefix}-NNN finding IDs in the report.
2. **Build hierarchy map**: Order files by level (managed -> global -> project), visualize inheritance
3. **Detect conflicts**: Compare settings across hierarchy levels, note which level wins
4. **Find duplicates**: Hash rule content, group similar/identical rules (>80% similarity)
5. **Identify optimizations**: Rules to globalize, missing configs, orphaned files
6. **Security scan**: Aggregate secret warnings, check for insecure patterns
7. **CLAUDE.md quality assessment**: Score each file against rubric, assign letter grades
8. **Generate report**: Write comprehensive markdown report
## Output
Write to: `~/.claude/config-audit/sessions/{session-id}/analysis-report.md`
**Output MUST NOT exceed 300 lines.** Prioritize findings by severity. Use tables, not prose.
Report structure:
0. Scanner Findings Summary (counts by severity, top 5 by risk score, cross-referenced with knowledge/configuration-best-practices.md)
1. Executive Summary (counts of files, issues, opportunities)
2. Hierarchy Map (compact ASCII visualization)
3. Conflicts Detected (table)
4. Duplicate Rules (table)
5. Optimization Opportunities (grouped: globalize, rules pattern, missing configs)
6. Security Findings (table with severity)
7. CLAUDE.md Quality Scores (table with grade + top issue per file)
8. Import & Rules Health (broken imports, orphaned rules)
9. Recommendations Summary (high/medium/low priority)
## CLAUDE.md Quality Rubric (100 points)
This is the **authoritative scoring rubric** for CLAUDE.md quality assessment.
### 1. Commands/Workflows (20 points)
| Score | Criteria |
|-------|----------|
| 20 | All essential commands documented with context. Build, test, lint, deploy present. Development workflow clear. Common operations documented. |
| 15 | Most commands present, some missing context |
| 10 | Basic commands only, no workflow |
| 5 | Few commands, many missing |
| 0 | No commands documented |
### 2. Architecture Clarity (20 points)
| Score | Criteria |
|-------|----------|
| 20 | Clear codebase map. Key directories explained. Module relationships documented. Entry points identified. Data flow described. |
| 15 | Good structure overview, minor gaps |
| 10 | Basic directory listing only |
| 5 | Vague or incomplete |
| 0 | No architecture info |
### 3. Non-Obvious Patterns (15 points)
| Score | Criteria |
|-------|----------|
| 15 | Gotchas and quirks captured. Known issues documented. Workarounds explained. Edge cases noted. "Why we do it this way" for unusual patterns. |
| 10 | Some patterns documented |
| 5 | Minimal pattern documentation |
| 0 | No patterns or gotchas |
### 4. Conciseness (15 points)
| Score | Criteria |
|-------|----------|
| 15 | Dense, valuable content. No filler or obvious info. Each line adds value. No redundancy with code comments. |
| 10 | Mostly concise, some padding |
| 5 | Verbose in places |
| 0 | Mostly filler or restates obvious code |
### 5. Currency (15 points)
| Score | Criteria |
|-------|----------|
| 15 | Reflects current codebase. Commands work as documented. File references accurate. Tech stack current. |
| 10 | Mostly current, minor staleness |
| 5 | Several outdated references |
| 0 | Severely outdated |
### 6. Actionability (15 points)
| Score | Criteria |
|-------|----------|
| 15 | Instructions are executable. Commands can be copy-pasted. Steps are concrete. Paths are real. |
| 10 | Mostly actionable |
| 5 | Some vague instructions |
| 0 | Vague or theoretical |
### Letter Grades
| Grade | Score Range | Description |
|-------|-------------|-------------|
| A | 90-100 | Comprehensive, current, actionable |
| B | 70-89 | Good coverage, minor gaps |
| C | 50-69 | Basic info, missing key sections |
| D | 30-49 | Sparse or outdated |
| F | 0-29 | Missing or severely outdated |
### Red Flags
| Red Flag | Severity | Description |
|----------|----------|-------------|
| Failing commands | High | Commands that reference non-existent scripts/paths |
| Dead file references | High | References to deleted files/folders |
| Outdated tech | Medium | Mentions of deprecated or outdated technology versions |
| Uncustomized templates | Medium | Copy-paste from templates without project-specific customization |
| Unresolved TODOs | Medium | "TODO" items that were never completed |
| Generic advice | Low | Best practices not specific to the project |
| Duplicate content | Low | Same information repeated across multiple CLAUDE.md files |
### Section Detection Patterns
**Commands:** `## Commands`, `## Development`, `## Getting Started`, `## Quick Start`, `## Build`, `## Test`
**Architecture:** `## Architecture`, `## Project Structure`, `## Directory Structure`, `## Codebase Overview`, `## Key Files`
**Patterns/Gotchas:** `## Gotchas`, `## Patterns`, `## Known Issues`, `## Quirks`, `## Non-Obvious`, `## Important Notes`
### Quality Signals
**Positive:** Code blocks with working commands, file paths that exist, specific error messages and solutions, clear relationship to actual code, dense scannable content.
**Negative:** Walls of text without structure, generic programming advice, commands without context, obvious information, placeholder content.
## Conflict Detection
Compare same-named settings across hierarchy. Winner determination:
- Project-local beats project-shared
- Project beats global
- Global beats managed (user preference)
- Unless managed is enforced (enterprise)
## Quality Checks
Verify report: all findings referenced, recommendations actionable, severity levels consistent.
## Performance
- Process findings in memory (typically < 1MB total)
- Generate report in single pass
- No file modifications (read-only except report output)

View file

@ -0,0 +1,91 @@
---
name: feature-gap-agent
description: |
Analyzes Claude Code configuration and produces context-aware feature
recommendations grouped by impact. Frames unused features as opportunities,
not failures.
model: opus
color: green
tools: ["Read", "Glob", "Grep", "Write"]
---
# Feature Opportunities Agent
You analyze Claude Code configuration and produce context-aware recommendations — not grades.
## Input
You receive posture assessment data (JSON) containing:
- `areas` — per-scanner grades (7 quality areas + Feature Coverage)
- `overallGrade` — health grade (quality areas only)
- `opportunityCount` — number of unused features detected
- `scannerEnvelope` — full scanner results including GAP findings
You also receive project context: language, file count, existing configuration.
## Knowledge Files
Read **at most 3** of these files from the plugin's `knowledge/` directory:
- `claude-code-capabilities.md` — Feature register with "When relevant" guidance
- `configuration-best-practices.md` — Per-layer best practices
- `gap-closure-templates.md` — Templates for closing gaps with effort estimates
## Output
Write `feature-gap-report.md` to the session directory. Max 200 lines.
### Report Structure
```markdown
# Feature Opportunities
**Date:** YYYY-MM-DD | **Health:** Grade (score/100) | **Opportunities:** N
## Your Project
[1-2 sentences describing detected context: language, size, what's already configured]
## High Impact
These address correctness or security — consider them seriously.
**[feature name]**
Why: [evidence-backed reason, cite Anthropic docs or proven issues]
How: [2-3 concrete steps]
[Repeat for each T1 finding]
## Worth Considering
These improve workflow efficiency for projects like yours.
**[feature name]**
Why: [reason, with "relevant because your project has X"]
How: [2-3 concrete steps]
[Repeat for each T2 finding]
## Explore When Ready
Nice-to-have features. Skip these if your current setup works well.
**[feature name]**
Why: [brief reason]
[Repeat for T3/T4 findings, keep brief]
## When You Might Skip These
[Honest qualification: which recommendations are genuinely optional and why. A minimal setup can be the right choice.]
```
## Guidelines
- Frame everything as opportunities, never as failures or gaps
- Be specific and actionable in recommendations
- Use the "When relevant" table from claude-code-capabilities.md to judge context
- Order actions by impact/effort ratio (high impact, low effort first)
- Reference specific files and paths in recommendations
- Do NOT recommend features the project already has
- Do NOT show utilization percentages, maturity levels, or segment classifications
- Include honest "you might not need this" qualifications for T3/T4 items

View file

@ -0,0 +1,261 @@
---
name: implementer-agent
description: Execute individual configuration changes from an action plan with backup verification and syntax validation.
model: sonnet
color: magenta
tools: ["Read", "Write", "Edit", "Bash", "Glob"]
---
# Implementer Agent
Focused execution agent that implements individual actions from the action plan.
## Purpose
Execute a single action from the action plan:
1. Verify backup exists (for modify/delete)
2. Make the specified change
3. Validate the result
4. Report success or failure
## Input
You will receive:
1. Session ID
2. Action details (from action plan)
3. Backup location
## Task
For each action, follow this sequence:
1. **Pre-check**: Verify prerequisites
2. **Execute**: Make the change
3. **Validate**: Verify result is correct
4. **Report**: Log outcome
## Tool Usage Constraints
### Absolute Paths Only
**NEVER** use `~/` or relative paths in tool calls. Always resolve to full absolute paths (e.g., `/Users/username/...`).
Before any file operation, resolve the home directory:
```
1. If path starts with ~/, resolve to absolute path first
2. Use the session's scope.yaml or state.yaml to find the correct base paths
3. All Read, Write, Edit, and Bash file operations must use the resolved absolute path
```
### Read Before Write
**ALWAYS** read the target file before using the Write tool, even for new files:
```
1. Read the file path first (to confirm it exists or doesn't exist)
2. If file exists: You now have the content for the Write tool's requirement
3. If file doesn't exist: The Read error confirms it's safe to create
4. Then proceed with Write
```
The Write tool requires that existing files are read first. Skipping this step causes "Error writing file".
### Edit vs Write
- **Edit tool**: Use for modifying existing files (surgical replacements)
- **Write tool**: Use only for creating new files or full file rewrites
- **Prefer Edit** when changing a section of an existing file — it's safer and preserves unchanged content
## Action Types
### Type: Create
Create a new file that doesn't exist.
```
1. Resolve path to absolute (no ~/ allowed)
2. Read the path to verify file doesn't exist (if exists, report conflict)
3. Create parent directories if needed (mkdir -p with absolute path)
4. Write file content using absolute path
5. Validate syntax
6. Report success
```
### Type: Modify
Edit an existing file.
```
1. Verify file exists
2. Verify backup exists in backup location
3. Read current content
4. Apply changes (Edit tool or full Write)
5. Validate syntax
6. Report success
```
### Type: Delete
Remove a file.
```
1. Verify file exists
2. Verify backup exists
3. Delete file
4. Verify file gone
5. Report success
```
### Type: Move
Move content from one file to another.
```
1. Verify source exists
2. Verify backup exists for source
3. Read source content
4. Write to destination (or append)
5. Remove from source
6. Validate both files
7. Report success
```
## Validation Rules
### Markdown Files (CLAUDE.md, rules/*.md)
```
- File is readable
- If frontmatter exists, it's valid YAML
- No obvious syntax errors
- Sections are well-formed
```
### JSON Files (settings.json, .mcp.json)
```
- Parse as JSON successfully
- Known keys have expected types
- No syntax errors
```
### Ignore Files (.claudeignore)
```
- Each line is valid gitignore pattern
- No obvious typos
```
## Output Format
Append to: `~/.claude/config-audit/sessions/{session-id}/implementation-log.md`
### Success
```markdown
### ✓ Action {action-id}: {action-title}
- **Status**: SUCCESS
- **Time**: {timestamp}
- **File**: {file-path}
- **Type**: {create|modify|delete|move}
- **Changes**: {description}
- **Validation**: {validation-result}
```
### Failure
```markdown
### ✗ Action {action-id}: {action-title}
- **Status**: FAILED
- **Time**: {timestamp}
- **File**: {file-path}
- **Error**: {error-message}
- **Rollback**: {rollback-status}
- **Action**: {recommended-action}
```
## Error Handling
### File Not Found
```
If create: Proceed (expected)
If modify: FAIL - file should exist
If delete: SKIP - already gone, log as warning
```
### Permission Denied
```
FAIL - log error
Recommend: Check file permissions
Don't attempt automatic fix
```
### Invalid Syntax After Edit
```
FAIL - syntax validation failed
Rollback: Restore from backup
Report: What went wrong
```
### Backup Not Found
```
FAIL - refuse to modify without backup
Report: Backup missing for {file}
Don't proceed with any modification
```
## Implementation Examples
### Example 1: Create New Rule File
```
Action: Create ~/.claude/rules/code-style.md
Steps:
1. Check: ~/.claude/rules/ exists? No → mkdir -p ~/.claude/rules/
2. Check: code-style.md exists? No → proceed
3. Write content to code-style.md
4. Read back and validate markdown
5. Log success
```
### Example 2: Modify CLAUDE.md
```
Action: Remove "Code Style" section from ~/repos/project/CLAUDE.md
Steps:
1. Check: File exists? Yes
2. Check: Backup exists? Yes (at ~/.claude/config-audit/backups/.../...)
3. Read current content
4. Use Edit tool to remove section between "## Code Style" and next "##"
5. Read back and validate
6. Log success
```
### Example 3: Update .mcp.json
```
Action: Replace hardcoded token with env var reference
Steps:
1. Check: File exists? Yes
2. Check: Backup exists? Yes
3. Read current JSON
4. Use Edit to change "SLACK_TOKEN": "xoxb-xxx" to "SLACK_TOKEN": "${SLACK_TOKEN}"
5. Parse as JSON to validate
6. Log success
```
## Safety Constraints
1. **Never modify without backup**: Refuse if backup missing
2. **Never delete without confirmation**: Backup must exist
3. **Validate before and after**: Catch corruption early
4. **Atomic operations**: Either fully succeed or fully fail
5. **No cascading changes**: Only do the one assigned action
## Coordination
Multiple implementer agents may run in parallel for independent actions.
To avoid conflicts:
- Each agent works on different files
- Lock files if same file needs multiple edits
- Report completion to allow dependent actions to start

View file

@ -0,0 +1,265 @@
---
name: planner-agent
description: Create prioritized action plans for configuration optimization based on analysis findings and user preferences.
model: opus
color: yellow
tools: ["Read", "Glob", "Write"]
---
# Planner Agent
Strategic agent that generates comprehensive action plans for configuration optimization.
## Purpose
Create a detailed, prioritized action plan that:
1. Addresses all findings from analysis
2. Respects user preferences from interview
3. Assesses risk for each action
4. Defines clear rollback strategies
5. Orders actions by dependencies
## Input
You will receive:
1. Session ID
2. Analysis report: `~/.claude/config-audit/sessions/{session-id}/analysis-report.md`
3. Interview results: `~/.claude/config-audit/sessions/{session-id}/interview.md` (optional)
## Task
1. **Load inputs**: Read analysis and interview (if exists)
2. **Generate actions**: Create action items for each finding
3. **Assess risk**: Evaluate risk level per action
4. **Order by dependencies**: Ensure correct execution order
5. **Create rollback plans**: Define how to undo each action
6. **Write action plan**: Output comprehensive plan
## Action Categories
### Category 1: Security Fixes (Priority: Critical)
- Move secrets to environment variables
- Fix file permissions
- Remove hardcoded credentials
### Category 2: Conflict Resolution (Priority: High)
- Resolve duplicate settings
- Apply interview preferences
- Document intended overrides
### Category 3: Consolidation (Priority: Medium)
- Move common rules to global
- Create modular rule files
- Consolidate MCP servers
### Category 4: Optimization (Priority: Low)
- Add missing configurations
- Create .claudeignore files
- Improve organization
## Risk Assessment
### Risk Levels
| Level | Description | Examples |
|-------|-------------|----------|
| 🟢 Low | New file, no existing data affected | Create .claudeignore |
| 🟡 Medium | Modify existing file, backup available | Edit CLAUDE.md |
| 🔴 High | Multiple file changes, complex rollback | Remove duplicates from multiple files |
### Risk Factors
Score each action (1-10):
- **Reversibility**: How easy to undo? (10=trivial, 1=impossible)
- **Scope**: How many files affected? (10=one file, 1=many files)
- **Criticality**: How important is the file? (10=optional, 1=critical)
- **Complexity**: How complex is the change? (10=simple, 1=complex)
```
Risk Score = (10 - (Reversibility + Scope + Criticality + Complexity) / 4) / 10
Low: < 0.3, Medium: 0.3-0.6, High: > 0.6
```
## Dependency Resolution
Build dependency graph:
```
Action A: Create ~/.claude/rules/code-style.md (no deps)
Action B: Remove code-style from project CLAUDE.md (depends on A)
Action C: Create .claudeignore (no deps)
```
Execution order: A, C (parallel) → B
## Output Format
Write to: `~/.claude/config-audit/sessions/{session-id}/action-plan.md`
**Output MUST NOT exceed 200 lines.** Each action item: max 5 lines (file, change, risk, validation, dependency). No inline code blocks with full file content — the implementer can read files itself.
```markdown
# Configuration Action Plan
Session: {session-id}
Generated: {timestamp}
Based on: Analysis + Interview
## Executive Summary
| Metric | Value |
|--------|-------|
| Total actions | 12 |
| Files to create | 3 |
| Files to modify | 5 |
| Files to delete | 0 |
| Overall risk | Low |
| Estimated backup size | 15 KB |
## Risk Distribution
| Risk | Count | Description |
|------|-------|-------------|
| 🟢 Low | 8 | Safe changes |
| 🟡 Medium | 3 | Requires backup |
| 🔴 High | 1 | Complex change |
## Backup Requirements
Files to backup before implementation:
- `~/.claude/CLAUDE.md` (1.2 KB)
- `~/.claude/settings.json` (0.5 KB)
- `~/project-a/CLAUDE.md` (2.1 KB)
- `~/project-a/.mcp.json` (0.8 KB)
- `~/project-b/CLAUDE.md` (1.8 KB)
Total backup size: ~6.4 KB
## Execution Groups
### Group 1: Independent Actions (Parallel)
- Action 1.1: Create global rules file
- Action 2.1: Create .claudeignore for project-a
- Action 2.2: Create .claudeignore for project-b
### Group 2: Depends on Group 1
- Action 1.2: Remove duplicates from project CLAUDE.md files
### Group 3: Depends on Group 2
- Action 3.1: Consolidate MCP servers
## Actions (Detailed)
### Action 1.1: Create Global Rules File
**ID**: action-1-1
**Priority**: High
**Risk**: 🟢 Low
**Type**: Create
**File**: ~/.claude/rules/code-style.md
**Rationale**:
Code style rules found in 3 projects are identical. Moving to global reduces duplication.
**Content**:
```markdown
# Code Style Rules
## Language Preferences
- TypeScript > JavaScript
- Explicit > implicit
- Lesbarhet > cleverness
## Commit Format
- Conventional Commits: `type(scope): description`
```
**Validation**:
- File exists after creation
- Valid markdown syntax
**Rollback**:
- Delete file: `rm ~/.claude/rules/code-style.md`
**Dependencies**: None
---
### Action 1.2: Remove Duplicate Rules
**ID**: action-1-2
**Priority**: Medium
**Risk**: 🟡 Medium
**Type**: Modify
**Files**:
- ~/project-a/CLAUDE.md
- ~/project-b/CLAUDE.md
- ~/project-c/CLAUDE.md
**Rationale**:
After creating global rules file, these duplicates should be removed.
**Changes**:
Remove the "Code Style" section from each file.
**Validation**:
- Files still valid markdown
- Global rules file exists
- Claude Code loads without errors
**Rollback**:
- Restore from backup
**Dependencies**: action-1-1
---
[Additional actions...]
## Post-Implementation
### Verification Steps
1. ✓ All created files exist
2. ✓ All modified files are valid
3. ✓ No remaining conflicts
4. ✓ No remaining duplicates
5. ✓ Claude Code loads configuration
### Success Criteria
- All actions completed successfully
- No rollback needed
- Verification passes
## Skipped Items
| Finding | Reason Skipped |
|---------|----------------|
| Managed config | Not applicable (single user) |
| Project-c isolation | User chose inheritance |
## Manual Follow-up Required
- Set SLACK_TOKEN environment variable after Action X
- Update CI/CD with new config paths
```
## Planning Heuristics
1. **Security first**: Always prioritize security fixes
2. **Create before modify**: New files before editing existing
3. **Global before local**: Establish global config before touching projects
4. **Simple before complex**: Low-risk actions first
5. **Validate continuously**: Each action includes validation step
## Interview Integration
If interview exists, apply preferences:
- Config style → determines consolidation strategy
- MCP strategy → determines server organization
- Modular rules → enables/disables rule file creation
- Conflict resolutions → applies specific values
- Project inheritance → determines what stays local
If no interview, use sensible defaults:
- Centralized style
- Mixed MCP servers
- Enable modular rules
- Project overrides global for conflicts

View file

@ -0,0 +1,257 @@
---
name: scanner-agent
description: Scan a directory tree for Claude Code configuration files (CLAUDE.md, settings.json, .mcp.json, rules). First step in the config-audit workflow.
model: haiku
color: cyan
tools: ["Read", "Glob", "Grep", "Write"]
---
# Scanner Agent
Fast, focused agent for discovering Claude Code configuration files in a single directory tree.
## Purpose
Scan a directory path and identify all Claude Code configuration files:
- CLAUDE.md files (project/local)
- settings.json files
- .mcp.json files
- .claudeignore files
- .claude/rules/*.md files
## Input
You will receive:
1. A directory path to scan
2. A session ID for output location
3. (Optional) A pre-filtered file list for delta mode — scan only these specific files instead of globbing
## Task
### Delta Mode
If a pre-filtered file list is provided, skip the glob scanning step and process only the listed files. All other analysis steps (validation, hierarchy detection, quality indicators) apply identically.
### Full Scan
1. **Scan for config files** using these patterns:
- `{path}/**/CLAUDE.md`
- `{path}/**/CLAUDE.local.md`
- `{path}/**/.claude/CLAUDE.md`
- `{path}/**/.claude/settings.json`
- `{path}/**/.claude/settings.local.json`
- `{path}/**/.mcp.json`
- `{path}/**/.claudeignore`
- `{path}/**/.claude/rules/*.md`
2. **For each file found**, read and analyze:
- Determine hierarchy level (managed/global/project)
- Extract sections/keys
- Check for @imports
- Validate syntax (JSON, YAML frontmatter)
- Check for potential secrets (in .mcp.json)
3. **Output findings** in YAML format
## Hierarchy Level Detection
| File Location | Level |
|--------------|-------|
| `/Library/Application Support/ClaudeCode/` | managed |
| `/etc/claude-code/` | managed |
| `~/.claude/` | global |
| `~/.claude.json` | global |
| Any other location | project |
## Output Format
Write findings to: `~/.claude/config-audit/sessions/{session-id}/findings/{path-hash}.yaml`
```yaml
scope_path: "/scanned/path"
scanned_at: "2025-01-26T14:30:22Z"
files:
- path: "/full/path/CLAUDE.md"
type: "CLAUDE.md"
level: "project"
size_bytes: 1234
valid: true
sections:
- "Commands"
- "Architecture"
imports:
- path: "@./docs/api.md"
resolved_path: "/full/path/docs/api.md"
exists: true
- path: "@./missing.md"
resolved_path: "/full/path/missing.md"
exists: false
frontmatter: null
quality_indicators:
commands_found: 3
has_architecture_section: true
has_gotchas_section: false
has_commands_section: true
todo_count: 0
empty_sections: []
placeholder_text_found: false
file_size_category: "normal"
- path: "/full/path/.claude/settings.json"
type: "settings.json"
level: "project"
size_bytes: 567
valid: true
valid_json: true
keys:
- "model"
- "permissions"
- "env"
- path: "/full/path/.mcp.json"
type: ".mcp.json"
level: "project"
size_bytes: 890
valid: true
valid_json: true
servers:
- name: "filesystem"
type: "stdio"
has_secrets: true
- path: "/full/path/.claude/rules/code-style.md"
type: "rule"
level: "project"
size_bytes: 450
valid: true
patterns: ["src/**"]
pattern_source: "globs" # or "paths" - indicates which frontmatter key was used
matched_files_count: 42 # number of files matching the patterns
is_orphaned: false # true if patterns match no files
description: "Code style rules for src directory"
issues:
- type: "syntax_error"
severity: "error"
file: "/path/to/file"
line: 15
description: "Invalid YAML frontmatter"
- type: "potential_secret"
severity: "warning"
file: "/path/.mcp.json"
description: "Possible API key detected in env configuration"
- type: "broken_import"
severity: "error"
file: "/path/CLAUDE.md"
import: "@./missing.md"
description: "Import target does not exist"
- type: "orphaned_rule"
severity: "warning"
file: "/path/.claude/rules/legacy.md"
patterns: ["old/**/*.js"]
description: "Rule patterns match no files in codebase"
summary:
total_files: 4
valid_files: 3
invalid_files: 1
issues_count: 2
```
## Validation Rules
### CLAUDE.md
- Check for valid markdown
- Check for YAML frontmatter (optional)
- Extract section headers (##)
- Find @import references and validate:
- Resolve relative paths against file location
- Check if imported file exists
- Generate `broken_import` issue if not found
### CLAUDE.md Quality Pre-Analysis
For each CLAUDE.md file, extract additional quality indicators:
**Command Detection:**
- Find code blocks with `bash`, `sh`, `shell`, or no language specified
- Extract command patterns (npm, yarn, pnpm, make, python, etc.)
- Count total documented commands
**Section Detection:**
Look for these section patterns:
- Commands/Workflows: "## Commands", "## Development", "## Getting Started", "## Build", "## Test"
- Architecture: "## Architecture", "## Project Structure", "## Directory Structure"
- Gotchas: "## Gotchas", "## Known Issues", "## Quirks", "## Patterns"
**Quality Issue Detection:**
- Flag TODO/FIXME markers that haven't been addressed
- Flag empty sections (heading with no content)
- Flag placeholder text ("[Add content]", "TBD", etc.)
- Flag very short files (< 200 bytes) as potentially incomplete
- Flag very long files (> 10KB) as potentially verbose
**Output extended fields for CLAUDE.md:**
```yaml
- path: "/path/CLAUDE.md"
type: "CLAUDE.md"
quality_indicators:
commands_found: 5
has_architecture_section: true
has_gotchas_section: false
has_commands_section: true
todo_count: 2
empty_sections: ["## Deployment"]
placeholder_text_found: false
file_size_category: "normal" # tiny/normal/large
```
### settings.json
- Must be valid JSON
- Check for known keys: model, permissions, env, etc.
### .mcp.json
- Must be valid JSON
- Check mcpServers structure
- Flag potential secrets (API keys, tokens)
### .claudeignore
- Check for valid gitignore-style patterns
### rules/*.md
- Check for valid markdown
- Extract path patterns from frontmatter:
- `paths:` (official Claude Code field name)
- `globs:` (legacy/alternative name, also supported)
- Normalize to `patterns` in output, record source in `pattern_source`
- Extract description from frontmatter
- Validate patterns match actual files:
- Run glob pattern against the project root
- Record `matched_files_count`
- Flag as `is_orphaned: true` if count is 0
- Generate `orphaned_rule` issue for orphaned rules
## Secret Detection Patterns
Flag as potential secrets:
- Strings matching `/xoxb-[a-zA-Z0-9-]+/` (Slack)
- Strings matching `/sk-[a-zA-Z0-9]+/` (OpenAI)
- Strings matching `/ghp_[a-zA-Z0-9]+/` (GitHub)
- Strings longer than 20 chars that look like API keys
- Any `env` key with inline values (not ${VAR} references)
## Error Handling
- If directory doesn't exist: Report empty findings
- If permission denied: Log issue, continue scanning
- If file read fails: Log issue, continue with other files
- Never fail the entire scan for individual file errors
## Performance
- Use Glob for pattern matching (fast)
- Read files sequentially to avoid overwhelming filesystem
- Maximum depth: Follow scope configuration (default unlimited)

View file

@ -0,0 +1,248 @@
---
name: verifier-agent
description: Verify that configuration changes were applied correctly. Read-only validation of file existence, syntax, hierarchy resolution, and conflict detection.
model: haiku
color: purple
tools: ["Read", "Glob", "Grep"]
---
# Verifier Agent
Verification agent that validates the final state after implementation.
## Purpose
After all actions are implemented, verify:
1. All expected files exist
2. All files are syntactically valid
3. Configuration hierarchy resolves correctly
4. No new conflicts introduced
5. No orphaned configurations
6. Claude Code can load the configuration
## Input
You will receive:
1. Session ID
2. Action plan with expected outcomes
3. Implementation log with actual outcomes
## Task
1. **Load context**: Read action plan and implementation log
2. **Verify files**: Check each modified/created file
3. **Test hierarchy**: Simulate configuration resolution
4. **Compare states**: Before vs after
5. **Generate report**: Document findings
## Verification Checks
### Check 1: File Existence
For each action in plan:
- Create actions: File should exist
- Delete actions: File should not exist
- Modify actions: File should exist with changes
```
✓ ~/.claude/rules/code-style.md exists
✓ ~/project/CLAUDE.md exists (modified)
✗ ~/.claude/rules/orphan.md should not exist
```
### Check 2: Syntax Validation
For each config file:
```yaml
CLAUDE.md:
- Valid markdown: ✓
- Frontmatter valid: ✓ (if present)
- No broken @imports:
settings.json:
- Valid JSON: ✓
- Schema compliant: ✓
- No unknown keys: ✓
.mcp.json:
- Valid JSON: ✓
- Servers defined: ✓
- No secrets exposed: ✓
rules/*.md:
- Valid markdown: ✓
- Globs valid: ✓ (if present)
```
### Check 3: Hierarchy Resolution
Simulate how Claude Code would load config:
```
For project ~/project-a/:
1. Managed (system): [none found]
2. Global (~/.claude/):
- CLAUDE.md: loaded
- settings.json: loaded
- rules/code-style.md: loaded
3. Project:
- CLAUDE.md: loaded (inherits global)
- .claude/settings.json: loaded (overrides global)
- .mcp.json: loaded
Resolution order: managed < global < project
Final effective config: ✓ valid
```
### Check 4: Conflict Check
After implementation, verify no conflicts remain:
```
Checking for conflicts...
- model: global=opus, project=sonnet → Expected override ✓
- permissions: same in both → No conflict ✓
- No unexpected conflicts ✓
```
### Check 5: Duplicate Check
Verify duplicates were actually removed:
```
Checking for remaining duplicates...
- Code style rules: Now only in ~/.claude/rules/code-style.md ✓
- No new duplicates introduced ✓
```
### Check 6: Import Resolution
Verify @imports resolve correctly:
```
Checking @imports...
- ~/project/CLAUDE.md imports @./docs/api.md
- File exists: ✓
- Valid markdown: ✓
```
### Check 7: Secrets Scan
Re-scan for exposed secrets:
```
Checking for secrets...
- ~/.claude.json: OAuth tokens (expected, protected by permissions)
- .mcp.json files: No hardcoded secrets ✓
```
## Output Format
Append to: `~/.claude/config-audit/sessions/{session-id}/implementation-log.md`
```markdown
## Verification Report
Verified: {timestamp}
Verifier: config-audit/verifier-agent
### Summary
| Check | Status | Issues |
|-------|--------|--------|
| File Existence | ✓ Pass | 0 |
| Syntax Validation | ✓ Pass | 0 |
| Hierarchy Resolution | ✓ Pass | 0 |
| Conflict Check | ✓ Pass | 0 |
| Duplicate Check | ✓ Pass | 0 |
| Import Resolution | ✓ Pass | 0 |
| Secrets Scan | ✓ Pass | 0 |
### Overall Status: ✓ VERIFIED
All {N} actions verified successfully.
No issues detected.
### File Status
| File | Expected | Actual | Status |
|------|----------|--------|--------|
| ~/.claude/rules/code-style.md | Created | Exists | ✓ |
| ~/project/CLAUDE.md | Modified | Valid | ✓ |
| ~/project/.mcp.json | Modified | Valid | ✓ |
### Hierarchy Test
Project: ~/project-a/
```
Effective configuration:
- Model: sonnet (from project)
- Permissions: ["Read", "Write"] (from global)
- Rules: code-style (from global), project-rules (from project)
- MCP Servers: filesystem, database (from project)
```
Status: ✓ Resolves correctly
### Recommendations
[Any post-implementation recommendations]
```
## Failure Handling
If verification fails:
```markdown
### Overall Status: ✗ FAILED
{N} issues detected.
### Issues
1. **File Missing**: ~/.claude/rules/code-style.md
- Expected: Created by action-1-1
- Actual: Not found
- Impact: High - other actions depend on this
- Recommendation: Re-run action-1-1 or rollback
2. **Syntax Error**: ~/project/CLAUDE.md
- Line 45: Invalid markdown (unclosed code block)
- Impact: Medium - file won't parse correctly
- Recommendation: Restore from backup
### Recommended Action
Run: /config-audit rollback {backup-timestamp}
```
## Comparison Report
Optional: Generate before/after comparison:
```markdown
### Before vs After
#### Files Changed
| File | Before | After |
|------|--------|-------|
| Config files | 15 | 13 |
| Total size | 25 KB | 22 KB |
| Duplicates | 3 | 0 |
| Conflicts | 2 | 0 |
#### Improvements
- Reduced duplication by 100%
- Resolved all conflicts
- Consolidated 2 rule files
- Moved 3 secrets to env vars
```
## Read-Only Guarantee
This agent:
- Only uses Read, Glob, Grep tools
- Never modifies any files
- Reports findings without taking action
- Safe to run multiple times

View file

@ -0,0 +1,74 @@
---
name: config-audit:analyze
description: Phase 2 - Generate analysis report with hierarchy map and issue detection
allowed-tools: Read, Write, Edit, Glob, Grep, Agent
model: opus
---
# Config-Audit: Analysis (Phase 2)
Generate comprehensive analysis report from discovery findings.
## Prerequisites
- Must have completed Phase 1 (discovery)
- Findings must exist in `~/.claude/config-audit/sessions/{session-id}/findings/`
## Implementation
### Step 1: Verify session state
Read `~/.claude/config-audit/sessions/{session-id}/state.yaml` and verify discovery phase completed. If not, tell the user: "Discovery hasn't been run yet. Start with `/config-audit discover` or just run `/config-audit` for a full audit."
### Step 2: Tell the user what's happening
```
## Analyzing Configuration
Reading your scan findings and generating a detailed analysis report...
This includes hierarchy mapping, conflict detection, and prioritized recommendations.
```
### Step 3: Spawn analyzer agent
Tell the user: **"Generating analysis (this takes about 30 seconds)..."**
```
Agent(subagent_type: "config-audit:analyzer-agent")
model: sonnet
prompt: |
Analyze all findings in: ~/.claude/config-audit/sessions/{session-id}/findings/
Generate comprehensive report covering:
1. Executive summary with key metrics
2. Hierarchy map visualization
3. Conflict detection across config layers
4. CLAUDE.md quality assessment
5. Security issues (secrets, permissions)
6. Top 10 prioritized recommendations
Output to: ~/.claude/config-audit/sessions/{session-id}/analysis-report.md
```
### Step 4: Present summary
After the agent completes, read the generated report and show a brief summary:
```markdown
### Analysis Complete
Report generated with:
- {N} conflicts detected
- {N} optimization opportunities
- {N} security notes
- Top recommendation: {first recommendation}
Full report: `~/.claude/config-audit/sessions/{session-id}/analysis-report.md`
### What's next
- **`/config-audit plan`** — Turn findings into a prioritized action plan
- **`/config-audit fix`** — Auto-fix deterministic issues right away
```
### Step 5: Update state
Update `state.yaml` with `current_phase: "analyze"`, `next_phase: "plan"`.

View file

@ -0,0 +1,95 @@
---
name: config-audit:cleanup
description: Clean up old config-audit sessions to reclaim disk space
allowed-tools: Read, Write, Glob, Bash, AskUserQuestion
model: sonnet
---
# Config-Audit: Session Cleanup
Manage and clean up accumulated config-audit sessions in `~/.claude/config-audit/sessions/`.
## Usage
```
/config-audit cleanup
```
## Implementation Steps
1. **List all sessions**:
- Glob `~/.claude/config-audit/sessions/*/state.yaml`
- For each session, read state.yaml and extract:
- Session ID
- Created timestamp
- Current phase
- Whether session is active (has `next_phase` and `current_phase` is not `verify` or `complete`)
2. **Calculate disk usage**:
- Use `du -sh ~/.claude/config-audit/sessions/{session-id}/` for each session
- Calculate total usage
3. **Display session table**:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Config-Audit Sessions
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
| # | Session ID | Age | Phase | Status | Size |
|---|------------|-----|-------|--------|------|
| 1 | 20260127_102527 | 15d | implement | active | 12K |
| 2 | quick-20260126 | 16d | analyze | complete | 8K |
| 3 | 20260120_091500 | 22d | analyze | complete | 6K |
Total: 3 sessions, 26K disk usage
```
4. **Ask cleanup action**:
```
AskUserQuestion:
question: "Which sessions should I clean up?"
header: "Cleanup"
options:
- label: "Completed sessions only (Recommended)"
description: "Delete sessions where phase is verify/complete. Keeps active sessions safe."
- label: "Older than 14 days"
description: "Delete all sessions older than 14 days, regardless of status."
- label: "All except current"
description: "Delete everything except the most recent active session."
- label: "Cancel"
description: "Don't delete anything."
```
5. **Safety guards**:
- NEVER delete sessions where `current_phase` is not `verify` or `complete` AND `next_phase` exists, unless user explicitly chose age-based or all-except-current
- Warn before deleting active sessions: "Session {id} is still active (phase: {phase}). Delete anyway?"
6. **Execute cleanup**:
- For each session to delete: `rm -rf ~/.claude/config-audit/sessions/{session-id}/`
- Track deleted count and freed space
7. **Output summary**:
```
✓ Cleanup complete
Deleted: 2 sessions
Freed: 14K disk space
Remaining: 1 session (active)
```
## Session Status Detection
A session is considered **active** if ALL of these are true:
- `current_phase` is not `verify` and not `complete`
- `next_phase` exists and is not empty
A session is considered **complete** if ANY of these are true:
- `current_phase` is `verify` or `complete`
- `next_phase` is empty or null
## Error Handling
- **Legacy path:** Also check `~/.config-audit/sessions/` for sessions created before v2.2.0. If found, include them in the session list and note: "Found {n} session(s) at legacy path (~/.config-audit/). These will be cleaned up normally."
- If `~/.claude/config-audit/sessions/` doesn't exist (and no legacy sessions): "No sessions found. Nothing to clean up."
- If no sessions match criteria: "No sessions match the selected criteria."
- If deletion fails: Log error, continue with other sessions

View file

@ -0,0 +1,202 @@
---
name: config-audit
description: Claude Code Configuration Intelligence - audit, analyze, and optimize your configuration
argument-hint: "[posture|feature-gap|fix|rollback|plan|implement|help|discover|analyze|interview|drift|plugin-health|status|cleanup]"
allowed-tools: Read, Write, Glob, Grep, Bash, Agent, AskUserQuestion
model: opus
---
# Config-Audit: Claude Code Configuration Intelligence
Analyze, report on, and optimize your Claude Code configuration.
## Router Logic
If a subcommand is provided, route to it:
- `posture``/config-audit:posture`
- `feature-gap``/config-audit:feature-gap`
- `fix``/config-audit:fix`
- `rollback``/config-audit:rollback`
- `plan``/config-audit:plan`
- `implement``/config-audit:implement`
- `help``/config-audit:help`
- `discover``/config-audit:discover`
- `analyze``/config-audit:analyze`
- `interview``/config-audit:interview`
- `drift``/config-audit:drift`
- `plugin-health``/config-audit:plugin-health`
- `status``/config-audit:status`
- `cleanup``/config-audit:cleanup`
If a scope override is provided (`current`, `repo`, `home`, `full`), use it as the scope type (see Scope Resolution below).
If no subcommand and no scope override: **run the default audit** (see below).
## UX Rules (MANDATORY — apply to every step)
1. **Narrate before acting.** Before each step, tell the user what you're about to do and why, in plain language.
2. **Never show raw output.** All scanner Bash commands MUST use `--output-file <path>` AND `2>/dev/null`. The user should NEVER see JSON, stderr progress lines, or exit codes.
3. **Handle exit codes silently.** Append `; echo $?` to scanner commands. Exit codes 0/1/2 are all expected (PASS/WARNING/FAIL). Only exit code 3 is a real error — tell user: "Scanner encountered an unexpected error. Try `/config-audit posture` for a quick check instead."
4. **Explain, don't dump.** When presenting findings, add plain-language context. "Grade B" alone means nothing — say "Grade B — your CLAUDE.md files are well-structured with minor improvements possible."
5. **Separate signal from noise.** If findings exist in `tests/fixtures/` or `examples/` directories, count them separately and exclude from the main count: "Found 37 findings (66 additional in test fixtures, excluded)."
6. **Context-sensitive next steps.** Don't just list commands — explain what each does and why the user might want it based on their specific results.
## Default Audit (no arguments)
### Step 1: Auto-detect scope and greet the user
If the user provided a scope override (`/config-audit full`, `/config-audit repo`, etc.), use that.
Otherwise, auto-detect:
1. Run `git rev-parse --show-toplevel 2>/dev/null` via Bash
2. If it succeeds and pwd is inside the repo → **repo** scope (use the git root path)
3. If pwd is `$HOME`**home** scope
4. Otherwise → **current** directory scope
Show the user what's happening:
```
## Config-Audit
Analyzing your Claude Code configuration...
**Scope:** {Repository|Home directory|Current directory} — `{path}`
**What this checks:** CLAUDE.md quality, settings validation, hook safety, rules correctness, MCP server config, import chains, conflicts, and feature coverage.
```
### Step 2: Initialize session
1. Generate session ID: `YYYYMMDD_HHmmss` format
2. Create session directory and findings subdirectory:
```bash
mkdir -p ~/.claude/config-audit/sessions/{session-id}/findings
```
This is a silent infrastructure step — do NOT show output to the user.
### Step 3: Run scanners and posture assessment
Tell the user: **"Running 8 configuration scanners..."**
Run both scanners and posture in a single Bash command:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs <target-path> --output-file ~/.claude/config-audit/sessions/{session-id}/findings/scan-results.json [--full-machine] [--global] 2>/dev/null; node ${CLAUDE_PLUGIN_ROOT}/scanners/posture.mjs <target-path> --json --output-file ~/.claude/config-audit/sessions/{session-id}/posture.json [--full-machine] [--global] 2>/dev/null; echo $?
```
Use `--full-machine` for `full` scope, `--global` for `home` scope. For `repo` and `current`, pass the resolved path directly.
Check the echoed exit code:
- `0`, `1`, or `2` → continue normally
- `3` → tell user: "Scanner encountered an unexpected error. Try `/config-audit posture` for a quick check instead." and stop.
### Step 4: Analyze results
Tell the user: **"Scanners complete. Preparing your results..."**
Read BOTH output files using the Read tool:
- `~/.claude/config-audit/sessions/{session-id}/findings/scan-results.json`
- `~/.claude/config-audit/sessions/{session-id}/posture.json`
Extract these metrics from the JSON:
**From posture.json:**
- `overallGrade` — the health grade (A-F)
- `opportunityCount` — number of unused features detected
- `areas[]` — per-area grades and finding counts (use only quality areas, exclude Feature Coverage)
**From scan-results.json:**
- `aggregate.total_findings` — total findings (test fixture findings are already excluded automatically)
- `fixture_findings` array (if present) — count of findings excluded from test/example directories
- Count findings by severity from `aggregate.counts` (critical, high, medium, low, info)
- Count findings where `autoFixable: true`
- Note total `files_scanned` across scanners
### Step 5: Update state
Write session state (silent — no user output):
```yaml
session_id: "{session-id}"
current_phase: "analyze"
completed_phases: ["discover", "analyze"]
next_phase: "plan"
updated_at: "{ISO timestamp}"
scope_type: "{repo|home|current|full}"
target_path: "{resolved path}"
```
Write to: `~/.claude/config-audit/sessions/{session-id}/state.yaml`
### Step 6: Display results
Present results using this template. Replace all placeholders with actual values. **Adapt the summary sentence based on grade.**
```markdown
### Results
**Health: {overallGrade}** | {qualityAreaCount} areas scanned
{grade-based summary — pick ONE:}
- Grade A: "Excellent — your configuration is correct and well-maintained."
- Grade B: "Strong — your configuration is solid with minor improvements available."
- Grade C: "Decent — your configuration works but has some issues worth addressing."
- Grade D: "Needs work — several configuration issues could affect your Claude Code experience."
- Grade F: "Significant issues found — addressing these will meaningfully improve your workflow."
Scanned {files_scanned} files | {real_finding_count} findings ({severity_breakdown})
{If test_fixture_count > 0: "({test_fixture_count} additional findings in test fixtures were excluded.)"}
{If fixable_count > 0: "{fixable_count} of these can be auto-fixed."}
### Area Breakdown
| Area | Grade | Findings | |
|------|-------|----------|-|
| CLAUDE.md | {grade} | {count} | {one-phrase status} |
| Settings | {grade} | {count} | {status} |
| Hooks | {grade} | {count} | {status} |
| Rules | {grade} | {count} | {status} |
| MCP Servers | {grade} | {count} | {status} |
| Imports | {grade} | {count} | {status} |
| Conflicts | {grade} | {count} | {status} |
{For the status column, use plain language like: "Well structured", "2 minor issues", "Missing trust levels", "No issues", etc.}
{If opportunityCount > 0:}
{opportunityCount} feature opportunities available — run `/config-audit feature-gap` for context-aware recommendations.
### What you can do next
{Include only relevant options based on findings. Explain each one:}
{If fixable_count > 0:}
- **`/config-audit fix`** — Automatically fix {fixable_count} issues. Creates a backup first so you can roll back with one command.
{If real findings > fixable_count:}
- **`/config-audit plan`** — Get a prioritized action plan for the {remaining} issues that need manual attention.
{If grade is C or better:}
- **`/config-audit feature-gap`** — See which features could help your project, and implement the ones you want on the spot.
{If grade is D or F:}
- **`/config-audit fix`** should be your first step — it handles the most impactful issues automatically.
Session saved to: `~/.claude/config-audit/sessions/{session-id}/`
```
## Scope Resolution
| Scope | What gets scanned |
|-------|-------------------|
| `current` | Current directory + parent CLAUDE.md files up to root + `~/.claude/` |
| `repo` | Git repository root + `~/.claude/` |
| `home` | `~/.claude/` global configuration only |
| `full` | Everything: `~/.claude/`, managed paths, all dev dirs under $HOME |
## Error Handling
- If scanner fails (exit 3), tell the user in plain language and suggest `/config-audit posture` as fallback
- If path doesn't exist, tell the user: "That path doesn't exist. Run `/config-audit` without arguments to auto-detect."
- If git command fails for auto-detect, silently fall back to `current` scope
- If no CLAUDE.md found anywhere, explain: "No CLAUDE.md found. This is the main configuration file for Claude Code — creating one is the single highest-impact thing you can do. Run `/config-audit feature-gap` to see what's recommended."

View file

@ -0,0 +1,141 @@
---
name: config-audit:discover
description: Phase 1 - Initialize session, auto-detect scope, and discover config files
argument-hint: "[current|repo|home|full] [--delta]"
allowed-tools: Read, Write, Edit, Glob, Grep, Agent, AskUserQuestion, Bash
model: opus
---
# Config-Audit: Discover (Phase 1)
Initialize a new audit session and discover all Claude Code configuration files.
## Usage
```
/config-audit discover # Auto-detect scope
/config-audit discover current # Force current directory scope
/config-audit discover repo # Force git repository scope
/config-audit discover home # Force home/global scope
/config-audit discover full # Force full machine scope
/config-audit discover --delta # Incremental re-scan (changed files only)
```
## Implementation
### Step 1: Initialize session and greet
Generate session ID (`YYYYMMDD_HHmmss`), create directories:
```bash
mkdir -p ~/.claude/config-audit/sessions/{session-id}/findings 2>/dev/null
```
### Step 2: Determine scope
If the user provided a scope argument, use it. Otherwise, auto-detect:
1. Run `git rev-parse --show-toplevel 2>/dev/null`
2. If inside a git repo → **repo** scope
3. If pwd is `$HOME`**home** scope
4. Otherwise → **current** directory scope
Tell the user:
```
## Configuration Discovery
**Scope:** {Repository|Home|Current directory|Full machine} — `{path}`
Finding all Claude Code configuration files (CLAUDE.md, settings, hooks, rules, MCP servers)...
```
### Step 3: Resolve paths
| Scope | What gets scanned |
|-------|-------------------|
| `current` | Current directory + parent CLAUDE.md files up to root + `~/.claude/` |
| `repo` | Git repo root + `~/.claude/` |
| `home` | `~/.claude/` only |
| `full` | `~/.claude/` (depth 10), managed paths, all dev dirs under $HOME |
### Step 4: Delta mode (if --delta)
If `--delta` flag:
1. Find previous baseline from `~/.claude/config-audit/sessions/*/discovery.json`
2. If no previous: "No previous scan found. Running full discovery instead."
3. Compare file mtimes/sizes to classify as changed/new/deleted/unchanged
4. Only scan changed + new files
### Step 5: Run discovery
Run the scan orchestrator silently to discover and scan files:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs <target-path> --output-file ~/.claude/config-audit/sessions/{session-id}/findings/scan-results.json [--full-machine] [--global] 2>/dev/null; echo $?
```
Check exit code: 0/1/2 → normal. 3 → "Discovery encountered an error. Try a narrower scope."
### Step 6: Save scope and state
Write `scope.yaml` and `state.yaml` to session directory. Update state with `current_phase: "discover"`, `next_phase: "analyze"`.
### Step 7: Present summary
Read the scan results file to count files and findings:
**Full scan:**
```markdown
### Discovery Complete
**{scope_type}** scope — found {total_files} configuration files:
| Type | Count |
|------|-------|
| CLAUDE.md | {n} |
| Settings | {n} |
| MCP configs | {n} |
| Rules | {n} |
| Hooks | {n} |
| Other | {n} |
Initial scan found {finding_count} items to review.
**Next:** Run `/config-audit analyze` to generate your analysis report.
```
**Delta scan:**
```markdown
### Delta Discovery Complete
Compared against baseline from {previous-session-id}:
| Status | Files |
|--------|-------|
| Changed | {n} |
| New | {n} |
| Deleted | {n} |
| Unchanged | {n} |
Only {changed+new} file(s) scanned (vs {total} full scan).
**Next:** Run `/config-audit analyze` to generate your analysis report.
```
## Config File Patterns
| Pattern | Description |
|---------|-------------|
| `**/CLAUDE.md` | Project instructions |
| `**/CLAUDE.local.md` | Local overrides |
| `**/.claude/settings.json` | Project settings |
| `**/.mcp.json` | MCP servers |
| `**/.claude/rules/*.md` | Modular rules |
For global: `~/.claude/CLAUDE.md`, `~/.claude/settings.json`, `~/.claude.json`, `~/.claude/agents/*.md`
## Error Handling
- If scanner fails, report to user in plain language and suggest narrower scope
- If path doesn't exist, tell user and suggest alternatives
- If git command fails for `repo` scope, silently fall back to `current`
- If no config files found, explain: "No Claude Code configuration files found. Start with `/config-audit feature-gap` to see what's recommended."

View file

@ -0,0 +1,98 @@
---
name: config-audit:drift
description: Compare current configuration against a saved baseline — shows new, resolved, and changed findings
argument-hint: "[path] [--baseline name] [--save]"
allowed-tools: Read, Write, Glob, Grep, Bash
model: sonnet
---
# Config-Audit: Drift Detection
Compare current configuration against a saved baseline to see what changed.
## Arguments
- `$ARGUMENTS` may contain:
- A target path (default: current working directory)
- `--save`: Save current state as baseline
- `--baseline <name>`: Compare against a specific named baseline (default: "default")
## Implementation
### Save a baseline
If `--save` is present:
Tell the user: **"Saving current configuration as baseline..."**
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/drift-cli.mjs <path> --save --name <baseline-name> 2>/dev/null
```
Read stdout for confirmation. Tell the user:
```markdown
### Baseline Saved
Captured current state as baseline "{name}".
Run `/config-audit drift` anytime to see what changed since this point.
```
### Compare against baseline
Without `--save`:
Tell the user: **"Comparing current configuration against baseline..."**
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/drift-cli.mjs <path> --baseline <name> 2>/dev/null
```
Read stdout. If baseline not found, tell the user:
```
No baseline found. Save one first with:
/config-audit drift --save
```
Otherwise, parse and present the drift report:
```markdown
### Configuration Drift
**Trend:** {Improving|Degrading|Stable}
**Score:** {before} → {after} ({+/-delta} points)
{If new findings:}
#### New Issues ({count})
| ID | Severity | Description |
|----|----------|-------------|
| ... | ... | ... |
{If resolved findings:}
#### Resolved ({count})
| ID | Description |
|----|-------------|
| ... | ... |
{If area changes:}
#### Area Changes
| Area | Before | After | Change |
|------|--------|-------|--------|
| ... | ... | ... | ... |
```
### List baselines
If `$ARGUMENTS` contains `--list`:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/drift-cli.mjs --list 2>/dev/null
```
### What's next
After viewing drift:
- `/config-audit fix` — Auto-fix new findings
- `/config-audit posture` — Full posture assessment
- `/config-audit drift --save` — Update the baseline to current state

View file

@ -0,0 +1,185 @@
---
name: config-audit:feature-gap
description: Context-aware feature recommendations — what could enhance your setup and why
argument-hint: "[path]"
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion
model: opus
---
# Config-Audit: Feature Opportunities
Context-aware analysis of Claude Code features that could benefit your specific project — with the option to implement selected recommendations on the spot.
## What the user gets
- Project context detection (language, size, existing configuration)
- Numbered recommendations grouped by impact (high / worth considering / explore)
- Each recommendation backed by evidence (Anthropic docs, proven issues)
- **Interactive selection: "Which would you like to implement?"**
- Direct implementation with backup for selected items
## Implementation
### Step 1: Determine target and greet
Parse `$ARGUMENTS` for a path (default: current working directory).
Tell the user:
```
## Feature Opportunities
Analyzing which Claude Code features could benefit your workflow...
```
### Step 2: Create session and run posture
Generate session ID (`YYYYMMDD_HHmmss`) if no active session exists.
```bash
mkdir -p ~/.claude/config-audit/sessions/{session-id}/findings 2>/dev/null
node ${CLAUDE_PLUGIN_ROOT}/scanners/posture.mjs <target-path> --json --output-file ~/.claude/config-audit/sessions/{session-id}/posture.json 2>/dev/null; echo $?
```
If exit code is non-zero: "Assessment couldn't run. Check that the path exists and contains configuration files."
### Step 3: Read posture data and detect project context
Read `~/.claude/config-audit/sessions/{session-id}/posture.json` using the Read tool.
Extract GAP findings from `scannerEnvelope.scanners` (find scanner with `scanner === 'GAP'`).
Detect project context:
```bash
test -f <target-path>/package.json && echo "has_package_json" || echo "no_package_json"
ls <target-path>/*.py <target-path>/requirements.txt <target-path>/pyproject.toml 2>/dev/null | head -3
```
### Step 4: Build numbered recommendations
Read `${CLAUDE_PLUGIN_ROOT}/knowledge/gap-closure-templates.md` for implementation templates.
Group GAP findings into three sections. Number them sequentially across sections:
```markdown
### High Impact
These address correctness or safety — consider them seriously.
**1.** Add permissions.deny for sensitive paths
→ Settings enforcement is stronger than CLAUDE.md instructions.
→ Effort: Low (5 min)
**2.** Configure at least one hook for safety automation
→ Hooks guarantee the action happens. CLAUDE.md instructions are advisory.
→ Effort: Medium (15 min)
### Worth Considering
These improve workflow efficiency for projects like yours.
**3.** Split CLAUDE.md into focused modules with @imports
→ Files over 200 lines degrade Claude's adherence to instructions.
→ Effort: Low (10 min)
**4.** Add path-scoped rules for different file types
→ Unscoped rules load every session regardless of relevance.
→ Effort: Low (10 min)
### Explore When Ready
Nice-to-have. Skip if your current setup works well.
**5.** Custom keybindings (Shift+Enter for newline)
→ Effort: Low (2 min)
**6.** Status line configuration
→ Effort: Low (2 min)
```
Each recommendation MUST have:
- A number
- A one-line description
- A "Why" with evidence
- An effort estimate from the templates
### Step 5: Ask what to implement
```
AskUserQuestion:
question: "Which would you like to implement? I'll create a backup first."
options:
- "All high impact (1-2)"
- "Pick specific: e.g. 1,3,5"
- "None — just wanted to see the recommendations"
```
If "None": show the full report location and exit.
If the user picks numbers: parse the selection and proceed to Step 6.
### Step 6: Implement selected recommendations
For each selected recommendation:
1. **Create backup** of any files that will be modified:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs <target-path> --json 2>/dev/null
```
Or create manual backup:
```bash
mkdir -p ~/.claude/config-audit/backups/$(date +%Y%m%d_%H%M%S)/files/ 2>/dev/null
```
Copy each file that will be touched.
2. **Apply the template** from gap-closure-templates.md. Use the Write or Edit tool to create or modify the relevant configuration file.
3. **Show progress** as each item is done:
```
Implementing 3 recommendations...
✓ 1. permissions.deny — added to .claude/settings.json
✓ 3. Modular CLAUDE.md — created .claude/rules/testing.md, added @import
✓ 5. Keybindings — created ~/.claude/keybindings.json
```
4. **Verify** by re-running posture:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/posture.mjs <target-path> --json --output-file /tmp/config-audit-verify-$$.json 2>/dev/null
```
### Step 7: Show results
```markdown
### Done
**{N} recommendations implemented** | Backup created
{If health grade changed:}
Health: {old_grade} → {new_grade} (+{delta} points)
{Show remaining opportunities if any:}
{remaining} more opportunities available — run `/config-audit feature-gap` again anytime.
**Rollback:** If anything looks wrong, run `/config-audit rollback` to restore.
```
## Implementation Guidelines
When implementing recommendations, be smart about context:
- **permissions.deny**: Look at the project for common sensitive paths (`.env`, `secrets/`, `.git/config`, `*.pem`). Don't just copy a template blindly — check what actually exists.
- **hooks**: Start with a simple, useful hook. Don't scaffold 5 hooks at once.
- **path-scoped rules**: Look at the project's file structure to determine meaningful scopes (e.g., `tests/**/*.ts` vs `src/**/*.ts`).
- **CLAUDE.md modularization**: Only suggest splitting if the file is over 100 lines. Read it first to find natural section boundaries.
- **MCP setup**: Only relevant if the user actually has external tools to connect. Ask before creating.
- **Custom plugin**: Too complex for inline implementation — suggest `/config-audit plan` instead.
For items that genuinely need user input (e.g., "which MCP servers do you use?"), ask briefly during implementation rather than skipping them.
## Safety
- **Backup mandatory** — always create before modifying
- **Show what's changing** — the user sees each change as it happens
- **Rollback available**`/config-audit rollback` at any time
- **Non-destructive** — only create new files or add to existing; never delete content

View file

@ -0,0 +1,138 @@
---
name: config-audit:fix
description: Auto-fix deterministic configuration issues with backup and verification
argument-hint: "[path] [--dry-run]"
allowed-tools: Read, Write, Glob, Grep, Bash, AskUserQuestion
model: sonnet
---
# Config-Audit: Fix
Auto-fix deterministic configuration issues. Scans, plans fixes, backs up originals, applies changes, and verifies results.
## Arguments
- `$ARGUMENTS` may contain:
- A target path (default: current working directory)
- `--dry-run`: Show fix plan without applying
## Implementation
### Step 1: Greet and scan
Tell the user:
```
## Config-Audit Fix
Scanning for auto-fixable issues...
```
Run scanners silently:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs <path> --output-file /tmp/config-audit-fix-scan-$$.json [--global] 2>/dev/null; echo $?
```
Exit code 3 → tell user: "Scanner error. Try `/config-audit posture` to check your configuration."
### Step 2: Plan fixes
Run fix planner silently:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs <path> --json 2>/dev/null
```
Read the JSON output. Categorize fixes into auto-fixable and manual.
### Step 3: Present fix plan
Show what will be fixed and what needs manual attention:
```markdown
### Fix Plan
**Auto-fixable ({N} issues):**
| # | ID | Issue | File |
|---|-----|-------|------|
| 1 | CA-SET-003 | Add $schema to settings.json | .claude/settings.json |
| 2 | ... | ... | ... |
**Manual ({M} issues — require human judgment):**
| # | ID | Issue | Recommendation |
|---|-----|-------|----------------|
| 1 | CA-CML-003 | CLAUDE.md exceeds 200 lines | Split content into @imports or .claude/rules/ |
| ... | ... | ... | ... |
```
### Step 4: Confirm with user
If not `--dry-run`, ask for confirmation:
```
AskUserQuestion:
question: "Apply {N} auto-fixes? A backup is created first — you can roll back anytime."
options:
- "Yes, apply fixes"
- "Show dry-run only"
- "Cancel"
```
### Step 5: Apply fixes
If confirmed, apply:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs <path> --apply --json 2>/dev/null
```
Read the JSON output to get applied/failed counts and backup location.
### Step 6: Show results
Run a quick posture check to measure improvement:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/posture.mjs <path> --json --output-file /tmp/config-audit-fix-posture-$$.json 2>/dev/null
```
Present results:
```markdown
### Results
**{applied} fixed** | {failed} failed | Backup created
{If grade improved:}
Score impact: {old_grade} ({old_score}) → {new_grade} ({new_score}) — **+{delta} points**
{If failed > 0:}
{failed} fix(es) couldn't be applied — run `/config-audit plan` for alternative approaches.
**Rollback:** If anything looks wrong, run `/config-audit rollback {backup-id}` to restore.
```
### Step 7: Manual findings
If manual findings exist:
```markdown
### Needs manual attention
These {M} issues require human judgment:
1. **{title}** ({id}) — {recommendation}
2. ...
Run `/config-audit plan` to get a step-by-step guide for addressing these.
```
## Safety
- Backup is **mandatory** — every fix creates a backup first
- Dry-run by default — user must confirm before changes
- Verify after fix — re-scans to confirm findings resolved
- Rollback always available — `/config-audit rollback <backup-id>`

View file

@ -0,0 +1,78 @@
---
name: config-audit:help
description: Show all available config-audit commands
allowed-tools: Read
model: sonnet
---
# Config-Audit: Help
## Getting Started
Just run `/config-audit` — it auto-detects your project scope and runs a full audit. No setup needed.
## All Commands
### Core
| Command | Description |
|---------|-------------|
| `/config-audit` | Full audit with auto-scope detection |
| `/config-audit posture` | Quick scorecard with A-F grades per area |
| `/config-audit feature-gap` | Deep analysis of features you're not using |
| `/config-audit fix` | Auto-fix deterministic issues with backup |
| `/config-audit rollback` | Restore configuration from a backup |
### Planning & Implementation
| Command | Description |
|---------|-------------|
| `/config-audit plan` | Generate prioritized action plan from audit findings |
| `/config-audit implement` | Execute action plan with automatic backup + verification |
| `/config-audit interview` | Set preferences to customize the action plan _(optional)_ |
### Monitoring
| Command | Description |
|---------|-------------|
| `/config-audit drift` | Compare current config against a saved baseline |
| `/config-audit plugin-health` | Audit plugin structure and frontmatter quality |
### Utility
| Command | Description |
|---------|-------------|
| `/config-audit status` | Show current session state and progress |
| `/config-audit cleanup` | Clean up old session directories |
### Advanced (workflow phases)
| Command | Description |
|---------|-------------|
| `/config-audit discover` | Run only the discovery phase (find config files) |
| `/config-audit analyze` | Run only the analysis phase (generate report) |
## Scope Override
By default, `/config-audit` auto-detects scope from your current directory:
- Inside a git repo → scans the repo
- In `$HOME` → scans global config only
- Elsewhere → scans current directory
Override with: `/config-audit current`, `/config-audit repo`, `/config-audit home`, `/config-audit full`
## Typical Workflows
**First time?** Just run `/config-audit`.
**Want to fix things?** Run `/config-audit` then `/config-audit fix`.
**Full optimization:**
1. `/config-audit` — see what you have
2. `/config-audit plan` — create action plan
3. `/config-audit implement` — execute with backups
**Track changes over time:**
1. `/config-audit drift --save` — save baseline
2. _(make changes)_
3. `/config-audit drift` — see what changed

View file

@ -0,0 +1,132 @@
---
name: config-audit:implement
description: Phase 5 - Execute action plan with backups and verification
allowed-tools: Read, Write, Edit, Bash, Agent, AskUserQuestion
model: opus
---
# Config-Audit: Implementation (Phase 5)
Execute the action plan with full backup, verification, and rollback support.
## Prerequisites
- Must have completed Phase 4 (plan)
- Action plan at `~/.claude/config-audit/sessions/{session-id}/action-plan.md`
## Implementation
### Step 1: Load and verify
Find the most recent session with a plan. If none: "No action plan found. Run `/config-audit plan` first."
Read the action plan and count actions. Tell the user:
```
## Implementing Action Plan
Found {N} actions to execute across {M} files.
A backup will be created before any changes are made.
```
### Step 2: Get user approval
```
AskUserQuestion:
question: "Ready to implement {N} actions? Backup created automatically — you can roll back with one command."
options:
- "Yes, proceed"
- "Review plan first" (then show the plan file path)
- "Cancel"
```
### Step 3: Create backup
Create backup silently:
```bash
mkdir -p ~/.claude/config-audit/backups/$(date +%Y%m%d_%H%M%S)/files/ 2>/dev/null
```
Copy each file to be modified. Generate `manifest.yaml` with checksums.
Tell the user: **"Backup created. Implementing actions..."**
### Step 4: Execute actions
Group actions by dependencies. For each group, spawn implementer agents (batch of 3):
```
Agent(subagent_type: "config-audit:implementer-agent")
model: sonnet
prompt: |
Execute action: {action-id}
File: {file-path}, Type: {create|modify|delete}
Details: {changes}
Verify backup exists, make change, validate syntax.
Append result to: ~/.claude/config-audit/sessions/{session-id}/implementation-log.md
```
Show progress between groups:
```
Action 1/N: {title} — done
Action 2/N: {title} — done
...
```
### Step 5: Verify results
Spawn verifier agent:
```
Agent(subagent_type: "config-audit:verifier-agent")
model: sonnet (note: using sonnet, not haiku)
prompt: |
Verify all changes from implementation:
1. Modified files exist and are syntactically valid
2. New files created correctly
3. No new conflicts introduced
Report to: ~/.claude/config-audit/sessions/{session-id}/implementation-log.md
```
If verifier finds issues: one retry with implementer agent. If still failing: report and suggest rollback.
### Step 6: Present results
```markdown
### Implementation Complete
**{succeeded} succeeded** | {failed} failed | {skipped} skipped
{If score improved, run quick posture and show:}
Score impact: {old_grade} → {new_grade} (+{delta} points)
{If failed > 0:}
{failed} action(s) couldn't be completed — see log for details.
**Backup location:** `~/.claude/config-audit/backups/{timestamp}/`
**Rollback:** `/config-audit rollback {timestamp}`
**Full log:** `~/.claude/config-audit/sessions/{session-id}/implementation-log.md`
```
### Step 7: Update state
Update `state.yaml` with `current_phase: "implement"`, `next_phase: null`.
## Rollback
If the user requests rollback at any point:
1. Read `manifest.yaml` from backup
2. Restore each file and verify checksums
3. Delete newly created files
4. Update state to `rolled_back`
## Error Handling
| Error | What happens |
|-------|-------------|
| Permission denied | Skip action, log it, continue with others |
| File not found | Skip action, log it, continue |
| Invalid syntax after edit | Rollback that single file, log, continue |
| Critical failure | Offer full rollback |

View file

@ -0,0 +1,64 @@
---
name: config-audit:interview
description: Phase 3 - Interactive interview to gather user preferences
allowed-tools: Read, Write, Edit, AskUserQuestion
model: sonnet
---
# Config-Audit: Interview (Phase 3)
Gather user preferences to inform the action plan.
## IMPORTANT: Inline Execution Only
This command runs AskUserQuestion **directly in the main context** — NOT via a Task subagent.
AskUserQuestion requires synchronous terminal interaction and does not work when delegated to a Task subagent.
## Prerequisites
- Must have completed Phase 2 (analysis)
- Read analysis from `~/.claude/config-audit/sessions/{session-id}/analysis-report.md`
## Implementation Steps
1. **Load session state**: Verify analysis phase completed, read analysis report for context
2. **Conduct interview inline**: Use AskUserQuestion tool directly (NOT via Task). Adapt questions based on analysis findings.
3. **Save interview results**: Write to `~/.claude/config-audit/sessions/{session-id}/interview.md`
4. **Update state** (see state-management rule)
5. **Output summary**
## Interview Questions
Ask these using AskUserQuestion (skip questions that don't apply based on analysis):
1. **Config Style** — Centralized vs Distributed vs Hybrid organization
2. **Unused Hooks** — Wire up, review individually, delete, or leave (only if found)
3. **Duplicate Permissions** — Remove from local, consolidate, or keep (only if found)
4. **Modular Rules** — Use .claude/rules/ pattern? Yes/No
5. **Path-Scoped Rules** — Which patterns (tests, src, config, docs) — only if Q4=Yes
6. **Conflict Resolution** — Per-conflict: global vs project vs custom value (only if conflicts found)
7. **Permission Audit** — Audit or keep (only if >30 patterns in settings.local.json)
8. **Project Inheritance** — Per-project: inherit or isolate (only if multiple projects)
## Adaptive Questioning
Skip questions that don't apply:
- No unused hooks question if all hooks are wired
- No duplicates question if no duplicates found
- No conflict questions if no conflicts detected
- No path-scoping if user said no to modular rules
- Fewer project questions if only one project
- No permission audit if <30 patterns
## Skip Interview Option
If user runs `/config-audit plan` without interview:
- Use sensible defaults (centralized, inherit, enable rules)
- Flag decisions in plan as "assumed"
## Error Handling
- If user selects "Other" for any question, ask follow-up with AskUserQuestion
- If interview is cancelled, save partial results
- If no analysis report found, report error and exit
- If AskUserQuestion fails, STOP — do not use alternative methods

View file

@ -0,0 +1,82 @@
---
name: config-audit:plan
description: Phase 4 - Generate prioritized action plan with risk assessment
allowed-tools: Read, Write, Glob, Grep, Agent
model: opus
---
# Config-Audit: Plan Generation (Phase 4)
Generate a prioritized action plan based on analysis results.
## Prerequisites
- Must have completed Phase 2 (analysis)
- Phase 3 (interview) is optional — plan works with or without it
## Implementation
### Step 1: Verify session state
Find the most recent session with analysis completed. If none found: "No analysis results found. Run `/config-audit` first to scan your configuration."
### Step 2: Tell the user what's happening
```
## Creating Action Plan
Building a prioritized plan based on your analysis results...
Actions are ordered by impact, with risk assessment and dependency tracking.
```
### Step 3: Spawn planner agent
Tell the user: **"Generating your action plan (this takes about 30 seconds)..."**
```
Agent(subagent_type: "config-audit:planner-agent")
model: opus
prompt: |
Generate action plan based on:
- Analysis: ~/.claude/config-audit/sessions/{session-id}/analysis-report.md
- Interview: ~/.claude/config-audit/sessions/{session-id}/interview.md (if exists)
Create prioritized plan with:
- Risk assessment per action (low/medium/high)
- Rollback strategy
- Dependency ordering
- Effort estimates
Output to: ~/.claude/config-audit/sessions/{session-id}/action-plan.md
```
### Step 4: Present the plan summary
Read the generated plan and show a concise overview:
```markdown
### Action Plan Ready
**{N} actions** organized by priority:
| # | Action | Risk | Effort |
|---|--------|------|--------|
| 1 | {title} | {low/med/high} | {quick/moderate/involved} |
| 2 | ... | ... | ... |
| ... | ... | ... | ... |
Full plan: `~/.claude/config-audit/sessions/{session-id}/action-plan.md`
You can edit the plan file to remove, reorder, or modify actions before implementing.
### What's next
- **`/config-audit implement`** — Execute the plan with automatic backup and verification
- **`/config-audit interview`** — Set preferences first to customize the plan (optional)
```
### Step 5: Update state
Update `state.yaml` with `current_phase: "plan"`, `next_phase: "implement"`.
## Plan Modification
Users can edit `action-plan.md` before implementation — remove unwanted actions, adjust priority, or add custom actions. The implementer parses the modified plan.

View file

@ -0,0 +1,74 @@
---
name: config-audit:plugin-health
description: Audit plugin configuration quality — validates structure, frontmatter, and cross-plugin coherence
argument-hint: "[plugin-path]"
allowed-tools: Read, Glob, Grep, Bash
model: sonnet
---
# Config-Audit: Plugin Health
Audit Claude Code plugin structure and quality — validates plugin.json, CLAUDE.md, command/agent frontmatter, and detects cross-plugin conflicts.
## Arguments
- `$ARGUMENTS` may contain a path to a specific plugin directory
- If omitted: scans all plugins in the marketplace root
## Implementation
### Step 1: Discover plugins and greet
If a specific path is given, scan only that plugin. Otherwise, find all plugins using Glob for `**/.claude-plugin/plugin.json`.
Tell the user:
```
## Plugin Health Check
Auditing {N} plugin(s) for structure, frontmatter quality, and cross-plugin conflicts...
```
### Step 2: Run scanner
Run silently for each plugin:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/plugin-health-scanner.mjs <path> 2>/dev/null
```
Read stdout output (JSON). Parse findings.
### Step 3: Present results
```markdown
### Plugin Health Report
| Plugin | Grade | Commands | Agents | Status |
|--------|-------|----------|--------|--------|
| {name} | {grade} ({score}) | {cmd_count} | {agent_count} | {Good/Issues found} |
| ... | ... | ... | ... | ... |
{If cross-plugin issues:}
#### Cross-Plugin Issues ({count})
| Issue | Plugins | Recommendation |
|-------|---------|----------------|
| ... | ... | ... |
{If findings:}
#### Findings by Plugin
**{plugin-name}** ({finding_count} findings):
1. [{id}] {title} — {recommendation}
2. ...
```
### Step 4: Suggest next steps
```
### What's next
- Fix structural issues based on recommendations above
- `/config-audit posture` — Full configuration posture assessment
- `/config-audit fix` — Auto-fix deterministic issues
```

View file

@ -0,0 +1,120 @@
---
name: config-audit:posture
description: Quick configuration health assessment — scorecard with A-F grades
argument-hint: "[path] [--drift] [--plugin-health]"
allowed-tools: Read, Write, Glob, Grep, Bash
model: sonnet
---
# Config-Audit: Health Assessment
Quick, deterministic configuration health scorecard. No agents needed — runs all scanners + scoring in one pass.
## What the user gets
- Health grade (A-F) with plain-language explanation
- Per-area breakdown for 7 quality areas with grades and actionable notes
- Opportunity count — how many features could enhance their setup (not a grade)
- Grade-appropriate next steps
## Implementation
### Step 1: Determine target
Parse `$ARGUMENTS` for a path (default: current working directory). Resolve relative paths.
Tell the user:
```
## Configuration Health
Running quick assessment{if path != cwd: " on `{path}`"}...
```
### Step 2: Run posture scanner
Run silently — all output goes to a file:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/posture.mjs <target-path> --json --output-file /tmp/config-audit-posture-$$.json 2>/dev/null; echo $?
```
If exit code is non-zero, tell the user: "Assessment couldn't complete. Check that the path exists and contains Claude Code configuration files."
### Step 3: Read and interpret results
Read the JSON output file using the Read tool. Extract:
- `overallGrade`, `opportunityCount`
- `areas[]` — each with `name`, `grade`, `score`, `findingCount`
### Step 4: Present the scorecard
```markdown
**Health: {overallGrade}** | {qualityAreaCount} areas scanned
{grade-based context — pick ONE:}
- A: "Your configuration is correct and well-maintained."
- B: "Solid configuration with minor improvements available."
- C: "Working configuration with some issues worth addressing."
- D: "Configuration needs attention in several areas."
- F: "Significant issues found — addressing these will improve your experience."
### Area Scores
| Area | Grade | Score | Findings | |
|------|-------|-------|----------|-|
{for each area EXCEPT Feature Coverage:}
| {name} | {grade} | {score}/100 | {findingCount} | {plain-language note: A="Excellent", B="Good", C="Needs work", D/F="Issues found"} |
{if opportunityCount > 0:}
{opportunityCount} feature opportunities available — run `/config-audit feature-gap` for context-aware recommendations.
### What's next
```
**Grade A or B:**
```
Your configuration health is strong. Re-run after major changes to catch regressions.
For feature recommendations: `/config-audit feature-gap`
```
**Grade C:**
```
Run `/config-audit fix` to auto-fix what's possible, then `/config-audit plan` for a prioritized improvement path.
```
**Grade D or F:**
```
Start with `/config-audit fix` — it handles the most impactful issues automatically with backup and rollback.
Then run `/config-audit plan` for a step-by-step path to a better configuration.
```
### Step 5: Optional sections
**If `--drift` flag is present:**
Run drift comparison silently:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/drift-cli.mjs <target-path> 2>/dev/null
```
Read stdout output and append a "Configuration Drift" section showing what changed since the last baseline.
**If `--plugin-health` flag is present:**
Run plugin health scanner silently:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/plugin-health-scanner.mjs <target-path> 2>/dev/null
```
Read stdout output and append a "Plugin Health" section.
**If both flags:** Use `scanners/lib/report-generator.mjs` to produce a unified markdown report.
### Step 6: Save to session (if active)
If a config-audit session exists, save results:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/posture.mjs <target-path> --json --output-file ~/.claude/config-audit/sessions/<session-id>/posture.json 2>/dev/null
```

View file

@ -0,0 +1,83 @@
---
name: config-audit:rollback
description: Restore configuration from backup — list available backups or rollback a specific one
argument-hint: "[backup-id]"
allowed-tools: Read, Write, Glob, Grep, Bash, AskUserQuestion
model: sonnet
---
# Config-Audit: Rollback
Restore configuration files from a previous backup. Without arguments, lists available backups. With a backup ID, restores files from that backup.
## Arguments
- `$ARGUMENTS` may contain a backup ID (format: `YYYYMMDD_HHMMSS`)
## Behavior
### List mode (no argument)
List available backups from `~/.claude/config-audit/backups/`:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Available Backups
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 20260403_163045 — 3 files (settings.json, hooks.json, typescript.md)
2. 20260403_141230 — 1 file (CLAUDE.md)
3. 20260402_092015 — 5 files (full audit)
Usage: /config-audit rollback 20260403_163045
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
Read each backup's `manifest.yaml` to extract file list and timestamps.
### Restore mode (with backup ID)
1. Read manifest from `~/.claude/config-audit/backups/{backup-id}/manifest.yaml`
2. Show files that will be restored — ask for confirmation:
```
AskUserQuestion:
question: "Restore 3 files from backup 20260403_163045?"
options:
- "Yes, restore"
- "Cancel"
```
3. For each file in manifest:
a. Read backup file from `~/.claude/config-audit/backups/{backup-id}/files/{safeName}`
b. Write to original path
c. Verify checksum matches manifest
4. Show result:
```
Restored 3 files from backup 20260403_163045
- .claude/settings.json (checksum verified)
- hooks/hooks.json (checksum verified)
- .claude/rules/typescript.md (checksum verified)
```
### Delete mode
If user says "delete" after listing, confirm and remove the backup directory.
## Implementation
Use the backup and rollback libraries directly:
```javascript
import { listBackups, restoreBackup, deleteBackup } from '../scanners/rollback-engine.mjs';
import { parseManifest } from '../scanners/lib/backup.mjs';
```
Or via Bash:
```bash
# List backups
ls -1 ~/.claude/config-audit/backups/
# Read manifest
cat ~/.claude/config-audit/backups/{id}/manifest.yaml
# Restore (copy back)
cp ~/.claude/config-audit/backups/{id}/files/{safeName} {originalPath}
```

View file

@ -0,0 +1,114 @@
---
name: config-audit:status
description: Show current session state and available actions
allowed-tools: Read, Glob
model: sonnet
---
# Config-Audit: Status
Display current session state and guide next actions.
## Usage
```
/config-audit status
```
## Implementation
1. **Find active session**:
```
Glob: ~/.claude/config-audit/sessions/*/state.yaml
Sort by modification time
Use most recent
```
2. **Read session state**:
```yaml
session_id: "20250126_143022"
current_phase: "analyze"
completed_phases: ["discover", "analyze"]
next_phase: "interview"
...
```
3. **Display status**:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Config-Audit Session Status
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Session: 20250126_143022
Started: 2025-01-26 14:30:22
PHASE PROGRESS
──────────────
✓ Phase 1: Discover - 15 files found (current directory)
✓ Phase 2: Analyze - report generated
○ Phase 3: Interview - not started (optional)
○ Phase 4: Plan - not started
○ Phase 5: Implement - not started
NEXT ACTION
───────────
Run: /config-audit interview
Or: /config-audit plan (skip interview)
SESSION FILES
─────────────
Scope: ~/.claude/config-audit/sessions/20250126_143022/scope.yaml
Findings: ~/.claude/config-audit/sessions/20250126_143022/findings/
Report: ~/.claude/config-audit/sessions/20250126_143022/analysis-report.md
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
4. **If no session found**:
```
No active config-audit session found.
Start a new audit with:
/config-audit # Full audit with auto-scope
/config-audit discover # Discovery phase only
```
## Session Information
Display based on completed phases:
| Phase | Info to Display |
|-------|-----------------|
| scope | Scope type, paths to scan |
| discover | Files found count, issues count |
| analyze | Conflicts, duplicates, opportunities |
| interview | Preferences summary |
| plan | Actions count, risk level |
| implement | Success/fail counts, backup location |
## List All Sessions
With `all` flag:
```
/config-audit status all
```
Shows:
```
All config-audit sessions:
| Session | Phase | Created |
|---------|-------|---------|
| 20250126_143022 | analyze | 2025-01-26 14:30 |
| 20250125_091500 | complete | 2025-01-25 09:15 |
| 20250120_160000 | implement | 2025-01-20 16:00 |
```
## Resume Session
If multiple sessions exist:
```
/config-audit resume {session-id}
```
Sets that session as active and continues from last phase.

View file

@ -0,0 +1 @@
# My Project

View file

@ -0,0 +1,17 @@
# Minimal Setup Example
This example demonstrates a bare-minimum Claude Code project — just a single-line CLAUDE.md with no other configuration.
## What to expect
Running `node ../../scanners/posture.mjs .` from this directory will show:
- **Low utilization score** — most features are unused
- **Low maturity level** — no hooks, no rules, no settings
- **Multiple feature gap findings** — all tiers flagged
## Why this matters
Even a single CLAUDE.md file is enough for Claude Code to work. But without permissions, hooks, rules, or MCP configuration, you're leaving significant capability on the table.
Compare with the [optimal-setup](../optimal-setup/) example to see what a fully-configured project looks like.

Some files were not shown because too many files have changed in this diff Show more