Compare commits
1 commit
main
...
feat/add-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b795b1240 |
451 changed files with 68695 additions and 6116 deletions
|
|
@ -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
16
.gitignore
vendored
|
|
@ -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
|
||||
|
|
@ -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$''',
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
4
.mailmap
4
.mailmap
|
|
@ -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>
|
||||
39
CLAUDE.md
39
CLAUDE.md
|
|
@ -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`).
|
||||
|
|
@ -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).
|
||||
|
||||
131
GOVERNANCE.md
131
GOVERNANCE.md
|
|
@ -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
256
README.md
|
|
@ -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 5–7× 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 1–3 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 D1–D8 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._
|
||||
|
|
@ -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"
|
||||
|
|
@ -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'));
|
||||
});
|
||||
|
|
@ -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)"
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -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"
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -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); });
|
||||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
|
@ -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"
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
@ -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 }); }
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
# Polyrepo migration — operator-window RUNBOOK
|
||||
|
||||
The single document the operator follows in the authorized window. Everything before this (Steps 1–9,
|
||||
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:00–23: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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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 }); }
|
||||
});
|
||||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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, D1–D8). 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 SC1–SC8 (§4), the scope
|
||||
fence (§5), decisions D1–D8 (§7), risks R1–R8 (§8), the five §9 open questions, and the execution
|
||||
model (§10). Exploration surfaced **six findings the brief under-scoped** (F1–F6 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 3–9, 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 1–9, 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 30–60% 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 1–11). These are the cross-step, end-to-end checks
|
||||
mapping to the brief's SC1–SC8.*
|
||||
|
||||
- [ ] **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 1–5) 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 2–4)
|
||||
- **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 | SC1–SC8 + D1–D8 + R1–R8 + §9 traced; F1–F6 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 SC1–SC8 / D1–D8 / §9 / R1–R8 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) |
|
||||
|
|
@ -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 1–11) 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": []
|
||||
}
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 §8–9: 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.
|
||||
|
|
@ -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).
|
||||
|
|
@ -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:00–23: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:00–23:00, weekend anytime).
|
||||
8
plugins/ai-psychosis/.claude-plugin/plugin.json
Normal file
8
plugins/ai-psychosis/.claude-plugin/plugin.json
Normal 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
18
plugins/ai-psychosis/.gitignore
vendored
Normal 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
|
||||
130
plugins/ai-psychosis/CHANGELOG.md
Normal file
130
plugins/ai-psychosis/CHANGELOG.md
Normal 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
|
||||
85
plugins/ai-psychosis/CLAUDE.md
Normal file
85
plugins/ai-psychosis/CLAUDE.md
Normal 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
|
||||
21
plugins/ai-psychosis/LICENSE
Normal file
21
plugins/ai-psychosis/LICENSE
Normal 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.
|
||||
379
plugins/ai-psychosis/README.md
Normal file
379
plugins/ai-psychosis/README.md
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
<!-- badges -->
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 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:00–05: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:00–05: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)
|
||||
256
plugins/ai-psychosis/commands/interaction-report.md
Normal file
256
plugins/ai-psychosis/commands/interaction-report.md
Normal 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.
|
||||
48
plugins/ai-psychosis/hooks/hooks.json
Normal file
48
plugins/ai-psychosis/hooks/hooks.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
239
plugins/ai-psychosis/hooks/scripts/lib.mjs
Normal file
239
plugins/ai-psychosis/hooks/scripts/lib.mjs
Normal 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 */ }
|
||||
}
|
||||
140
plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs
Normal file
140
plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs
Normal 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();
|
||||
}
|
||||
67
plugins/ai-psychosis/hooks/scripts/session-end.mjs
Normal file
67
plugins/ai-psychosis/hooks/scripts/session-end.mjs
Normal 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);
|
||||
69
plugins/ai-psychosis/hooks/scripts/session-start.mjs
Normal file
69
plugins/ai-psychosis/hooks/scripts/session-start.mjs
Normal 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);
|
||||
166
plugins/ai-psychosis/hooks/scripts/tool-tracker.mjs
Normal file
166
plugins/ai-psychosis/hooks/scripts/tool-tracker.mjs
Normal 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);
|
||||
54
plugins/ai-psychosis/skills/ai-psychosis/SKILL.md
Normal file
54
plugins/ai-psychosis/skills/ai-psychosis/SKILL.md
Normal 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.
|
||||
44
plugins/ai-psychosis/tests/privacy.test.mjs
Normal file
44
plugins/ai-psychosis/tests/privacy.test.mjs
Normal 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`);
|
||||
});
|
||||
});
|
||||
313
plugins/ai-psychosis/tests/prompt-analyzer.test.mjs
Normal file
313
plugins/ai-psychosis/tests/prompt-analyzer.test.mjs
Normal 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'));
|
||||
});
|
||||
});
|
||||
66
plugins/ai-psychosis/tests/session-end.test.mjs
Normal file
66
plugins/ai-psychosis/tests/session-end.test.mjs
Normal 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');
|
||||
});
|
||||
});
|
||||
49
plugins/ai-psychosis/tests/session-start.test.mjs
Normal file
49
plugins/ai-psychosis/tests/session-start.test.mjs
Normal 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);
|
||||
});
|
||||
});
|
||||
53
plugins/ai-psychosis/tests/test-helper.mjs
Normal file
53
plugins/ai-psychosis/tests/test-helper.mjs
Normal 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));
|
||||
}
|
||||
94
plugins/ai-psychosis/tests/tool-tracker.test.mjs
Normal file
94
plugins/ai-psychosis/tests/tool-tracker.test.mjs
Normal 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);
|
||||
});
|
||||
});
|
||||
8
plugins/config-audit/.claude-plugin/plugin.json
Normal file
8
plugins/config-audit/.claude-plugin/plugin.json
Normal 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"
|
||||
}
|
||||
}
|
||||
27
plugins/config-audit/.claude/rules/agent-development.md
Normal file
27
plugins/config-audit/.claude/rules/agent-development.md
Normal 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
|
||||
24
plugins/config-audit/.claude/rules/command-development.md
Normal file
24
plugins/config-audit/.claude/rules/command-development.md
Normal 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
|
||||
15
plugins/config-audit/.claude/rules/state-management.md
Normal file
15
plugins/config-audit/.claude/rules/state-management.md
Normal 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.
|
||||
32
plugins/config-audit/.claude/rules/ux-rules.md
Normal file
32
plugins/config-audit/.claude/rules/ux-rules.md
Normal 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
|
||||
16
plugins/config-audit/.config-audit-ignore
Normal file
16
plugins/config-audit/.config-audit-ignore
Normal 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
19
plugins/config-audit/.gitignore
vendored
Normal 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/
|
||||
262
plugins/config-audit/CHANGELOG.md
Normal file
262
plugins/config-audit/CHANGELOG.md
Normal 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
|
||||
160
plugins/config-audit/CLAUDE.md
Normal file
160
plugins/config-audit/CLAUDE.md
Normal 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
|
||||
21
plugins/config-audit/LICENSE
Normal file
21
plugins/config-audit/LICENSE
Normal 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.
|
||||
465
plugins/config-audit/README.md
Normal file
465
plugins/config-audit/README.md
Normal 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.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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
|
||||
175
plugins/config-audit/agents/analyzer-agent.md
Normal file
175
plugins/config-audit/agents/analyzer-agent.md
Normal 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)
|
||||
91
plugins/config-audit/agents/feature-gap-agent.md
Normal file
91
plugins/config-audit/agents/feature-gap-agent.md
Normal 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
|
||||
261
plugins/config-audit/agents/implementer-agent.md
Normal file
261
plugins/config-audit/agents/implementer-agent.md
Normal 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
|
||||
265
plugins/config-audit/agents/planner-agent.md
Normal file
265
plugins/config-audit/agents/planner-agent.md
Normal 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
|
||||
257
plugins/config-audit/agents/scanner-agent.md
Normal file
257
plugins/config-audit/agents/scanner-agent.md
Normal 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)
|
||||
248
plugins/config-audit/agents/verifier-agent.md
Normal file
248
plugins/config-audit/agents/verifier-agent.md
Normal 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
|
||||
74
plugins/config-audit/commands/analyze.md
Normal file
74
plugins/config-audit/commands/analyze.md
Normal 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"`.
|
||||
95
plugins/config-audit/commands/cleanup.md
Normal file
95
plugins/config-audit/commands/cleanup.md
Normal 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
|
||||
202
plugins/config-audit/commands/config-audit.md
Normal file
202
plugins/config-audit/commands/config-audit.md
Normal 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."
|
||||
141
plugins/config-audit/commands/discover.md
Normal file
141
plugins/config-audit/commands/discover.md
Normal 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."
|
||||
98
plugins/config-audit/commands/drift.md
Normal file
98
plugins/config-audit/commands/drift.md
Normal 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
|
||||
185
plugins/config-audit/commands/feature-gap.md
Normal file
185
plugins/config-audit/commands/feature-gap.md
Normal 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
|
||||
138
plugins/config-audit/commands/fix.md
Normal file
138
plugins/config-audit/commands/fix.md
Normal 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>`
|
||||
78
plugins/config-audit/commands/help.md
Normal file
78
plugins/config-audit/commands/help.md
Normal 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
|
||||
132
plugins/config-audit/commands/implement.md
Normal file
132
plugins/config-audit/commands/implement.md
Normal 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 |
|
||||
64
plugins/config-audit/commands/interview.md
Normal file
64
plugins/config-audit/commands/interview.md
Normal 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
|
||||
82
plugins/config-audit/commands/plan.md
Normal file
82
plugins/config-audit/commands/plan.md
Normal 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.
|
||||
74
plugins/config-audit/commands/plugin-health.md
Normal file
74
plugins/config-audit/commands/plugin-health.md
Normal 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
|
||||
```
|
||||
120
plugins/config-audit/commands/posture.md
Normal file
120
plugins/config-audit/commands/posture.md
Normal 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
|
||||
```
|
||||
83
plugins/config-audit/commands/rollback.md
Normal file
83
plugins/config-audit/commands/rollback.md
Normal 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}
|
||||
```
|
||||
114
plugins/config-audit/commands/status.md
Normal file
114
plugins/config-audit/commands/status.md
Normal 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.
|
||||
1
plugins/config-audit/examples/minimal-setup/CLAUDE.md
Normal file
1
plugins/config-audit/examples/minimal-setup/CLAUDE.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# My Project
|
||||
17
plugins/config-audit/examples/minimal-setup/README.md
Normal file
17
plugins/config-audit/examples/minimal-setup/README.md
Normal 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
Loading…
Add table
Add a link
Reference in a new issue