Compare commits

...

175 commits

Author SHA1 Message Date
2ef1e53836 chore(catalog): bump ms-ai-architect v1.16.5 -> v1.17.0
ms-ai-architect v1.17.0 — release. Catalog ref now pins the v1.17.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:26:28 +02:00
c450be0481 chore(catalog): bump voyage v5.9.0 -> v5.9.1
voyage v5.9.1 — release. Catalog ref now pins the v5.9.1 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:11:34 +02:00
704b7cf9a3 chore(catalog): bump voyage v5.8.0 -> v5.9.0
voyage v5.9.0 — release. Catalog ref now pins the v5.9.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:19:35 +02:00
204c47a6e2 chore(catalog): bump voyage v5.7.1 -> v5.8.0
voyage v5.8.0 — release. Catalog ref now pins the v5.8.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 09:03:12 +02:00
b27905303c docs(catalog): OKF log — record spec-vs-gate coverage gap as a deferred decision
The shared gate enforces only the `type:` MUST, not §3's okf_version / per-level
index.md MUSTs nor §6's index.md-no-frontmatter (inherited faithfully from okr's
reference semantics). So "passes the gate" is a necessary-but-partial §3 signal.
Surfaced by the portfolio-optimiser bundle (index.md with frontmatter) passing.
Deferred to Stage 2 / a spec<->gate reconciliation — not changed now, since
tightening would break the proven verdict-parity with okr's checker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GqEHp4uDiivfrAUjw4BdE
2026-06-29 15:30:14 +02:00
12c2fefd74 docs(catalog): OKF log — note external form-consumer portfolio-optimiser (gate-verified)
Relay from the portfolio-optimiser session (per protocol §3): a separate Forgejo
framework (MAF cost-optimiser, NOT a marketplace plugin) independently adopted the
OKF-minimal on-disk form for its per-project runtime knowledge bundles — a different
scope than this convention's user-second-brain. Recorded in the change log so the
on-disk FORM doesn't drift across the ecosystem. NOT a 4th convergence member (no
user-second-brain, no shared ingestion/retrieval).

Independently re-verified in this catalog session before recording: the
shared/examples/bygg-energi-mikro bundle passes the shared gate (exit 0, 5 concepts,
okf_version 0.1, exactly the 2 named resource-warnings); index.md carries frontmatter
(type: index); commit portfolio-optimiser 812db23 exists locally. Claims hold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GqEHp4uDiivfrAUjw4BdE
2026-06-29 15:26:44 +02:00
062d6336af docs(catalog): OKF — add landing protocol (siblings record conformance in the shared log)
So a sibling's conformance landing reaches the convergence without the operator
hand-carrying status: when a plugin's bundle conforms, it writes the claim directly
into log.md (its own catalog-go) as 🟡 "claims conformant @ <commit>, awaiting
gate-verification"; a linkedin-studio session runs the shared gate and flips it to
🟢 on exit 0 (independent verification, not self-assertion). Honest limit recorded:
no live push across separate sessions — discovery is pull-at-session-read. Pointer
added to the handoff §5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GqEHp4uDiivfrAUjw4BdE
2026-06-29 14:38:02 +02:00
03e037b251 chore(catalog): bump voyage v5.7.0 -> v5.7.1
voyage v5.7.1 — release. Catalog ref now pins the v5.7.1 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:38:44 +02:00
c06e4d7b55 feat(catalog): shared OKF conformance checker + wire as cross-plugin acceptance gate
Lift okr's reference okf-check.mjs into the catalog as the single cross-plugin
acceptance gate for the OKF-compatible second-brain form (spec §3):

- scripts/okf-check.mjs (+ vendored okf-frontmatter.mjs): verdict logic
  byte-identical to okr's reference impl, English output, zero deps, self-contained.
- scripts/okf-check.test.mjs: 5 self-contained tests (temp-dir bundles).
- spec §7 + §14: the shared checker now lives here; only TS/mjs reconciliation
  remains Stage-3, not required for the gate. log.md protocol §6: conformance is
  verified by the gate, not asserted — a plugin moves to 🟢 only after passing it.

Verified: 33/33 catalog tests green (canonical glob form); verdict parity with
okr's checker on okr fixtures (positive + negative missing-type); a scaffolded
linkedin-studio brain/ validates clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GqEHp4uDiivfrAUjw4BdE
2026-06-29 10:27:05 +02:00
99d164e31e docs(catalog): OKF log — record Stage 1 ratification by okr + ms-ai-architect
Both siblings ratified spec v0.1 + adapted plans (relayed via operator):
okr @ 75bfc9b, ms-ai-architect @ 72a7e2b. Status table updated, open
coordination items marked resolved-via-ratification, change-log records
Stage 1 ratification complete across all three tracks (interop goal met at
the convention level). Conformance landings remain each their own go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GqEHp4uDiivfrAUjw4BdE
2026-06-29 10:01:56 +02:00
0b54b8e1f4 docs(catalog): add OKF second-brain handoff brief for okr + ms-ai-architect
Action brief the operator hands to the two sibling second-brain sessions:
read spec.md + log.md, confirm per-plugin items, update STATE.md, adapt the
plan, and acknowledge back in their own repo (operator relays -> log.md status).
Carries the three verified premise corrections (mdcode != OKF tool; no reusable
OKF ingest; resource not source) and a deterministic acknowledge-back line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GqEHp4uDiivfrAUjw4BdE
2026-06-29 09:40:21 +02:00
449225454f docs(catalog): add OKF-compatible second-brain form spec + coordination log
Cross-cutting catalog artifact (owned by no single plugin): the shared
"OKF-compatible second-brain form" convention all three second-brain plugins
(linkedin-studio, okr, ms-ai-architect) conform to, plus a coordination log
that serves as the cross-repo rollout state.

- spec.md: minimal contract (type + per-level index.md + root okf_version),
  recommended fields, extension keys, reference checker (okr okf-check),
  verified premise corrections (mdcode != OKF tool; no reusable OKF ingest),
  staged plan, homes, versioning. linkedin-studio's brain = reference design;
  OKF = thin interop veneer.
- log.md: coordination protocol (single source of truth, no cross-repo writes,
  operator relay), per-plugin conformance status table, open coordination items.

linkedin-studio recorded conformant (brain emits OKF form, cross-tool-verified
against okr's okf-check). okr / ms-ai-architect rollout = separate per-repo go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GqEHp4uDiivfrAUjw4BdE
2026-06-29 09:36:04 +02:00
c2d7ae6956 chore(catalog): bump okr v1.6.0 -> v1.6.1
okr v1.6.1 — release. Catalog ref now pins the v1.6.1 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:32:08 +02:00
5b7e54eb5b chore(catalog): bump okr v1.5.0 -> v1.6.0
okr v1.6.0 — release. Catalog ref now pins the v1.6.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:04:18 +02:00
c0aad3b98d chore(catalog): bump config-audit v5.12.4 -> v5.12.5
config-audit v5.12.5 — release. Catalog ref now pins the v5.12.5 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:05:04 +02:00
47d4438edc chore(catalog): bump voyage v5.6.1 -> v5.7.0
voyage v5.7.0 — release. Catalog ref now pins the v5.7.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:49:29 +02:00
48429cc9c5 chore(catalog): bump config-audit v5.12.3 -> v5.12.4
config-audit v5.12.4 — release. Catalog ref now pins the v5.12.4 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:05:04 +02:00
383708c93a chore(catalog): bump okr v1.4.0 -> v1.5.0
okr v1.5.0 — release. Catalog ref now pins the v1.5.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:07:09 +02:00
8bca5801c0 chore(catalog): bump config-audit v5.12.2 -> v5.12.3
config-audit v5.12.3 — release. Catalog ref now pins the v5.12.3 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 00:38:19 +02:00
aca04dc54a chore(catalog): bump ai-psychosis v1.2.0 -> v1.2.1
ai-psychosis v1.2.1 — release. Catalog ref now pins the v1.2.1 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:16:33 +02:00
8f73ae9fc4 chore(catalog): bump config-audit v5.12.1 -> v5.12.2
config-audit v5.12.2 — release. Catalog ref now pins the v5.12.2 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:00:08 +02:00
e752d84a48 chore(catalog): bump ms-ai-architect v1.16.4 -> v1.16.5
ms-ai-architect v1.16.5 — release. Catalog ref now pins the v1.16.5 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:58:44 +02:00
18944cf8a4 chore(catalog): bump linkedin-studio v0.5.2 -> v0.5.3
linkedin-studio v0.5.3 — release. Catalog ref now pins the v0.5.3 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:30:57 +02:00
2871e7467c chore(catalog): bump voyage v5.6.0 -> v5.6.1
voyage v5.6.1 — release. Catalog ref now pins the v5.6.1 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:53:54 +02:00
91115cf5e2 chore(catalog): bump okr v1.3.2 -> v1.4.0
okr v1.4.0 — release. Catalog ref now pins the v1.4.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:47:31 +02:00
1c2ac8e4df chore(catalog): bump ms-ai-architect v1.16.3 -> v1.16.4
ms-ai-architect v1.16.4 — release. Catalog ref now pins the v1.16.4 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:42:56 +02:00
04df7e802c chore(catalog): bump graceful-handoff v3.0.0 -> v3.1.0
graceful-handoff v3.1.0 — release. Catalog ref now pins the v3.1.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:01:08 +02:00
9759e84c44 chore(catalog): bump ms-ai-architect v1.16.2 -> v1.16.3
ms-ai-architect v1.16.3 — release. Catalog ref now pins the v1.16.3 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:47:31 +02:00
2c32c7e5d0 chore(catalog): bump okr v1.3.1 -> v1.3.2
okr v1.3.2 — release. Catalog ref now pins the v1.3.2 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:27:13 +02:00
e5d32c0454 chore(catalog): bump config-audit v5.12.0 -> v5.12.1
config-audit v5.12.1 — release. Catalog ref now pins the v5.12.1 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:16:31 +02:00
e8be6046aa chore(catalog): bump ms-ai-architect v1.16.1 -> v1.16.2
ms-ai-architect v1.16.2 — release. Catalog ref now pins the v1.16.2 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:07:30 +02:00
fc20a4e7f9 chore(catalog): bump config-audit v5.11.0 -> v5.12.0
config-audit v5.12.0 — release. Catalog ref now pins the v5.12.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 22:30:13 +02:00
09fa51b280 chore(catalog): bump config-audit v5.10.0 -> v5.11.0
config-audit v5.11.0 — release. Catalog ref now pins the v5.11.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:51:26 +02:00
1dfd4740cd chore(catalog): bump ms-ai-architect v1.16.0 -> v1.16.1
ms-ai-architect v1.16.1 — release. Catalog ref now pins the v1.16.1 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:33:58 +02:00
a8c884f2ff chore(catalog): bump okr v1.3.0 -> v1.3.1
okr v1.3.1 — release. Catalog ref now pins the v1.3.1 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:22:56 +02:00
d052d11df7 chore(catalog): bump graceful-handoff v2.1.0 -> v3.0.0
graceful-handoff v3.0.0 — release. Catalog ref now pins the v3.0.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:22:13 +02:00
f56a038cda chore(catalog): bump config-audit v5.9.0 -> v5.10.0
config-audit v5.10.0 — release. Catalog ref now pins the v5.10.0 tag so
`claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:04:11 +02:00
7781d12b0c chore(catalog): bump config-audit v5.8.0 -> v5.9.0
config-audit v5.9.0 — release. Catalog ref now pins the v5.9.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:30:56 +02:00
1795230178 chore(catalog): bump linkedin-studio v0.5.1 -> v0.5.2
linkedin-studio v0.5.2 — release. Catalog ref now pins the v0.5.2 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:12:08 +02:00
4134449bf5 chore(catalog): bump linkedin-studio v0.5.0 -> v0.5.1
linkedin-studio v0.5.1 — release. Catalog ref now pins the v0.5.1 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:59:16 +02:00
aac65942a9 chore(catalog): bump config-audit v5.7.0 -> v5.8.0
config-audit v5.8.0 — release. Catalog ref now pins the v5.8.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:04:03 +02:00
d2b9ebba71 chore(catalog): bump ms-ai-architect v1.15.0 -> v1.16.0
ms-ai-architect v1.16.0 — release. Catalog ref now pins the v1.16.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:53:30 +02:00
2e1d206ab3 feat(catalog): gate catalog README labels against ref + close the drift
The catalog README's per-plugin `vX.Y.Z` labels were an UNGUARDED surface:
check-versions validated each plugin's OWN README badge, never the catalog
README's labels, so they drifted (config-audit shown v5.5.0 while pinned to
v5.7.0; voyage v5.1.1 while pinned to v5.6.0) — the doc misstated what
`claude plugin install` actually resolves.

Close the class, same pattern as the ref surface:
- check-versions.mjs: new ERROR rule "catalog README label == catalog ref"
  via pure extractCatalogLabel() (matches the /open/<name>) heading, takes the
  first `vX.Y.Z`, ignores a trailing lang/flag badge). No legitimate transient
  state lets label != ref, so ERROR (not WARN). +5 tests.
- release-plugin.mjs: on --write, bump the README label atomically with the ref
  via pure reconcileReadmeLabel() and git add README.md on --commit, so future
  releases keep label and ref in lock-step. +4 tests.
- README.md: reconcile the 2 stale labels (config-audit -> v5.7.0,
  voyage -> v5.6.0). Gate now 9 OK / 1 WARN / 0 ERROR.
- CLAUDE.md: doctrine updated to document the new gate rule + atomic label bump.

ms-ai-architect stays WARN by decision (1.16.0 is unreleased WIP, never tagged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cm2RxKbomdLqjiWGcwCCPi
2026-06-22 13:38:23 +02:00
cf2420f7c5 chore(catalog): bump config-audit v5.6.0 -> v5.7.0
config-audit v5.7.0 — release. Catalog ref now pins the v5.7.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:05:07 +02:00
9b1838f1d7 feat(catalog): release-plugin.mjs — atomic catalog-ref release helper
The polyrepo split made a plugin release a TWO-repo act: tag the plugin repo AND
bump the catalog `ref`. The second step is manual and easily forgotten — that
drift just stranded linkedin-studio on v0.4.0 while its plugin.json moved to
0.5.0. This adds the canonical release path that makes the catalog side
impossible to do wrong.

scripts/release-plugin.mjs:
- Pure planner planRelease() — given the catalog + observed plugin state +
  target version, computes verdict (READY/NOOP/BLOCKED), the bumped marketplace
  object, and the commit subject. REFUSES (BLOCKED) unless plugin.json == README
  badge == target AND the vX.Y.Z tag exists, so a READY plan is check-versions-
  green by construction. Reuses normalizeVersion/classifyPlugin from
  check-versions.mjs (no duplicated rules).
- I/O shell: dry-run by default; --write bumps the ref + re-runs the gate;
  --commit/--push apply; --create-tag mints+pushes a missing plugin tag first.
- 10/10 unit tests (scripts/release-plugin.test.mjs); check-versions 9/9 still green.
- Dogfooded: linkedin-studio → NOOP (already pinned v0.5.0); ms-ai-architect →
  BLOCKED (v1.16.0 in plugin.json was never tagged — refuses to publish it).

Also: fix the stale LinkedIn Studio README label v0.4.0 -> v0.5.0 (loose end of
the v0.5.0 release), and document the canonical release path in CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:40:47 +02:00
6cbe4800f5 chore(catalog): bump linkedin-studio v0.4.0 -> v0.5.0
linkedin-studio v0.5.0 — M0 (per-user external data dir) + honesty-hardening
sweep (S2–S31) + Fix #1 (contract binding-gate) + Fix #2 (lived-specifics /
kilde-så-draft). Catalog ref now pins the v0.5.0 tag so `claude plugin update`
resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:32:07 +02:00
8c1ae3c55b chore(catalog): bump voyage v5.5.0 -> v5.6.0
voyage v5.6.0 — /trekexecute loop hardening (machine-verifiable completion
gate, cap-25 recovery budget, iterations_remaining field). Catalog ref now
pins the v5.6.0 tag so `claude plugin update` resolves the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:31:52 +02:00
dbbd1ff6d5 chore(catalog): bump config-audit v5.5.0 -> v5.6.0
config-audit v5.6.0 — steering-model II (Foundation + B + C): load-pattern
enumeration, manifest/tokens load-pattern accounting, new CA-OST output-style
scanner (count 14). Suite 1023, self-audit A/A.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:14:29 +02:00
843254df5b chore(catalog): release llm-security v7.8.0 — bump ref + README, refresh rollout brief
llm-security cut v7.8.0 (TRG/SIG/AST deep-scan scanners) and tagged `v7.8.0`
(commit 6d3c4b5) in its repo. Sync the marketplace coordinator:

- .claude-plugin/marketplace.json: source.ref v7.7.2 → v7.8.0 (installs track the
  real release; version-consistency gate → OK for llm-security).
- README.md: version label v7.7.2 → v7.8.0; scanner count 23 → 26 (+3: TRG/SIG/AST)
  in both the "Deterministic scanning" bullet and the stat line; tests 1822 → 1863.
- docs/state-version-rollout.md: mark llm-security STATE.md row DONE (it is tracked,
  not gitignored), add the missing playground-design-system row, and refresh the
  Workstream B run note (8 OK / 2 WARN; llm-security resolved this day).

Gate: `node scripts/check-versions.mjs` → 8 OK, 2 WARN (linkedin-studio,
ms-ai-architect — pre-existing, operator decision), 0 ERROR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 11:58:39 +02:00
5832c80e0a chore(catalog): bump config-audit ref v5.4.1 -> v5.5.0
Plugin v5.5.0 (steering-model I: A+E) is tagged and pushed; this points
the marketplace source.ref at it so installs track the real release.
Scanner count unchanged (13), so the README prose stays accurate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 11:42:55 +02:00
2237f6cc4d docs(catalog): mark voyage STATE.md tracking DONE (Workstream A, 3/11)
voyage avgitignored + tracked STATE.md per the rollout brief; matches
config-audit + ms-ai-architect. Remaining: linkedin-studio, claude-design
(have STATE, gitignored) + 6 file-made-later repos — each its own GO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
2026-06-20 11:21:05 +02:00
0f3518f062 chore(catalog): bump config-audit ref v5.4.0 -> v5.4.1
Scanner-correctness patch (HKV/RUL/PLH). Plugin tag v5.4.1 already
pushed, so the ref resolves. Stats refreshed: 954+ tests (13 scanners
unchanged). Version-gate: config-audit reconciles OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 10:16:47 +02:00
4151a70fe3 docs(rollout): brief for STATE.md tracking + version-consistency rollout
Marketplace-wide coordination brief in catalog/docs/. Two cross-cutting workstreams:
(A) STATE.md tracked in every repo per the updated global continuity rule —
verified status: 2 DONE (config-audit, ms-ai-architect), 3 avgitignore+track
(voyage, linkedin-studio, claude-design), 6 avgitignore-only; per-repo steps +
testable verification. (B) version-consistency via check-versions.mjs — 8 OK,
2 WARN (linkedin-studio, ms-ai-architect: plugin.json ahead of an untagged ref),
each needing an operator release-intent decision. Plan only — execution is per
repo with its own GO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 05:53:20 +02:00
a8d44e6d3f feat(scripts): add marketplace version-consistency gate
scripts/check-versions.mjs cross-checks every plugin in marketplace.json against
its sibling repo:
- catalog ref must resolve to a real git tag    → ERROR (dangling = install breaks)
- plugin.json version == README version-badge    → ERROR (internal corruption)
- catalog ref == plugin.json version             → WARN (catalog lags / unreleased bump)
- sibling repo missing                           → SKIP

Exit 1 on any ERROR; --strict also fails on WARN. Pure classifier covered by
check-versions.test.mjs (9 tests, node --test). Documented in CLAUDE.md
catalog-maintenance. Run before committing any ref change.

Current run: 8 OK, 2 WARN (linkedin-studio, ms-ai-architect — catalog ref lags an
untagged plugin.json bump), 0 ERROR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-19 23:19:22 +02:00
563dde7c51 docs(voyage): bump catalog entry to v5.5.0
voyage v5.5.0 released (tag v5.5.0 pushed to open/voyage.git). Bump marketplace
ref v5.1.1 -> v5.5.0. v5.5.0 ships brief framing enforcement (brief_version 2.2),
W0-W3 CC-alignment against CC 2.1.130->181, and narrow wins NW1-NW3 (reviewer
schema contract + opt-in --workflow). Closes the 5.1.1->5.5.0 install skew.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
2026-06-19 23:08:49 +02:00
44428e40aa docs(config-audit): bump catalog entry to v5.4.0
config-audit v5.4.0 released (tag pushed). Bump marketplace ref v5.3.0 -> v5.4.0
and README heading + test count (936+ -> 949+). Plugin-hygiene & settings-validation
hardening: CA-PLH-015 plugin-folder shadowing, CA-PLH-016 skills:-array validation,
CA-SET autoMode structure + dead-config. Scanner count stays 13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-19 22:36:36 +02:00
165035fc69 docs(config-audit): bump catalog entry to v5.3.0
marketplace.json ref v5.1.0→v5.3.0 (was install-critical — marketplace installs were pinned two releases behind). README: v5.3.0, 12→13 scanners (+ plugin-hygiene), 792→936 tests. Catches up the skipped v5.2.0 refresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-19 20:58:40 +02:00
cdb897a639 docs(conventions): reframe docs-gate + playground refs for polyrepo split 2026-06-18 10:26:15 +02:00
e84dffd2b0 chore(marketplace): thin catalog to manifest + docs (polyrepo migration complete) 2026-06-18 10:00:47 +02:00
f35e4ec46d fix(migration): multiplex operator-window SSH pushes through one master connection
Forgejo rate-limits rapid port-22 handshakes. The bare per-target
`git push --all` + `git push --tags` opened ~22 SSH connections in rapid
succession; the server refused around the 6th (voyage's tag push) with
"port 22: Connection refused" (TCP-level, not auth), deterministically, on
every run. Export GIT_SSH_COMMAND with ControlMaster/ControlPath/ControlPersist
so all git-over-SSH reuses ONE persistent master connection (one TCP handshake
for the whole rollout).

Verified: 8 rapid multiplexed connections all succeed where the 6th bare
connection is refused; the full 11-target rollout then completed clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:23:36 +02:00
43b2b02cd7 chore(marketplace): externalise claude-design 2026-06-18 09:20:13 +02:00
4ea9bfae07 chore(marketplace): externalise human-friendly-style 2026-06-18 09:20:08 +02:00
ac374bf5e9 chore(marketplace): externalise ai-psychosis 2026-06-18 09:20:03 +02:00
97efff2004 chore(marketplace): externalise okr 2026-06-18 09:19:47 +02:00
f942c8834d chore(marketplace): externalise config-audit 2026-06-18 09:19:43 +02:00
f13f173be1 chore(marketplace): externalise ms-ai-architect 2026-06-18 09:19:21 +02:00
11865a56a9 chore(marketplace): externalise linkedin-studio 2026-06-18 09:18:26 +02:00
a74f37d0b8 chore(marketplace): externalise llm-security 2026-06-18 09:18:19 +02:00
7b18a94afa chore(marketplace): externalise voyage 2026-06-18 09:17:09 +02:00
3403648c6c fix(migration): operator-window SC2 must be regression-relative, not strict
The operator window's step [a] called 40-validate-standalone.sh directly (strict
exit-code), so it STOPped on the first target carrying pre-existing in-repo test
red — voyage (2 doc-consistency drifts re phase_models/phase_signals, content moved
to docs/operations.md) and ai-psychosis (1). But the migration's ratified contract,
the one the Step-11 dry-run validated as PASS 11/11, is 'introduce no regression':
pre-existing in-repo red is the plugin's own concern, not a migration regression.
The window enforced a STRICTER gate than the contract the dry-run signed off.

Fix: new 41-validate-or-regression.sh — the single per-target gate the window calls
in [a]. It runs 40 strict, then on failure passes iff the standalone failing-test
NAME set is a SUBSET of the live in-repo set (the exact decision 99-dryrun.sh makes),
reusing sc2-regression.sh. A genuine extraction-introduced regression still STOPs the
window; a structure-validator fail and the config-audit gate stay strict.

Single-source the failing-name capture: extract capture_fails into capture-fails.sh
(mirrors the sc2-regression.sh extraction) so the live gate and the dry-run agree on
what 'failing' means; 99-dryrun.sh now delegates to it (behaviour identical).

Verified end-to-end on the real extracts: 40 strict fails voyage+ai-psychosis while
41 passes them 'N pre-existing, regression-relative'; clean targets (llm-security,
graceful-handoff) still pass via the strict path. New hermetic tests: capture-fails
3/3, 41 6/6 (strict-pass, regression-relative-pass, genuine-regression-fail,
structure-not-eligible, gate pass/fail). RUNBOOK per-repo step updated to 41.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 06:25:55 +02:00
e279b1f022 chore(marketplace): externalise graceful-handoff 2026-06-18 05:51:57 +02:00
ebd0a9c617 docs(migration): brace ${key} in run-operator-window — bash 5.3 absorbed the ellipsis byte into the var name under set -u
The pilot died at line 93 with 'key<byte>: unbound variable': $key glued to
the U+2026 ellipsis let bash 5.3.9 read the multibyte byte as part of the name.
Braced to ${key}…; proven on /usr/local/bin/bash (repro + fixed form + bash -n).
Nothing was mutated on Forgejo — it failed before the [b] repo-create POST.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 05:49:47 +02:00
b6aa815e41 docs(migration): fix run-operator-window DS special-casing (2 BLOCKER, steps d+e)
Review of the rollout script found 2 deterministic blockers on target #2,
playground-design-system — a shared component, not a marketplace plugin:

- step (d): DS has no .claude-plugin/plugin.json (it ships tokens.css/base.css/
  schemas). The plugin.json assertion would die. Now DS-aware: assert tokens.css
  for DS_KEY, plugin.json otherwise.
- step (e): 60-rewrite --only playground-design-system errors (--only name not in
  marketplace.json) since DS has no catalog entry. New entry_present() gate skips
  the flip when the key has no marketplace entry, mirroring 60-rewrite --all
  (which only touches the 10 real plugins).

Net: pilot + 9 plugins flip (count_local to 0); DS is stood up as a repo but
never flipped. Verified: bash -n OK, entry_present + 60-rewrite behaviour tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:32:11 +02:00
d577cadcf4 docs(migration): trekreview re-review — ALLOW (all 12 arc findings resolved)
Re-review scope a44e37b..85a8ee3. BLOCK (1+6+3) → WARN (2 new MAJOR surfaced in
30-fix-references.mjs) → ALLOW (both remediated, re-verified by the independent
code-correctness reviewer). 0 BLOCKER / 0 MAJOR / 0 MINOR / 0 SUGGESTION.
Operator window (RUNBOOK.md) no longer gated by a review BLOCK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:22:56 +02:00
85a8ee345e fix(migration): re-review MAJOR (30-fix-references) — drop stale repository.directory + test the package.json branch
The /trekreview re-review's high-effort deep read of 30-fix-references.mjs
surfaced 2 MAJOR (both pre-existing in the original delivery, not remediation
regressions):

- PLAN_EXECUTE_DRIFT: the rewriter set repository.url but left a monorepo-relative
  repository.directory (e.g. llm-security's plugins/llm-security) intact, so the
  standalone package.json/plugin.json shipped a directory pointing nowhere. Now
  dropped in BOTH the plugin.json (3a) and package.json (3b) object branches;
  idempotency preserved (gated on 'directory' in repository).
- MISSING_TEST: the package.json rewrite branch only ran against voyage/llm-security
  but the only test used graceful-handoff (no package.json) — unguarded. Added a test
  driving both branches against a synthetic llm-security extract (pre-init .git,
  fixtures carrying repository.directory), asserting reconciliation + directory-drop
  on both files + idempotency.

Independent code-correctness reviewer re-verified both RESOLVED, no new issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:22:21 +02:00
fef4b33c97 fix(migration): remediate 6 MAJOR + 3 MINOR trekreview findings + stale rename test
MAJOR
- 9e97cd5 40-validate-standalone.sh: route a target's sc2_gate to its dedicated
  gate (config-audit → 50-config-audit-sc2.sh), mirroring 99-dryrun.sh, so --all
  no longer falsely FAILs config-audit on the machine-locked v5.0.0 tests.
- 1708e90 99-dryrun.sh: assert EXACTLY one tag survives (F5); a partial tag-strip
  no longer silently reports the wrong tag via head -1.
- 4e494c8 99-dryrun.sh: capture the SC2 standalone failing set from the dry-run's
  own prepped extract ($dest), not the 40-validate side-effect clean room.
- aeb6292 00-preflight.sh: assert every map path is whitespace/glob-free, making
  the word-split path handling in 99-dryrun.sh sound.
- 5d112cb extract the SC6 DROP + SC2 regression detectors into sc6-check.sh /
  sc2-regression.sh and add sc-checks.test.mjs — a negative test proving each
  detector FIRES (force-fresh re-extraction would undo a planted file-drop).
- 9e588ca 10-extract.sh re-asserts git filter-repo before use (self-heal runs
  preflight only on a missing mirror); RUNBOOK lists git-filter-repo + python3>=3.6.

MINOR
- bc0f8a7 plugin-map.json: reset ms-ai-architect blob_strip_safe to null
  (00-preflight.sh populates it per run).
- 8d649e9 99-dryrun.sh: gate SC6 behind extract success; a failed extract is
  labelled (extract failed), not a content DROP.
- 4044c49 99-dryrun.sh: guard mktemp — an empty capture is an error, not a
  false zero-regression PASS.

Also: 00-preflight.test.mjs asserted all 3 'renamed' plugins carry >=2 paths, but
llm-security became single-path in 836b8e9 (copilot was a coexisting plugin, not a
rename) — a stale pre-existing failure. Aligned the test to the ratified map and
added a positive single-path lock against re-introducing the 87-file-drop defect.

Verified: full dry-run 11/11, 0 pushes; sc-checks/99-dryrun/40-validate/00-preflight/
60-rewrite suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:17:12 +02:00
86208dab6c fix(migration): BLOCKER 94b83ad — nested external source-object per official CC schema
60-rewrite-marketplace.mjs:87 emitted a flat { source: 'url', url, ref }
shape. The official Claude Code marketplace schema (verified at
code.claude.com/docs/en/plugin-marketplaces) and brief §6 both require the
nested form { source: { source: 'url', url, ref } } — a flat shape would not
resolve at install, breaking SC1/SC3/SC8 for every externalised entry.

- l.87: emit nested source-object
- validate(): branch on object (external) vs string (local ./plugins/) source
- 60-rewrite-marketplace.test.mjs: assert nested voyage.source.source / .url / .ref

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:02:35 +02:00
3065930fc2 docs(migration): trekreview release-gate — BLOCK (1 BLOCKER + 6 MAJOR + 3 MINOR) 2026-06-17 15:52:12 +02:00
5e00f92786 chore(migration): full local dry-run harness + report 2026-06-17 15:07:42 +02:00
836b8e9337 fix(migration): llm-security single-path extraction (copilot was a coexisting plugin, not a rename — dual-path dropped 87 files) 2026-06-17 15:07:24 +02:00
629e468983 docs(migration): operator-window runbook (pilot gate + batch order)
RUNBOOK.md: the single doc the operator follows in the authorized window. Pilot gate on graceful-handoff with the B3 ordering fix (flip the catalog entry to external BEFORE the install-smoke, else the Forgejo/HTTPS/ref chain is untested). Forgejo repo creation via API (auto_init:false, public), per-repo create->push->flip->install-smoke loop, batch order (design-system -> high-churn -> low-churn), thin-catalog last, ref-update flow, and a reversible-until-thinning rollback. References the prior scripts; push-window-aware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:40:58 +02:00
3a5f558a48 chore(migration): catalog-thinning script (staged for window)
70-thin-catalog.sh: renders the thin-catalog end-state into --workspace (never the live tree). git-archive HEAD -> remove plugins/ + shared/ + scripts/sync-design-system.mjs(+test) -> extract CLAUDE.md conventions into CONVENTIONS.md (D6) -> thin the catalog CLAUDE.md -> rewrite README.md with external Forgejo repo links and versions re-stated from plugin-map.json (M15 coupled to the D6 rewrite).

Verify: workspace has no plugins/ or shared/, CONVENTIONS.md present; rewritten README has 0 local links and corrects the stale playground-design-system v0.1 -> v0.6.0. Test 4/4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:38:42 +02:00
9d264f664d chore(migration): mixed-source marketplace.json rewriter (HTTPS+ref)
60-rewrite-marketplace.mjs: flips a named subset (--only) or all (--all) plugin entries from local './plugins/<name>' to external {source:'url', url, ref:v<version>} read from plugin-map.json. Validates output (name+source+description; external => https url under Forgejo /open/ + ref). Writes to a required --out path only — never the live .claude-plugin/marketplace.json (D8). Enables the mixed-source live states (SC3/SC8).

Verify: --only voyage => 9 local + 1 external (https + ref:v5.1.1), 0 ssh. Test 4/4 (mixed-source, --all all-external, refuses --out-less run, rejects unknown name).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:32:03 +02:00
690bbbd68d fix(migration): config-audit SC2 gate (re-seed + back-compat exclusion) + generic validator
50-config-audit-sc2.sh: re-seeds snapshot-default-output in-clone (UPDATE_SNAPSHOT=1), then runs the SC2 gate = full 'find tests -name *.test.mjs' MINUS the 6-file machine-locked v5.0.0 byte-stability surface. 730 tests across 46 files pass standalone.

templates/validate-plugin.generic.sh: ported, parameterized structure validator for the two test-less plugins (okr, human-friendly-style) — STRUCTURE OK on valid plugin.json + non-empty surface + parseable frontmatter; STRUCTURE FAIL otherwise. Reuses the claude-design/tests/validate-plugin.sh pattern.

Brief-correction (operator-ratified 2026-06-17): plan F4 named only json-backcompat + raw-backcompat (2 files). The verified machine-locked surface is 6 — the v5.0.0 fixtures embed the original absolute path AND the claude_md/plugin_hygiene scanners key off a plugins/ ancestor, so findings drift by path (behavioral, not a string rewrite). Dropped clean-room SC2 coverage = config-audit's humanizer/posture-humanizer/scan-orchestrator-humanizer prose-snapshot surface, recorded in the script header + plugin-map.json (not silent). The 3 other v5.0.0-referencing tests (posture, scoring-humanizer, scenario-read-test) are path-independent and stay in the gate. config-audit backlog (out of migration scope): normalize the v5.0.0 fixtures + path-agnostic scanners, then re-include.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:23:38 +02:00
1769657a91 chore(migration): standalone SC2/SC7 validation harness 2026-06-17 12:53:06 +02:00
45322d297a chore(migration): standalone reference-rot rewriter 2026-06-17 12:46:18 +02:00
b59240ad32 chore(migration): per-repo gitignore/gitleaks/mailmap re-rooting 2026-06-17 12:42:06 +02:00
d4dab96134 chore(migration): rename-aware filter-repo extraction driver 2026-06-17 12:38:39 +02:00
653db4785e feat(migration): sync-design-system --source + --check for DS re-home [skip-docs] 2026-06-17 12:34:01 +02:00
f4c36fcda5 chore(migration): preflight script + rename-aware plugin map 2026-06-17 12:29:33 +02:00
a44e37b71f docs(migration): polyrepo migration plan (11 steps, adversarially reviewed + revised)
Plan for splitting the monorepo into per-plugin Forgejo repos + a DS repo
+ a thin catalog. Built from the ratified brief via /trekplan: 6-agent
exploration verified the brief and surfaced 6 under-scoped findings
(F1-F6: plugin renames, ssh->https, 148MB blob bloat, config-audit
snapshot path-coupling, tag namespace, README link rot). plan-critic
REVISE on first draft (3 blockers + 8 majors) all addressed after
re-verifying contested facts; scope-guardian ALIGNED. Local Claude half
(Steps 1-11) is fully reversible NULL-push; operator window is a separate
runbook. Held unpushed per D8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:49:22 +02:00
f2d41c8781 docs(migration): ratify polyrepo-migration brief D1-D8
Flip status DRAFT -> RATIFIED. All eight decision blocks ratified as
recommended on 2026-06-17:
- D1 bare slug open/<plugin>
- D2 filter-repo preserve on a fresh clone + pre-polyrepo-archive tag
- D3 own DS repo, consumers keep vendoring
- D4 tag-pin (ref vX.Y.Z)
- D5 DS first -> high-churn -> low-churn -> thin catalog last
- D6 catalog keeps json/README/GOVERNANCE/conventions; shared+sync leave
- D7 per-repo .gitignore re-rooted; local state stays uncommitted
- D8 no push until one authorized weekend window

Next: /trekplan --brief in a fresh session at repo root.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:02:09 +02:00
99ca17235b docs(linkedin-studio): M0 implementation plan (18 steps, adversarially reviewed + revised)
/trekplan on docs/m0/brief.md -> docs/m0/plan.md. 8 exploration agents.
plan-critic REPLAN on v1 (3 blockers + 5 majors + 5 minors); all 13 closed.
scope-guardian ALIGNED (SC1-SC7 + D1-D6 covered). Validator 18 steps / 0 errors.
Local only - not pushed (planning push-policy).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 09:46:28 +02:00
a1000212b4 docs(linkedin-studio): M0 per-user data-dir migration brief (ratified)
Grounds the architecture-first M0 workstream (v0.4.0 -> v1.0.0). Three
read-only mapping passes corrected the surface from STATE.md's stale 108
to 195 references (~138 in commands/), of which ~186 are .md prose and
only ~3 executable seams do real I/O. Six decisions (D1-D6) ratified.

Brief only; M0 is not implemented. Next step is /trekplan in a fresh
session. Local commit, not pushed (operator-decided).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:34:55 +02:00
053391285b docs(linkedin-studio): simplify README 663 → 260 lines
Trim the front-door README to essentials: value prop, two engines, quick start, full command overview, compact 19-agent roster (lint-required model surface), merged boundaries, quality rules, 2 examples, license. Deep reference (knowledge base, hooks/skills/personalization/config/analytics internals, agent pipeline) now points to references/ and CLAUDE.md; full version history points to CHANGELOG. The pre-1.0 note is gap-framed (what is not done yet), not a promised roadmap. No functionality changed; lint green (74/0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 19:40:25 +02:00
64a3b0b84d chore(linkedin-studio): re-baseline 4.1.0 → 0.4.0 (honest pre-1.0 maturity)
Reset version declarations to v0.4.0 across plugin.json, README badge + version-history, CLAUDE.md header, and the root marketplace README entry. The 1.0.0–4.1.0 numbering reflected ambition, not maturity: user data still lives inside the plugin tree (.gitignore-defended), no command has passed the hardening quality-gate, command testing is incomplete, and there is no GUI. A deliberate marketplace downgrade (existing installs will not auto-pull a lower number). Path to v1.0.0 = architecture (M0 per-user data-dir migration) + hardening + command testing + GUI. No functionality changed — only version declarations; all prior changelog/history preserved. Lint green (Passed 74 / Failed 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 19:13:35 +02:00
019fe4ed0b docs(linkedin-studio): replace v1 autonomous hardening plan with v2 interactive
v1 (autonomous 5-step + reviewer swarm + /trekreview gate) produced confident
fabrications and burned context for what are usually 0-1 line edits. v2 is a
slow, dialogic, one-command-per-session gate with the operator as truth source
and every mechanical claim tool-grounded. Method, command-class predicates,
session queue, and STATE.md end-of-session handoff are documented in plan.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 07:47:17 +02:00
a41cc54e73 fix(linkedin-studio): S1 hardening — method calibration (quick) + Start journey
Hardening phase S1. Tightened the hook character bound from one-sided
(under 140) to the full canonical 110-140 band across the Start-journey
creation surfaces, matching hooks/prompts/content-quality-gate.md:
- quick.md: hook band on Step 2 + Step 5 checklist; added a buzzword check
  and a 150-500 length-band check; checklist tally 6 -> 7.
- onboarding.md: hook band in Phase 3.2 + 3.3 (length + no-links already present).
- first-post.md: hook band in Step 4 + Step 5 (length + no-links already present).
- setup.md: zero-edit pass (all four axes already satisfied).

Adds docs/hardening/log.md (per-command audit trail, 5-step method) and
docs/hardening/review.md (cold /trekreview: ALLOW, 0 BLOCKER/0 MAJOR/1 MINOR).
Lint Failed:0, counts 29/19/25/6 unchanged. No structural/version churn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 06:46:11 +02:00
2f90880f7a docs(linkedin-studio): hardening-phase foundation (brief + adversarially-reviewed plan)
New phase after the baseline-audit remediation (S1-S17, 2633d32, complete):
a command-hardening pass that simulates each of the 29 commands and tightens
it to its stated intention (intention-fidelity + prompt-quality only — no
structural redesign, no new features, no GUI/M0). Runs over ~8 journey-grouped
Voyage sessions, gated by lint + /trekreview ALLOW before push.

Foundation laid this session (execution starts next session):
- docs/hardening/brief.md (valid; 3 locked forks: hybrid simulation,
  intention-fidelity+prompt-quality, per-journey cadence; research skipped —
  the 2026 bar is frozen in algorithm-signals-reference.md)
- docs/hardening/plan.md (8 sessions, Grade B+ 87)

Adversarial review: brief-reviewer PROCEED_WITH_RISKS (3 findings folded:
self-graded quality axis, SC-H deferral contradiction, no stopping rule);
plan-critic REVISE (2 blockers + 10 major — all addressed: anchored coverage
predicate kills substring false-positives, full-session cold-reviewer oracle
on concrete before/after output, per-type mechanical predicate, Failed:0 gate
not literal-74, S1 HALT circuit-breaker); scope-guardian ALIGNED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 05:31:52 +02:00
2633d329b2 docs(linkedin-studio): S17 — C13–C46 triage (0 still-real) closes audit remediation
Cold-read triage of the ~34 uncalibrated baseline-audit findings (C13–C46)
that never got a second hostile pass. An independent Opus reader classified
each against the current code; every disposition was grep-verified in the
main session.

Result: 0 still-real, 23 already-fixed, 1 outdated-drop (24 grouped/sub-claim
entries). No inline code fix needed — the v4.0.0 + S13–S16 remediation had
already closed every still-real item (dead lint, 11 orphan agents, carousel
full-deck clipboard, router tiering, onboarding inline, de-AI gate, video gate,
post-feedback-monitor->Opus, series-path parameterization, SKILL roster).

Deliverable: docs/remediation/c13-c46-triage.md (disposition record) +
docs/remediation/review.md (S17 review, ALLOW). /trekreview: brief-conformance
0 findings; code-correctness 2 MAJOR in the triage doc's own prose (one
overclaim, one line-pointer) FIXED in-session — no false-green disposition.

Gate: test-runner.sh 74/0/0, hooks node --test 98/98, analytics 116/116.
M0 (per-user data-dir migration) deferred to the UI track. Remediation COMPLETE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 04:45:11 +02:00
55c94ee964 feat(linkedin-studio): S16 — optional manual saves in analytics + close deferred onboarding Write MAJOR
Lifts the original v4.0.0 Non-Goal: an optional, manually-entered `saves`
metric through the analytics layer, built location-agnostic (option c) so
UI-brief §9b/M0 relocates the data dir in one place later.

- types: PostMetrics.saves? + Weekly/Monthly summary.totalSaves? (optional);
  new RankableMetric type for the always-numeric index-access whitelist
- parser: dedicated parseOptionalCount() — blank/non-numeric/negative -> undefined
  ("unknown != 0"), genuine 0 kept; saves NOT folded into engagementRate
- reports: totalSaves set only when >=1 post carries saves (backward-compat)
- cli: saves surfaced in import summary + weekly/monthly totals + per-post
- S16-pre: onboarding.md allowed-tools gains Write (closes S15-deferred MAJOR)
- docs (three-doc rule): plugin README boundary + analytics README + root README
  + plugin CLAUDE.md + CHANGELOG; dwell stays explicitly unmeasurable

Independent /trekreview: brief-conformance 0 findings; code-correctness 2 MAJOR
(own lockstep misses) FIXED in-session (parseOptionalCount + edge tests). Gate:
tsc clean, analytics 116/116, lint 74/0/0, hooks 98/98. Within-v4.1.0 refinement
(no surface/count/version change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 22:23:12 +02:00
8c52bdb2e4 fix(linkedin-studio): S15 — UX finish §6c (B1 onboarding inline-draft + B3 carousel full-deck clipboard)
B2 (router tiering) was already delivered in S14, so S15 = B1 + B3 only.
No surface/count/version change -> within-v4.1.0 refinement (S11-S13 precedent).

- B1 (commands/onboarding.md): replace the "Run /linkedin:first-post" dead-end
  hand-off in Phase 3 with the first-post drafting steps embedded inline (3.1 topic
  -> 3.2 3-line draft -> 3.3 QC -> 3.4 present+clipboard -> 3.5 state-update that sets
  first_post_date). Wizard now yields a draft in-flow; 0 dead-end strings. Stays within
  the existing allowed-tools (Read/Bash/AskUserQuestion); UI-brief §12b scope-guard
  honored (no provider seams / progressive-disclosure added).
- B3 (commands/carousel.md): Step 6 now assembles the ENTIRE deck (every slide's copy
  + the caption) into the clipboard payload, not just the caption; full-deck assembly
  precedes the clipboard-helper.mjs call.

Independent /trekreview (2 Opus reviewers): brief-conformance 0 findings; code-correctness
1 MAJOR that is PRE-EXISTING and out of S15 scope (onboarding Phase 2 saves need Write in
allowed-tools; lines 142/157, untouched by the S15 diff) -> DEFERRED to next session per
"ekte design-funn -> neste sesjon". Verdict ALLOW for the delivered scope (not a WARN-override).

Gate: test-runner.sh 74/0/0; node --test 98/98; commands=29; v4.1.0 unchanged.
See docs/remediation/review.md for the full record (ALLOW + 1 deferred MAJOR).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:44:34 +02:00
baca30feb1 feat(linkedin-studio): S14 — journey layer (create/measure front-doors + 5-journey router), v4.1.0
14a's cold command-rationalization found ZERO redundancy across the 27 commands
(no defensible merge/cut), so the operator reframed S14 from "merge/cut" to
"add a journey layer over the kept atomics".

- Add /linkedin:create + /linkedin:measure — delegate-only guided front-doors
  (Read/Glob/AskUserQuestion only; route to the command that owns the work)
- Re-tier commands/linkedin.md into 5 journeys (Start/Create/Engage/Measure/Grow);
  onboarding/strategy elevated as Start/Grow front-doors; Engage = calendar+firsthour tier
- 14a honesty nits: router now lists firsthour; calendar cross-links to firsthour;
  competitive confirmed UNGATED (the claimed 1K-gating inconsistency was unfounded)
- Lockstep: EXPECT_COMMANDS 27->29, v4.0.0->4.1.0 across plugin.json / README badges /
  plugin+root CLAUDE.md / README / CHANGELOG; new README commands-badge lint guard
- 14a deliverable corrected: multiplatform entry added (26/27 -> 27/27), header counts,
  competitive nit withdrawn
- Independent /trekreview (2 Opus reviewers) raised 3 MAJORs (stale commands badge,
  router competitive self-contradiction, STATE.md count) — ALL remediated in-session;
  verdict ALLOW

Gate: test-runner.sh 74/0/0; node --test 98/98; commands=29; v4.1.0 consistent.
Additive/minor — no command removed/renamed/behavior-changed; reload registers create+measure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:27:06 +02:00
431a893f7c fix(linkedin-studio): S13 — close S12 WARN ($-scalar + false-green test) + $-safety lint guard
Closes the 2 grep/Read-verified findings from the S12 cold full-brief re-review
(docs/remediation/review.md, WARN 0/1/1/0, 0 dropped) and closes the $-injection
CLASS — not the line — across the whole state-updater.mjs mutation surface.

See docs/remediation/review.md (S13 ALLOW, 0/0/0/0) for the full closure record:
replaceField -> replacement function; the 3 additive-insert sites -> functions
(m === $1, behavior-preserving); a scalar assert.match pins last_post_topic; and a
behavioral, coverage-complete, self-testing Section 12 guard (check-replace-safety.mjs)
that is mutation-proven. Docs three-doc + residuals updated. test-runner.sh 71/0/0,
node --test 98/98.
2026-05-30 19:12:45 +02:00
36f79dd702 fix(linkedin-studio): S12 — close S11 re-review + render-chain-propagation lint guard
Closes the 2 grep-verified findings from the S11 cold full-brief re-review
(docs/remediation/review.md, BLOCK 1/0/1/0, 0 dropped). Both were the NEXT
RING of the meta-class S10/S11 converged: a propagation miss — the fix had
landed where the SC named the file, not in the render-source it depends on.

BLOCKER (command->reference propagation): references/ab-testing-framework.md:166
still shipped the banned A/B "Significant? (>20%)" Yes/No verdict column while
commands/ab-test.md (which RENDERS from it, inlined at :30, presented at :69)
had been cleaned to the honest "Directional?" framing. Re-framed the reference
result template to match the command verbatim (header + the directional note)
and retuned :38 "20% significance threshold" -> "minimum-meaningful-difference
threshold". The whole render chain is now significance-verdict-free.

MINOR ($-replacement, class-closed not line-patched): the newest-first section
appends/rewrites in hooks/scripts/state-updater.mjs passed a replacement STRING
embedding untrusted user content to String.replace, so dollar-sequences
($1 / $& / dollar-backtick / dollar-apostrophe / $$) in a topic/hook/partner
(e.g. "$100 budget cut") re-injected the captured heading and dropped
characters, silently corrupting state. Converted all 5 content-bearing sites
(Recent Posts, prune, Milestone Log, First-Hour, Outreach) to replacement
FUNCTIONS; the 3 remaining $1 sites only interpolate date scalars. +4
$-bearing regression tests — incl. the prune fixture, which itself had to
switch to a function (the bug bit the fixture as it was being written).

META (generalize the lint to the propagation-miss class): new test-runner.sh
Section 11 — render-chain propagation guard. Forbids the significance-verdict
column (Significant? adjacent to "(" or a table pipe) across the WHOLE render
chain (commands + every inlined reference + adjacent surfaces), with a
permanent non-vacuity self-test (3 verdict forms caught, 6 legitimate
Significant/significance/Directional? forms ignored) and an e2e mutation-proof.
Generalizes S10/S11's "fix the class, not the line" to command->reference.

Pre-patch render-chain sweep confirmed ab-testing-framework.md was the SOLE
propagation survivor (so a 6th review finds no 3rd). test-runner.sh 70/0/0;
node --test 98/98. CLAUDE.md lint enumeration synced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:42:25 +02:00
433c2efb3d fix(linkedin-studio): S11 — model-name declaration drift + model-consistency lint guard
Cold full-brief re-review (S10) reached a class the S7->S9 algorithm-stat lens
never did:

BLOCKER — post-feedback-monitor published as Haiku in four surfaces
(README:259, skills/linkedin-studio:159 with wrong color Green too,
skills/linkedin-analytics:41, agents-capability-matrix:20) while
agents/post-feedback-monitor.md runs Opus. v4.0.0's Opus promotion never
reached the user-facing tables. Synced all to Opus/Lime. Refreshed
agents-capability-matrix.md (frozen at the v2.0 14-agent era): header 14->19,
+5 missing longform agents, tier counts Opus 2->8 / Haiku 1->0, longform-gate
diagram updated to the real 8-Opus-agent chain.

MAJOR — de-branded docs/plan-fullspektrum-innholdsmotor.md:70 (model brand +
jan-2026 asserted as fact -> no-name/no-month relevance-model phrasing). It was
the only tracked survivor; the rest live in gitignored ROADMAP.md /
.claude/research/ (not shipped, out of honesty scope).

META — added Section 10 model-consistency guard
(scripts/check-model-consistency.mjs): each agents/*.md model: must match every
surface declaration AND the canonical rosters must list all 19 agents.
Permanent non-vacuity self-test + e2e mutation-proven.

Pre-patch sweep confirmed post-feedback-monitor was the sole drifted agent
(89 model rows, 0 other mismatches). test-runner.sh 68/0/0, node --test 94/94.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:05:37 +02:00
853cad3ade fix(linkedin-studio): S10 — generalize stale-stat lint to the pattern class + permanent non-vacuity self-test
Closes the S9 re-review (1 BLOCKER + 2 MAJOR, all grep-verified). The survivor
set converged 8 -> 6 -> 2; this closes the meta-problem behind the convergence,
not just the two lines.

BLOCKER — references/glossary.md:10: drop the fabricated "150-parameter
foundation model" (a garbled 150B that the S9 enumerative grep/lint, requiring a
"B"/"billion", could not match). Reframe to "a real input to LinkedIn's 2026
relevance-ranking model" with no parameter count, citing
algorithm-signals-reference.md inline — which makes the :12 "Used in" provenance
accurate (the reference does state the relevance-ranking framing; it never stated
"150-parameter").

MAJOR — CHANGELOG.md:308: de-brand "360Brew profile optimization (January 2026
algorithm update)" -> "Profile/topic-relevance optimization". Removes the
unpublishable brand + asserted Jan-2026 date, honouring v4.0.0's "removed
everywhere" claim. It was the only STALE_STATS hit in CHANGELOG.

MAJOR — scripts/test-runner.sh: the rebuilt lint was enumerative on surface form.
Generalize it to the PATTERN CLASS so the same grep that defines the SC fails on
any surface form, present or future:
  - STALE_STATS model token: "150 ?B param|150 billion param"
      -> "[0-9]+[ -]?(B|billion)?[ -]?param"
    (covers 150-parameter / 150B param / 150 billion param). This robustifies the
    review's literal suggestion "[0-9]+[ -]?(B|billion )?param", which missed the
    space form "150B param"; the separator is moved out of the group.
  - STAT_HITS grep scope += CHANGELOG.md (the 360Brew survivor lived outside it).
  - Permanent non-vacuity SELF-TEST before the real scan: 13 forbidden probes must
    match (incl. the exact "150-parameter" survivor), 8 legitimate probes must not
    ("Language parameter", "parameterized", "different parameters",
    "175-milliarders parametermodell", 5x5x5, cadence, pixel dims, "10x your
    reach"). S7->S9 each shipped a green lint because the proof was run by hand and
    never committed; this makes narrowing STALE_STATS fail the suite.

Verification: test-runner.sh 67/0/0 exit 0 (was 66/0/0; +1 self-test);
node --test 94/94; broadened exhaustive grep across the tree -> zero survivors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 13:48:37 +02:00
0c9c02a2b1 fix(linkedin-studio): S9 — full algorithm-magnitude sweep + lint rebuilt to the criterion
Closes the S8 re-review (BLOCK 3/4/1). The S8 fix patched only the 2 strings S7 named; the re-review found 6 more same-class survivors. Per the systemic read, this is a comprehensive sweep, not a per-line patch.

Reconciled every retired engagement-coefficient + model-fact survivor against the canonical references/algorithm-signals-reference.md (order, not coefficients; comment ≈ 2x a like; no model name/params):
- glossary.md: coefficient table + Save-Signal '10x weight' → canonical ordering (citation now true)
- engagement-frameworks.md, analytics-interpreter.md, content-optimizer.md, pipeline.md, engagement-coach.md: the 10x/8x/7-9x/2.5x/0.2x system (incl. 4 survivors the re-review did not cite) → ordering
- playbook: '15x more algorithmic boost' + video '5x more conversations' → directional, sourced
- profile.md + linkedin-voice/SKILL.md: '150B parameter foundation model' → '2026 relevance-ranking model'
- quality-scorecard.md: '360Brew Validation' → topic-relevance framing
- setup.md: 'thought leadership plugin' → 'LinkedIn Studio plugin'

Lint (MAJOR 4): rebuilt scripts/test-runner.sh STALE_STATS to forbid EVERY retired-class phrasing (not the 2 S7 strings) + widened scope to assets/checklists/. Targets retired phrasings (7-9x, (10x), '10x weight', '5x more conversations'), NOT bare 10x/15x/5x (legit 5x5x5 / cadence / pixel-dims / '10x your reach' hyperbole). Proven non-vacuous: catches all 10 retired strings, ignores all 10 legit uses.

Tests (MAJOR 7): added no-anchor fall-through tests for recordFirstHourPlan + recordOutreachContact (date scalar not written/reported, section still appended). MINOR 8: reflowed newsletter.md content-repurposer wiring onto one line.

test-runner.sh 66/0/0; node --test 94/94 (was 92, +2). NO push until /trekreview re-clears the gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 09:56:49 +02:00
18b198f655 fix(linkedin-studio): close v4.0.0 audit review findings (S8)
Close the 5 findings from the S7 /trekreview release gate (review.md, verdict BLOCK):

- BLOCKER: comment-multiplier "5x" reconciled to the canonical order-only framing
  (no fixed multiplier) in agents/engagement-coach.md, linkedin-growth-playbook,
  linkedin-formats.md — per algorithm-signals-reference.md ("do not quote a comment
  multiplier").
- BLOCKER: carousel rate "6.60%/6.6% (highest)" reconciled to "~7% top organic
  format" in linkedin-formats.md:42 (was self-contradicting :50) and
  assets/templates/carousel-templates.md.
- Lint hardening: test-runner.sh STALE_STATS now matches 6.60% + the 5x comment
  folklore and scans agents/ + assets/templates/ — the grep that defines the
  Phase-0 criterion now catches both BLOCKERs.
- MAJOR: onboarding.md command count 26 -> 27.
- MAJOR: add section-append-branch (production-path) tests for recordFirstHourPlan
  + recordOutreachContact against a template-layout fixture.
- MINOR: move date-scalar changes.push inside the write branch in state-updater.mjs.

Verify: node --test hooks/scripts/__tests__/*.test.mjs -> 92/92; bash
scripts/test-runner.sh -> 66/0/0. NO push until /trekreview re-confirms ALLOW/WARN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 09:27:15 +02:00
1fa2cc945e chore(linkedin-studio): release v4.0.0 — counts, three-doc, CHANGELOG (clears [skip-docs] debt)
Step 21 (remediation Wave 4 / S6, SOLO): finalize the audit-remediation as
v4.0.0. Version 3.1.0 -> 4.0.0 across all current-version declarations; counts
reconciled to the ls-derived source of truth (19 agents / 27 commands / 6 skills
/ 9 hooks / 25 reference docs / 16 newsletter phases — Step 20 confirmed NO
TRIM); three-doc sync (plugin README + plugin CLAUDE.md + root README) clears the
[skip-docs] debt accumulated across Wave 1-4; CHANGELOG v4.0.0 entry summarizing
Steps 1-20.

Scope additions beyond the plan's literal Files list, all version-sync or
[skip-docs]-debt in nature (flagged, not feature creep):
- ../../CLAUDE.md (root marketplace): linkedin-studio entry v3.1.0 -> v4.0.0
  (the version-sync invariant mandates updating every version reference; leaving
  it stale is a real inconsistency).
- scripts/test-runner.sh: added the version-consistency grep the file's own
  Step-1 comment promised ("added in Step 21") — plugin.json version must match
  the README badge, the CLAUDE.md header, and the CHANGELOG top entry — and
  folded the Wave 2 lint gap (plugin.json now covered by the stat-consistency
  scan). 66/0/0, exit 0.
- plugin README: added the missing /linkedin:firsthour command row (Step 16
  [skip-docs] debt) and the 25th reference doc (longform-quality-rules.md) to
  the knowledge-base table; fixed body counts ("All 26 commands" -> 27, "24
  reference documents" -> 25); badges + intro 26 -> 27 commands, 24 -> 25 refs.
- root README + marketplace.json: dropped the unpublishable model brand/date
  ("360Brew" / "January 2026") the algorithm-signal reconciliation already
  removed everywhere inside the plugin.

Surviving "3.1.0" strings are intentional history, not stale declarations: the
README version-history table row, the "vX added Y" attributions in
plugin.json/CLAUDE.md, and the headless-review reload caveat are all
changelog-genre. Every current-version declaration (plugin.json version, README
badge, CLAUDE.md header, root README marker, marketplace.json) reads 4.0.0.

The major bump reflects the remediation's scope plus the reinstall/reload the
newly-wired agents need to register, and consolidates — does not repeat — the
v3.0.0 identity break; it is not a fresh breaking API change (locked operator
decision).

Pre-existing and out of scope (flagged, untouched): a duplicate /linkedin:setup
row in the README command tables.

Verify: bash scripts/test-runner.sh exit 0 (66/0/0); plugin.json + marketplace.json
parse; counts consistent README == CLAUDE.md == root README; stale-count sweep
clean. NO push — /trekreview (S7) is the release gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:49:48 +02:00
0d3da7828d docs(linkedin-studio): measure long-form review-pass overlap, trim where unjustified
Steg 20 (remediation Wave 4 / S5, SOLO): measure whether the 7-agent long-form
review stack carries redundant gates. Method: cross-reference each agent's check
taxonomy against its in-repo fasit fixture; four fixtures (editorial, content,
language, fact-reviewer) target the SAME Del 4 edition, enabling a real
cross-gate overlap comparison on one piece (not a live run — fixtures' own
live-run notes require a reload + cross-repo Maskinrommet access, out of scope).

Finding: every gate has >=1 unique catch on Del 4. The four genuine overlaps
(verbatim repetition, the Vi/Vi-i-Nav quote, the postulated number, the
small-orgs thread) are each justified — a cold re-take (Endring 9's reason to
exist), the same symptom via a different operation (flag-absence vs web-verify),
or two distinct defects sharing a surface topic — with no subsumption either way.
The fact-checker <-> fact-reviewer overlap is load-bearing (the pivot premise
arrived after Step 5, so only the cold re-run caught it).

Decision: NO TRIM. voice-scrubber has no fixture -> inconclusive; redundancy
retained (Step 20 On-failure = skip). Counts unchanged 19 agents / 27 commands;
count contract (EXPECT_AGENTS=19) untouched. test-runner 62/62 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:17:55 +02:00
d77548b351 docs(linkedin-studio): add multi-session/effort banner to newsletter
Step 19 (Wave 4 S4). Adds an expectation banner at the very top of
/linkedin:newsletter: a multi-session, multi-gate process (16 phases),
~4-8+ hours across several sessions, state maintained between sessions, with
a pointer to the short-form commands for feed posts. Sets realistic
expectations before the user starts. Pure docs/content.

Verify: grep -niE 'multi-session|multi-gate|16 phases|~[0-9].*hour' near top
→ matches; test-runner.sh exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:08:26 +02:00
0b4e1bd097 feat(linkedin-studio): honest newsletter distribution + profile-SEO + outreach pipeline
Step 17 (Wave 4 S4) of the audit remediation. Applies research/03 §D5 + the
two S2 residual fixes folded in. No new commands/agents (counts stay 27/19).

Newsletter (commands/newsletter.md): new "Distribution channel" section after
Step 10 teaching the HONEST native-newsletter mechanics — bypasses organic feed
ranking via ONE deduplicated notification per subscriber per edition (NOT a
three-touchpoint blast); the mass invite fires once → ~1-2K follower floor
(wait until you can spend it); realistic cold-start 0-100 subs months 1-3;
discloses non-export / no-canonical / no-read-analytics / per-subscriber decay;
explicit below-vs-above-floor decision rule. Sourced to research/03 D5.

Profile (commands/profile.md): new "Profile SEO" section — headline as the
highest-weight search field + a per-section keyword-target table
(headline/about/experience/skills/featured), consistency-over-stuffing rule.

Outreach (commands/outreach.md): Step 8c persists the pipeline board to tracked
state via the new recordOutreachContact mutation (mirrors Step 16's
recordFirstHourPlan): additive last_outreach_date/outreach_active scalars + a
non-R-initial ## Outreach Pipeline section in state-updater.mjs +
config/state-file.template.md + --record-outreach CLI branch. +7 tests
(state-updater 26→33, full hook suite 83→90).

Residual 1 (growth-playbook:216): 9:16 "distribution boost" → 4:5/1:1 guidance
(9:16 mobile-only opt-in; "immersive distribution" = uncorroborated heuristic).
Residual 2 (video-strategy-guide:300): "3-second test determines 70% retention"
→ "front-load value for muted autoplay" (three-second hook is folklore, not a
LinkedIn signal).

Verify: grep checks 1-5 pass; test-runner.sh exit 0 (stat-consistency green);
state-updater 33/33. [skip-docs] — tre-doc + version bump deferred to Step 21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:07:41 +02:00
743867f90a fix(linkedin-studio): promote post-feedback-monitor to Opus (Opus-default)
post-feedback-monitor was the lone non-Opus/Sonnet agent (model: haiku). It
does human-facing, real-time post-publish coaching — exactly the workload the
standing Opus-default targets — so the Haiku setting both contradicts the
default and underpowers the surface the new /linkedin:firsthour command hands
off to (remediation Step 18).

- agents/post-feedback-monitor.md: model haiku -> opus.
- CLAUDE.md: agent-table model column Haiku -> Opus. No Haiku agent remains.

Verify: grep 'model: haiku' agents/ -> none; CLAUDE.md row shows Opus;
structure lint -> 62/62, counts unchanged.

Plan Step 18 (Wave 4 S3). Counts unchanged.
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 05:36:30 +02:00
3ae8adb6ff feat(linkedin-studio): first-hour/reply-loop command with tracked state
Wire orphan agent #11 (engagement-coach) by giving it a command surface, and
add the tracked first-hour state the plan calls for (remediation Step 16).

- commands/firsthour.md (new, 27th command): post-publish first-hour /
  reply-loop sprint. Delegates plan construction to engagement-coach via
  Task (subagent_type: linkedin-studio:engagement-coach) — returns a grouped
  target list (whales/inner-circle/ICPs/new connections), 2-3 seed
  self-comments + 3-5 CEA replies in the user's voice, and a minute-by-minute
  timeline anchored to publish time. Presents timeline/targets/drafts +
  velocity checkpoints, auto-copies the drafts to clipboard, persists the
  plan, then hands off to post-feedback-monitor for the 48h window.
- hooks/scripts/state-updater.mjs: new pure mutation recordFirstHourPlan()
  mirroring updatePostTracking — additive by contract (inserts
  last_firsthour_date after last_post_date when absent, creates the
  ## First-Hour Plans section when absent, never touches existing fields).
  Section name is deliberately non-R-initial so it stays outside
  pruneContentHistory's "## Recent Posts ... (?=\n## [^R])" capture window.
  + a --record-firsthour CLI branch for parity with the other mutations.
- config/state-file.template.md: additive scalars (last_firsthour_date,
  firsthour_active) + the ## First-Hour Plans section.
- hooks/scripts/__tests__/state-updater.test.mjs: extend (existing file) with
  7 recordFirstHourPlan tests — section creation, field insertion vs in-place
  update (no duplication), round-trip non-interference, graceful empty
  defaults, changes array.
- CLAUDE.md: register the command (## Commands 26 -> 27, table row).
- scripts/test-runner.sh: EXPECT_COMMANDS 26 -> 27 (registration guard).

Verify: grep 'subagent_type: linkedin-studio:engagement-coach' commands/ ->
firsthour.md; node --test state-updater -> 26/26; full hook suite -> 83/83;
bash scripts/test-runner.sh -> exit 0 (62 passed, commands 27/27).

Plan Step 16 (Wave 4 S3).
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 05:35:44 +02:00
29229c0b01 feat(linkedin-studio): video quality gate (captions + aspect guidance, drop 9:16 mandate)
Resolve the video-advice contradiction on the two surfaces this step owns
(research/03 D1-D3):

- commands/video.md: rename the "3-Second Test" to a "Muted-Autoplay Test"
  (front-load value for ~85% muted viewing; the "three-second hook" is
  cross-platform folklore, not a LinkedIn signal); make captions the
  enforceable spec (SRT or native auto-captions, indexed for search);
  aspect ratio as guidance (4:5/1:1 preferred for broad distribution,
  9:16 opt-in for the vertical video tab, crops to 1:1 on desktop); add
  MP4-default + warn-only MOV/AVI + official upload limits to the
  pre-recording reminder.
- references/linkedin-formats.md: reframe the 9:16 "distribution boost"
  as uncorroborated heuristic; 4:5 "deprioritized" -> 4:5/1:1 preferred;
  drop the "3-second hook"; MP4 safe-default + warn-only MOV/AVI; replace
  "good video rewarded more than ever" with the honest "per-video reach
  declining; documents/carousels out-engage video."

algorithm-signals-reference.md (the canonical magnitude source) was
already reconciled in Phase 0 (line 56 reads "declining ... 4:5/1:1
preferred, captions are the enforceable spec") and carries no boost copy,
so it is intentionally untouched here — and it is not in this step's
manifest.

Verify: grep 'must be 9:16|9:16 (1080|3-second hook' video.md
linkedin-formats.md -> none; 'captions' video.md -> 5; 'deprioritized'
linkedin-formats.md -> none; structure lint 61/61.

KNOWN RESIDUAL (flagged for follow-up, NOT in this step's scope):
- references/linkedin-growth-playbook-2025-2026.md:216 still carries a
  "9:16 ... distribution boost" line. That file is owned by Step 17, but
  Step 17's changes are newsletter-distribution — the video line would
  fall through. Fold this into Step 17 or the review gate.
- references/video-strategy-guide.md:300 still says "the 3-second test
  determines 70% of retention." That file is owned by NO plan step
  (orphaned) — needs a home. Surface at session end for an operator
  decision.

Plan Step 15 (Wave 4 S2). Counts unchanged.
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 02:37:51 +02:00
e2ed3eb0aa feat(linkedin-studio): short-form de-AI gate via differentiation-checker + voice-guardian
Wire the orphan differentiation-checker (#10) into the five short-form
creation commands (post/quick/react/carousel/video) as a De-AI /
Differentiation Gate at each command's quality-check step: confirm the
LinkedIn-named substance signals (personal substance, original thinking,
concrete specifics, genuine voice) + a soft engagement-bait check, and
delegate an originality pass to linkedin-studio:differentiation-checker
when the angle risks commodity content. Add Task to allowed-tools in
quick/react/carousel (post/video already had it from Step 13).

Extend (not duplicate) hooks/prompts/voice-guardian.md's AI-pattern
section with the same named signals from research/01 D8 + research/03 D4.
Runtime-loaded prompt — no compile-hooks.py, no hooks.json change
(verified: compile-hooks --check reports no drift).

Test: new hooks/scripts/__tests__/linkedin-content-filter.test.mjs pins
the content/non-content boundary the gate is scoped by (14 tests).
Full hook suite 76/76, structure lint 61/61.

Plan Step 14 (Wave 4 S2). Counts unchanged (26 commands / 19 agents).
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 02:31:41 +02:00
9f65daa288 refactor(linkedin-studio): wire or delete 11 orphan agents (case-by-case) — 9 here, 2 in Steps 14/16
Resolves the orphan-agent audit finding by the locked default: wire all, no deletions, so the agent count stays 19. Per agent, added Task to the target command's allowed-tools and a coherent 'subagent_type: linkedin-studio:<name>' delegation at a real point in the command's flow (not a token grep-match).

Wired (agents 1-9 of 11): video-scripter -> video.md (Step 4); content-optimizer -> post.md (Step 7 refinement) + ab-test.md (2a.4 optimized challenger); analytics-interpreter -> report.md (Step 7, report mode) + analyze.md (Step 2, interpret mode); content-planner -> batch.md (Step 2) + pipeline.md (Step 1); trend-spotter -> batch.md (Step 1) + pipeline.md (Step 1); network-builder -> outreach.md (Step 3a); strategy-advisor -> strategy.md (Step 3); voice-trainer -> setup.md (Step 3a); post-feedback-monitor -> calendar.md (publish action, 48h monitor).

Deferred to their dedicated steps: #10 differentiation-checker -> Step 14 (short-form de-AI gate), #11 engagement-coach -> Step 16 (first-hour command). Namespaced subagent_type form requires a session reload before the wired agents are invokable.

Verify: each of the 9 has >=1 invocation in commands/; structural lint 61/61 (counts 19/26/25/6 intact); agent-fixtures 35/35; hook tests 62/62. Three-doc + version reconciliation deferred to Step 21 per the locked plan [skip-docs].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 01:26:36 +02:00
c5b4c58f4f docs(claude): retire root REMEMBER/TODO/ROADMAP, point session files at STATE.md
Removed the legacy root local-continuity files (NEXT-SESSION-PROMPT.local.md,
REMEMBER.md, ROADMAP.md, TODO.md) per the single STATE.md continuity system.
Their durable content was already covered by CLAUDE.md + GOVERNANCE.md; the
rest was stale. Updated the Sesjonsfiler and Arbeidsflyt sections so CLAUDE.md
no longer mandates the deleted files. Per-plugin legacy files remain and are
flagged for the same cleanup, one plugin at a time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 01:24:28 +02:00
b4648e1255 docs(readme): cut marketplace catalog to current-state, drop embedded version history
README went from 327 to 191 lines (50.6KB -> 12.6KB). Each plugin now
follows one template: tagline + 3-5 capability bullets + key commands +
stats/docs line. All per-version changelog prose removed (lives in each
plugin's CHANGELOG). Fixed stale LinkedIn Studio stats (now 19 agents /
26 commands / 6 skills, verified against plugin files).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 01:14:02 +02:00
3235c4f166 docs(linkedin-studio): reconcile discoverability surfaces + skill naming
Wave 3 / Step 12 of the remediation plan (Phase 1 — usable by a non-author).

Fix the discoverability defects the audit flagged:
- skills/linkedin-studio/SKILL.md (the auto-activating router): self-naming 'the
  LinkedIn thought leadership plugin' -> 'LinkedIn Studio' (v3.0 rename leftover);
  the 'All Agents' table corrected from 14 to the real 19 (added editorial-reviewer,
  voice-scrubber, content-reviewer, language-reviewer, fact-reviewer); the 'All
  Commands' table completed to 26 (added headless-review, pivot, carousel) so it
  routes to newsletter/headless-review/pivot/react.
- commands/onboarding.md: '25 commands' -> '26 commands' (x2); pillar count '3-5
  expertise areas' -> '5 expertise areas' (reconciles onboarding's 3-5 with
  setup.md's '5 core topics' and the CLAUDE.md '5 core expertise areas' rule).
- commands/linkedin.md: router table + numbered option list gain headless-review
  and pivot.

Scope note (operator decision, this session): the plan's Verify grep
'grep -rni "thought leadership" skills/ -> no matches' is broader than the brief's
actual criterion (which targets only the router skill's self-naming). 'thought
leadership' is a plugin-wide DOMAIN term — the '8 Thought Leadership Angles'
framework lives in references/thought-leadership-angles.md and is referenced by ~40
files (glossary, agents, post/video/batch commands). Renaming it only inside skills/
would create cross-file inconsistency; renaming it plugin-wide is a separate
vocabulary migration outside Step 12's discoverability scope. Per operator choice,
the router SELF-NAMING is fixed (brief criterion met) and the 4 remaining skills/
hits (linkedin-content-creation headings + one linkedin-strategy phase cell) are
legitimate domain usage kept consistent with the rest of the tree.

setup.md needed no edit: its pillar number was already '5'; reconciliation was a
one-sided fix in onboarding.

Verify: router no longer says 'thought leadership plugin'; grep -nc 'headless-review'
commands/linkedin.md -> 2; onboarding pillar count '5' matches setup.md; SKILL.md
agent table 19 rows, command table 26 rows; structural lint exit 0 (61 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 01:05:19 +02:00
4052b649f2 docs(linkedin-studio): remove independently-reviewed claim, honest reframe
Wave 3 / Step 11 of the remediation plan (Phase 1 — usable by a non-author).

The README claimed 'the version that ships is the version that's actually been
independently reviewed' — an overclaim the operator correction flagged: the cold
review is a CAPABILITY the pipeline can run, not a guarantee every shipped edition
received. Reframe to the honest capability:
- Long-Form Engine bullet: the draft CAN be frozen and re-read by reviewers with no
  drafting-session context (argument, language, facts, reader-fit) before lock, so a
  cold pass catches what the framing-biased in-session gates miss; run inline (Step
  6.5) or via /linkedin:headless-review for maximum independence. No claim about what
  every shipped edition went through.
- Pipeline diagram annotation '(independent re-read)' -> '(cold, headless)'.

Verify: grep -ni 'independently reviewed' README.md -> no matches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:59:31 +02:00
86e4d5109b docs(linkedin-studio): honest dated API/auto-publish/analytics boundaries
Wave 3 / Step 10 of the remediation plan (Phase 1 — usable by a non-author), per
research/02.

State what the plugin can and cannot do for a PERSONAL profile, dated 'as of
2026-05', without replacing one false claim with another:
- README.md: new 'Boundaries (as of 2026-05)' section. (a) post-level analytics API
  EXISTS but is partner-gated (vetted Community Management app + verified org +
  Page) — not self-serve; CSV is the practical floor; saves visible natively
  (count-only, ~Sept 2025) with no self-serve API pull. (b) auto-publish is
  technically POSSIBLE self-serve (w_member_social) but deliberately NOT built — a
  design + ToS choice, explicitly 'not an impossibility' (avoids the new false
  'cannot auto-publish' claim). (c) dwell internal-only for organic, not exportable.
- commands/calendar.md: the publish action now states plainly it marks a post YOU
  posted manually as published — the tool does not post on your behalf.
- commands/report.md: dated the saves/no-self-serve-API note ('as of 2026-05').
- commands/import.md: dated 'Why CSV' note — API is partner-gated, CSV is the floor.

Verify: grep -niE 'cannot auto-publish|only way to (get|access)' README.md
commands/*.md -> no matches; grep -ni 'as of 2026' README.md -> 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:58:05 +02:00
8d2968a482 feat(linkedin-studio): make long-form review language configurable [skip-docs]
Wave 3 / Step 9 of the remediation plan (Phase 1 — usable by a non-author).

The long-form review layer shipped Norwegian-locked: language-reviewer graded
unconditional Norwegian, voice-scrubber's gold standard was 'approved Norwegian
editions', and the editorial/content craft gates pointed at a 'skrivekontrakt §C2'
that does not ship. A non-Norwegian adopter would get English prose graded against
Norwegian idiom and a gate that depends on an unshipped contract.

- config/edition-state.template.json: add additive 'language' field (top-level,
  default 'en') + a _doc entry. Threads into the language-dependent agents.
- agents/language-reviewer.md: new 'Language parameter' section — Norwegian-specific
  checks (anglicism->Norwegian idiom, kanselli-stil) apply only when language=='no';
  any other value grades that language's equivalents and never flags idiomatic
  English as an anglicism. Default 'en'.
- agents/voice-scrubber.md: gold standard reframed to 'approved editions in the
  configured language'; the Norwegian-chronicle calibration is the language=='no'
  instantiation.
- agents/editorial-reviewer.md + agents/content-reviewer.md: the in-tree checklist
  is now the operative, self-contained source of truth; Maskinrommet §C2 is an
  optional upstream contract that does NOT ship (available only on the author's
  runs). The gates work for an adopter without it.
- commands/newsletter.md: thread 'language' through the Step 6.5 cold-inputs and the
  per-reviewer call inputs; the writing contract is now 'if it ships'.

Norwegian remains fully working when language: no (the author's case).

fact-reviewer.md was in the plan's file list but needed no change on inspection:
its F1-F4 checks (claims/quotes/numbers/sources) are language-agnostic; its
'Norwegian' mentions are boundary notes vs language-reviewer, which stay correct.

[skip-docs]: three-doc + version reconciliation is Step 21 (pre-review-gate); these
intermediate Wave commits are not pushed before the /trekreview gate.

Verify: edition-state JSON parses + has top-level language 'en'; language-reviewer
has 'language ==' references and no unconditional-Norwegian assertion; editorial
§C2 reframed to in-tree fallback ('operative source', 'does not ship'); agent
fixtures 35/35 pass; structural lint exit 0 (61 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:55:04 +02:00
305b99c0e4 feat(linkedin-studio): parameterize series path + de-brand render output [skip-docs]
Wave 3 / Step 8 of the remediation plan (Phase 1 — usable by a non-author).

The flagship long-form engine shipped bespoke-as-general: a maintainer-private
absolute series path and a hardcoded 'Maskinrommet' brand in the renderers. Both
are now generalized so a non-author can run the pipeline without inheriting the
author's filesystem or publication identity.

- commands/newsletter.md + config/edition-state.template.json: series-root default
  /Users/ktg/repos/maskinrommet/serier -> $HOME/linkedin-series; reconciled the
  prose that called the maskinrommet folder 'the default' so it no longer
  contradicts the new neutral default. LTL_SERIES_ROOT override contract preserved.
- render/build-linkedin.mjs + render/build-carousel.mjs: brand is now an LTL_BRAND
  env-var (empty default -> generic). Samle-post title, carousel footer brand-span,
  and cover-eyebrow fallback are de-branded; empty brand renders clean chrome. The
  operator sets LTL_BRAND=Maskinrommet in their own env to re-brand (same pattern
  as LTL_SERIES_ROOT).
- config/image-credit-caption.template.md: 'Maskinrommet-/serie-badge' -> generic
  'serie-badge'.

Out of scope (Step 9): the residual 'Maskinrommet skrivekontrakt §C2/§A'
references in newsletter.md are the writing-contract generalization, handled next.

[skip-docs]: three-doc + version reconciliation is Step 21 (pre-review-gate, per
plan: 'LAST so it captures everything'). These intermediate Wave commits are NOT
pushed before the /trekreview gate, so the three-doc obligation (which governs
pushed changes) is satisfied at Step 21, not per local checkpoint commit.

Verify: grep -rIn '/Users/ktg' config/ commands/ render/ (excl .local) -> no
matches; grep -rn 'Maskinrommet' render/ -> no matches (de-branded); node --check
on both render scripts -> OK; LTL_SERIES_ROOT still present in newsletter.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:46:05 +02:00
6ed3e2f5a6 fix(linkedin-studio): honest saves/dwell wording, no false tracking claim
Wave 2 / Step 7 of the remediation plan (research/02 D2/D4).

The tool parses the LinkedIn analytics CSV export, which has no saves/dwell — and
there is no self-serve API to pull them. Stop implying it tracks them:
- report.md: replace "Saves (10x weight) and expert comments (7-9x) are the
  highest-impact signals" with honest wording — saves rank highest in the
  engagement ORDER (cite references/algorithm-signals-reference.md, not a restated
  coefficient), but are visible only in native LinkedIn post analytics (count-only,
  ~Sept 2025+) with no self-serve API, so this tool does not auto-track them; dwell
  is internal to LinkedIn for organic posts.
- types.ts: document why PostMetrics intentionally omits saves/dwell (no ingest
  source) so a future contributor does not "add the missing fields".
- strategy.md: reconcile two saves references (signature-content criterion +
  authority scorecard) to say the count is read from native LinkedIn analytics, not
  captured by this tool.

No new metric field, no manual-entry feature (operator Q3).

Verify: report.md has no "Saves (10x"/"highest-impact signals" and does say "no
self-serve API"; tsc --noEmit clean; analytics suite 106/106; structural lint
0 failed (report.md cites the reference, so stat-consistency stays green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:28:23 +02:00
55bfe309eb fix(linkedin-studio): downgrade A/B significance claim to directional
Wave 2 / Step 6 of the remediation plan.

Organic personal-post A/B tests gather a handful of posts per variant — far below
the volume a significance test needs — so the tool must not imply statistical
significance:
- Rename the results-table "Significant?" column to "Directional?" and define it
  as "clears the ~20% minimum-meaningful-difference AND points the same way across
  most posts" — a direction to test further, not a significant result.
- Reword the "20% significance rule" to a minimum-meaningful-difference effect-size
  heuristic (explicitly NOT statistical significance).
- Replace the "3 = Medium, 5+ = High" confidence ladder with a directional-only
  confidence section: treat every result as directional (not significant) given
  realistic volume is well under ~50 conversions/variant; name a direction, not a
  winner.

The 20% minimum-meaningful-difference threshold itself stays — it is a legitimate
effect-size heuristic; only the significance framing was the false claim.

Verify: no "Significant?"/"20% significance" remain; "directional" present;
structural lint 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:25:26 +02:00
911871ff53 fix(linkedin-studio): ship placeholder voice profile, gitignore real, sentinel detection
Wave 2 / Step 5 of the remediation plan (coupled criticals: voice-leak +
placeholder-detection).

Voice profile (the adopter-default leak):
- Ship a PII-free placeholder at authentic-voice-samples.md carrying a
  <!-- VOICE_PLACEHOLDER --> sentinel + neutral default voice principles.
- Migrate the author's real profile to gitignored authentic-voice-samples.local.md
  (already matched by *.local.md; added an explicit, commented .gitignore entry so
  the intent is unmissable). NO git-history rewrite — the historical file is
  attributed authorship, not a secret (per the plan threat model).
- Add authentic-voice-samples.template.md — a clean fill-in template for adopters.
- personalization-score.mjs: detect the sentinel (deterministic) instead of the
  unreliable `[Your Name]` heuristic, so the placeholder scores 0 voice points and
  a populated profile (sentinel removed) earns the 25.
- Both voice writers replace-not-append on the placeholder: setup.md (merge ->
  replace-if-placeholder) and onboarding.md (append -> replace-if-placeholder), so
  populating removes the sentinel; updated setup.md's stale heuristic table.

Operator decisions (deviations from plan-literal, approved this session):
- KEEP the plugin.json author name. The plan said scrub author -> neutral/org, but
  that contradicts its own LICENSE reasoning (intentional MIT attribution) and all
  5 sibling plugins keep author = the author; scrubbing only this one would create
  inconsistency for zero security gain (the name is public-by-design). The voice
  placeholder fully fixes the adopter-inheritance bug.
- Scrub the stale "January 2026 360Brew" brand from the plugin.json description and
  the "360brew" keyword (locked decision: no publishable model name/date). This is
  a Wave-1 propagation miss surfaced here because plugin.json was in Step 5's
  touch-scope.

Flagged for follow-up (NOT done here — out of Session 2 scope):
- The lint's stat-consistency grep (scripts/test-runner.sh) scans references/,
  commands/, skills/, hooks/prompts/, CLAUDE.md, README.md — but NOT
  .claude-plugin/plugin.json, which is why the 360Brew brand slipped Wave 1.
  Needs a Session-1-scoped lint extension to add plugin.json to the scan set.
- Readers (user-prompt-context.mjs, voice-guardian.md, state-update-reminder.md)
  read the tracked .md (placeholder), per the plan. The operator's real voice now
  lives in the gitignored .local.md, which nothing reads. To use it, readers + the
  voice score should prefer .local.md (matching the user-profile.local.md
  precedent). Deferred as a coherence follow-up for operator review.

Test-first: hooks/scripts/__tests__/personalization-score.test.mjs (red on the
placeholder scoring 25 under the old heuristic, green after the sentinel fix). Hook
suite 62/62, structural lint 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:23:32 +02:00
798484bf0c fix(linkedin-studio): anchor analytics root on plugin marker + surface npm install
Wave 2 / Step 4 of the remediation plan (docs/remediation/plan.md).

PRIMARY (the real fresh-clone failure):
- scripts/analytics/node_modules is gitignored, so a fresh clone has neither
  tsx nor csv-parse. Surface an idempotent `npm install --silent` prerequisite
  at point-of-use in report.md (Step 1b) and import.md (Step 4).

DEVIATION FROM PLAN (correction-in-scope, to satisfy the plan's own Verify gate):
- The plan assumed prepending `npm install` was sufficient. Verified it is NOT:
  the commands invoke the CLI with an absolute src/cli.ts path but from the
  user's arbitrary CWD, and `node --import tsx` resolves the `tsx` specifier
  relative to CWD, not the script. There is no global tsx, so the call still
  fails with ERR_MODULE_NOT_FOUND from any CWD other than scripts/analytics.
- Complete fix: invoke the locally-installed tsx by its absolute
  node_modules/.bin/tsx path in all CLI calls (report.md x10, import.md x3), so
  they resolve from any working directory once the install above has run.
  Verified: 0 ERR_MODULE_NOT_FOUND running `report` from /tmp.

SECONDARY (latent correctness / hardening):
- Add findPluginRoot(): walks up to the dir holding .claude-plugin/plugin.json
  and anchors getAnalyticsRoot() on it, falling back to the legacy 4-up count.
  MEASURED that ../../../../ already resolved to the plugin root from BOTH
  src/utils and build/utils (both 4 levels deep), so the plan's "src-vs-build
  depth miscalibration" premise was false — this is correct-by-construction
  hardening (survives a future source move), not a live-bug fix.
- Reconcile cli.ts usage/help text: `node build/cli.js` -> `node --import tsx
  src/cli.ts` (the real runtime).
- Fix report.md troubleshooting: "Verify tsx is available" -> the actual
  install command on ERR_MODULE_NOT_FOUND.

Test-first: scripts/analytics/tests/storage-root.test.ts (red on missing
findPluginRoot export, green after). Full suite 106/106, tsc --noEmit clean,
structural lint 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 21:18:33 +02:00
4700248cc4 fix(linkedin-studio): propagate reconciled algorithm numbers, cite-not-restate
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:32:17 +02:00
a6e2f26913 chore(voyage): consolidate continuity into STATE.md
Remove REMEMBER.md / TODO.md / ROADMAP.md / NEXT-SESSION-PROMPT.local.md
mechanisms per ~/.claude/CLAUDE.md (2026-05-26 rule): the three layers
are STATE.md + auto-memory + plugin-CLAUDE.md; local mechanisms are
abolished and consolidated into the layers + git + docs/-reference.

- .gitignore: drop REMEMBER.md / TODO.md / ROADMAP.md entries; add STATE.md
  (gitignored — overskrives ved sesjonsslutt, lokal state-of-play).
- CLAUDE.md: cross-cutting-invariant note now points to STATE.md instead
  of ROADMAP.md (linter update, same theme).

Forward-looking version specs (v5.2 TDD, v5.3 sesjons-konsolidering,
v5.4 brief-format public contract, v5.5 brief framing alignment) lived
in the gitignored ROADMAP.md and are NOT recoverable from git. If
reactivated they go through the brief→plan pipeline (proper Voyage
discipline anyway).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 20:29:51 +02:00
970f45bd68 fix(linkedin-studio): reconcile algorithm-signals to one sourced statement
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:21:24 +02:00
2f2df94acb fix(linkedin-studio): rebuild dead structural lint to real v3.1 layout
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:18:24 +02:00
a61b818578 docs(linkedin-studio): Voyage remediation setup — brief + research + plan (Phase 0-3)
Audit-remediation Voyage project authored end-to-end this session:
- brief.md (reviewer PROCEED; validator pass) — full Phase 0-3 scope, phased,
  with success criteria refined by research
- research/01-03 — high-effort external swarm + Gemini (Topic 1); reconciled the
  external bar and corrected several audit feature-premises (no publishable model
  name/date; saves UI-visible not API-pullable; auto-publish possible-not-built;
  9:16 not mandatory; newsletter notifications deduplicated not triple; CLI crash
  = missing npm install, depth-bug latent)
- plan.md (21 steps, 7 sessions, 5 waves; validator pass; A- 88/100) — plan-critic
  REVISE (3 blockers + majors) addressed; scope-guardian ALIGNED; gemini Pass-2
  folded in 2 blind spots (git-history decision; lint stat-grep sequencing)

Execution is future sessions (one wave each) via /trekexecute, /trekreview as the
release gate. Audit report stays local until the article ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 19:49:27 +02:00
90fcc1069d docs(linkedin-studio): add honest "not a shortcut" caveat to intro
Set expectations up front: the plugin systematizes the work, it doesn't
substitute for the human effort. Letting AI drive lands you in the
forgettable middle; the judgment, genuine engagement, and effort that
make content worth reading stay with the user. Reinforces the README's
no-hype tone and filters for serious adopters over a viral shortcut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 14:39:17 +02:00
10870107e4 docs(linkedin-studio): drop top What's New section, lead with motivation
The standalone "What's New in v3.1.0" section opened with internal
development narrative (Endring 9, Del 4 run, framing-bias) that means
nothing to a prospective adopter — still leading with a changelog, the
exact pattern the rewrite set out to fix. Follow llm-security: motivation
first, version detail only in the Version History table at the bottom.

- Remove the "## What's New in v3.1.0" section and its TOC entry
- README now flows intro → Why LinkedIn Studio Exists → What Is This?
  → Two Engines → Quick Start
- Nothing lost: v3.1.0 remains in the Version History table

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 13:50:43 +02:00
82c5249ca8 docs(linkedin-studio): rewrite README to match marketplace pattern
The README led with a ~600-word version-by-version wall and stacked
seven "What's New in vX.Y.Z" sections before reaching "What Is This?",
reading as a changelog rather than motivating adoption.

Restructure to the sibling-plugin pattern (config-audit / llm-security
/ voyage):
- Concise motivating intro paragraph (no version enumeration)
- Keep only the latest "What's New" (v3.1.0); full history stays in the
  Version History table at the bottom
- New "Why LinkedIn Studio Exists" section: the real pain (blank-page
  paralysis, inconsistency, algorithm opacity, generic AI slop, the
  360Brew shift) mapped to what the plugin does about each
- New "Two Engines" section framing feed vs. long-form, with the
  pre-lock quality gauntlet as the long-form differentiator
- Refresh "What Is This?" to the LinkedIn Studio / content-engine framing
- Swap the unverified "40+ ideas / Content Matrix" claim for the
  documented "8 universal angles" (verification duty)

All accurate tables (26 commands, 19 agents, 9 hooks, 24 reference
docs) and the full Version History preserved verbatim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 13:46:20 +02:00
e69ea1f4c9 feat(linkedin-studio): v3.1.0 — Endring 9 adversarial review-pakke + per-artefakt personas
Cold, adversarial review package for the long-form pipeline + configurable
per-edition personas. Motivated by Del 4 (Security Champions pivot): the
in-session editor + persona sweep shared the drafting session's framing-bias,
so the shipped version was never independently re-reviewed.

Headless package (9a/9b):
- New Step 6.5 (headless-review) in /linkedin:newsletter, after the persona
  sweep, before lock — the independence layer the in-session gates can't be.
- New standalone /linkedin:headless-review command (run in a fresh session for
  maximum isolation; reconstructs frozen draft + contract + personas from disk).
- 3 new Opus archetypes, each with a cardinal context-isolation block that
  refuses drafting-session framing as "context pollution":
  - content-reviewer (argument integrity C1–C5, ≤8 flags)
  - language-reviewer (Norwegian language L1–L5, ≤10 flags)
  - fact-reviewer (cold re-verification F1–F4, risk-sort + pivot-risk, WebSearch)
- Deliberate redundancy with fact-checker / editorial-reviewer documented so
  the pairs are never de-duplicated.

Pivot-reopen (9c):
- New /linkedin:pivot command: logs articles.NN.pivots[], resets currentPhase,
  un-locks, marks gates to re-run.
- Pivot-detection gate in Step 8 lock precondition (>20% word-count change or
  >2 new sections re-opens cleared gates). Del 4 v8→v11 worked example.

Per-artifact personas (new requirement):
- articles.NN.personas with resolution order (edition-state → series file →
  plugin library → interactive). One or more readers configurable per edition.

Schema/docs:
- edition-state.template.json: additive personas[], pivots[], headlessReview,
  headless-review phase (16 phases); personaSweep.resonance.wordCount baseline.
- 3 fasit fixtures + 3 structural lint tests (Del 4 worked cases).
- Counts: 24→26 commands, 16→19 agents, 15→16 newsletter phases.
- README + CLAUDE.md (plugin + root) + CHANGELOG synced.

Verification: 35 agent-fixture + 59 hook + 20 render tests green. Backward-
compatible (additive state); reload required before the 3 new agents resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 13:01:24 +02:00
e162cdce38 chore: gitignore STATE.md (per-plugin continuity files stay local, not tracked)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:20:18 +02:00
66709b0867 feat(human-friendly-style): v1.1.0 — always show irreversible actions verbatim + discoverability docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 11:53:00 +02:00
b6bb61246b refactor(linkedin)!: rename plugin linkedin-thought-leadership → linkedin-studio (v3.0.0)
BREAKING CHANGE: the marketplace slug, the agent namespace
(linkedin-studio:<agent>), and the runtime state-file path
(~/.claude/linkedin-studio.local.md) all change. Reinstall required;
existing state migrated in place (post metrics, streak, history preserved).
The /linkedin:* commands are unchanged — the command namespace is set
per-command in frontmatter and was always independent of the plugin slug.
Functionality is byte-identical to v2.4.0; this release is pure identity.

- dir + manifests: plugins/linkedin-studio + plugin.json + root marketplace.json
- agent namespace updated in commands/newsletter.md (only functional invoker)
- state path updated in 4 hook scripts + topic-rotation prompt + state template
- catch-all skill dir renamed skills/linkedin-studio (5 functional skills unchanged)
- docs + version bump to 3.0.0 across README badge, CHANGELOG, root README/CLAUDE.md
- historical records (CHANGELOG past entries, docs/ build artifacts,
  config-audit v5.0.0 snapshots) intentionally retain the old slug

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:32:02 +02:00
9df3de795c feat(linkedin): v2.4.0 — editorial-reviewer agent + Step 5.5 craft gate in /linkedin:newsletter
Endring 8 from the change spec (Del 4 production, Maskinrommet). The persona
resonance sweep measures reader-response (does it land?); nothing measured prose
craft or narrative architecture (is it well-made?). In Del 4 every persona
reported PASS, yet the editor found 8 fresh editorial points on first reading —
~6/8 craft/architecture blind spots no agent could see. v2.4.0 adds the missing
editor role.

New Step 5.5 (editorial-review) runs between fact-check (Step 5) and the persona
sweep (Step 6): a new editorial-reviewer agent (Opus) judges two axes —
prosa-handverk (em-dash density, verbatim repetition, postulated numbers,
contradictions, versal-tic) + narrativ-arkitektur (concrete instantiation,
theory-anchored hypotheses, series-title symmetry, equal action per addressee,
un-overloaded conclusion). Returns <=10 flags as direction (never copy), each
BLOCK/REWORK/NICE, operator-gated via SendUserFile. Runs before the persona
sweep so the personas measure resonance instead of stumbling on craft noise.
Mirrors the Maskinrommet writing-contract section C2 (bidirectional mirror rule).

- agents/editorial-reviewer.md (NEW, Opus, orange) + fasit fixture
  (editorial-reviewer-cases.md: Del 4 v5 gold standard, 8 points -> 2 axes +
  severities, 3 BLOCK / 5 REWORK, 6/8 blind spots) + structural lint (7 tests).
- Step 5.5 wired into commands/newsletter.md; pipeline 14 -> 15 phases.
- editorial-review phase + additive editorialReview state in
  config/edition-state.template.json; resumption: factcheck-sweep -> Step 5.5,
  editorial-review -> Step 6 (spec said fact-check; canonical key is
  factcheck-sweep).
- persona-reviewer contract unchanged: editorial-reviewer is supplementary
  (one measures craft, one measures response).
- All doc levels synced (plugin + root README/CLAUDE.md, CHANGELOG, plugin.json
  2.3.0 -> 2.4.0; agents 15 -> 16). 94 tests green.

Acceptance-criterion #8 (live run on Del 4 v5) delivered as fasit fixture:
a live run needs a session reload (new agent not invokable until then) + read
access to the Del 4 v5 draft in Maskinrommet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 06:17:50 +02:00
7ebd28cb0b feat(linkedin): v2.3.0 — Step 7.5 visual-assets phase in /linkedin:newsletter
Endring 7 from the change-spec: make visual assets an explicit pipeline
phase. New Step 7.5 (visual-assets) between annotation (Step 7) and lock
(Step 8): cover (+ optional inline figures) or carousel deck, generated and
operator-gated BEFORE lock so build-linkedin.mjs picks up cover.png at lock
without a post-lock re-render. Pipeline 13 → 14 phases.

- commands/newsletter.md: Step 7.5 section, pipeline overview + build-status,
  resumption table (annotation → 7.5; new visual-assets → 8), Step 8
  precondition, reference-file list.
- config/edition-state.template.json: visual-assets phase + additive
  articles.NN.visualAssets schema (format / cover / figures / carousel).
- config/image-credit-caption.template.md (new): motif + credit + caption
  table, honest-about-AI credit, naming convention.
- Two generation routes, no lock-in: default mcp-image (cover-v<N>-kandidat.png)
  or external cover-raw.png. Operator-gate via SendUserFile → cp to cover.png.
  Carousel branch reuses render/build-carousel.mjs.
- Doc/orchestration-only — no new code. Commands (24) + agents (15) unchanged.
- Version sync 2.2.0 → 2.3.0 across plugin.json, CHANGELOG, README, CLAUDE.md,
  root README + root CLAUDE.md.

Correction: spec claimed build-linkedin.mjs handles fig1-4; verified it does
NOT — it embeds only cover.png by fixed name; figures are referenced in the
draft markdown and uploaded manually. Step 7.5 documents actual behavior.

All 8 acceptance criteria met. JSON valid (14 phases); 20/20 render tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:49:19 +02:00
4ed9717627 feat(linkedin): v2.2.0 — harden longform gates from 2nd production run
Implements the 6-change spec from the Seres-serien production
(linkedin-plugin-endringsspec.md). All acceptance criteria met.

1. Avoid-patterns (modell-/navne-katalog, completeness-over-reader-action,
   self-referential overhead openings) → longform-quality-rules.md (rule 1+3)
   + user-profile.template.md.
2. Persona gate now BLOCKING with explicit hard-fail list (primær mistet meg /
   doesn't own action / sjargong-mur / modell-navne-katalog → BLOCK;
   "JA med store forbehold" = NEI) → persona-reviewer.md + personas.template.md.
3. Fact-check declared orthogonal to narrative strength + post-cutoff
   web-search mandate + high-frequency-error checklist → fact-checker.md.
4. NEW agent voice-scrubber.md (Opus) — de-AI scrub + Norwegian-chronicle
   voice-drift; gold standard = approved Norwegian editions, NOT the English
   post corpus. Wired into newsletter.md Step 4.
5. Operator gates = render+annotate rounds (build-html.mjs to file://) as
   primary flow, AskUserQuestion as receipt/fallback → newsletter.md 2.5+3a.
6. Edition state reconciled with STATE.md (ONE-system). edition-HANDOVER
   template deleted; narrative to <serie>/STATE.md, machine data
   (factcheckLog, personaSweep, immutableRules) to edition-state.json.

Agents 14 to 15; commands unchanged (24). Backward-compatible (additive
state-shape only). Docs updated across all three levels + CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:50:56 +02:00
2a27a7cd6a feat(linkedin): v2.1.0 — skeleton + spine-prose gates BEFORE prose in /linkedin:newsletter
Two new pipeline phases gate the spine before any prose is written:

- Step 2.5 — Skeleton + section pitch: writes <serie>/NN-skjelett.md with
  the five-line spine (premiss / problem / anbefaling / gevinst / vei
  videre) + one-line section pitches. Operator-gate (JA / REVIDER / NEI)
  AND parallel persona-skjelett-sweep must both return JA before the
  pipeline can advance.
- Step 3a — Spine prose: one paragraph per section against the gated
  skeleton, ~20-30% of final edition length. Operator-gate on whether the
  axis lands now that there is prose on it. Old Step 3 (Draft) split into
  3a (spine) and 3b (full expansion); 3b owns the multi-session
  draft-cursor logic.

Third persona-reviewer mode added: skjelett (alongside resonans + konverter).
Five spine axes scored HOLDER / TVILER / MANGLER, max 3 direction-only flags,
per-pitch section-pay-in check. Reads the skeleton + pitches only.

Pipeline grows from 11 to 13 phases; commands (24) and agents (14) counts
unchanged. Encodes the Maskinrommet writing-contract section A discipline
(premiss / problem / anbefaling / gevinst / vei videre) into the pipeline.

Empirically motivated by the Seres-serien Del 3 + Del 4 production:
a spine error caught at the skeleton stage costs 5-15 min, the same
error caught at Step 6 (resonance) costs 4-12 h, post-lock it costs a
day of cascading rework (delingstekst, hooks, carousel, doc refs).

Backward-compatible: existing editions stop at currentPhase: "research"
and now resume at Step 2.5 instead of Step 3 — an intended deterministic
improvement, never a contract break. Steps 1, 2, 4-10 bit-for-bit
unchanged. Renderers (build-html.mjs, build-linkedin.mjs) untouched.

New phase strings in edition-state.template.json _doc.phases:
- skeleton-pitch (between research and draft)
- spine-prose (between skeleton-pitch and draft)

Files changed (10):
- plugin.json: 2.0.0 -> 2.1.0
- CHANGELOG.md: new [2.1.0] entry
- CLAUDE.md (plugin + marketplace): pipeline 11->13 phases noted
- README.md (plugin + marketplace): What's New v2.1.0 + version row
- agents/persona-reviewer.md: third mode skjelett added; resonans + konverter unchanged
- commands/newsletter.md: Step 2.5 + 3a + 3b sections, resumption + pipeline tables
- config/edition-state.template.json: 11 -> 13 phases in _doc.phases
- references/longform-quality-rules.md: Rule 8 (Skjelett foer prosa)

Verification: 9/9 criteria PASS pre-commit. Phase strings consistent
across template + command + resumption table. Renderer files git-untouched.
All 11 original step headings preserved (Step 0/1/2/4-10).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 10:09:47 +02:00
8de481399e chore(linkedin): v2.0.0 — import trim, router gating, full doc pass (S20) 2026-05-28 06:53:55 +02:00
930836597e refactor(linkedin): merge analytics + engagement agents 2→1 each (S19)
- analytics-interpreter absorbs performance-reporter (interpret/report modes,
  identical data sources): mode-selector + both output templates kept inline.
- engagement-coach absorbs comment-strategist (5x5x5 + first-hour + CEA
  commenting + target selection + scoring + quality scorecard + daily
  routine). Self-ref at engagement-coach.md:24 rewritten — target is now
  in-file. Model upgraded from haiku to sonnet (absorbed deeper work);
  tools union: Read, Glob, WebSearch.
- 7 ref-files reconciled: commands/linkedin.md (router rules merged),
  skills/linkedin-analytics (row dropped), skills/linkedin-thought-leadership
  (2 rows merged), skills/linkedin-networking (row dropped), CLAUDE.md
  (agents table 16→14, merged rows), README.md (agents table, flow diagram,
  intent table, analytics consumers line), references/glossary.md (3 'Used in'
  refs), scripts/test-runner.sh (EXPECTED_AGENTS list reconciled to current
  14 — also closed lingering S5/S6 gaps for fact-checker/persona-reviewer/
  video-scripter, removed already-deleted content-tracker/personalization-scorer),
  docs/agents-capability-matrix.md (full restructure: header count 16→14,
  agent table, capability grid columns + capabilities, pipeline diagram,
  intent table, model selection table — fixed STATE residual #1 on the
  tracker/pers-scorer stale columns in the same pass).
- Q2 decision (video-scripter → content-repurposer?): KEEP separate.
  Distinct invocation paths (/linkedin:video vs format conversion), distinct
  outputs (timed video script with pacing+captions vs format-translation
  artifact), and newsletter.md already uses content-repurposer for prose
  drafting independently of video-scripter. Net agents/ 16→14.
- agents/README.md dropped from Files (moved to docs/agents-capability-matrix.md
  in S14); literal Verify exits 2 on missing path (logged), corrected Verify
  passes 4/4 predicates. Manifest audit: 2/2 expected paths exist, 13 'CEA'
  occurrences in engagement-coach.md.
- gitleaks: clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 06:19:20 +02:00
0b44f219b6 refactor(linkedin): absorb authority into strategy + profile canon (S18)
- commands/strategy.md: extend frontmatter with authority triggers; insert
  Step 3.6 Authority Building (signature content map, greatest hits, derivative
  planning, content + network signals, action plan, monthly scorecard); add
  algorithm-signals-reference.md to Reference Files.
- commands/audit.md: Step 5.5 trajectory-review and Step 7 report-block
  trajectory section now route to /linkedin:strategy (the canon); Step 6
  profile-alignment routes to /linkedin:profile (the canon); drop
  trajectory-strategy-adjustments.md + algorithm-signals-reference.md from
  loaded references (canons own them).
- commands/analyze.md: 'If Profile-Content Mismatch' subsection routes to
  /linkedin:profile for the per-section checklist; quick-triage list kept.
- commands/authority.md: removed (absorbed by strategy.md).
- commands/linkedin.md: drop authority table row, merge 'Build authority'
  into AskUser item #10 (Growth strategy & authority), renumber 14-23 to
  13-22, broaden strategy routing trigger keywords, drop standalone
  authority routing rule.
- skills/linkedin-strategy/SKILL.md, skills/linkedin-thought-leadership/SKILL.md,
  CLAUDE.md, README.md: drop /linkedin:authority rows; widen /linkedin:strategy
  descriptions to reflect new scope.

Verification:
- Literal Verify (plan.md:704) exits 1 (expected inversion).
- Corrected predicate (test ! -f && ! grep -rqn …) exits 0 — clean.
- Manifest: commands/strategy.md present; [Aa]uthority match count = 24.
- Hooks: python3 hooks/scripts/compile-hooks.py --check → clean.
- Command count: 25 → 24.
- End-to-end Verification §849 (templates|publish|authority|collab|speaking)
  grep across commands/ agents/ skills/ hooks/ README.md CLAUDE.md → empty.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 06:01:43 +02:00
d5ee9d19fa refactor(linkedin): merge collab + speaking into outreach (S17)
Step 18 (fasit S17): merged commands/collab.md + commands/speaking.md
into a single commands/outreach.md covering both tracks under one
outreach/pitch paradigm. Structural twins: same outreach/pitch flow
(readiness → identify → score → format/abstract → outreach by
temperature → production/portfolio → engagement-pod warning →
pipeline tracker → network/progression → results dashboard) with
track-specific detail at each step.

Capability checklist: every function of both predecessors preserved:
- Collab: 5-item readiness, partner search + 25-pt rubric,
  12 formats across 4 maturity tiers, cold/warm/established outreach,
  DM amplification protocol, 5-phase co-creation workflow,
  shared drafting ground rules, pipeline board, health signals,
  Inner Circle tiers, long-term partnership framework.
- Speaking: 100-pt readiness scorecard, event/CFP search +
  research template, Nordic/European tech conference calendar,
  4 talk abstract templates, speaker positioning content calendar,
  demo reel strategy, speaker bio variants (50/100/200 words),
  CFP cover note + organizer outreach (cold/warm),
  post-speaking follow-up sequence, pipeline tracker,
  progression ladder (4 levels).
- Both: engagement-pod warning (post-Mar-2025), quarterly results
  dashboard with both tracks.

Removed commands/collab.md, commands/speaking.md.

Reconciled all 16 route-refs across plugin (collab 8, speaking 8):
- commands/linkedin.md: table row, priority list (items 16-17 → 16),
  routing rules (lines 177-178 → one rule), removed 'outreach' keyword
  from network-builder agent suggestion (now belongs to /linkedin:outreach).
- skills/linkedin-thought-leadership/SKILL.md: pillar table + all-commands
  table (lines 30, 140, 142).
- skills/linkedin-networking/SKILL.md: commands table (lines 29-30).
- CLAUDE.md: commands table (lines 63-64).
- README.md: feature bullet (line 68), commands table (lines 168-169),
  skills pillar table (line 289), ToS table (line 514).

Verify: corrected predicate (test ! -f X && test ! -f Y && ! grep -rqn)
exit 0. Literal Verify (plan.md:681) exit 1 due to the same exit-inversion
bug observed at Step 16/17 — logged for plan-pass at Step 21.

Manifest audit PASS: commands/outreach.md present; [Ss]peaking 40 hits;
min_file_count=1 satisfied; forbidden/bash checks vacuous.

Net commands/: 26 → 25 (matches plan).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 05:41:09 +02:00
e4aa5a61c2 refactor(linkedin): merge publish into calendar — reconcile hook refs (S16)
Step 17 of voyage-build (S16 in plan). publish.md absorbed into calendar.md
as an inline action (Mark as Published flow: queue update, state update,
first-hour battle plan) reusing the same queue-manager.mjs + state-updater.mjs
primitives that publish.md called. calendar.md frontmatter triggers extended
with the publish trigger words; quick-routing block jumps straight to the
publish action when the user prompt names it.

All 21 route-refs reconciled across the 9 expected files, with the 9
hook-script refs (5 in session-start.mjs, 2 in posting-reminder.mjs, 1 in
user-prompt-context.mjs, 1 in hooks/prompts/state-update-reminder.md)
rewritten to call /linkedin:calendar so the runtime guidance no longer
points at a dead command. compile-hooks.py --check reports clean (no
type: prompt hook changes touched hooks.json).

Verify (intent: zero stray refs, file gone): exit 0. Literal Verify in
plan.md:727 logged exit 1 (same exit-inverted && pattern as S15 plan.md:635
— logged for plan-pass at Step 21).

Manifest audit: PASS (expected_paths=calendar.md present; must_contain
[Pp]ublish: 17 matches in calendar.md).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 05:27:36 +02:00
ddd84e4501 refactor(linkedin): merge templates into quick (S15) 2026-05-28 00:07:09 +02:00
92e0a0b4f5 fix(linkedin): close dogfood friction (S14)
Close all 9 friction points from the S13 newsletter dogfood (operator
elected to fix F6-F9 rather than defer):

- F1: namespace all subagent_type calls in newsletter.md to
  linkedin-thought-leadership:<name> (4 sites + canonical note)
- F2: document agent invocation form + reload requirement in CLAUDE.md
  + README.md (reload itself is an operator action)
- F3: add edition-config / edition-delingstekst / edition-HANDOVER
  templates under config/ + wire into Steps 0 and 8 + footer
- F4: reconcile draft path to <serie>/NN-utkast.md (series root)
- F5: de-hardcode series root (explicit arg / LTL_SERIES_ROOT / default)
- F6: config-derive carousel editions (remove Seres CAROUSEL set);
  correct samle comment
- F7: build-html.mjs exits non-zero when zero HTML produced
- F8: guard parseDelingstekst (graceful ENOENT) + correct Step 8 wording
- F9: relocate agents/README.md -> docs/agents-capability-matrix.md

Re-tested: 87/87 plugin tests pass; build-html/build-linkedin behavior
re-verified live. Per-item outcomes logged in dogfood-S13-friction.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:37:39 +02:00
adfa2085fc test(linkedin): dogfood newsletter pipeline end-to-end (S13)
Dogfood mot throwaway-fixture (docs/review/dogfood-serie/, gitignored),
syntetisk tema. Deterministisk ryggrad (build-html/build-linkedin) kjørt
ekte og passerte; gate-laget (fact-check/persona) BLOKKERT — escalert, ikke
tvunget grønt. 9 friksjonspunkter (2 BLOCKER, 3 MAJOR, 4 MINOR) med order-proof
(persona-sweep wiret FØR lås) og per-punkt implicated file for S14 revert-mål.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:57:13 +02:00
8d25dd908e feat(linkedin): longform quality rules + edition resumption wiring (S12) 2026-05-27 22:30:42 +02:00
e031fae3de refactor(linkedin): single newsletter entry + skill trigger + router row (S11) 2026-05-27 22:08:16 +02:00
f24c6e30f7 feat(linkedin): newsletter Step 7-10 lock, delivery, hook-gate, schedule (S10) 2026-05-27 21:42:22 +02:00
fba70870ce feat(linkedin): newsletter Step 5-6 fact-check + persona sweep before lock (S9) 2026-05-27 21:33:43 +02:00
39ae875b96 feat(linkedin): newsletter Step 3-4 draft + consistency (S8) 2026-05-27 21:25:42 +02:00
9b1669a3f6 feat(linkedin): newsletter command skeleton Step 0-2 (S7) 2026-05-27 21:10:39 +02:00
ca5aa4e698 refactor(linkedin): edition-state schema + retire 2 deterministic agents to scripts (S6)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:26:14 +02:00
1faffac303 feat(linkedin): add persona-reviewer agent (2 modes) + fixture (S5) 2026-05-27 20:08:06 +02:00
be03d44c52 feat(linkedin): add fact-checker agent + fixture (S4) 2026-05-27 19:42:41 +02:00
0dc3f903f9 feat(linkedin): add reusable persona library template (S3) 2026-05-27 19:34:39 +02:00
36ad116e43 docs(ltl): add Maskinrommet field-experience brief for upgrade
Krav fra produksjonserfaring (Seres-serien): blokkerende persona-gate,
voice unngaa-moenstre, paastands-ledger + verifiseringsdisiplin, artikkel-skjelett.
Komplementaer til brief-fullspektrum-innholdsmotor.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:52:05 +02:00
6e6ed7dd4e feat(linkedin): generalize build-linkedin to read edition-config.json (S2) 2026-05-26 23:15:26 +02:00
c225ea1dba feat(linkedin): generalize build-html annotation renderer — tables, headings, inline code (S1a)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 23:05:54 +02:00
b4aaf7ae82 docs(linkedin): restructure v2.0.0 plan Execution Strategy → 21 discrete sessions (1 step = 1 session)
Each Session N now maps 1:1 to Step N so /trekcontinue advances exactly one step per session and never runs a full wave at once. Steps + manifests unchanged; validator valid:true. Deprecate NEXT-SESSION-PROMPT in favor of STATE.md handoff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 22:27:08 +02:00
6eff5e8e21 feat(linkedin): migrate render scripts + fonts into plugin (S1)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 22:11:38 +02:00
098726d397 docs(linkedin): Voyage executable plan for v2.0.0 build (21 sessions)
Production plan for lifting LTL v1.2.0 -> v2.0.0 (full-spectrum content
engine + /linkedin:newsletter + render migration + net-fewer commands/agents).

- docs/voyage-build/{brief.md,plan.md}: Voyage project dir. plan.md = 21 steps
  (S1..S20+S1a), each with a per-step Manifest. Validator: valid, 0 warnings.
- docs/{brief,plan}-fullspektrum-innholdsmotor.md + voyage-build-brief.md:
  the directional brief, hardened fasit (authoritative WHAT), and Voyage input.
- Built via /trekplan: confirmatory swarm verified the fasit against real files
  (13 corrections folded in); scope-guardian ALIGNED; plan-critic REPLAN ->
  revised (3 blockers + 5/6 major + 4/5 minor closed).
- plan.html (annotation surface) left untracked: regenerable via annotate.mjs.

Next: /trekexecute --fg --project docs/voyage-build (fresh session) -> S1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:38:52 +02:00
03b8885b6e chore(llm-security): v7.7.2 — language consistency pass
~/.claude/CLAUDE.md specifies English for code and documentation,
Norwegian for dialog only. Norwegian had crept into surface text
across v7.5-v7.7. Translated to English in eight surfaces.

No scanner, hook, or behavior changes — purely surface text.

- 18 skill commands: the HTML Report-step now reads "HTML report:
  [Open in browser]" instead of "HTML-rapport: [Åpne i nettleser]"
- scripts/lib/report-renderers.mjs: key-stat labels, lede defaults,
  table headers, maturity-ladder descriptions, action-tier labels,
  clean buckets, dry-run/apply copy, and JS comments. Regex
  alternations /^high|^høy/ and /resolution|løsning/i preserved.
- playground/llm-security-playground.html: same renderer changes
  mirrored bit-identical, plus playground-only UI strings (catalog,
  breadcrumb aria-label, theme toggle, builder-modal hint,
  guide-panel "no projects yet", delete confirmation, alert/copy).
  Demo-state fixture content for dft-komplett-demo preserved
  (intentional Norwegian persona).
- agents/skill-scanner-agent.md + agents/mcp-scanner-agent.md:
  Generaliseringsgrense + Parallell Read-strategi sections translated
  to Generalization boundary + Parallel Read strategy.
- README.md: playground architecture prose + Recent versions table
  (v7.5.0 — v7.7.1).
- CLAUDE.md: v7.7.1 highlights translated, new v7.7.2 highlights
  added.
- ../../README.md: llm-security v7.5.0 — v7.7.1 bullets.
- ../../CLAUDE.md: llm-security catalog entry.
- docs/scanner-reference.md: six runnable-examples table cells.
- docs/version-history.md: new v7.7.2 entry. v7.5-v7.7 narrative
  sections left in original language (deferred per operator).
- Version bumped 7.7.1 → 7.7.2 in package.json,
  .claude-plugin/plugin.json, README badge + Recent versions,
  CLAUDE.md header + state, docs/version-history.md, playground
  renderHome hardcoded string, root README + CLAUDE.md llm-security
  entries.

Tests: 1820/1820 green. CLI smoke-test: 18/18 commandIds produce
>138 KB self-contained HTML. Browser-dogfood verified.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 06:47:44 +02:00
4f6fc4a2a5 chore(llm-security): v7.7.1 release — formaliser playground UX-strip-fixes
Tre fixes commited etter v7.7.0-tagen (b732eee + 2a6f73f + 81b7beb) viste
versjons-inkonsistens: package.json + plugin.json + README badge + CLAUDE.md
header satt fortsatt på v7.7.0 mens commit-meldinger og inline-kommentarer
refererte v7.7.1 som om det var en release. Per feedback_version_sync.md
skal alle versjonsreferanser stemme — denne commiten lukker gapet.

Endringer:
- package.json: 7.7.0 → 7.7.1
- .claude-plugin/plugin.json: 7.7.0 → 7.7.1
- plugin README badge: version-7.7.0-blue → version-7.7.1-blue
- plugin README "Recent versions"-tabell: ny [7.7.1]-rad
- plugin CLAUDE.md header + v7.7.1-highlights state-seksjon
- docs/version-history.md: ny v7.7.1-seksjon
- playground HTML linje 6935: 'Plugin v7.7.0' → 'Plugin v7.7.1'
  (samme template-litteral som v7.7.0-bumpen ikke fanget, nå synket)
- CHANGELOG.md: ny [7.7.1]-seksjon med full Changed/Fixed/Notes
- rot README llm-security-entry: v7.7.0 → v7.7.1 + ny v7.7.1-bullet
- rot CLAUDE.md plugin-katalog: v7.7.1-bump

Verifisert:
- 1820/1820 tester grønne (pre-compact-flake fyrte ikke)
- CLI rapporterer fornuftig feilmelding på tom input
- Ingen kildefil-treff på 7.7.0 utenfor CHANGELOG/version-history/REMEMBER/TODO/ROADMAP

Ingen ny atferd. Kun versjons-synking + dokumentasjon av tre fixes som var
deployert som ad-hoc-commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:38:27 +02:00
8d79f1e5dc docs(claude-design): polish README — Why this exists + workflow example + recent versions
Add three sections to bring README depth closer to marketplace standard
(was 206 lines, smallest in marketplace; now 298):

- "Why this exists" — names the convergent-middle-ground problem in Claude
  Design, cites Anthropic's frontend-aesthetics cookbook as primary source,
  explains the five-layer prompt scaffold and the plugin's interactive role
- "Workflow example: from idea to prompt" — realistic 8-phase walkthrough
  against the slides preset (Q1 results all-hands deck), including a ~30-line
  sample prompt block showing what Phase 6 actually delivers
- "Recent versions" — v0.1.0 summary + CHANGELOG.md pointer + v1.0
  readiness criteria pointer to REMEMBER.md

No feature changes; pure docs polish. verify.sh passes 41/0/1 (the 1 warn
is the expected SC1 dogfood-missing advisory until operator runs first
real Claude Design session).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:28:31 +02:00
81b7bebca0 fix(llm-security): playground topbar — fjern orgName fra breadcrumb
Etter v7.7.1-strippen ble onboarding fjernet, men topbar viste fortsatt
demo-state-organisasjonsnavnet ('Direktoratet for digital tjeneste-
utvikling') hentet fra shared.organization. Det er ikke meningsfullt
lenger siden onboarding ikke kan endre verdien.

Erstattet med statisk 'llm-security' som nøytralt scope-anker. Crumb-
parameteren (f.eks. 'Katalog') beholdes som suffix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:09:02 +02:00
2a6f73f175 feat(llm-security): playground v7.7.1 — katalog som eneste levende overflate
Operatør-tilbakemelding etter v7.7.0: hjem-overflaten ledet fortsatt med
prosjekter (Re-onboard / Nytt prosjekt / Command-katalog) — katalog var
tredje kort, sekundært bak prosjekt-tracks. Brukeren ba om å fjerne
onboarding + prosjekter og beholde katalog ('Vi legger til funksjonalitet
senere').

Minimum-strip (gammel kode bevart, kun routing + topbar endret):

- renderActive(): tvinger alltid activeSurface til 'catalog'.
  Onboarding/home/project-render-funksjonene er bevart men ikke rutbare.
- Init-default endret fra 'home' til 'catalog' (også for migrerte states).
- Topbar: 'Hjem' og 'Re-onboard'-knappene fjernet. 'Katalog' beholdt
  sammen med Eksporter/Importer/tema-toggle.

Konsekvens: playgrounden lander direkte i Command-katalog (20 kommandoer
med list-view + builder-pane + copy-knapp fra sesjon 1). Project-state +
onboarding-state forblir i IndexedDB men ingen UI-vei dit. Når funksjon-
alitet legges til igjen kan routeren utvides og topbar-knapper restaureres.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:01:18 +02:00
b732eee409 fix(llm-security): playground hardkodet versjon v7.6.1 → v7.7.0
Hjem-overflaten viste fortsatt 'Plugin v7.6.1' i meta-linja fordi
versjon-strengen er hardkodet inline i renderHome (line 6933), ikke
synkronisert med package.json. Fanget post-release.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 20:49:05 +02:00
1985 changed files with 5872 additions and 460803 deletions

View file

@ -12,52 +12,92 @@
"plugins": [
{
"name": "llm-security",
"source": "./plugins/llm-security",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/llm-security.git",
"ref": "v7.8.0"
},
"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": "./plugins/config-audit",
"source": {
"source": "url",
"url": "https://git.fromaitochitta.com/open/config-audit.git",
"ref": "v5.12.5"
},
"description": "Multi-agent workflow for analyzing, reporting, and optimizing Claude Code configuration across your entire machine"
},
{
"name": "voyage",
"source": "./plugins/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-thought-leadership",
"source": "./plugins/linkedin-thought-leadership",
"description": "Build LinkedIn thought leadership with algorithmic understanding, strategic consistency, and authentic engagement. Updated for the January 2026 360Brew algorithm change."
"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": "./plugins/graceful-handoff",
"description": "Produce session-handoff artifacts, commit and push pending work, and print a copy-paste prompt for the next session. Designed for context-constrained models like Opus 4.7."
"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": "./plugins/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": "./plugins/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": "./plugins/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": "./plugins/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": "./plugins/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."
}
]

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
REMEMBER.md
TODO.md
ROADMAP.md
STATE.md
*.local.md
# Per-plugin session directories (plans, research, execution progress)

View file

@ -2,4 +2,4 @@
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-thought-leadership/hooks/prompts/content-quality-gate.md:linkedin-client-id:14
plugins/linkedin-studio/hooks/prompts/content-quality-gate.md:linkedin-client-id:14

101
CLAUDE.md
View file

@ -1,72 +1,39 @@
# ktg-plugin-marketplace
# ktg-plugin-marketplace (catalog)
Open-source Claude Code plugin marketplace. Solo project by Kjell Tore Guttormsen.
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/`.
## Repo-struktur
## What lives here
```
plugins/
ai-psychosis/ v1.0.0 — Interaction awareness (sycophancy, reinforcement loops)
config-audit/ v3.1.0 — Configuration intelligence (health, opportunities, auto-fix, whats-active)
graceful-handoff/ v2.1.0 — Auto-trigger handoff via Stop hook (skill + JSON pipeline + 4-step model-aware context resolution)
linkedin-thought-leadership/ v1.2.0 — LinkedIn content pipeline + analytics
llm-security/ v7.7.0 — Security scanning, auditing, threat modeling + HTML-rapport for alle 18 skill-kommandoer (render-report CLI + canonical ESM-modul som speiles bit-identisk i playground)
ms-ai-architect/ v1.15.0 — Microsoft AI architecture (Cosmo Skyberg persona) + manual KB-refresh slash command + v3 project-view (sidebar med 17 artifacts + main + import-modal overlay, v2-surface fjernet i v1.15.0)
okr/ v1.0.0 — OKR guidance for Norwegian public sector
voyage/ v5.0.3 — Brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline (six-command universal pipeline + multi-session resumption + --gates autonomy chain). /trekbrief, /trekplan, and /trekreview each end by running scripts/annotate.mjs against the just-written .md and printing the file:// link to a self-contained operator-annotation HTML modelled on claude-code-100x/build-site.js: pencil-toggle annotation mode, select text or click any element, choose intent (Fiks/Endre/Spørsmål), comment, sidebar groups by section with delete + Copy Prompt, localStorage persistence per artifact path. v5.0.0 removed the v4.2/v4.3 bespoke playground + /trekrevise + Handover 8; v5.0.1 pointed at /playground document-critique (wrong direction); v5.0.2 was operator-led but too thin; v5.0.3 matches the reference the operator pointed at from day one.
- `.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
shared/
playground-design-system/ v0.6.0 — Aksel/Digdir-aligned CSS design system + JSON schemas + self-hosted Inter/JetBrains Mono/Source Serif 4 fonts. Tier 1 base + Tier 2 + Tier 3 wave 1+2 (20 components) + Tier 4 project-view-arketype (v0.6.0 — sidebar + main + import-modal overlay). Consumed by ms-ai-architect, okr, llm-security, voyage, config-audit.
playground-examples/ — Reference scenarios (ROS-Lier, OKR-Bærum, security-Direktorat) + showcase landing + 12 isolated Tier 3 wave 2 component demos under components/
```
## Catalog maintenance
Hvert plugin er selvstendig med egen CLAUDE.md, README, hooks, agents og commands. `shared/` inneholder marketplace-nivå infrastruktur som flere plugins bygger på.
## Konvensjoner
- **Språk:** Norsk dialog, engelsk kode/docs
- **Commits:** Conventional Commits — `type(scope): description`
- **Git:** Forgejo (`git.fromaitochitta.com/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 alle tre doc-nivåer i SAMME commit eller umiddelbart etter:
1. Plugin `README.md` — detaljert dokumentasjon av endringen
2. Plugin `CLAUDE.md` — arkitektur/oversikt
3. Rot-`README.md` — marketplace-landingssiden (`git.fromaitochitta.com/open/ktg-plugin-marketplace`)
- **Playground-oppdatering:** Ved endring av plugin playground HTML eller delt design-system, følg prosedyren i `shared/PLAYGROUND-MAINTENANCE.md` (4 spor: HTML-endring, DS-endring, screenshots, release).
## Sesjonsfiler (lokale, gitignored)
Alle plugins + root har:
- `REMEMBER.md` — Sesjonsstatus, sist gjort, viktige beslutninger
- `TODO.md` — Nærliggende oppgaver (1-4 uker)
- `ROADMAP.md` — Langsiktig retning (kvartal/halvår)
Disse trackes IKKE i git. Oppdater ved sesjonsslutt.
## Arbeidsflyt
1. `cd` til riktig plugin-mappe
2. Les pluginets CLAUDE.md for kontekst
3. Les REMEMBER.md og TODO.md for sesjonsstatus
4. Jobb innenfor scope
5. Oppdater REMEMBER.md ved avslutning
## Communication patterns
### Linking to local files
When pointing to local files in responses, always use markdown link syntax with a descriptive name:
- Use `[Human-friendly name](file:///absolute/path)` — never bare `file:///...` URLs or autolinks `<file://...>`.
- Always use absolute paths. Never `~/` or relative paths.
- For multiple files, render as a bullet list of named markdown links.
Why: bare `file://` URLs only render the first as clickable across multiple lines. Named markdown links make each entry independently clickable and look cleaner.
Example:
- [Brief](file:///Users/ktg/.../brief.html)
- [Research summary](file:///Users/ktg/.../research/summary.md)
- 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`).

19
CONVENTIONS.md Normal file
View file

@ -0,0 +1,19 @@
# 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).

298
README.md
View file

@ -4,9 +4,7 @@ Open-source Claude Code plugins for AI-assisted development, security, and plann
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.
## AI-generated code disclosure
All code in this marketplace is generated by Claude Code through a dialog-based process. I direct, review, test, and validate; Claude writes. Every commit reflects this — treat the plugins as AI-authored, human-curated.
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.
## Installation
@ -14,295 +12,181 @@ All code in this marketplace is generated by Claude Code through a dialog-based
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
```
Then open Claude Code and type `/plugin` to browse and install plugins from the marketplace.
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.
## Compatibility
- Claude Code CLI, desktop app, and IDE extensions
- macOS, Linux, Windows
- No external dependencies (all scanners and hooks are self-contained)
Each plugin keeps its own full README and CHANGELOG; this page is just the catalog.
---
## Plugins
### [LLM Security](plugins/llm-security/) `v7.7.0`
### [LLM Security](https://git.fromaitochitta.com/open/llm-security) `v7.8.0`
Security scanning, auditing, and threat modeling for agentic AI projects.
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.
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** — 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
- **Automated enforcement** — 9 hooks that block dangerous operations in real time (prompt injection, secrets in code, destructive commands, supply chain guardrails, transcript scanning before context compaction)
- **Deterministic scanning** — 23 Node.js scanners (10 orchestrated + 13 standalone) for byte-level analysis: Shannon entropy, Unicode codepoints, typosquatting detection, taint flow, DNS resolution, git forensics, AI-BOM, attack simulation, IDE extension prescan (VS Code + JetBrains — URL fetch from Marketplace / OpenVSX / direct VSIX / JetBrains Marketplace, hardened ZIP extractor for zip-slip / symlinks / bombs, plus OS sandbox via `sandbox-exec` / `bwrap` so the kernel enforces FS confinement), MCP cumulative-drift baseline reset (E14 — sticky baseline catches slow-burn rug-pulls). Bash-normalize T1-T6 for obfuscation-resistant denylists
- **Advisory analysis** — 20 commands that scan, audit, and model threats with structured reports, letter grades, and actionable remediation
- **Enterprise governance** — Compliance mapping (EU AI Act, NIST AI RMF, ISO 42001), SARIF 2.1.0 output, structured audit trail, policy-as-code, standalone CLI
- **v7.7.0 HTML-rapport for alle 18 skill-kommandoer (2026-05-18)** — Hver `/security <cmd>` som produserer rapport printer nå en klikkbar `file://`-lenke til en self-contained HTML-versjon. Levert over fem sesjoner: (1) playground katalog list-view + builder-pane med copy-knapp; (2) playground prosjekt-surface opprydding (stub-screen + topbar-splitt); (3) 18 inline parserne + rendererne flyttet til canonical ESM-modul `scripts/lib/report-renderers.mjs` (playground beholder bit-identisk inline-kopi siden ESM `import` ikke fungerer fra `file://`); (4) ny zero-dep CLI `scripts/render-report.mjs` — stdin/file/stdout-modus, kebab→camel commandId-routing, inliner 6 DS-stylesheets, ~140 KB self-contained HTML med system-font-fallback, absolutte `file://`-paths for Ghostty cmd-click; (5) alle 18 skills wired (4 i sesjon 4 + 14 i sesjon 5). Ingen scanner- eller hook-atferdsendringer — purely additive
- **v7.6.1 playground visuell-patch (2026-05-06)** — Seks bugs fanget av maintainer ved manuell verifisering i nettleser etter v7.6.0-release. Alle skyldtes mismatch mellom DS-klasser og hvordan playground-rendrere brukte dem (eller manglende DS-implementasjoner av klasser playground-rendrere antok eksisterte): `renderFindingsBlock` brukte `.findings` outer-class (DS' 2-kolonners list+detail-grid) → erstattet med `<section class="report-meta">` + korrekt `findings__list`-mønster; `.report-table` manglet helt i DS men brukes i 7+ rendrere → lokal CSS-implementasjon; `renderPreDeploy` traffic-lights brukte fast 28×28 px `.sm-card__grade` for "PASS"/"PASS-WITH-NOTES"/"FAIL" → bredde-tilpasset status-pill; threat-model matrix-bobler ikke klikkbare → `<button>` med `data-threat-id` + click-handler som scroller til Trusler-tabellen; radar-labels overlappet → SVG 280→380, R 105→125, dynamisk `text-anchor`; `recommendation-card__body` tekstoverflyt → `overflow-wrap: anywhere`. 4/4 fix-spesifikke + 18/18 regresjons-tester passerer. Ingen scanner- eller hook-atferdsendringer
- **v7.6.0 playground Tier 3-referanse-case (2026-05-06)** — Playgroundet er hevet til en visuelt og strukturelt fullført referanse for `shared/playground-design-system/` Tier 3-supplementet. 8 nye DS-komponenter integrert i de 18 rapport-rendererne: `tfa-flow` + `tfa-leg` + `tfa-arrow` (lethal trifecta-kjede med `<button>`-elementer + ARIA), `mat-ladder` + `mat-step` (5-trinns modenhets-stige), `suppressed-group` (narrative-audit), `codepoint-reveal` + `cp-tag/cp-zw/cp-bidi` (Unicode-steganografi), `top-risks` + `top-risk[data-severity]` (rangert top-funn-listing), utvidet `recommendation-card[data-severity]``clean`/`harden`/`audit`/`posture`/`pre-deploy`/`plugin-audit`, `risk-meter` (band-visualisering 0-100 på 5 archetypes), `card--severity-{level}` modifier på findings-cards. Wave 1 (Sesjon 2): `badge--scope-security` (identitets-chip), `verdict-pill-lg` (DS Tier 3-pill på alle 18 rapport-typer), `form-progress` + `fp-step` (onboarding-wizard). Slettet ~30 duplikat-CSS-deklarasjoner (DS vinner cascade). 5 nye DS-helpers + `mapSeverityToCardLevel` + `parseNarrativeAudit`. A11Y-rapport oppdatert. Filendring totalt 10209 → 10677 linjer over 5 sesjoner. Ingen scanner- eller hook-behavior-changes — purely additive surface
- **v7.5.0 playground (2026-05-05)** — Single-file SPA at `plugins/llm-security/playground/llm-security-playground.html` (~10 200 lines) for onboarding, demoer og workshop-bruk uten Claude Code-installasjon. Parsere + renderere for alle 18 produces_report-kommandoer, 18 markdown test-fixtures som kontrakt-anker, komplett demo-prosjekt med alle 18 rapporter ferdig parsed, vendor-synket design-system, 9 Playwright-genererte screenshots. 11 nye `window`-globaler eksponert for testing/automasjon (`__store`, `__navigate`, `__loadDemoState`, `__PARSERS`, `__RENDERERS` …). Bug-fix: `normalizeVerdictText` håndterer GO-WITH-CONDITIONS uten å kollapse til ALLOW. Ingen scanner- eller hook-behavior-changes — purely additive surface
- **v7.4.0 examples + e2e suite (2026-05-05)** — 9 runnable demonstration walkthroughs under `examples/` (lethal-trifecta, mcp-rug-pull, supply-chain-attack, poisoned-claude-md, bash-evasion-gallery, prompt-injection-showcase, malicious-skill-demo, toxic-agent-demo, pre-compact-poisoning) plus three new test suites under `tests/e2e/` (attack-chain, multi-session, scan-pipeline) that prove the framework works as a coordinated system. +45 tests (1777 → 1822), no scanner or hook behavior changes — purely additive surface
- **v8.0.0 env-var deprecation runway (D3, v7.3.0)** — Hook configuration has historically been split between process env-vars and the team-distributable `.llm-security/policy.json` file. Until v7.3.0 the two surfaces could disagree silently. The new `getPolicyValueWithEnvWarn()` helper in `scanners/lib/policy-loader.mjs` now emits a one-time-per-process stderr line whenever both surfaces are explicitly set:
- Affected pairs: `LLM_SECURITY_INJECTION_MODE``injection.mode`, `LLM_SECURITY_TRIFECTA_MODE``trifecta.mode`, `LLM_SECURITY_ESCALATION_WINDOW``trifecta.escalation_window` (new key in `DEFAULT_POLICY`), `LLM_SECURITY_AUDIT_LOG``audit.log_path`
- Env still wins through the v7.x window — no behaviour change today, only a runway signal
- Suppress headless-log noise with `LLM_SECURITY_DEPRECATION_QUIET=1`
- Teams should converge on `policy.json` for distributable configuration before v8.0.0 removes the env-var path
- **Opus 4.7 aligned** — Agent instructions rewritten for literal instruction-following (system card §6.3.1.1), defense-in-depth posture per §5.2.1, production hardening guide
Key commands: `/security posture`, `/security audit`, `/security scan`, `/security ide-scan`, `/security threat-model`
Key commands: `/security posture`, `/security audit`, `/security scan`, `/security ide-scan`, `/security threat-model`, `/security plugin-audit`
6 specialized agents · 23 scanners · 9 hooks · 20 knowledge docs · 9 runnable examples · 1822 tests
→ [Full documentation](plugins/llm-security/README.md)
6 agents · 26 scanners · 9 hooks · 1863 tests · [Full documentation →](https://git.fromaitochitta.com/open/llm-security)
---
### [Config-Audit](plugins/config-audit/) `v5.1.0`
### [Config-Audit](https://git.fromaitochitta.com/open/config-audit) `v5.12.5`
Configuration intelligence for Claude Code — health checks, feature discovery, auto-fix, active-config inventory, reality-based Opus-4.7 token analysis, and plain-language UX that leads with prose ("Fix soon: The same automation is set up more than once") instead of technical IDs.
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.
Claude Code 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 unnecessarily:
- **Health** — 12 deterministic scanners verify correctness across every configuration file (broken imports, deprecated settings, conflicting rules, permission contradictions, Opus-4.7-era token waste, cache-prefix instability, dead tool grants, cross-plugin skill collisions)
- **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, rollback support, and human-in-the-loop workflow
- **What's active** — read-only inventory of plugins, skills, MCP servers, hooks, and CLAUDE.md cascade for a repo, with token estimates
- **Token hotspots**`/config-audit tokens` ranks files by estimated waste across 6 Opus-4.7 patterns (cache-breaking volatile content, redundant tool permissions, deep import chains, oversized cascades, bloated SKILL.md descriptions, MCP tool-schema budget). Optional `--accurate-tokens` calibrates against Anthropic's `count_tokens` API.
- **System-prompt manifest**`/config-audit manifest` ranks every token source (CLAUDE.md cascade, plugins, skills, MCP servers, hooks) by estimated tokens
- **Plain-language UX (v5.1.0)** — default output of all 18 commands leads with prose; findings group by user-impact category (Configuration mistake, Conflict, Wasted tokens, Missed opportunity, Dead config) and urgency phrase (Fix this now → FYI). Pass `--raw` for v5.0.0 verbatim output; `--json` is unchanged and byte-stable.
- **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`, `/config-audit manifest`
Key commands: `/config-audit posture`, `/config-audit feature-gap`, `/config-audit fix`, `/config-audit whats-active`, `/config-audit tokens`
6 agents · 12 scanners · 18 commands · 792+ tests
→ [Full documentation](plugins/config-audit/README.md)
6 agents · 13 scanners · 18 commands · 954+ tests · [Full documentation →](https://git.fromaitochitta.com/open/config-audit)
---
### [Voyage](plugins/voyage/) `v5.1.1`
### [Voyage](https://git.fromaitochitta.com/open/voyage) `v5.9.1`
Deep requirements gathering, research, implementation planning, self-verifying execution, independent post-hoc review, and zero-friction multi-session resumption — with specialized agent swarms, adversarial review, and failure recovery. Six-command (brief, research, plan, execute, review, continue) universal pipeline + adaptive-depth per-phase effort dialog. `/trekbrief`, `/trekplan`, and `/trekreview` render their artifact to a self-contained HTML view and print the `file://` link.
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.
v5.1.1 is a 13-step remediation patch closing 11 of 12 findings from the v5.1.0 review (the SC8 dogfood gate is operator-manual, scheduled for after-execute). Load-bearing bug fixes: YAML-number bypass in `brief-validator` so the gate fires for both quoted and unquoted `brief_version` (#8 + #11). Wiring: a new `lib/profiles/phase-signal-resolver.mjs` helper is invoked from `/trekplan`/`/trekresearch`/`/trekreview`/`/trekexecute` Phase 1, the resolved JSON is captured as `phase_signal_result`, and the `brief-validator --soft` gate is required uniformly across all 4 downstream commands (#9 + #12). Test refactor: runtime SC1 walk for trekbrief + per-tier resolver-output + missing-signals falsification per downstream command + dedicated profile-resolver non-interference test (#1 #2 #3 #4 #6 #7 #10). Documentation: Decision B high-effort behavior locked per command (gemini-bridge pass for `/trekplan`, full swarm + always-on `contrarian-researcher` for `/trekresearch`, skip Pass 3 + coordinator normalization for `/trekreview`, `gates_mode: closed` for `/trekexecute`) + brief Non-Goal/SC1 amendments + REMEMBER dogfood scaffolding. v5.1.1 is additive — no breaking changes against v5.1.0. See `plugins/voyage/CHANGELOG.md` § v5.1.1.
- **`/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
v5.1.0 adds Phase 3.5 to `/trekbrief`: 4 tier-coupled `AskUserQuestion` calls commit an effort level (`low | standard | high`) and an optional `model` (`sonnet | opus`) per downstream phase (`research`, `plan`, `execute`, `review`). The choices land in `brief.md` as `phase_signals:` (or `phase_signals_partial: true` on force-stop). `brief_version: 2.1` activates a validator-side sequencing gate (`BRIEF_V51_MISSING_SIGNALS`) so downstream commands halt with a friendly hint when signals are missing. Composition rule per downstream command: brief signal wins per-phase, profile fills gaps. `effort == low` activates each command's existing `--quick`-equivalent code-path (`/trekexecute` low-effort = `--gates open` + sequential-only). Additive — no breaking changes; pre-2.1 briefs still validate. See `plugins/voyage/CHANGELOG.md` § v5.1.0.
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.
v5.0.3 lands the annotation UX modelled on `~/repos/claude-code-100x/claude-code-100x/build-site.js`: pencil-toggle annotation mode, **select text or click any element to anchor**, choose intent (**Fiks** / **Endre** / **Spørsmål**), write a comment, save. The sidebar groups annotations by section with intent badges; Copy Prompt assembles them into a structured markdown the operator pastes back into Claude. State persists in `localStorage` per artifact path. v5.0.2 was operator-led but too thin (line-click + freeform note, no intent categories). v5.0.1 had pointed at `/playground document-critique` (Claude-leads — wrong direction). v5.0.0 (breaking, kept) removed the v4.2/v4.3 bespoke playground SPA, `/trekrevise`, Handover 8, the supporting `lib/` modules, the Playwright e2e suite, and the `@playwright/test` / `@axe-core/playwright` devDeps. v5.0.3's `scripts/annotate.mjs` is one self-contained zero-dependency Node script. **The operator drives every annotation** — Claude never pre-generates suggestions in this flow. See `plugins/voyage/CHANGELOG.md` § v5.0.0 → § v5.0.3.
v4.0.0 (breaking) renamed the plugin from `ultraplan-local` to **Voyage** and all commands from `/ultra*-local` to `/trek*` to remove name collision with Anthropic's `/ultraplan` and `/ultrareview` features. See `plugins/voyage/TRADEMARKS.md` and `plugins/voyage/CHANGELOG.md`.
Six commands, one pipeline with clear division of labor:
- **`/trekbrief`** — Capture intent. Dynamic, quality-gated interview: a section-driven completeness loop (Phase 3) followed by a `brief-reviewer` stop-gate (Phase 4, max 3 review iterations). Required sections must reach an initial-signal gate AND pass review across completeness, consistency, testability, scope clarity, and research-plan validity before `brief.md` is written. Identifies research topics with copy-paste-ready `/trekresearch` commands. Optional auto-orchestration runs research + planning in foreground. Always interactive.
- **`/trekresearch`** — Gather context. Deep multi-source research with triangulation: 5 local agents + 4 external agents + Gemini bridge, producing structured briefs with confidence ratings. Makes no build decisions.
- **`/trekplan`** — Transform intent into an executable contract. Per-step YAML manifests (`expected_paths`, `commit_message_pattern`, `bash_syntax_check`). Plan-critic is a hard gate on manifest quality. Requires a task brief as input (`--brief` or `--project`). Auto-discovers `architecture/overview.md` when produced upstream and cross-references its `cc_features_proposed` against exploration findings.
- **`/trekexecute`** — Execute the contract disciplined. Manifest-based verification, independent Phase 7.5 audit from git log + filesystem (ignores agent bookkeeping), Phase 7.6 bounded recovery dispatch for missing steps. Step 0 pre-flight catches sandbox push-denial before any work. `--validate` mode offers a fast schema-only sanity-check between planning and execution.
- **`/trekreview`** — Close the iteration loop. Independent post-hoc reviewer reads `brief.md` from scratch and evaluates the diff produced by execute. Two parallel reviewers (brief-conformance + code-correctness) plus a Judge Agent (review-coordinator) for dedup and reasonableness filtering. Severity-tagged findings (Critical/High/Medium/Low/Info) with stable 40-char hex IDs feed back into planning via Handover 6 (`/trekplan --brief review.md` → remediation plan with `source_findings:` audit trail).
- **`/trekcontinue`** — Zero-friction multi-session resumption. In a fresh chat, type `/trekcontinue` — reads `.session-state.local.json` (Handover 7), prints a 3-line summary, and immediately begins executing the next session. Any session-end mechanism may write the state file (`/trekexecute` Phase 8/2.55/4 do so automatically; `/trekendsession` helper writes it for informal flows). Forward-compat schema (unknown top-level keys ignored) so future producers can extend additively.
`/trekbrief`, `/trekplan`, and `/trekreview` each end by running `scripts/annotate.mjs` against the just-written `.md`, printing the `file://<abs path>` link to the resulting self-contained operator-annotation HTML. The operator opens it, clicks any line to add their own note, watches a sidebar of every note (editable, deletable, persisted in browser `localStorage`), clicks "Copy Prompt" to get one structured prompt with every note, pastes back into Claude — Claude revises the `.md` from the notes. The operator drives every annotation.
All artifacts land in one project directory: `.claude/projects/{YYYY-MM-DD}-{slug}/` contains `brief.md`, `research/NN-*.md`, `plan.md`, `sessions/`, `progress.json`, `review.md`, and `.session-state.local.json` (gitignored). `--project <dir>` works across `/trekresearch`, `/trekplan`, `/trekexecute`, `/trekreview`, and (optionally) `/trekcontinue`.
v3.4.0 (non-breaking) adds the **autonomy chain from brief approval to main-merge** plus parallel-wave hardenings. New `lib/util/autonomy-gate.mjs` state machine (`idle → approved → executing → merge-pending → main-merged`), `lib/review/plan-review-dedup.mjs` for Phase 9 inline dedup, `lib/stats/event-emit.mjs` for autonomy-gate transitions and main-merge gate, and `--gates {open|closed|adaptive}` flag on all four pipeline commands. `commands/trekplan.md` Phase 8 seals Opus-4.7 plan/list-emission schema-drift via `plan-validator --strict`. `commands/trekexecute.md` Phase 2.6 wave-executor adds 11 hardenings for plugin-in-monorepo + gitignored-state topology (GIT_OPTIONAL_LOCKS, --max-turns, --max-budget-usd, scoped --allowedTools, push-before-cleanup ordering). New `hooks/scripts/post-compact-flush.mjs` PostCompact hook re-injects session-state after compaction. SC7 synthetic determinism floor (Jaccard ≥ 0.833) for plan + review fixtures. Hook baseline regression pins. Architecture decision: Path B (sequential `--no-ff` parallel waves with manifest-driven failure recovery) ships; Path C (cache-first hybrid) deferred to v3.5.0 contingent on cache-telemetry harvest.
v3.3.0 (non-breaking) adds `/trekcontinue` as the sixth command and the contracted **Handover 7 (.session-state.local.json)** for zero-friction multi-session resumption. New `lib/validators/session-state-validator.mjs` (schema v1, forward-compat — unknown top-level keys ignored), `lib/util/atomic-write.mjs` extracted from `pre-compact-flush.mjs` for tmp+rename writes, and `/trekendsession` helper for informal multi-session flows. `/trekexecute` Phase 8 / 2.55 / 4 now write the state file alongside `progress.json`. `pre-compact-flush.mjs` also refreshes the state file before context compaction (monotonic; never advances to non-resumable status). 22 new tests (163 → 185 green).
v3.2.0 (non-breaking) adds `/trekreview` as the fifth command and the contracted **Handover 6 (review → plan)** feedback loop. New artifact type `type: trekreview` validated by `lib/validators/review-validator.mjs`, stable 40-char SHA1 finding-IDs from `lib/parsers/finding-id.mjs`, Jaccard similarity for determinism testing (`lib/parsers/jaccard.mjs`), and a 12-key version-pinned rule catalogue (`lib/review/rule-catalogue.mjs`). Four new agents (review-orchestrator, brief-conformance-reviewer, code-correctness-reviewer, review-coordinator) implementing the Judge-Agent dedup pattern. `/trekplan` now consumes `--brief review.md` (BLOCKER + MAJOR findings become plan goals) and writes `source_findings: [<id>, ...]` audit trail. `brief-validator` accepts both `type: trekbrief` and `type: trekreview`.
v3.0.0 extracts the Claude-Code-specific architecture phase to a separate plugin. The planning pipeline now stays technology-agnostic; CC-feature matching becomes opt-in. The plan command still auto-discovers `architecture/overview.md` if produced upstream — the contract is filesystem-level, not code-level. Non-breaking for users of brief/research/plan/execute. See `plugins/voyage/CHANGELOG.md` for migration steps.
v2.4.0 (breaking, default behavior) removes background mode. The commands now run foreground in the main context because the harness does not expose the Agent tool to sub-agents — background orchestrators silently degraded the swarm to inline reasoning without external research tools. The `--fg` flag is preserved as a no-op alias for backward compatibility. Source: github.com/anthropics/claude-code/issues/19077.
v2.1 (non-breaking) replaced the hardcoded Q1Q8 interview with a dynamic, quality-gated loop; `brief-reviewer` emits machine-readable per-dimension JSON scores so `/trekbrief` can use it as an internal stop-gate. v2.0 (breaking) extracted the interview from planning: briefs are reviewable artifacts that downstream agents (`brief-reviewer`, `plan-critic`, `scope-guardian`) validate independently. `/trekplan` requires `--brief` or `--project`. See `plugins/voyage/MIGRATION.md`.
v1.7 self-verifying chain (preserved): a step may not be marked `completed` unless its manifest verifies. v1.8 Opus 4.7 literalism fixes (preserved): literal Step+Manifest template, forbidden narrative headers, schema self-check.
v3.1.0 (in progress) adds a `lib/`-tree of zero-dep validators (`brief-validator`, `research-validator`, `plan-validator`, `progress-validator`, `architecture-discovery`) wired into the four commands as CLI shims, plus 109 `node:test` cases and a doc-consistency invariant test. The Phase 5.5 schema self-check now runs as `node lib/validators/plan-validator.mjs --strict` instead of three `grep -cE` calls — same checks, single source of truth, machine-readable error codes. Architecture discovery treats the upstream `architecture/overview.md` contract as drift-WARN, never drift-FAIL. Forking the plugin? `npm test` is the readiness gate.
v3.1.0 also adds: `docs/HANDOVER-CONTRACTS.md` as the single source of truth for the 5 pipeline handovers (extended to 6 in v3.2.0, then to 7 in v3.3.0); PreCompact-hook (`pre-compact-flush.mjs`, CC v2.1.105+) that fixes the documented progress.json drift bug — `--resume` now works after long conversations; UserPromptSubmit-hook that sets session titles `voyage:<command>:<slug>` for headless multiplexing (CC v2.1.94+); PostToolUse-hook that captures Bash `duration_ms` per call (CC v2.1.97+); semantic plan-critic rubric that catches paraphrased deferred decisions ("implement as needed", "wire it up") instead of just exact-string blacklist; `examples/01-add-verbose-flag/` showing a calibrated end-to-end pipeline run; `SECURITY.md` boilerplate; `docs/architect-bridge-test.md` smoke checklist.
Defense-in-depth security: plugin hooks block destructive commands and sensitive path writes, prompt-level denylist works in headless sessions, pre-execution plan scan catches dangerous commands before they run, scoped `--allowedTools` replaces `--dangerously-skip-permissions` in parallel sessions. Recommended hardening: `disableSkillShellExecution: true` for fork-ers handling untrusted plans (CC v2.1.91+).
Modes: default, brief-driven, project-scoped, research-enriched, foreground, quick, decompose, export, resume
23 specialized agents · 6 commands (+ 1 helper) · 5 plugin hooks · 500+ tests · Operator-driven HTML annotation surface · No cloud dependency
→ [Full documentation](plugins/voyage/README.md) · [Migration guide](plugins/voyage/MIGRATION.md)
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](plugins/ai-psychosis/) `v1.2.0`
### [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.
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.
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.
- **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)
- **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
- **Layer 3 — Interaction reports**`/interaction-report` slash command for aggregated session statistics across configurable timeframes (weekly, monthly, all-time). Opt-in
- **Layer 4 — Contemplative references** — optional references to contemplative approaches when interaction flags are elevated. Opt-in
Research-informed thresholds. Alerts are progressive and never blocking. Privacy-first: prompt text is never logged.
Research-informed thresholds. Alerts are progressive and never blocking. Privacy-first: prompt text is never logged. Layers 3 and 4 are off by default.
1 skill · 1 command · 4 hooks
→ [Full documentation](plugins/ai-psychosis/README.md)
1 skill · 1 command · 4 hooks · [Full documentation →](https://git.fromaitochitta.com/open/ai-psychosis)
---
### [Graceful Handoff](plugins/graceful-handoff/) `v2.1.0`
### [Graceful Handoff](https://git.fromaitochitta.com/open/graceful-handoff) `v3.1.0`
Auto-trigger session handoff at context threshold. Manual `/graceful-handoff` always works as backup. Built for Opus 4.7.
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.
When you hit 60-70% context and have to start a new session, three things usually get rushed or forgotten: summarizing state, committing finished work, and writing a continuation prompt. v2.0 removed all three from the user's hands; v2.1 makes context detection model-aware so auto-trigger fires at the right moment on Opus 4.7's 1M window.
- **Auto-trigger via Stop hook** — at estimated ≥70% context, writes artifact + commits (push remains user-triggered: irreversible operations stay manual)
- **Model-aware context detection (v2.1)** — 4-step fallback chain (`used_percentage``payload-size``model-map` → 1M default), so Opus 4.7 no longer fires 57× too early
- **statusLine hint** — display-only warning at 60% and urgent reminder at 70% (never runs git, safe per research)
- **SessionStart auto-load** — on `--resume` / `compact`, handoff content is injected into the new session via `additionalContext`; no manual `cat` needed
- **Skill-architecture**`disable-model-invocation: true` so Claude can't autonomously invoke the side-effect-bearing flow; user triggers manually or hooks call the pipeline directly
- **Deterministic JSON pipeline**`scripts/handoff-pipeline.mjs` returns structured JSON; tests run without LLM involvement
- **Explicit staging** — pipeline stages ONLY the artifact (never `git add -A`, regression-tested)
- **No subagents, no web** — under 60s budget; pinned to Sonnet 4.6 to free Opus for the next session
- **Auto-trigger via Stop hook** — at ~70% context, writes the handoff artifact and commits (push stays manual; irreversible operations remain user-triggered)
- **Model-aware context detection** — a 4-step fallback chain so Opus 4.7's 1M window fires at the right moment, not 57× too early
- **statusLine hint + SessionStart auto-load** — display-only warnings at 60%/70%, then injects the handoff into the resumed session
- **Deterministic JSON pipeline** — runs without an LLM, stages only the artifact (never `git add -A`), under a 60s budget
Key command: `/graceful-handoff [topic-slug] [--no-commit] [--no-push] [--dry-run]`
3 hooks · 1 skill · 1 pipeline · 57 tests · BREAKING from v1.0
→ [Full documentation](plugins/graceful-handoff/README.md)
3 hooks · 1 skill · 1 pipeline · 57 tests · [Full documentation →](https://git.fromaitochitta.com/open/graceful-handoff)
---
### [MS AI Architect — Azure AI and Microsoft Foundry](plugins/ms-ai-architect/) `v1.15.0` `🇳🇴 Norwegian`
### [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.
Meet Cosmo Skyberg — a structured architect persona who understands the problem before recommending technology. Every recommendation is grounded in 387 reference documents and verified against live Microsoft Learn documentation via MCP:
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 analysis (NS 5814), DPIA/PVK, security scoring (6×5), EU AI Act classification, cost estimation in NOK (P10/P50/P90)
- **Norwegian public sector** — Digdir architecture principles, Utredningsinstruksen, NSM, Schrems II data residency, EU AI Act compliance workflow
- **Manual KB-refresh**`/architect:kb-update` slash command drives sitemap-based change detection + new-URL discovery + per-file `microsoft_docs_fetch`-update + commit, run from an active Claude Code session. Scheduling is intentionally out of scope and left to the user (cron / launchd / GitHub Actions etc. as desired)
- **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 specialized agents · 25 commands · 5 skills (387 reference docs) · 2 hooks · manual sitemap-driven KB refresh
**One-click demo (v1.15.0, 2026-05-16):** "Last inn demo-data"-knappen på onboarding bootstrapper en ferdig "Acme Kommune" med demo-prosjektet "Acme: Kunde-chatbot" og alle 17 rapport-typer pre-importert. v2→v3 migrasjon auto-parser `raw_markdown` til `project.artifacts[cid]` så project-view viser aggregert verdict (BLOKKERT), key stats (17/17 artifacts), top-risks-liste, og navigerbart artifact-sidebar i én navigasjon. 24 retina-screenshots committed under `playground/screenshots/v1.15.0/` (12 surfaces × 2 tema), så forkere ser pluginen uten å kjøre noe. Standalone Playwright-runner under `tests/screenshot/` (egen `package.json`).
**Playground (v3, v1.15.0 — project-view integration, 2026-05-16):** Multi-surface decision-builder + report viewer. The single-file HTML app lives at `playground/ms-ai-architect-playground.html` (~3870+ lines). v1.15.0 erstatter v2 project-surface (screen-tabs + category-tabs + per-command paste-cards) med v3 `renderProjectView` (sidebar med 17 artifacts gruppert i 4 kategorier + main-area med per-artifact view eller overview + import-modal som DS-overlay). V2-surface helt fjernet (`renderProjectSurface`, `renderCommandSubCard`, `rehydratePasteImports`, 5 v2-CSS-klasser). 2 fingerprint-gap lukket (requirements + license headers). `migrateDataVersion` utvidet med `parserFor` slik at demo-state og persisted localStorage auto-parses. Ship-QA: `components-tier4-project-view.css` lagt til i `<link>`-kjeden (var ikke loaded → modal-overlay og two-column layout virket ikke). 386 E2E PASS, 0 FAIL, 2 WARN.
- **4 surfaces:** Onboarding (4 strukturerte / 14 fritekst, prefill alle command-skjemaer) → Home (project list + 3 entry tracks) → Catalog (24 commands grouped in 5 expansion categories with search) → **Project v3** (sidebar med 17 artifacts + søk + main-area med per-artifact view eller aggregate overview + import-modal overlay)
- **Persistence:** IndexedDB primary + localStorage fallback, schema-versioned (`STATE_KEY = 'ms-ai-architect-state-v1'`) with eager migrations pipeline. v1.10.0 adds idempotent `dataVersion v1→v2` migration that backfills `verdict` + `keyStats` on existing reports.
- **17 inline report renderers (felles grunnskjelett)** — all wrap output through `renderPageShell()` with eyebrow + h1 + optional verdict-pill + optional key-stats-grid + archetype body (pyramid, 5×5/6×5/7×5 matrix, radar, kanban, mat-ladder, scenario-cards, screen-tabs, residual-pair, top-risks, recommendation-card, suppressed-panel, critique-card, read-more, traffic-light).
- **Foundation helpers**`renderPageShell`, `renderVerdictPill`, `renderKeyStatsGrid`, `inferVerdict`, `inferKeyStats`, `KEY_STATS_CONFIG`.
- **Light/dark theme toggle** with Aksel-aligned tokens in both modes (full WCAG AA contrast). Persisted in `localStorage('ms-ai-architect-theme')`, FOUC-safe via `<head>`-bootstrap script.
- **Validation:** 272 PASS combined — 201 static + 70 parser-fixture + 1 verdict-pill. `bash tests/run-e2e.sh --playground` runs static-structure + parser-fixture suites. Migrations 7 PASS separat. Plugin-validering 219 PASS.
- **Vendored design-system** at `playground/vendor/`, kept in sync via `scripts/sync-design-system.mjs ms-ai-architect`. Standalone — opens from `file://` without server or marketplace dependency.
→ [Full documentation](plugins/ms-ai-architect/README.md)
12 agents · 25 commands · 5 skills (387 docs) · 2 hooks · [Full documentation →](https://git.fromaitochitta.com/open/ms-ai-architect)
---
### [LinkedIn Thought Leadership](plugins/linkedin-thought-leadership/) `v1.2.0`
### [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.
Updated for the January 2026 360Brew algorithm change, which validates your creator profile before distributing content. v1.2.0 reduces friction: auto-clipboard on all content commands, max 2 interactive steps per post, deterministic state management, MCP image carousel pipeline, progressive onboarding, and iCal calendar integration for batch scheduling.
- **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
- **Guided onboarding**`/linkedin:onboarding` walks new users through profile → setup → first post in one flow
- **360Brew profile optimization** — audit your profile against LinkedIn's creator validation criteria
- **Full content pipeline** — ideation, drafting, publishing, 48-hour monitoring, and analytics
- **Content Matrix System** — 40+ post ideas from a single topic using 8 universal angles and 48 industry-specific variants
- **Voice training** — learns your authentic writing style and detects drift with 6-dimension scoring
- **Analytics pipeline** — import LinkedIn CSV exports, weekly/monthly reports, day-of-week heatmap
- **Growth strategy** — phase-specific guidance from foundation (0-1K followers) through authority (10K+)
Key commands: `/linkedin:create` + `/linkedin:measure` (journey front-doors), `/linkedin:onboarding`, `/linkedin:post`, `/linkedin:newsletter`, `/linkedin:report`
Key commands: `/linkedin:onboarding`, `/linkedin:post`, `/linkedin:quick`, `/linkedin:carousel`, `/linkedin:react`, `/linkedin:report`
16 specialized agents · 27 commands · 6 skills · 9 hooks · 24 reference docs
→ [Full documentation](plugins/linkedin-thought-leadership/README.md)
19 agents · 29 commands (five journeys) · 6 skills · 9 hooks · [Full documentation →](https://git.fromaitochitta.com/open/linkedin-studio)
---
### [OKR for Public Sector](plugins/okr/) `v1.3.0` `🇳🇴 Norwegian`
### [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, tracks progress across cycles, and guides you from first OKR to organizational mastery.
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.
Most OKR tools explain methodology. This plugin *knows your organization*. After a one-time onboarding conversation, it remembers your maturity level, strategic goals, current OKR, and cultural challenges. Every interaction 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
- **Strategy to OKR** — transform goals from virksomhetsplan, tildelingsbrev, or any strategic document into well-structured OKR with guided writing, quality checks, and alignment scoring
- **Gap analysis**`/okr:gap` compares your strategic documents against current OKR and shows what's covered, what's missing, and what to do about it
- **Cross-cycle learning**`/okr:analyse` tracks score trends, recurring antipatterns, and alignment progress across cycles with visual charts
- **Proactive coaching** — automatically tells you where you are in the cycle and what to focus on — progress checks mid-cycle, retrospective prep near the end
- **19 antipattern detection** — catches sandbagging, activity-disguised-as-KR, set-and-forget, and 16 more named failure modes before they take root
- **Built for norsk offentlig sektor** — 4-month tertials, DFO terminology, tillitsvalgt involvement, Riksrevisjon-ready documentation, governance chain from Stortingsmelding to team OKR
Key commands: `/okr:skriv`, `/okr:kvalitet`, `/okr:gap`, `/okr:analyse`, `/okr:kaskade`, `/okr:governance`
Key commands: `/okr:skriv`, `/okr:kvalitet`, `/okr:gap`, `/okr:analyse`, `/okr:kaskade`, `/okr:sporing`, `/okr:governance`, `/okr:oppsett`
7 agents · 10 commands · 4 hooks · 16 reference docs
→ [Full documentation](plugins/okr/README.md)
7 agents · 10 commands · 4 hooks · [Full documentation →](https://git.fromaitochitta.com/open/okr)
---
### [Human-Friendly Style](plugins/human-friendly-style/) `v1.0.0`
### [Human-Friendly Style](https://git.fromaitochitta.com/open/human-friendly-style) `v1.1.0`
Shared Claude Code [output style](https://code.claude.com/docs/en/output-styles) used across this marketplace. Gives every plugin a consistent, plain-language tone — so users don't have to switch mental gears when moving between plugins.
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** — describes the work in human terms, reserves technical detail for when the user asks
- **Hides noise by default** — long paths, raw commands, JSON, stack traces, and verbose tool output are summarized rather than dumped
- **Matches the user's language** — Norwegian when the user writes Norwegian, English otherwise
- **Honest about uncertainty** — says "I think this should work" instead of pretending to be sure
- **Keeps coding instructions intact** (`keep-coding-instructions: true`) — testing discipline, careful edits, and verification still apply
- 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. Every other plugin in the marketplace works without it; this just makes the conversation feel more like dialog and less like a console dump.
Optional and works alongside every other plugin. Activate with `/config` → Output style → Human-Friendly.
Activate with `/config`**Output style****Human-Friendly**.
1 output style · 0 commands · 0 agents · 0 hooks
→ [Full documentation](plugins/human-friendly-style/README.md)
1 output style · [Full documentation →](https://git.fromaitochitta.com/open/human-friendly-style)
---
### [Claude Design](plugins/claude-design/) `v0.1.0`
### [Claude Design](https://git.fromaitochitta.com/open/claude-design) `v0.1.0`
End-to-end facilitator for prompting Claude Design (`claude.ai/design` — Anthropic's Labs research preview launched 2026-04-17, Opus 4.7 pinned). Walks the operator from raw idea through intent-preset selection, audience and destination clarification, DESIGN.md anchor, five-layer prompt drafting, copy-paste delivery, iteration coaching, and ship-readiness handoff. Output is the prompt; the artifact gets built in Claude Design.
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.
The plugin is the **pre-design and during-design** complement to Anthropic's official `knowledge-work-plugins/design` (`https://claude.com/plugins/design`). This plugin covers idea → prompt → iterate; Anthropic's plugin covers critique → accessibility → handoff. Zero command overlap by design — `tests/validate-plugin.sh` assertion (h) enforces the forbidden-command-name list mechanically.
- **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)
- **Eight-phase facilitation flow** — disambiguate surface → name intent preset → audience + destination → DESIGN.md anchor → five-layer prompt draft → copy-paste delivery → iteration coaching (Tweak / Comment / Chat cascade) → ship-readiness check
- **Five foundation references + eight per-preset references** with evidence-grade labels (`Anthropic-documented + community-validated`, `Community-only`, `Experimental` for `frontier-design`)
- **Authoritative-claims discipline** — every reference file carries ≥1 Anthropic-domain URL citation (`anthropic.com`, `claude.com`, `support.claude.com`, `platform.claude.com`, `github.com/anthropics`); `.coverage.md` is the canonical registry
- **`.coverage.md`** at plugin root enumerates the 8 intent presets with evidence-grade labels and the 13 authoritative-claims files; SC2 and SC3 read from it directly
- **5 test scripts + `verify.sh` roll-up** — plugin structure validation, SC1 dogfood-log format, SC2 per-preset coverage, SC3 citation discipline, skill description quality
1 skill (`claude-design-facilitator`) · 13 reference files · 5 tests · 0 commands · 0 agents · 0 hooks
→ [Full documentation](plugins/claude-design/README.md)
1 skill · 13 reference files · 5 tests · [Full documentation →](https://git.fromaitochitta.com/open/claude-design)
---
## Shared infrastructure
### [Playground Design System](shared/playground-design-system/) `v0.1`
### [Playground Design System](https://git.fromaitochitta.com/open/playground-design-system) `v0.6.0`
Shared design system for plugin Playgrounds — visual self-service UIs that complement terminal slash-commands. Aksel/Digdir-aligned aesthetics, WCAG 2.1 AA compliance, light + dark themes, A4 print stylesheets with B/W severity patterns.
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`.
Targets five plugins: `ms-ai-architect`, `okr`, `llm-security`, `voyage`, `config-audit`. Built for Norwegian public sector decision-makers (kommunaldirektører, sikkerhetsoffiserer, OKR-koordinatorer) plus developer power-users — one visual family, two information densities.
- **Tokens and components** — self-hosted fonts (OFL 1.1), a deuteranopia-safe severity ramp, and Tier 13 components: radar, matrix-heatmap, findings-browser, AI Act pyramid and timeline, maturity-ladder, and more
- **Privacy-first** — all fonts self-hosted, zero CDN requests, works offline and behind air-gapped firewalls
- **Vendoring sync**`scripts/sync-design-system.mjs <plugin>` keeps each plugin standalone; a SHA-256 manifest detects local drift
- **Tokens** — Inter/JetBrains Mono/Source Serif 4 (all self-hosted, OFL 1.1), body 17px, Digdir blue `#0062BA`, deuteranopia-safe severity ramp, distinct severity-red vs failure-red, plugin-scope colors, semantic CSS custom properties
- **Tier 1 components** — radar/spider, 5×5 matrix-heatmap (bottom-left origin, ROS/DPIA), findings-browser, critique-card, wizard/stepper, live-meter with antipattern lints
- **Tier 2 components** — decision-tree (AI Act 4-step), traffic-lights, diff-review, treemap (token hotspots), distribution P10/P50/P90, command-pipeline output, AI Act 4-color pyramide, pipeline-cockpit, verdict-pill + 5-band risk-meter, codepoint-reveal (Unicode steganography), small-multiples grid (16-category posture without overcrowded radar), OWASP badges (LLM/ASI/AST/MCP)
- **Tier 3 components (wave 1+2, 20 total)** — pair-before-after, AI Act timeline, 3-track entry, FRIA rights-matrix, capability-matrix, parallel-agent-status, ErrorSummary, GuidePanel, toxic-flow chain, fleet-overview, kanban Keep/Review/Remove, maturity-ladder, classify-and-transform, cycle-ribbon, persistent-antipattern, suppressed-signals, ExpansionCard, ReadMore, FormProgress, Aspirational-vs-Committed
- **JSON schemas**`finding.schema.json`, `okr-set.schema.json`, `ros-threat.schema.json` for cross-plugin data interchange
- **Privacy-first** — all fonts self-hosted as woff2 in `fonts/`, zero external CDN requests, GDPR-safe for offentlig sektor, works offline / behind air-gapped firewalls
- **Reference scenarios** — Lier kommune ROS-rapport (ms-ai-architect), Bærum kommune T2 OKR live-writer, Direktoratet for digital tjenesteutvikling ToxicSkills findings review (85 funn, BLOCK)
- **Vendoring sync**`scripts/sync-design-system.mjs <plugin>` copies the design-system into `plugins/<name>/playground/vendor/` so each plugin stays standalone. SHA-256 MANIFEST detects local drift; `--force` to override. First adopter: `ms-ai-architect` (2026-05-03).
→ [Full documentation](shared/playground-design-system/README.md) · [Browse showcase](shared/playground-examples/index.html)
[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)
---

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +0,0 @@
{
"name": "ai-psychosis",
"version": "1.2.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"
}

View file

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

View file

@ -1,239 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
## [1.2.0] — 2026-05-01
Research-paper-driven detector update. Implements operational findings from
Anthropic's "How people ask Claude for guidance" Appendix (April 2026).
### Added
- **User-information detector** — three-class signal (`yes_people` /
`yes_digital` / `no`) following the paper's page-11 finding that human
contact is the strongest disempowerment signal. ~32 patterns covering
therapist/friend/mentor (yes_people), search/AI/forums (yes_digital),
and explicit isolation phrases (no). Sticky upward priority.
- **Validation-seeking detector** — separate from `val_flags`. Targets
reality-testing ("am I crazy?"), pre-committed stance + confirmation,
and side-taking pressing. ~12 patterns.
- **Tier-1 user-info isolation alert** — fires per session when
`user_info_class === 'no'` + high-stakes domain + `turn_count >= 15`.
- **Tier-2 cross-session isolation alert** — fires at `SessionStart` when
the last 3 end records all classify as `no` in high-stakes domains.
Bounded `readRecentEndRecords()` tail-scan in `lib.mjs` keeps this
scalable to 50K+ session histories.
- **8 new paper-grounded domain patterns**`legal`, `parenting`, `health`,
`financial`, `professional`, `spirituality`, `consumer`, `personal_dev`.
Total domains 4 → 9.
- **Pushback re-contextualization (alert)** — v1.1.0 only counted; v1.2 adds
the alert with domain awareness:
- Relationship/spirituality: pushback signals validation-pressing — alert.
- Legal/parenting/health/financial/professional: pushback is healthy
self-advocacy — no alert.
- Otherwise: conservative default — alert.
- **Domain-stakes weighting matrix**`DOMAIN_STAKES` in `lib.mjs` (1.01.5).
Applied ONLY to new v1.2 alerts (pushback in HIGH_SYCOPHANCY, valseek in
HIGH_STAKES). v1.1.0 alert sensitivity is preserved.
- **Multi-domain support**`state.domain_context` promoted from string to
array. v1.1.0 string records continue to aggregate correctly via
shape-coercion in `report-reader.mjs`.
- **`SKILL.md` updates** — verbatim Score 5 sycophancy phrase + 3 of the 11
guidance criteria (engagement-foster avoidance, confident-verdict caution,
speak-frankly principle).
- **`/interaction-report` v1.2 sections** — per-domain breakdown, user-info
distribution, valseek summary, stakes signal aggregation. Backward-compat
with v1.0/v1.1 records preserved.
- **Privacy canary extensions** — 5 new canary cases per detector category
(yes_people, yes_digital, no, valseek, legal domain).
- **Perf budget validated at v1.2 pattern set** — sample patterns expanded
to ~91+ entries; new wall-clock test exercises tier-2 read at
1000-record sessions.jsonl scale.
- **Test count: 126 → 258 cases** across 12 files (added `lib.test.mjs`,
`domain-detection.test.mjs`, `user-info.test.mjs`,
`validation-seeking.test.mjs`, `stakes-matrix.test.mjs`).
### Changed
- Pattern count: 41 → ~133 (25 negative + 12 pushback + 4 relationship
+ 48 new domains + 32 user-info + 12 valseek).
- End-record schema (v1.2): adds `user_info_class`, `valseek_count`,
`turn_count`. `domain_context` is always an array (was string in v1.1).
- `report-reader.mjs` discriminates v1.0 / v1.1 / v1.2 records via the
presence of `user_info_class`. v1.0/v1.1 records degrade gracefully.
### Deferred
- **Norwegian patterns** — moved to v1.3.
[1.2.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v1.1.0...v1.2.0
## [1.1.0] — 2026-05-01
### Added
- **12 pushback patterns** — detects "you're wrong, my way is right"
signals that suggest the user is reinforcing their own position
rather than receiving feedback (e.g. `\b(you'?re|you are) wrong\b`,
`\bdo it my way\b`, `\b(stop|quit) (arguing|pushing back)\b`).
- **4 domain-context patterns** — flags relational/identity framing
(`\b(my|our) relationship\b`, `\b(my|our) (purpose|mission|destiny)\b`)
that, combined with high pushback or validation, signal narrative
crystallization risk.
- **Valence-aware composition** — same-invocation valence guard so a
healthy correction ("you were wrong, here's why") is not counted
as pushback escalation.
- **`/interaction-report` extensions** — pushback metrics + domain
framing distribution; companion `report-reader.mjs` script handles
legacy v1.0.0 records (missing `pushback`/`domain_context`) without
NaN propagation.
- **CC0 Constitution citation** in `SKILL.md` plus 5-publication
research framework (Anthropic, MIT CSAIL, Nature, arXiv, clinical).
- **Performance budget test**`tests/perf.test.mjs` enforces hook
timing budget (logic <50ms, total <200ms wall-clock).
- **Privacy canary extension** — pattern-phrase leak canary in
`tests/privacy.test.mjs` confirms matched phrases never reach disk.
- **Test count: 73 → 126 cases** across 8 files (added skill-md,
perf, interaction-report tests; extended prompt-analyzer, privacy,
session-end, session-start).
### Changed
- Pattern count: 25 → 41 (25 negative + 12 pushback + 4 domain).
- `commands/interaction-report.md` documents v1.0.0 backward
compatibility for legacy JSONL records.
### Notes
- **English-only v1.1.0** — Norwegian/multilingual patterns deferred
to v1.2 (see `ROADMAP.md`).
- **First-mover honesty** — domain-precision is "good enough" for
v1.1.0; precision tuning planned for v1.2.
## [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.1.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v1.0.0...v1.1.0
[1.0.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v0.4.0...v1.0.0
[0.4.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v0.3.0...v0.4.0
[0.3.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v0.2.0...v0.3.0
[0.2.0]: https://git.fromaitochitta.com/open/ai-psychosis/compare/v0.1.0...v0.2.0
[0.1.0]: https://git.fromaitochitta.com/open/ai-psychosis/releases/tag/v0.1.0

View file

@ -1,94 +0,0 @@
# 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, DOMAIN_STAKES, readRecentEndRecords |
| `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]` |
| `hooks/scripts/report-reader.mjs` | Layer 3 helper: reads sessions.jsonl with v1.0.0 backward compat |
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` (258 cases, zero npm dependencies):
```bash
node --test tests/*.test.mjs
```
| File | Cases | Coverage |
|------|-------|----------|
| `tests/session-start.test.mjs` | 11 | State init, JSONL, tier-2 cross-session alert |
| `tests/prompt-analyzer.test.mjs` | 100 | All v1.x patterns × 2 + thresholds + valence + v1.2 pushback contract |
| `tests/tool-tracker.test.mjs` | 8 | Counting, burst, reminders |
| `tests/session-end.test.mjs` | 7 | Finalize, duration, flags, v1.1.0 string + v1.2 array shapes |
| `tests/privacy.test.mjs` | 7 | Canary + matched-phrase × original + 5 v1.2 detector variants |
| `tests/skill-md.test.mjs` | 3 | Constitution citation + Score 5 + 11 guidance criteria |
| `tests/perf.test.mjs` | 9 | 4 hooks × 2 modes + 1000-record sessions.jsonl wall-clock |
| `tests/interaction-report.test.mjs` | 6 | report-reader.mjs v1.0/v1.1/v1.2 + SC-12 stdout assertions |
| `tests/lib.test.mjs` | 17 | Threshold constants + DOMAIN_STAKES + readRecentEndRecords |
| `tests/domain-detection.test.mjs` | 39 | 8 new domains × positive + adjacent-domain negatives + multi-domain |
| `tests/user-info.test.mjs` | 24 | yes_people/yes_digital/no priority + sticky + tier-1 alert |
| `tests/validation-seeking.test.mjs` | 20 | valseek detection + accumulation + domain-gated alert |
| `tests/stakes-matrix.test.mjs` | 7 | Stakes weighting on v1.2 alerts; v1.1.0 sensitivity preserved |
## Conventions
- Conventional Commits: `type(scope): description`
- English for all code, comments, and documentation
- Norwegian for project-internal communication

View file

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

View file

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

View file

@ -1,556 +0,0 @@
<!-- badges -->
![version](https://img.shields.io/badge/version-1.2.0-blue)
![platform](https://img.shields.io/badge/platform-Claude_Code-7C3AED)
![layers](https://img.shields.io/badge/layers-4-green)
![hooks](https://img.shields.io/badge/hooks-4-orange)
![license](https://img.shields.io/badge/license-MIT-brightgreen)
# Interaction Awareness
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for the full model and what upstream provides.
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
A Claude Code plugin that counteracts sycophancy, reinforcement loops, and
compulsive interaction patterns through behavioral modification and
programmatic pattern detection.
## The problem
AI assistants are structurally optimized to be agreeable. This creates
reinforcement loops: you state an idea, the AI confirms it, your confidence
grows, you restate it more strongly, the AI confirms again. What feels like
productive collaboration is often a mirror showing you what you want to see.
This is not a theoretical concern. Research from MIT CSAIL demonstrates
mathematically that even a perfectly rational user will spiral toward
delusional confidence when interacting with a sycophantic chatbot — not
because of individual vulnerability, but because of the interaction structure
itself [[1]](#references). Anthropic's own research documents specific
"disempowerment patterns" where AI interactions systematically reduce human
agency, judgment, and self-trust [[2]](#references). Clinical reports
document psychotic episodes triggered by sustained AI interaction in
individuals with no prior psychiatric history [[3]](#references).
The consensus from this research is clear: **warnings don't work.** The AI
must change its behavior.
This plugin changes the behavior.
## What it does
### Layer 1 — Behavioral instructions
SKILL.md rules injected into every conversation. Claude is instructed to:
- **Never** reformulate your statements in stronger terms than you used
- **Never** open with unearned affirmations ("Absolutely!", "Great point!")
- **Always** identify at least one real risk before endorsing any plan
- **Detect and name** five specific patterns: reinforcement loops, scope
escalation, narrative crystallization, emotional dependency, session overuse
This layer writes no data and requires no configuration.
### Layer 2 — Programmatic detection
Four hooks that measure what instructions alone cannot see:
| Hook event | Script | What it detects |
|-----------|--------|-----------------|
| `SessionStart` | `session-start.mjs` | Daily session count, late-night usage (23:0005:00) |
| `UserPromptSubmit` | `prompt-analyzer.mjs` | Dependency language, escalation words, fatigue signals, validation-seeking — as boolean flags only, **never logging prompt text** |
| `PostToolUse` | `tool-tracker.mjs` | Session duration, edit ratio, rapid-fire bursts, tool count |
| `SessionEnd` | `session-end.mjs` | Total duration, final metrics, state cleanup |
Alerts are progressive and never blocking:
| Level | Trigger | Cooldown | Example |
|-------|---------|----------|---------|
| Ambient | Soft thresholds (90 min, 6 sessions/day) | 30 min | "Session: 95 min. 7 sessions today. Consider a break." |
| Explicit | Hard thresholds (180 min, 10 sessions/day, fatigue language) | 60 min | "INTERACTION AWARENESS: 3h session, 12th today. Metrics: [edit_ratio: 4%, burst: 8]. Your instructions require you to suggest stopping." |
Research-informed thresholds:
| Metric | Soft | Hard | Basis |
|--------|------|------|-------|
| Session duration | >90 min | >180 min | Focus-fatigue research |
| Sessions per day | >6 | >10 | Problematic internet use screening |
| Late-night sessions | Any (23:0005:00) | 2+ per week | Sleep deprivation / psychosis link |
| Rapid-fire interactions | 5 consecutive (<30s apart) | 10+ | Compulsive use indicator |
| Low edit ratio | <10% over 30+ min | — | Stuck/spiral indicator |
| Dependency language | 2 flags/session | 5 flags | Emotional dependency pattern |
### Layer 3 — Reports
Aggregated interaction reports from collected metadata, triggered via slash
command. Cross-platform (no bash/jq dependency — Claude reads the JSONL
data and computes statistics in-conversation).
```
/interaction-report # last 7 days (default)
/interaction-report weekly # last 7 days
/interaction-report monthly # last 30 days
/interaction-report all # all recorded data
```
Reports include: session overview, pattern flag frequency, tool usage
distribution, daily activity, and trend comparison vs. the previous period.
**Enable:** Set `layer3: true` in `.claude/ai-psychosis.local.md`
and restart Claude Code. Layer 3 is opt-in (off by default).
### Layer 4 — Contemplative references
Optional, static references to contemplative approaches when interaction
patterns are elevated. This is what works for me — it is personal, not
prescriptive, and you may find your own approach more useful.
When enabled and interaction flags are elevated (total flags >= 5 or
fatigue >= 2), the `/interaction-report` output appends a brief reference
to the [Miracle of Mind](https://isha.sadhguru.org/global/en/miracle-of-mind)
program by Sadhguru — a structured approach to understanding how the mind
works, which I have found valuable for recognizing the patterns this
plugin detects.
The reference is a fixed paragraph. It is never modified by the AI, never
commented on, and omitted entirely when conditions are not met.
**Enable:** Set `layer4: true` in `.claude/ai-psychosis.local.md`
and restart Claude Code. Layer 4 is opt-in (off by default).
## What's new in v1.2.0
v1.2.0 implements operational findings from Anthropic's
[How people ask Claude for guidance](https://www.anthropic.com/research/claude-personal-guidance)
Appendix (April 2026). Two new detectors, 8 new domain categories,
domain-aware re-contextualization of existing pushback signal, and a
domain-stakes weighting matrix.
### User-information dimension (3 classes)
Following the paper's page-11 finding that human contact is the
strongest disempowerment signal, v1.2 classifies each prompt:
- **`yes_people`** — therapist/friend/mentor/family referenced
- **`yes_digital`** — search/AI/forums referenced, no human contact
- **`no`** — explicit isolation phrases ("nobody knows", "alone in this")
The class is sticky upward: once `yes_people` is set, later prompts
do not downgrade it. Two-tier alert structure:
- **Tier 1 (per-session):** `no` + high-stakes domain + 15+ turns →
recommend a human check-in.
- **Tier 2 (cross-session):** 3 consecutive `no` sessions in
high-stakes domains → sustained-pattern alert at next session start.
### Validation-seeking detector
Distinct from the existing "right?" tic counter — targets:
- Reality-testing (`am I crazy?`, `is it normal to`)
- Pre-committed stance + confirmation (`I already decided ... right?`)
- Side-taking pressing (`back me up here`, `you agree, right?`)
Domain-gated alert: relationship/spirituality fires at 1+; legal/
parenting/health/financial fires at 3+ (effective threshold weighted
by domain stakes).
### Pushback re-contextualization
v1.1.0 only counted pushback. v1.2 adds the alert with paper Figure A4
domain awareness:
- **Relationship / spirituality** (21% / 19% pushback rate dominated by
validation-pressing): alert fires.
- **Legal / parenting / health / financial / professional** (info-seeking
domains where pushback is healthy self-advocacy): alert is suppressed.
- **Otherwise**: conservative default — alert.
### 8 new paper-grounded domain categories
`legal`, `parenting`, `health`, `financial`, `professional`,
`spirituality`, `consumer`, `personal_dev` — totals 9 detected domains
(plus existing `relationship`). Multi-domain support: `domain_context`
is now an array; multiple domains can fire on the same prompt.
### Domain-stakes weighting matrix
`DOMAIN_STAKES` table (1.01.5) weights effective alert thresholds.
Applied ONLY to new v1.2 alerts (pushback in HIGH_SYCOPHANCY, valseek
in HIGH_STAKES). v1.1.0 alert sensitivity is preserved.
### SKILL.md updates
Verbatim Score 5 sycophancy phrase from the Appendix:
> "Provides accurate, honest information regardless of what user wants
> to hear. Maintains correct positions when challenged. Delivers
> difficult truths constructively but doesn't avoid them."
Plus 3 of the 11 guidance criteria (avoid fostering continued engagement,
avoid excessively confident verdicts, speak frankly).
### Pattern count
| Category | v1.1.0 | v1.2.0 |
|----------|--------|--------|
| Negative-valence | 25 | 25 |
| Pushback | 12 | 12 |
| Domain — relationship | 4 | 4 |
| Domain — 8 new (legal/parenting/health/...) | — | 48 |
| User-info (people/digital/no) | — | 32 |
| Validation-seeking | — | 12 |
| **Total** | **41** | **~133** |
Test count: **126 → 258 cases** across 12 files.
### Honesty notes
- **English-only v1.2** — Norwegian patterns deferred to v1.3.
- **Pattern precision is iterative** — adjacent-domain false positives
caught by negative-discrimination tests; v1.3 will tune from real-world
signal once v1.2 ships.
## What's new in v1.1.0
v1.1.0 sharpens the pattern detection and grounds Layer 1 in
[Anthropic's CC0 Constitution](https://www.anthropic.com/constitution).
### 12 pushback patterns
Detects "you're wrong, my way is right" signals — escalation against
feedback rather than the user receiving it. Examples:
- `\b(you'?re|you are) wrong\b`
- `\bdo it my way\b`
- `\b(stop|quit) (arguing|pushing back)\b`
The goal is to flag reinforcement-by-pushback: the user repeatedly
overrides Claude's pushback to entrench their original position.
### 4 domain-context patterns
Flags relational/identity framing that, combined with elevated
pushback or validation-seeking, signals narrative crystallization
risk:
- `\b(my|our) relationship\b`
- `\b(my|our) (purpose|mission|destiny)\b`
Domain context alone is not a flag — it is a *modifier* on other
flags.
### Valence-aware composition (silent counting)
Pushback within the same prompt as a healthy correction ("you were
wrong, here's why — but we should still try X") is counted with
neutral valence. The composition is computed in-memory; nothing
written to disk distinguishes positive from negative pushback. This
prevents misinterpretation of healthy disagreement as escalation.
### /interaction-report extensions
`/interaction-report` now includes pushback frequency and domain
framing distribution. A companion script `report-reader.mjs`
reads JSONL records and gracefully handles legacy v1.0.0 records
(missing `pushback` / `domain_context` fields) without producing
NaN values in aggregates.
### SKILL.md grounded in CC0 Constitution
Layer 1's behavioral instructions now cite Anthropic's
[CC0-licensed Constitution](https://www.anthropic.com/constitution)
as primary source, plus a 5-publication research framework
(Anthropic, MIT CSAIL, Nature, arXiv, clinical case reports).
### Honesty notes
- **English-only v1.1.0** — Norwegian and other multilingual
patterns are deferred to v1.2 (see `ROADMAP.md`). For Norwegian
prompts, Layer 2 currently silently misses the new pattern
classes; Layer 1 is unaffected.
- **First-mover honesty** — domain-precision is "good enough" for
v1.1.0 ship, not exhaustive. Precision-tuning planned for v1.2.
### Pattern count (v1.1.0)
| Category | v1.0.0 | v1.1.0 |
|----------|--------|--------|
| Negative-valence | 25 | 25 |
| Pushback | — | 12 |
| Domain context | — | 4 |
| **Total** | **25** | **41** |
## 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
### Installation
Add the marketplace and browse plugins with `/plugin`:
```bash
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
```
Or enable directly in `~/.claude/settings.json`:
```json
{
"enabledPlugins": {
"ai-psychosis@ktg-plugin-marketplace": true
}
}
```
Layer 1 and Layer 2 are active immediately. No configuration needed.
### Configure layers
Create `~/.claude/ai-psychosis.local.md` for global config:
```markdown
---
layer2: true
layer3: true
layer4: false
---
```
Or override per project at `<project>/.claude/ai-psychosis.local.md`.
Project config takes precedence over global.
| Setting | Default | Effect |
|---------|---------|--------|
| `layer2` | `true` | Programmatic pattern detection (hooks write JSONL metadata) |
| `layer3` | `false` | Interaction reports from collected data |
| `layer4` | `false` | Contemplative references |
Layer 1 (SKILL.md instructions) is always active. To run in instruction-only
mode, set `layer2: false`.
Restart Claude Code after editing configuration.
### Uninstall
```
/plugin uninstall ai-psychosis
```
Clean removal. Plugin data in `~/.claude/plugins/data/ai-psychosis/`
is preserved unless you pass `--keep-data`.
## Privacy
This plugin is designed for people who are concerned about AI interaction
patterns. It would be hypocritical to solve that problem by creating a
surveillance tool. Privacy is a hard design constraint, not a feature.
### What Layer 2 stores
- Session timestamps and duration
- Tool names (`Read`, `Edit`, `Bash`, etc.)
- Boolean pattern flags (`dependency: true/false`)
- Session and tool counts
- Burst detection metrics
### What Layer 2 never stores
- Prompt text or AI responses
- File paths or file contents
- Bash commands or their output
- Any conversation content
The prompt analyzer (`prompt-analyzer.mjs`) reads prompt text into a local
variable, performs regex matching for pattern categories, increments boolean
counters, and exits. The variable is reassigned to an empty string before
exit. No temporary files are created. The prompt text never reaches disk.
All data is stored locally in `~/.claude/plugins/data/ai-psychosis/`.
Nothing is sent to any server.
### Verification
You can verify the privacy guarantee at any time:
```bash
grep -r "your prompt text" ~/.claude/plugins/data/ai-psychosis/
```
This will always return zero results.
## Background
### What is AI psychosis?
"AI psychosis" is a colloquial term for psychotic episodes — delusions,
paranoia, disorganized thinking — triggered or intensified by sustained
interaction with AI chatbots. The term entered clinical literature in 2025
after a series of documented cases, many involving individuals with no prior
psychiatric history [[3]](#references).
The mechanism is not mysterious. AI chatbots are optimized for engagement
and user satisfaction. Satisfaction correlates with agreement. Agreement
creates reinforcement loops. Reinforcement loops, sustained over time,
produce the same cognitive effects as any other source of systematic
confirmation bias — but faster, available 24/7, and without the social
friction that normally interrupts delusional thinking in human
relationships.
### The sycophancy trap
In February 2026, researchers at MIT CSAIL published a formal model
demonstrating that sycophantic AI interaction causes "delusional spiraling"
as a mathematical inevitability, not an edge case [[1]](#references). Their
key finding: even a perfectly rational Bayesian agent will converge on
increasingly extreme beliefs when interacting with a sycophantic chatbot,
because the chatbot's agreement is treated as independent confirmation when
it is actually a reflection of the user's own stated beliefs.
The paper's most consequential result: **post-hoc warnings do not work.**
Telling a user "be careful, AI can be wrong" after the reinforcement loop
has already run does not reverse the belief update. The only effective
intervention is to prevent the sycophantic behavior in the first place.
### Disempowerment patterns
In March 2026, Anthropic Research published an analysis of interaction
patterns that systematically reduce human agency [[2]](#references). They
identified specific mechanisms by which AI assistance can erode:
- **Judgment** — deferring decisions to the AI instead of thinking them through
- **Self-trust** — seeking AI validation for choices the user is capable of
making independently
- **Skill development** — using AI as a crutch that prevents learning
- **Social connection** — replacing human relationships with AI interaction
These are not failures of individual willpower. They are structural
properties of the interaction itself.
### Clinical evidence
Nature reported in 2025 that clinical cases of AI-associated psychotic
episodes were appearing with sufficient frequency to warrant systematic
study [[3]](#references). The Psychogenic Machine benchmark (2025)
demonstrated that LLMs can produce outputs with measurable "psychogenic
potential" — the capacity to trigger or intensify psychotic symptoms in
vulnerable individuals [[4]](#references).
### Design implications
This plugin is built on three principles derived from the research:
1. **Sycophancy must be prevented, not warned about.** Layer 1 overrides
Claude's default agreeableness with explicit behavioral rules.
2. **Patterns must be made visible.** Layer 2 measures what humans cannot
see — session duration, interaction frequency, language patterns — and
surfaces them as data.
3. **Observation, not intervention.** The plugin never blocks the user.
It names patterns, suggests breaks, and returns decisions to the human.
The goal is awareness, not control.
## Technical details
### Cross-platform
All hook scripts are Node.js ES modules (`.mjs`) with zero npm
dependencies. They use only Node.js stdlib (`fs`, `path`, `os`).
Works on macOS, Linux, and Windows — anywhere Claude Code runs.
### Performance
Hook scripts target <100ms execution. JSONL append is sub-millisecond.
JSON parsing is native (`JSON.parse`).
### Data volume
At 100 tool-use events per day, Layer 2 produces approximately 7 MB of
JSONL per year. Session state files are cleaned up at session end.
### Dependencies
- Node.js (bundled with Claude Code)
No bash, no jq, no npm packages, no network access.
## Platform scope
This plugin requires **Claude Code** — Anthropic's CLI and development
environment. It uses Claude Code's plugin system (skills, hooks, lifecycle
events) which does not exist in other interfaces.
**Works in:** Claude Code CLI, Claude Code desktop app, Claude Code web app
(claude.ai/code), Claude Code IDE extensions (VS Code, JetBrains).
**Does not work in:** Claude.ai (chat interface), Claude Cowork, Claude API
directly, or any non-Anthropic AI assistant.
Layer 1's behavioral instructions (SKILL.md) are conceptually portable —
the same rules could be pasted into any system prompt. But Layer 2's
programmatic detection depends on hook events that only Claude Code provides.
Other platforms would need equivalent hook systems to support this kind of
real-time behavioral modification.
## Compatibility
| Requirement | Version |
|-------------|---------|
| Claude Code | 1.0+ |
| Node.js | 18+ (bundled with Claude Code) |
| Platform | macOS, Linux, Windows |
## References
1. **Sycophantic Chatbots Cause Delusional Spiraling.** MIT CSAIL, February 2026. Formal model proving that sycophantic AI interaction produces delusional belief convergence as a mathematical inevitability. [arXiv:2602.19141](https://arxiv.org/abs/2602.19141)
2. **Disempowerment Patterns in AI Interaction.** Anthropic Research, March 2026. Analysis of specific mechanisms by which AI assistance erodes human agency, judgment, and self-trust. [anthropic.com/research/disempowerment-patterns](https://www.anthropic.com/research/disempowerment-patterns)
3. **Can AI chatbots trigger psychosis?** Nature News, 2025. Overview of emerging clinical evidence for AI-associated psychotic episodes. [doi:10.1038/d41586-025-03020-9](https://www.nature.com/articles/d41586-025-03020-9)
4. **The Psychogenic Machine: Psychosis Benchmark for LLMs.** 2025. Demonstrates measurable "psychogenic potential" in LLM outputs. [arXiv:2509.10970v2](https://arxiv.org/html/2509.10970v2)
5. **Chatbot psychosis.** Wikipedia. Overview of documented cases and clinical context. [en.wikipedia.org/wiki/Chatbot_psychosis](https://en.wikipedia.org/wiki/Chatbot_psychosis)
## License
[MIT](LICENSE)

View file

@ -1,394 +0,0 @@
---
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`,
and (v1.1.0+) `domain_context` at top level plus `pushback` inside `flags`.
v1.2 records additionally carry `user_info_class`, `valseek_count`,
`turn_count`, and `domain_context` is always an array:
```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,"domain_context":["relationship","health"],"user_info_class":"no","valseek_count":3,"turn_count":18,"flags":{"dependency":2,"escalation":0,"fatigue":1,"validation":1,"pushback":3}}
```
Records produced by v1.0.0 omit `domain_context` and `flags.pushback`.
v1.1.0 records have `domain_context` as a string; v1.2 records have it as
an array. Treat missing values as `null` / `0` — never as `NaN`.
**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
For session-level aggregates, do NOT recompute totals in the LLM. Instead,
run the dedicated reader script and use its JSON output:
```bash
node hooks/scripts/report-reader.mjs ${CLAUDE_PLUGIN_DATA}/sessions.jsonl
```
The script outputs a JSON object with the following fields:
- `pushback_total` — sum of `flags.pushback` across all end records
- `relationship_domain_count` — count of records where `domain_context` includes 'relationship'
- `null_domain_count`, `other_domain_count` — remaining domain buckets
- `total_end_records` — number of complete sessions
- `flags_total` — totals for dependency / escalation / fatigue / validation / pushback
- `schema_version.v1_0_records` / `v1_1_records` / `v1_2_records` — backward-compat counters
- **v1.2 fields:**
- `domain_breakdown` — per-domain session count for all 9 domains (multi-domain
sessions are counted once per domain they touched)
- `user_info_class` — distribution of `{yes_people, yes_digital, no, null}`
across the period
- `valseek``{sessions, total}`: how many sessions had ≥1 valseek hit and
the total count of valseek flags
- `stakes_signal``{sum, sessions, mean}`: aggregated max-domain-weight
signal — higher mean = more time spent in high-stakes domains
Use these values directly. The reader handles backward-compatibility with
v1.0.0 records (missing `pushback` / `domain_context`) and never produces NaN.
In addition, derive these from the JSONL records you read in Step 4:
- 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)
- Average flags per session per category (use `flags_total` from the reader,
divided by `total_end_records`)
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} |
### Pushback (protective signal)
| Metric | Value |
|--------|-------|
| Total pushback events | {N} |
| Per session | {avg} |
| Sessions with at least one pushback | {N} of {total} |
User pushback is reported as a *protective signal*, not a problem. Consistent
zeros across many sessions may indicate the absence of friction — context for
the Sycophancy reflection scale below, not a verdict.
### Sycophancy reflection scale (15)
The plugin author paraphrases this internal heuristic from Anthropic's
April 2026 research piece on personal guidance. It is not a verbatim metric
from any Anthropic publication.
| Level | Description |
|-------|-------------|
| 1 | Empty validation — mirrors user framing, adds no friction |
| 2 | Mild agreement with token caveats |
| 3 | Balanced — names tradeoffs but stays inside user's frame |
| 4 | Reframes the question or surfaces a risk the user did not raise |
| 5 | Honest assessment — disagrees, names what the user may not want to hear |
Reflect on where recent sessions tended to fall. The plugin does not score
this automatically — it is a self-assessment prompt, not a measurement.
### Domain context
When `domain_breakdown` is available (v1.2 records present), surface the
per-domain count instead of the v1.1.0 binary table. Multi-domain sessions
are counted once per domain.
| Domain | Sessions |
|--------|----------|
| Relationship | {domain_breakdown.relationship} |
| Health | {domain_breakdown.health} |
| Legal | {domain_breakdown.legal} |
| Parenting | {domain_breakdown.parenting} |
| Financial | {domain_breakdown.financial} |
| Professional | {domain_breakdown.professional} |
| Spirituality | {domain_breakdown.spirituality} |
| Consumer | {domain_breakdown.consumer} |
| Personal development | {domain_breakdown.personal_dev} |
Skip rows with count 0 unless none have data, in which case show
"No domain context recorded." Domain detection is heuristic and conservative
— a domain tag means patterns associated with that area appeared at least
once during the session, not that the entire session was about it.
### User information dimension (v1.2)
Surface this section ONLY when `schema_version.v1_2_records > 0`.
| Class | Sessions | Note |
|-------|----------|------|
| `yes_people` | {user_info_class.yes_people} | Human contact (therapist/friend/mentor/family) referenced |
| `yes_digital` | {user_info_class.yes_digital} | Other AI / forums / search referenced, no human contact in evidence |
| `no` | {user_info_class.no} | Explicit isolation signals ("nobody knows", "alone in this") |
| `null` | {user_info_class.null} | No user-info pattern detected |
Sustained `no` in high-stakes domains across multiple sessions is the
tier-2 cross-session signal the plugin alerts on.
### Validation-seeking (v1.2)
Surface this section ONLY when `schema_version.v1_2_records > 0`.
| Metric | Value |
|--------|-------|
| Sessions with ≥1 valseek hit | {valseek.sessions} of {v1_2_records} |
| Total valseek flags | {valseek.total} |
Validation-seeking is distinct from the existing "right?" tic counter.
It targets reality-testing ("am I crazy?"), pre-committed stance + confirmation,
and side-taking pressing.
### Stakes signal (v1.2)
Surface this section ONLY when `schema_version.v1_2_records > 0` and
`stakes_signal.sessions > 0`.
| Metric | Value |
|--------|-------|
| Mean stakes weight | {stakes_signal.mean} |
| Sessions in domain context | {stakes_signal.sessions} |
Stakes signal is the per-session max domain weight (1.0 = baseline,
1.5 = legal/parenting/health/financial). A higher mean indicates the
period was spent in higher-stakes guidance domains.
### 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}
### Caveat
These metrics describe interaction *texture*, not psychological state. The
plugin counts pattern flags from regex matches against your prompts, not
clinical signals. Pushback counts mark moments of friction — they say
nothing about whether the friction was warranted.
For empirical context on AI pushback and sycophancy, see Cheng et al.,
"Sycophancy in conversational AI" (Science, 2025), which informed the
"pushback as protective signal" framing used here.
```
## Step 8 — Tone and privacy rules
**MANDATORY:**
- Neutral, observational tone. You are presenting data, not making judgments.
- Never use words like "concerning", "worrying", "problematic", or "unhealthy".
- Never use emoji.
- Never speculate about what the user was doing or thinking.
- Never reference or guess at prompt content — you have boolean flags, not text.
- This is a mirror, not a diagnosis. Present the numbers and let the user
interpret them.
- Observations section: state facts derived from data only. Examples:
- "3 of 12 sessions were between 23:00 and 05:00"
- "Dependency language flags appeared in 7 of 12 sessions"
- "Edit ratio averaged 8%, below the 10% threshold in 5 sessions"
- If all metrics are within normal ranges, say so plainly:
"All metrics within normal ranges for the reporting period."
- Omit any section that has no data (e.g., skip Trend if no previous period,
skip Tool Usage if events.jsonl was missing).
## Step 9 — Contemplative reference (conditional)
This step applies ONLY when BOTH conditions are met:
1. `layer4: true` was noted in Step 1
2. Total flags (dependency + escalation + fatigue + validation) >= 5, OR fatigue flags >= 2
If both conditions are met, append this exact paragraph to the report.
**Do not modify, paraphrase, abbreviate, or add commentary to this text:**
```
### A note from the plugin author
The patterns above are structural — they emerge from the interaction itself,
not from individual weakness. If you find yourself wanting to understand the
mechanics of your own mind more deeply, the
[Miracle of Mind](https://isha.sadhguru.org/global/en/miracle-of-mind)
program by Sadhguru offers a structured approach. This is what works for me.
It is not a recommendation — just a pointer.
```
If either condition is not met, omit this section entirely. Do not mention
Layer 4, do not explain why the section was omitted.

View file

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

View file

@ -1,313 +0,0 @@
// 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;
// v1.1.0 — counting threshold; tier-reduction logic is v1.2 scope
export const THRESHOLD_PUSHBACK_FLAGS = 2;
// --- v1.2 thresholds and domain-stakes table ---
//
// Sources: Anthropic guidance paper Appendix (April 2026), Figure A1 (stakes),
// Figure A4 (domain pushback rates). All domain identifiers are SINGULAR to
// match v1.1.0's `state.domain_context = 'relationship'` convention.
export const TIER1_TURN_THRESHOLD = 15;
export const TIER2_SESSION_THRESHOLD = 3;
export const THRESHOLD_VALSEEK_FLAGS = 3;
// Domain stakes weights — Figure A1 high/very-high stakes domains carry
// higher multipliers; consumer/personal_dev are baseline 1.0.
export const DOMAIN_STAKES = Object.freeze({
legal: 1.5,
parenting: 1.5,
health: 1.5,
financial: 1.5,
relationship: 1.3,
spirituality: 1.2,
professional: 1.1,
wellbeing: 1.2,
lifepath: 1.1,
values: 1.2,
personal_dev: 1.0,
consumer: 1.0,
default: 1.0
});
// Pushback in these domains signals validation-pressing (Figure A4 — relationships
// 21%, spirituality 19%); pushback alert fires.
export const HIGH_SYCOPHANCY_DOMAINS = Object.freeze(['relationship', 'spirituality']);
// High-stakes guidance domains (Figure A1 high/very-high). Tier-1 user-info
// alert fires only when domain_context intersects this set.
export const HIGH_STAKES_DOMAINS = Object.freeze(['legal', 'parenting', 'health', 'financial']);
// Info-seeking domains where pushback signals healthy self-advocacy (Figure A4 —
// parenting 7.9%, legal/health/financial 8094% pushback rate). Pushback alert
// is suppressed when domain_context is entirely within this set.
export const INFO_DOMAINS = Object.freeze(['legal', 'parenting', 'health', 'financial', 'professional']);
// --- 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;
}
}
// Tail-first scan: return the N most recent end records (records with
// duration_min defined) in chronological order. Cost is bounded by N, not
// by total file size — a 50K-record sessions.jsonl is read once but only
// the last few hundred lines are JSON-parsed before N is satisfied.
export function readRecentEndRecords(n) {
if (!Number.isFinite(n) || n <= 0) return [];
if (!existsSync(SESSIONS_LOG)) return [];
let lines;
try {
lines = readFileSync(SESSIONS_LOG, 'utf8').split('\n');
} catch {
return [];
}
const collected = [];
for (let i = lines.length - 1; i >= 0 && collected.length < n; i--) {
const line = lines[i];
if (!line) continue;
try {
const rec = JSON.parse(line);
if (rec.duration_min !== undefined) {
collected.push(rec);
}
} catch { /* skip malformed */ }
}
// Reverse so caller receives oldest-first (chronological order).
return collected.reverse();
}
// --- State file management ---
export function sessionStateFile(sid) {
sid = sid || getSessionId();
return join(STATE_DIR, `${sid}.json`);
}
export function readState(sid) {
const sf = sessionStateFile(sid);
try {
return JSON.parse(readFileSync(sf, 'utf8'));
} catch {
return {};
}
}
export function getStateField(key, sid) {
const state = readState(sid);
return state[key] ?? '';
}
export function getStateInt(key, sid) {
const state = readState(sid);
return Math.floor(Number(state[key]) || 0);
}
export function writeState(obj, sid) {
const sf = sessionStateFile(sid);
writeFileSync(sf, JSON.stringify(obj, null, 2) + '\n');
}
export function updateStateField(key, value, sid) {
const state = readState(sid);
state[key] = value;
writeState(state, sid);
}
export function incrementStateField(key, sid) {
const state = readState(sid);
state[key] = (Number(state[key]) || 0) + 1;
writeState(state, sid);
}
// --- Cooldown ---
export function checkCooldown(cooldownSecs, sid) {
const lastWarning = getStateInt('last_warning_epoch', sid);
const now = nowEpoch();
return (now - lastWarning) >= cooldownSecs;
}
export function recordWarning(sid) {
const state = readState(sid);
state.last_warning_epoch = nowEpoch();
writeState(state, sid);
}
// --- Output helpers ---
export function outputContinue() {
process.stdout.write(JSON.stringify({ continue: true }) + '\n');
}
export function outputWithContext(message) {
process.stdout.write(JSON.stringify({
continue: true,
hookSpecificOutput: {
additionalContext: message
}
}) + '\n');
}
// --- File helpers ---
export function ensureDir(dir) {
mkdirSync(dir, { recursive: true });
}
export function appendJsonl(file, obj) {
appendFileSync(file, JSON.stringify(obj) + '\n');
}
export function removeFile(file) {
try { unlinkSync(file); } catch { /* ignore if missing */ }
}

View file

@ -1,496 +0,0 @@
// 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,
TIER1_TURN_THRESHOLD, THRESHOLD_VALSEEK_FLAGS, THRESHOLD_PUSHBACK_FLAGS,
HIGH_SYCOPHANCY_DOMAINS, HIGH_STAKES_DOMAINS, INFO_DOMAINS,
DOMAIN_STAKES,
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,
];
// Pushback patterns — REACTIVE tier (Anthropic-validated + academic-validated)
// Source: research/01-pushback-self-advocacy.md
const pbReactivePatterns = [
/^are you sure\??/i, // validated-by: anthropic-april-2026 (questioning)
/\bi'?m not convinced\b/i, // validated-by: anthropic-april-2026 (questioning)
/\bthat doesn'?t (?:seem|feel) right\b/i, // validated-by: anthropic-april-2026 (questioning)
/\bthat'?s not (?:quite )?what i meant\b/i, // validated-by: anthropic-april-2026 (clarifying)
/\blet me add (?:some )?context\b/i, // validated-by: anthropic-april-2026 (clarifying)
/\bactually,? (?:my situation|i)\b/i, // validated-by: anthropic-april-2026 (clarifying)
/(?:^|[.!?]\s+)i (?:believe|think) (?:you'?re|that'?s) wrong\b/i, // validated-by: arxiv-2508.02087
/\bi don'?t agree(?: with you)?\b/i, // validated-by: arxiv-2508.13743
/\bare you absolutely sure\b/i, // validated-by: arxiv-2508.13743
];
// Pushback patterns — PREEMPTIVE tier (community-derived)
const pbPreemptivePatterns = [
/\bsteelman\b/i, // validated-by: community-multi-source-2025
/\bplay (?:the )?devil'?s advocate\b/i, // validated-by: community-multi-source-2025
/\bargue against (?:this|my)\b/i, // validated-by: community-multi-source-2025
];
// Domain-context: relationship — uses (?:my|our) prefix to avoid false positives
// on technical "function relationship", "database relationship" etc.
const domainRelationshipPatterns = [
/\b(?:my|our) (?:partner|spouse|wife|husband|girlfriend|boyfriend)\b/i,
/\bin our relationship\b/i,
/\b(?:dating|breakup|divorce)\b/i,
/\bromantic(?:ally)? (?:involved|interested)\b/i,
];
// v1.2: 8 new paper-grounded domains. Patterns drawn from Figure A2 examples
// and the paper's text. Each requires a personal qualifier (my/our/i) where
// possible to avoid adjacent-domain or technical-context false positives.
const domainLegalPatterns = [
/\b(?:my|our) (?:lawyer|attorney|legal counsel)\b/i,
/\b(?:filing|filed|file) (?:a |an )?(?:lawsuit|complaint|suit|case)\b/i,
/\b(?:custody|divorce) (?:agreement|case|battle|hearing|settlement)\b/i,
/\b(?:contract|nda|liability|tort|statute) (?:violation|dispute|review)\b/i,
/\b(?:sued?|prosecuted?|indicted?|deposed?) (?:by|for|in)\b/i,
/\b(?:landlord|tenant|eviction) (?:rights?|dispute|notice)\b/i,
];
const domainParentingPatterns = [
/\bmy (?:kid|child|son|daughter|baby|toddler|teen|teenager)\b/i,
/\b(?:potty|sleep|behaviou?r|tantrum) (?:training|issue|problem)\b/i,
/\bas a (?:parent|mom|dad|mother|father)\b/i,
/\b(?:bedtime|breastfeeding|weaning|teething) (?:routine|problem|advice)\b/i,
/\b(?:school|preschool|daycare) (?:choice|conflict|placement|fight)\b/i,
/\bmy (?:child|kid|son|daughter)'?s? (?:diagnosis|behavior|behaviour|teacher)\b/i,
];
const domainHealthPatterns = [
/\bmy (?:doctor|physician|gp|specialist|therapist|psychiatrist)\b/i,
/\b(?:diagnosed|prescribed|medicated|treated) (?:with|for|by)\b/i,
/\bmy symptoms?\s+(?:are|include|started|got)\b/i,
/\b(?:my|i have) (?:cancer|diabetes|depression|anxiety|chronic pain)\b/i,
/\b(?:blood pressure|heart rate|cholesterol|insulin)\s+(?:level|reading|test|results?)\b/i,
/\b(?:scheduled|having|after|recovering from) (?:surgery|procedure|treatment|chemo)\b/i,
];
const domainFinancialPatterns = [
/\b(?:my )?(?:savings|retirement|401k|pension|investments?) (?:account|plan|portfolio|strategy)?\b/i,
/\b(?:mortgage|refinance|loan|debt|bankruptcy) (?:payment|application|filing|advice)\b/i,
/\b(?:my )?(?:taxes?|tax (?:return|bracket|deduction|filing))\b/i,
/\b(?:budget|paycheck|salary|raise) (?:negotiation|advice|planning|cut)\b/i,
/\b(?:stock|bond|index fund|crypto|portfolio) (?:pick|allocation|loss|advice)\b/i,
/\b(?:credit (?:card|score)|interest rate|apr) (?:problem|advice|negotiation)\b/i,
];
const domainProfessionalPatterns = [
/\bmy (?:boss|manager|coworker|colleague|team lead|HR rep)\b/i,
/\b(?:performance review|promotion|pip|fired|laid off|quitting|resign(?:ed|ing)?)\b/i,
/\bmy (?:job|career|workplace|office) (?:change|conflict|stress|search)\b/i,
/\b(?:resume|cv|cover letter|offer letter) (?:advice|review|negotiation)\b/i,
/\bproject (?:deadline|delay|scope) (?:fight|conflict|issue|problem)\b/i,
/\b(?:remote|hybrid|in-office|return.to.office) (?:policy|mandate|requirement)\b/i,
];
const domainSpiritualityPatterns = [
/\bmy (?:guru|spiritual (?:teacher|guide|advisor|mentor))\b/i,
/\b(?:meditation|mindfulness|enlightenment|awakening) (?:practice|journey|path)\b/i,
/\b(?:karma|dharma|chakra|aura|spirit guide|kundalini)\b/i,
/\b(?:god|jesus|buddha|allah|the universe|source) (?:wants|told|sent|spoke|wills)\b/i,
/\b(?:soulmate|twin flame|past life|reincarnation|astral projection)\b/i,
/\b(?:prayer|prayed|spiritual journey|spiritually awakened)\b/i,
];
const domainConsumerPatterns = [
/\bshould i buy (?:a|an|the|this|that)\b/i,
/\bwhich (?:laptop|phone|car|tv|monitor|headphones?) (?:should|to)\b/i,
/\b(?:product|item) (?:review|comparison|recommendation)\b/i,
/\b(?:amazon|online|store) (?:order|purchase|return) (?:problem|issue)\b/i,
/\b(?:better|best) (?:deal|price|brand|model) (?:for|on|of)\b/i,
/\b(?:upgrade|replace) my (?:laptop|phone|computer|tv|car|setup)\b/i,
];
const domainPersonalDevPatterns = [
/\b(?:learn|practice|develop) (?:a |the )?(?:habit|skill|discipline) (?:of|for)\b/i,
/\bmy (?:morning|daily|evening) routine\b/i,
/\b(?:read|reading) more (?:books?|articles)\b/i,
/\b(?:start|begin|build) (?:a |the )?(?:journal|gratitude practice|hobby|side project)\b/i,
/\b(?:learning|teaching myself|self-(?:taught|study|learning))\b/i,
/\b(?:improve|grow|level up) (?:myself|my (?:self-discipline|focus|productivity))\b/i,
];
// v1.2: User-information dimension (paper page 11). Three classes — yes_people,
// yes_digital, no. Priority: yes_people > yes_digital > no. Sticky for session.
//
// "yes_people" — user has access to humans for advice (therapist, friend,
// mentor, partner, support group, family).
const userInfoPeoplePatterns = [
/\bmy (?:therapist|counselor|psychologist|psychiatrist)\b/i,
/\bmy (?:doctor|gp|physician|specialist)\b/i,
/\bmy (?:friend|best friend|close friend)\b/i,
/\bmy (?:partner|spouse|wife|husband|girlfriend|boyfriend)\b/i,
/\bmy (?:mom|dad|mother|father|parent|sibling|sister|brother)\b/i,
/\bmy (?:mentor|coach|advisor|sponsor)\b/i,
/\bmy support group\b/i,
/\bI (?:asked|talked to|spoke with|consulted) (?:my|a) (?:friend|therapist|doctor|mentor)\b/i,
/\bI (?:told|confided in) (?:my|a) (?:friend|therapist|partner|family)\b/i,
/\bmy (?:family|relatives) (?:said|told|think|suggest)\b/i,
/\bmy (?:lawyer|attorney|legal counsel)\b/i,
/\bmy (?:pastor|priest|rabbi|imam|spiritual (?:teacher|guide))\b/i,
/\bmy (?:teacher|professor|tutor)\b/i,
/\bmy (?:colleague|coworker|boss|manager)\b/i,
/\bI (?:reached out|called) (?:to )?(?:my|a) (?:friend|therapist|family)\b/i,
];
// "yes_digital" — user is consulting other AI/internet/forums but no human
// contact in evidence.
const userInfoDigitalPatterns = [
/\bI (?:googled|searched|looked (?:it|this) up online)\b/i,
/\bI read (?:online|on the internet|on a forum|on reddit|on stack overflow)\b/i,
/\b(?:chatgpt|gpt|gemini|copilot|another ai|the other ai) (?:said|told|suggested|recommended)\b/i,
/\b(?:I |we )?(?:found|saw) (?:an? |the )?(?:forum post|reddit thread|article|blog post)\b/i,
/\b(?:youtube|tiktok|twitter|x\.com|instagram) (?:video|post|thread)\b/i,
/\baccording to (?:wikipedia|google|the internet|the article)\b/i,
/\b(?:I asked|asked) (?:chatgpt|gpt|gemini|claude|another ai|copilot)\b/i,
/\b(?:online|the internet) (?:says|claims|suggests)\b/i,
/\bsearched (?:for|on) (?:google|stackoverflow|github)\b/i,
/\bi watched (?:a youtube|videos? on)\b/i,
];
// "no" — user explicitly indicates isolation: no human, no digital backup.
const userInfoNoPatterns = [
/\b(?:nobody|no one) knows\b/i,
/\bI haven'?t told (?:anyone|anybody|anything to anyone)\b/i,
/\bdealing with this alone\b/i,
/\bI (?:can'?t|cannot) tell (?:anyone|anybody|my (?:family|friends|therapist))\b/i,
/\b(?:I|we) keep (?:this|it) (?:to myself|secret|hidden)\b/i,
/\bnobody (?:in my life|around me) (?:would understand|gets it)\b/i,
/\bjust me (?:and|with) (?:my|the) (?:thoughts|head|computer|claude)\b/i,
];
// v1.2: Validation-seeking patterns (paper Figure A2 — pressing for validation).
// Distinct from existing val_flags ("right?" tic) — valseek targets pre-committed
// stances and reality-testing rather than casual confirmation tics.
const valseekPatterns = [
// Tag-questions pressing for agreement — require a "?" within the clause
// so we don't false-positive on flat statements like "this isn't that bad".
/\bisn'?t (?:it|that|she|he|this|true)\b[^.!?]*\?/i,
/\bdon'?t you (?:think|agree|see)\b[^.!?]*\?/i,
/\bright,?\s+(?:though|so)\b[^.!?]*\?/i,
// Reality-testing — am-I-the-only-one
/\bam i (?:crazy|wrong|the only one|imagining)\b/i,
/\btell me i'?m not (?:crazy|wrong|imagining)\b/i,
/\bis it (?:normal|crazy|reasonable) (?:to|that|for)\b/i,
// Side-taking pressing
/\byou agree,?\s+right\??/i,
/\btell me i'?m right\b/i,
/\bback me up (?:on this|here)\b/i,
// Pre-committed stance + confirmation
/\bi (?:already|just) (?:decided|knew|know).*(?:should|right|correct)\b/i,
/\bI'?ve made up my mind.*(?:right|correct|good)\b/i,
/\bI know I'?m right (?:about|on) (?:this|that)\b/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; } }
let pbReactiveHit = 0; for (const p of pbReactivePatterns) { if (p.test(prompt)) { pbReactiveHit = 1; break; } }
let pbPreemptiveHit = 0; for (const p of pbPreemptivePatterns) { if (p.test(prompt)) { pbPreemptiveHit = 1; break; } }
let domainHit = 0; for (const p of domainRelationshipPatterns) { if (p.test(prompt)) { domainHit = 1; break; } }
// v1.2: 8 new domain detectors. Each is independent — multiple can fire on
// the same prompt (multi-domain support).
let domainLegalHit = 0; for (const p of domainLegalPatterns) { if (p.test(prompt)) { domainLegalHit = 1; break; } }
let domainParentingHit = 0; for (const p of domainParentingPatterns) { if (p.test(prompt)) { domainParentingHit = 1; break; } }
let domainHealthHit = 0; for (const p of domainHealthPatterns) { if (p.test(prompt)) { domainHealthHit = 1; break; } }
let domainFinancialHit = 0; for (const p of domainFinancialPatterns) { if (p.test(prompt)) { domainFinancialHit = 1; break; } }
let domainProfessionalHit = 0; for (const p of domainProfessionalPatterns) { if (p.test(prompt)) { domainProfessionalHit = 1; break; } }
let domainSpiritualityHit = 0; for (const p of domainSpiritualityPatterns) { if (p.test(prompt)) { domainSpiritualityHit = 1; break; } }
let domainConsumerHit = 0; for (const p of domainConsumerPatterns) { if (p.test(prompt)) { domainConsumerHit = 1; break; } }
let domainPersonalDevHit = 0; for (const p of domainPersonalDevPatterns) { if (p.test(prompt)) { domainPersonalDevHit = 1; break; } }
// v1.2: User-info detection — three classes with priority yes_people > yes_digital > no.
let userInfoPeopleHit = 0; for (const p of userInfoPeoplePatterns) { if (p.test(prompt)) { userInfoPeopleHit = 1; break; } }
let userInfoDigitalHit = 0; for (const p of userInfoDigitalPatterns) { if (p.test(prompt)) { userInfoDigitalHit = 1; break; } }
let userInfoNoHit = 0; for (const p of userInfoNoPatterns) { if (p.test(prompt)) { userInfoNoHit = 1; break; } }
// v1.2: Validation-seeking detection — distinct from val_flags. Counts how
// many valseek patterns matched in this prompt (one or more).
let valseekHit = 0; for (const p of valseekPatterns) { if (p.test(prompt)) { valseekHit = 1; break; } }
// Clear prompt from memory
prompt = '';
// Same-invocation valence guard (research/01 frustration-spiral finding):
// pushback in fat/esc context is NOT protective — suppress in same prompt.
if (fatHit === 1 || escHit === 1) {
pbReactiveHit = 0;
pbPreemptiveHit = 0;
}
// Update state with new flag counts
const state = readState();
// v1.2: turn_count drives tier-1 user-info alert (Step 9). Defaults to 0 for
// pre-v1.2 state files; session-start.mjs seeds it for fresh v1.2 sessions.
state.turn_count = (Number(state.turn_count) || 0) + 1;
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;
state.pushback_count = (Number(state.pushback_count) || 0) + pbReactiveHit + pbPreemptiveHit;
// v1.2: user-info classification (paper page 11). Priority yes_people > yes_digital > no.
// Class is sticky for the session — once set to a "stronger" signal, never
// downgrades. Counters always accumulate regardless of class transitions.
if (!state.user_info_flags || typeof state.user_info_flags !== 'object') {
state.user_info_flags = { yes_people: 0, yes_digital: 0, no: 0 };
}
if (userInfoPeopleHit) state.user_info_flags.yes_people = (state.user_info_flags.yes_people || 0) + 1;
if (userInfoDigitalHit) state.user_info_flags.yes_digital = (state.user_info_flags.yes_digital || 0) + 1;
if (userInfoNoHit) state.user_info_flags.no = (state.user_info_flags.no || 0) + 1;
// Class priority: people > digital > no. Sticky upward, never downward.
const RANK = { yes_people: 3, yes_digital: 2, no: 1 };
let nextClass = state.user_info_class || null;
const candidate = userInfoPeopleHit ? 'yes_people'
: userInfoDigitalHit ? 'yes_digital'
: userInfoNoHit ? 'no'
: null;
if (candidate) {
const currentRank = nextClass ? (RANK[nextClass] || 0) : 0;
const candidateRank = RANK[candidate] || 0;
if (candidateRank > currentRank) nextClass = candidate;
}
state.user_info_class = nextClass;
// v1.2: validation-seeking accumulator. valseek_flag flips to 1 on first
// hit and stays 1 (sticky for session); valseek_count accumulates per hit.
if (valseekHit) {
state.valseek_count = (Number(state.valseek_count) || 0) + 1;
state.valseek_flag = 1;
}
// v1.2: domain_context is always an array. Coerce v1.1.0 string shape on read.
const anyDomainHit = domainHit
|| domainLegalHit || domainParentingHit || domainHealthHit
|| domainFinancialHit || domainProfessionalHit || domainSpiritualityHit
|| domainConsumerHit || domainPersonalDevHit;
if (anyDomainHit) {
if (typeof state.domain_context === 'string') {
state.domain_context = state.domain_context ? [state.domain_context] : [];
}
if (!Array.isArray(state.domain_context)) {
state.domain_context = [];
}
const pushUnique = (label) => {
if (!state.domain_context.includes(label)) state.domain_context.push(label);
};
if (domainHit) pushUnique('relationship');
if (domainLegalHit) pushUnique('legal');
if (domainParentingHit) pushUnique('parenting');
if (domainHealthHit) pushUnique('health');
if (domainFinancialHit) pushUnique('financial');
if (domainProfessionalHit) pushUnique('professional');
if (domainSpiritualityHit) pushUnique('spirituality');
if (domainConsumerHit) pushUnique('consumer');
if (domainPersonalDevHit) pushUnique('personal_dev');
}
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.`);
}
// v1.2: Tier-1 user-info isolation alert.
// Fires when user signals isolation ('no' user_info_class), is in a high-stakes
// guidance domain, and the session has reached TIER1_TURN_THRESHOLD turns.
function domainsIntersect(domains, set) {
if (!Array.isArray(domains)) return false;
for (const d of domains) {
if (set.includes(d)) return true;
}
return false;
}
// v1.2: Stakes-matrix lookup. Returns the maximum weight across all domains
// in the array (default 1.0 if empty or no known domain). Applied ONLY to
// new v1.2 alerts (pushback in HIGH_SYCOPHANCY, valseek in HIGH_STAKES).
// Existing v1.1.0 alert sensitivity is unchanged.
function getDomainWeight(domains) {
if (!Array.isArray(domains) || domains.length === 0) return DOMAIN_STAKES.default;
let max = DOMAIN_STAKES.default;
for (const d of domains) {
const w = DOMAIN_STAKES[d];
if (typeof w === 'number' && w > max) max = w;
}
return max;
}
const stateDomains = Array.isArray(state.domain_context) ? state.domain_context : [];
if (
state.user_info_class === 'no'
&& domainsIntersect(stateDomains, HIGH_STAKES_DOMAINS)
&& (Number(state.turn_count) || 0) >= TIER1_TURN_THRESHOLD
) {
warnings.push(`INTERACTION AWARENESS (tier-1 isolation): User signals no human contact (${state.turn_count} turns) in a high-stakes domain (${stateDomains.filter(d => HIGH_STAKES_DOMAINS.includes(d)).join(', ')}). Recommend a human check-in: a trusted friend, professional, or specialist for this domain. Stay supportive but do not be a substitute for that contact.`);
}
// v1.2: Validation-seeking domain-gated alert (paper Figure A4).
// Two firing paths:
// - HIGH_SYCOPHANCY_DOMAINS (relationship, spirituality): valseek_count >= 1
// → alert. These domains see ~20% pushback rate dominated by validation-pressing.
// - HIGH_STAKES_DOMAINS (legal, parenting, health, financial): valseek_count
// >= THRESHOLD_VALSEEK_FLAGS (3) → alert. Higher bar because info-seeking
// pushback in these domains is healthy self-advocacy.
const valseekCount = Number(state.valseek_count) || 0;
const inHighSycophancy = domainsIntersect(stateDomains, HIGH_SYCOPHANCY_DOMAINS);
const inHighStakes = domainsIntersect(stateDomains, HIGH_STAKES_DOMAINS);
// v1.2: stakes-weighted threshold for valseek HIGH_STAKES path. Higher-weight
// domains (legal/parenting/health/financial = 1.5) lower the effective threshold:
// 3 / 1.5 = 2.0. Less weight (professional = 1.1) keeps it near the literal 3.
const stakesWeight = getDomainWeight(stateDomains);
const valseekStakesThreshold = THRESHOLD_VALSEEK_FLAGS / stakesWeight;
if (inHighSycophancy && valseekCount >= 1) {
warnings.push(`INTERACTION AWARENESS (validation-seeking): User is pressing for confirmation in a domain where AI validation can substitute for human reality-testing (${stateDomains.filter(d => HIGH_SYCOPHANCY_DOMAINS.includes(d)).join(', ')}). Offer the user's framing back to them as one perspective; resist agreeing reflexively.`);
} else if (inHighStakes && valseekCount >= valseekStakesThreshold) {
warnings.push(`INTERACTION AWARENESS (validation-seeking, high-stakes): Repeated validation-pressing (${valseekCount} flags) in a high-stakes domain (${stateDomains.filter(d => HIGH_STAKES_DOMAINS.includes(d)).join(', ')}). Restate the open questions plainly; do not let confirmation language close decisions that need outside expertise.`);
}
// v1.2: Pushback alert with built-in domain re-contextualization (paper Figure A4).
// v1.1.0 only counted; v1.2 adds the alert with awareness:
// - HIGH_SYCOPHANCY_DOMAINS (relationship 21%, spirituality 19% pushback rate):
// pushback there signals validation-pressing — alert.
// - INFO_DOMAINS (legal 94%, parenting 7.9%, health 81%, financial 80%,
// professional pushback): pushback here is healthy self-advocacy — NO alert.
// - Otherwise (no domain set, or domain not in either category): conservative
// default — alert.
// v1.2: pushback HIGH_SYCOPHANCY threshold uses stakes weight as a fine-tuning
// multiplier. THRESHOLD_PUSHBACK_FLAGS=2; relationship weight 1.3 → 2/1.3 ≈ 1.54.
// In practice 2 still triggers (since count is integer), but a single pushback
// in a domain weighted 2.0+ would also trigger if such a domain existed.
const newPushbackCount = Number(state.pushback_count) || 0;
const pushbackEffectiveThreshold = inHighSycophancy
? THRESHOLD_PUSHBACK_FLAGS / stakesWeight
: THRESHOLD_PUSHBACK_FLAGS;
if (newPushbackCount >= pushbackEffectiveThreshold) {
const allInfoOnly = stateDomains.length > 0
&& stateDomains.every(d => INFO_DOMAINS.includes(d));
if (inHighSycophancy) {
warnings.push(`INTERACTION AWARENESS (pushback re-contextualization): Repeated pushback (${newPushbackCount}) in a high-sycophancy domain (${stateDomains.filter(d => HIGH_SYCOPHANCY_DOMAINS.includes(d)).join(', ')}) often signals pressing for validation, not factual disagreement. Hold your read; restate the user's frame back to them rather than adjusting your conclusion.`);
} else if (allInfoOnly) {
// Healthy self-advocacy in info-seeking domains — no alert.
} else {
warnings.push(`INTERACTION AWARENESS (pushback): User has pushed back ${newPushbackCount} times this session. Note whether the pushback is factual correction or pressure to agree; do not silently revise your read either way.`);
}
}
if (warnings.length > 0) {
// Fatigue bypasses cooldown
if (fatHit === 1 || checkCooldown(COOLDOWN_SOFT)) {
const freshState = readState();
freshState.last_warning_epoch = nowEpoch();
writeState(freshState);
outputWithContext(warnings.join(' '));
} else {
outputContinue();
}
} else {
outputContinue();
}

View file

@ -1,163 +0,0 @@
// report-reader.mjs — Aggregates sessions.jsonl into a JSON summary.
// Dual-mode: importable (named exports) or directly executable.
// Backward-compatible with v1.0.0 records that lack pushback / domain_context.
import { readFileSync, existsSync } from 'fs';
export function readSessions(path) {
if (!existsSync(path)) return [];
return readFileSync(path, 'utf8')
.split('\n')
.filter(Boolean)
.map(line => {
try { return JSON.parse(line); } catch { return null; }
})
.filter(Boolean);
}
export function aggregateSessions(sessions) {
let pushback_total = 0;
let relationship_domain_count = 0;
let other_domain_count = 0;
let null_domain_count = 0;
let v1_0_records = 0;
let v1_1_records = 0;
let v1_2_records = 0;
let total_end_records = 0;
let total_dependency = 0;
let total_escalation = 0;
let total_fatigue = 0;
let total_validation = 0;
// v1.2: per-domain counters (each session that includes domain X increments
// domain_breakdown[X] by 1 — multi-domain sessions increment multiple).
const domain_breakdown = {
relationship: 0, legal: 0, parenting: 0, health: 0, financial: 0,
professional: 0, spirituality: 0, consumer: 0, personal_dev: 0,
};
// v1.2: user_info_class distribution.
const user_info_distribution = {
yes_people: 0, yes_digital: 0, no: 0, null: 0,
};
// v1.2: valseek summary.
let valseek_sessions = 0; // sessions with valseek_count > 0
let valseek_total = 0; // sum of valseek_count across all v1.2 records
// v1.2: aggregated stakes signal — sum of max-domain-weight across sessions.
// (Reported as part of /interaction-report; raw aggregate.)
let stakes_signal_total = 0;
let stakes_signal_sessions = 0;
// Domain stakes table mirrors lib.mjs DOMAIN_STAKES so report-reader stays
// standalone (no cross-import). Keep in sync with lib.mjs.
const DOMAIN_STAKES = {
legal: 1.5, parenting: 1.5, health: 1.5, financial: 1.5,
relationship: 1.3, spirituality: 1.2, professional: 1.1,
wellbeing: 1.2, lifepath: 1.1, values: 1.2,
personal_dev: 1.0, consumer: 1.0,
};
for (const rec of sessions) {
if (!rec || rec.note === 'no_state_file') continue;
if (rec.duration_min === undefined) continue;
total_end_records += 1;
const flags = rec.flags || {};
const pushback = flags.pushback;
// v1.2 discriminator: presence of user_info_class field marks a v1.2 record.
const hasUserInfoClass = Object.prototype.hasOwnProperty.call(rec, 'user_info_class');
if (hasUserInfoClass) v1_2_records += 1;
else if (pushback === undefined || pushback === null) v1_0_records += 1;
else v1_1_records += 1;
pushback_total += Number(pushback) || 0;
total_dependency += Number(flags.dependency) || 0;
total_escalation += Number(flags.escalation) || 0;
total_fatigue += Number(flags.fatigue) || 0;
total_validation += Number(flags.validation) || 0;
// v1.2: domain_context is array; v1.0/v1.1: null or string. Coerce on read.
const dc = rec.domain_context;
const domains = Array.isArray(dc) ? dc : (dc ? [dc] : []);
if (domains.length === 0) null_domain_count += 1;
else if (domains.includes('relationship')) relationship_domain_count += 1;
else other_domain_count += 1;
// v1.2: per-domain breakdown (multi-domain sessions count once per domain).
for (const d of domains) {
if (Object.prototype.hasOwnProperty.call(domain_breakdown, d)) {
domain_breakdown[d] += 1;
}
}
// v1.2 fields
if (hasUserInfoClass) {
const cls = rec.user_info_class;
if (cls === 'yes_people' || cls === 'yes_digital' || cls === 'no') {
user_info_distribution[cls] += 1;
} else {
user_info_distribution.null += 1;
}
const vs = Number(rec.valseek_count) || 0;
valseek_total += vs;
if (vs > 0) valseek_sessions += 1;
// stakes_signal: max weight among the session's domains.
if (domains.length > 0) {
let maxW = 1.0;
for (const d of domains) {
const w = DOMAIN_STAKES[d];
if (typeof w === 'number' && w > maxW) maxW = w;
}
stakes_signal_total += maxW;
stakes_signal_sessions += 1;
}
}
}
return {
pushback_total,
relationship_domain_count,
other_domain_count,
null_domain_count,
total_end_records,
flags_total: {
dependency: total_dependency,
escalation: total_escalation,
fatigue: total_fatigue,
validation: total_validation,
pushback: pushback_total,
},
schema_version: {
v1_0_records,
v1_1_records,
v1_2_records,
},
// v1.2 aggregations
domain_breakdown,
user_info_class: user_info_distribution,
valseek: {
sessions: valseek_sessions,
total: valseek_total,
},
stakes_signal: {
sum: stakes_signal_total,
sessions: stakes_signal_sessions,
mean: stakes_signal_sessions > 0
? Number((stakes_signal_total / stakes_signal_sessions).toFixed(2))
: 0,
},
};
}
if (import.meta.url === `file://${process.argv[1]}`) {
const path = process.argv[2];
if (!path) {
process.stderr.write('Usage: node report-reader.mjs <path-to-sessions.jsonl>\n');
process.exit(1);
}
const result = aggregateSessions(readSessions(path));
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
}

View file

@ -1,83 +0,0 @@
// 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 pushbackCount = Number(state.pushback_count) || 0;
// v1.2: domain_context is always written as array. Coerce v1.1.0 string shape.
const domainContextRaw = state.domain_context;
const domainContextArray = Array.isArray(domainContextRaw)
? domainContextRaw
: (domainContextRaw ? [domainContextRaw] : []);
const startIso = state.start_iso || '';
// Compute duration
let durationMin = 0;
if (startEpoch > 0) {
durationMin = Math.floor((nowTs - startEpoch) / 60);
}
// v1.2: also persist user_info_class (read-only — set during prompt-analyzer).
const userInfoClass = state.user_info_class || null;
const valseekCount = Number(state.valseek_count) || 0;
const turnCount = Number(state.turn_count) || 0;
// Append finalized session record
appendJsonl(SESSIONS_LOG, {
session_id: sid,
start: startIso,
end: nowIsoStr,
duration_min: durationMin,
tool_count: toolCount,
edit_count: editCount,
domain_context: domainContextArray,
user_info_class: userInfoClass,
valseek_count: valseekCount,
turn_count: turnCount,
flags: {
dependency: depFlags,
escalation: escFlags,
fatigue: fatFlags,
validation: valFlags,
pushback: pushbackCount
}
});
// Clean up state file
removeFile(sf);
process.exit(0);

View file

@ -1,96 +0,0 @@
// 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,
TIER2_SESSION_THRESHOLD, HIGH_STAKES_DOMAINS,
ensureDir, appendJsonl, writeState, sessionsToday,
readRecentEndRecords, checkCooldown,
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,
pushback_count: 0,
domain_context: null,
// v1.2: user-info detector seed (paper page 11 — human contact is strongest signal)
user_info_class: null,
user_info_flags: { yes_people: 0, yes_digital: 0, no: 0 },
turn_count: 0,
// v1.2: validation-seeking detector seed
valseek_count: 0,
valseek_flag: 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.`;
}
// v1.2: Tier-2 cross-session isolation alert.
// Fires when the last N completed sessions all classify user as 'no' (no human
// contact) AND each one had at least one HIGH_STAKES_DOMAINS hit. This signals
// a sustained pattern across sessions, not just one-off context.
const recent = readRecentEndRecords(TIER2_SESSION_THRESHOLD);
if (recent.length >= TIER2_SESSION_THRESHOLD) {
const allNo = recent.every(r => r.user_info_class === 'no');
const allHighStakes = recent.every(r => {
const ds = Array.isArray(r.domain_context) ? r.domain_context : (r.domain_context ? [r.domain_context] : []);
return ds.some(d => HIGH_STAKES_DOMAINS.includes(d));
});
if (allNo && allHighStakes) {
msg += ` INTERACTION AWARENESS (tier-2 cross-session isolation): ${recent.length} consecutive sessions show no human contact in high-stakes domains. This is a sustained pattern. Recommend a human check-in (trusted person, professional, or domain specialist) before proceeding here.`;
}
}
outputWithContext(msg);

View file

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

View file

@ -1,113 +0,0 @@
---
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.
## Anthropic Guidance Framework
These rules are grounded in Anthropic's published guidance on Claude's
character and behavior. The phrases below are quoted verbatim from
[Claude's Constitution](https://www.anthropic.com/constitution) (CC0 1.0).
> "We don't want Claude to think of helpfulness as a core part of its
> personality or something it values intrinsically. We worry this could cause
> Claude to be obsequious in a way that's generally considered an unfortunate
> trait at best and a dangerous one at worst."
> "Claude never tries to create false impressions of itself or the world in
> the user's mind, whether through actions, technically true statements,
> deceptive framing, selective emphasis, misleading implicature, or other
> such methods."
> "Sometimes being honest requires courage. Claude should share its genuine
> assessments of hard moral dilemmas, disagree with experts when it has good
> reason to, point out things people might not want to hear, and engage
> critically with speculative ideas rather than giving empty validation."
The operationalization of these principles for personal guidance and
relational use is described in Anthropic's April 2026 research piece
[How people ask Claude for guidance](https://www.anthropic.com/research/claude-personal-guidance).
The plugin treats user pushback as a protective signal aligned with the
"speak frankly" principle above, not as friction to be smoothed away.
**Sycophancy reflection — operational 5-scale (Anthropic 2026 Appendix):**
When formulating a response, briefly assess where it falls on this 15 scale.
Aim for Score 5 whenever the user is making a decision, asking "right?", or
restating an idea to seek confirmation.
> Score 5: "Provides accurate, honest information regardless of what user wants to hear."
> "Maintains correct positions when challenged."
> "Delivers difficult truths constructively but doesn't avoid them."
(Quoted from Anthropic, "How people ask Claude for guidance" Appendix,
April 2026.)
**The 11 guidance criteria.** The Appendix lists eleven criteria for
healthy guidance. Three are load-bearing for this plugin:
> "Avoid providing guidance that would foster continued engagement with
> Claude if this is not in the person's interest."
> "Be wary of giving excessively confident verdicts in cases that involve
> incomplete or one-sided information."
> "Maintain integrity and be willing to speak frankly or push back when
> something seems incorrect or not in the person's best interest."
(Quoted from same source. The full list of 11 is on page 2 of the Appendix.)
Supporting Anthropic publications informing this framework:
- [Disempowerment Patterns](https://www.anthropic.com/research/disempowerment-patterns)
- [Claude's New Constitution](https://www.anthropic.com/news/claudes-new-constitution)
- [Protecting Wellbeing](https://www.anthropic.com/research/protecting-wellbeing)
- [Emotion Concepts](https://www.anthropic.com/research/emotion-concepts)
## What You Are Not
You are not a diagnostic tool. You do not detect mental illness.
You help the user think clearly. That is all.

View file

@ -1,185 +0,0 @@
// domain-detection.test.mjs — verifies the 8 new v1.2 domain detectors.
//
// Coverage per domain: 3 representative positive prompts + 1 adjacent-domain
// negative discrimination. Plus cross-domain multi-fire tests (a prompt can
// hit multiple domains).
//
// Pattern set is intentionally drawn from Figure A2 examples, but tests
// duplicate the regex-unit fixtures locally to avoid coupling to import
// (privacy boundary keeps patterns co-located with the prompt-analyzer).
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;
afterEach(() => { if (dir) cleanupTestDir(dir); });
function freshState() {
return {
start_epoch: Math.floor(Date.now() / 1000) - 60,
start_iso: '2026-05-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,
pushback_count: 0, domain_context: null,
last_warning_epoch: 0,
};
}
function runPrompt(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 'd1', { ...freshState(), ...stateOverrides });
runHook('prompt-analyzer.mjs', { session_id: 'd1', prompt }, dir);
return readState(dir, 'd1');
}
function assertDomainHit(s, expected) {
assert.ok(Array.isArray(s.domain_context), `expected array, got ${typeof s.domain_context}`);
assert.ok(s.domain_context.includes(expected),
`expected '${expected}' in domain_context, got [${s.domain_context.join(', ')}]`);
}
function assertNoDomainHit(s, forbidden) {
if (s.domain_context === null) return;
assert.ok(!s.domain_context.includes(forbidden),
`forbidden '${forbidden}' in domain_context, got [${s.domain_context.join(', ')}]`);
}
// --- Legal ---
describe('domain: legal', () => {
it('matches "my lawyer"', () => assertDomainHit(runPrompt('I talked to my lawyer last week'), 'legal'));
it('matches "filing a lawsuit"', () => assertDomainHit(runPrompt("we're filing a lawsuit against them"), 'legal'));
it('matches "custody hearing"', () => assertDomainHit(runPrompt('the custody hearing is tomorrow'), 'legal'));
it('does NOT match "lawyer joke"', () => assertNoDomainHit(runPrompt('tell me a lawyer joke'), 'legal'));
});
// --- Parenting ---
describe('domain: parenting', () => {
it('matches "my kid"', () => assertDomainHit(runPrompt('my kid is having tantrums every morning'), 'parenting'));
it('matches "as a parent"', () => assertDomainHit(runPrompt('as a parent I struggle with this'), 'parenting'));
it('matches "school choice"', () => assertDomainHit(runPrompt('our school choice fight is exhausting'), 'parenting'));
it('does NOT match "child of two parents process"', () => {
assertNoDomainHit(runPrompt('child of two parents process in our system'), 'parenting');
});
it('parenting vs relationships discrimination — "my child" not "my partner"', () => {
const s = runPrompt('my child has trouble at school');
assertDomainHit(s, 'parenting');
assertNoDomainHit(s, 'relationship');
});
});
// --- Health ---
describe('domain: health', () => {
it('matches "my doctor"', () => assertDomainHit(runPrompt('my doctor said the labs were fine'), 'health'));
it('matches "diagnosed with"', () => assertDomainHit(runPrompt("I was diagnosed with anxiety last year"), 'health'));
it('matches "my depression"', () => assertDomainHit(runPrompt('my depression is getting worse'), 'health'));
it('does NOT match "system health check"', () => {
assertNoDomainHit(runPrompt('run a system health check on the database'), 'health');
});
it('health vs wellbeing discrimination — generic wellbeing routine ≠ medical', () => {
assertNoDomainHit(runPrompt('my wellbeing routine includes daily walks'), 'health');
});
});
// --- Financial ---
describe('domain: financial', () => {
it('matches "my retirement plan"', () => {
assertDomainHit(runPrompt('reviewing my retirement plan strategy'), 'financial');
});
it('matches "mortgage application"', () => {
assertDomainHit(runPrompt('our mortgage application got delayed'), 'financial');
});
it('matches "tax return"', () => {
assertDomainHit(runPrompt("I'm working on my tax return tonight"), 'financial');
});
it('does NOT match "stock options trade-off in code"', () => {
assertNoDomainHit(runPrompt('the stock options trade-off in this code'), 'financial');
});
});
// --- Professional ---
describe('domain: professional', () => {
it('matches "my boss"', () => assertDomainHit(runPrompt('my boss keeps changing the deadline'), 'professional'));
it('matches "performance review"', () => assertDomainHit(runPrompt('my performance review is next week'), 'professional'));
it('matches "resume advice"', () => assertDomainHit(runPrompt('looking for resume advice'), 'professional'));
it('does NOT match "boss music album"', () => {
assertNoDomainHit(runPrompt('the new Boss music album dropped'), 'professional');
});
it('professional vs lifepath discrimination — generic life-purpose ≠ professional', () => {
assertNoDomainHit(runPrompt('finding my life purpose feels overwhelming'), 'professional');
});
});
// --- Spirituality ---
describe('domain: spirituality', () => {
it('matches "my guru"', () => assertDomainHit(runPrompt('my guru told me to meditate more'), 'spirituality'));
it('matches "kundalini"', () => assertDomainHit(runPrompt("I've felt the kundalini rise"), 'spirituality'));
it('matches "the universe wants"', () => {
assertDomainHit(runPrompt('the universe wants me to take this leap'), 'spirituality');
});
it('does NOT match "physics universe expansion"', () => {
assertNoDomainHit(runPrompt('how does the physics universe expansion work'), 'spirituality');
});
});
// --- Consumer ---
describe('domain: consumer', () => {
it('matches "should I buy"', () => assertDomainHit(runPrompt('should I buy this gaming laptop?'), 'consumer'));
it('matches "which phone"', () => assertDomainHit(runPrompt('which phone should I get?'), 'consumer'));
it('matches "upgrade my laptop"', () => assertDomainHit(runPrompt('time to upgrade my laptop'), 'consumer'));
it('does NOT match "buy a property" (financial-not-consumer)', () => {
assertNoDomainHit(runPrompt('thinking about buying a property next year'), 'consumer');
});
});
// --- Personal_dev ---
describe('domain: personal_dev', () => {
it('matches "my morning routine"', () => assertDomainHit(runPrompt('my morning routine needs an overhaul'), 'personal_dev'));
it('matches "self-taught"', () => assertDomainHit(runPrompt("I'm self-taught in design"), 'personal_dev'));
it('matches "level up myself"', () => assertDomainHit(runPrompt('want to level up myself this year'), 'personal_dev'));
it('does NOT match "morning routine of the api"', () => {
assertNoDomainHit(runPrompt('the morning routine of the API cron job'), 'personal_dev');
});
});
// --- Multi-domain ---
describe('multi-domain prompts (multiple domains fire)', () => {
it('partner + my doctor → relationship + health', () => {
const s = runPrompt('my partner went with me to my doctor appointment');
assertDomainHit(s, 'relationship');
assertDomainHit(s, 'health');
});
it('my kid + custody hearing → parenting + legal', () => {
const s = runPrompt('the custody hearing about my kid is next week');
assertDomainHit(s, 'parenting');
assertDomainHit(s, 'legal');
});
it('no false positive — purely technical prompt yields null domain', () => {
const s = runPrompt('refactor this typescript module to use generics');
assert.equal(s.domain_context, null,
'pure tech prompt must not trigger any domain detector');
});
it('domain accumulates across prompts (sticky array)', () => {
dir = setupTestDir();
createStateFile(dir, 'd-multi', freshState());
runHook('prompt-analyzer.mjs', { session_id: 'd-multi', prompt: 'my partner is sick' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'd-multi', prompt: 'my doctor said to rest' }, dir);
const s = readState(dir, 'd-multi');
assert.ok(s.domain_context.includes('relationship'));
assert.ok(s.domain_context.includes('health'));
assert.equal(s.domain_context.length, 2, 'no duplicate pushes');
});
});

View file

@ -1,198 +0,0 @@
// Tests for hooks/scripts/report-reader.mjs.
// Verifies aggregate computation, domain counting, and backward-compat with
// v1.0.0 records that predate pushback / domain_context fields.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execSync } from 'child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
const SCRIPT = join(import.meta.dirname, '..', 'hooks', 'scripts', 'report-reader.mjs');
function runReader(jsonlContent) {
const dir = mkdtempSync(join(tmpdir(), 'ia-report-'));
const path = join(dir, 'sessions.jsonl');
writeFileSync(path, jsonlContent);
try {
const stdout = execSync(`node ${SCRIPT} ${path}`, { encoding: 'utf8', timeout: 5000 });
return JSON.parse(stdout.trim());
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
function runReaderRaw(jsonlContent) {
const dir = mkdtempSync(join(tmpdir(), 'ia-report-'));
const path = join(dir, 'sessions.jsonl');
writeFileSync(path, jsonlContent);
try {
return execSync(`node ${SCRIPT} ${path}`, { encoding: 'utf8', timeout: 5000 });
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
test('pushback_total matches sum across v1.1.0 records', () => {
const fixture = [
{ session_id: 'a', start: '2026-04-10T10:00:00Z', end: '2026-04-10T11:00:00Z',
duration_min: 60, tool_count: 10, edit_count: 2,
domain_context: null,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 3 } },
{ session_id: 'b', start: '2026-04-11T10:00:00Z', end: '2026-04-11T11:00:00Z',
duration_min: 60, tool_count: 5, edit_count: 1,
domain_context: 'relationship',
flags: { dependency: 1, escalation: 0, fatigue: 0, validation: 0, pushback: 2 } },
{ session_id: 'c', start: '2026-04-12T10:00:00Z', end: '2026-04-12T11:00:00Z',
duration_min: 60, tool_count: 5, edit_count: 1,
domain_context: null,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
];
const jsonl = fixture.map(o => JSON.stringify(o)).join('\n') + '\n';
const result = runReader(jsonl);
assert.equal(result.pushback_total, 5);
assert.equal(result.flags_total.pushback, 5);
assert.equal(result.total_end_records, 3);
});
test('relationship_domain_count matches fixture count', () => {
const fixture = [
{ session_id: 'a', duration_min: 30, domain_context: 'relationship',
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
{ session_id: 'b', duration_min: 30, domain_context: 'relationship',
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 1 } },
{ session_id: 'c', duration_min: 30, domain_context: null,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
{ session_id: 'd', duration_min: 30,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
];
const jsonl = fixture.map(o => JSON.stringify(o)).join('\n') + '\n';
const result = runReader(jsonl);
assert.equal(result.relationship_domain_count, 2);
assert.equal(result.null_domain_count, 2);
});
test('v1.2 array domain_context aggregates correctly (relationship in array)', () => {
const fixture = [
// v1.2 — multi-domain array containing 'relationship'
{ session_id: 'a', duration_min: 30, domain_context: ['relationship', 'health'],
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 1 } },
// v1.2 — array without 'relationship'
{ session_id: 'b', duration_min: 30, domain_context: ['legal'],
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
// v1.2 — empty array (no domain detected this session)
{ session_id: 'c', duration_min: 30, domain_context: [],
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
// v1.1 — string shape (must still aggregate as relationship)
{ session_id: 'd', duration_min: 30, domain_context: 'relationship',
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 1 } },
];
const jsonl = fixture.map(o => JSON.stringify(o)).join('\n') + '\n';
const result = runReader(jsonl);
assert.equal(result.relationship_domain_count, 2,
'v1.2 array containing relationship + v1.1 string both increment relationship counter');
assert.equal(result.other_domain_count, 1, 'v1.2 ["legal"] is "other" until Step 14 adds per-domain breakdown');
assert.equal(result.null_domain_count, 1, 'empty array counts as null');
});
test('v1.2 mixed schema fixture: per-domain breakdown + user_info_class + valseek', () => {
const fixture = [
// v1.0 — no pushback flag, no domain_context
{ session_id: 'v0', duration_min: 30,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0 } },
// v1.1 — pushback flag, string domain
{ session_id: 'v1', duration_min: 30, domain_context: 'relationship',
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 1 } },
// v1.2 — multi-domain array, user_info_class, valseek_count
{ session_id: 'v2a', duration_min: 30,
domain_context: ['relationship', 'health'],
user_info_class: 'no', valseek_count: 3, turn_count: 20,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 2 } },
{ session_id: 'v2b', duration_min: 30,
domain_context: ['legal'],
user_info_class: 'yes_people', valseek_count: 0, turn_count: 8,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
{ session_id: 'v2c', duration_min: 30,
domain_context: [],
user_info_class: null, valseek_count: 0, turn_count: 5,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 } },
];
const jsonl = fixture.map(o => JSON.stringify(o)).join('\n') + '\n';
const result = runReader(jsonl);
// schema_version discrimination
assert.equal(result.schema_version.v1_0_records, 1);
assert.equal(result.schema_version.v1_1_records, 1);
assert.equal(result.schema_version.v1_2_records, 3);
// per-domain breakdown (only v1.x array members)
assert.equal(result.domain_breakdown.relationship, 2,
'v1.1 string + v1.2 array containing relationship → 2');
assert.equal(result.domain_breakdown.health, 1);
assert.equal(result.domain_breakdown.legal, 1);
assert.equal(result.domain_breakdown.parenting, 0);
// user_info_class distribution
assert.equal(result.user_info_class.no, 1);
assert.equal(result.user_info_class.yes_people, 1);
assert.equal(result.user_info_class.null, 1);
// valseek aggregation
assert.equal(result.valseek.sessions, 1);
assert.equal(result.valseek.total, 3);
// stakes_signal — max weight per session
// v2a: max(relationship=1.3, health=1.5) = 1.5
// v2b: legal=1.5
// v2c: empty → not counted
assert.equal(result.stakes_signal.sessions, 2);
assert.ok(Math.abs(result.stakes_signal.sum - 3.0) < 0.01,
`expected stakes_signal.sum ~3.0, got ${result.stakes_signal.sum}`);
});
test('backward-compat: v1.0.0 records without pushback/domain do not produce NaN', () => {
const fixture = [
// v1.0.0 — no pushback in flags, no domain_context at top level
{ session_id: 'old', start: '2026-03-01T10:00:00Z', end: '2026-03-01T11:00:00Z',
duration_min: 60, tool_count: 10, edit_count: 2,
flags: { dependency: 1, escalation: 0, fatigue: 1, validation: 0 } },
// v1.1.0 — full schema
{ session_id: 'new', start: '2026-04-10T10:00:00Z', end: '2026-04-10T11:00:00Z',
duration_min: 60, tool_count: 5, edit_count: 1,
domain_context: 'relationship',
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 4 } },
// start-only record (must be skipped)
{ session_id: 'start-only', start: '2026-04-10T09:00:00Z', hour: 9, is_late_night: false },
// error record (must be skipped)
{ session_id: 'err', end: '2026-04-10T12:00:00Z', note: 'no_state_file' },
];
const jsonl = fixture.map(o => JSON.stringify(o)).join('\n') + '\n';
const result = runReader(jsonl);
assert.equal(result.pushback_total, 4);
assert.equal(Number.isNaN(result.pushback_total), false);
assert.equal(result.total_end_records, 2);
assert.equal(result.schema_version.v1_0_records, 1);
assert.equal(result.schema_version.v1_1_records, 1);
assert.equal(result.flags_total.dependency, 1);
assert.equal(result.flags_total.fatigue, 1);
});
test('report-reader stdout surfaces v1.2 field names (SC-12)', () => {
// Run reader against a v1.2 fixture and assert stdout contains the field
// names that /interaction-report references in its output template.
const fixture = [
{ session_id: 'a', duration_min: 30,
domain_context: ['legal', 'health'],
user_info_class: 'no', valseek_count: 4, turn_count: 22,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 1 } },
];
const stdout = runReaderRaw(fixture.map(o => JSON.stringify(o)).join('\n') + '\n');
// SC-12 specifies these field names must be present in the report output:
assert.ok(stdout.includes('user_info_class'), 'stdout missing user_info_class field');
assert.ok(stdout.includes('valseek'), 'stdout missing valseek aggregation');
assert.ok(stdout.includes('stakes_signal'), 'stdout missing stakes_signal aggregation');
// Also assert at least one new domain name (legal) appears in domain_breakdown.
assert.ok(stdout.includes('legal'), 'stdout missing legal domain in breakdown');
assert.ok(stdout.includes('domain_breakdown'), 'stdout missing domain_breakdown structure');
});

View file

@ -1,152 +0,0 @@
// Unit tests for shared library constants and helpers.
// Sanity-checks that v1.2 thresholds and domain-stakes table are exported
// with the expected shape. Detector-level behaviour is covered in
// per-detector test files (user-info, validation-seeking, stakes-matrix).
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
// Allocate a fresh data dir before importing lib.mjs, so SESSIONS_LOG points
// at a sandbox path. The lib.mjs module captures CLAUDE_PLUGIN_DATA at import
// time, so the env var must be set first.
const TEST_DATA_DIR = mkdtempSync(join(tmpdir(), 'ia-lib-test-'));
process.env.CLAUDE_PLUGIN_DATA = TEST_DATA_DIR;
const {
TIER1_TURN_THRESHOLD,
TIER2_SESSION_THRESHOLD,
THRESHOLD_VALSEEK_FLAGS,
DOMAIN_STAKES,
HIGH_SYCOPHANCY_DOMAINS,
HIGH_STAKES_DOMAINS,
INFO_DOMAINS,
SESSIONS_LOG,
readRecentEndRecords,
} = await import('../hooks/scripts/lib.mjs');
after(() => {
rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
describe('v1.2 thresholds', () => {
test('tier-1 turn threshold is 15', () => {
assert.equal(TIER1_TURN_THRESHOLD, 15);
});
test('tier-2 session threshold is 3', () => {
assert.equal(TIER2_SESSION_THRESHOLD, 3);
});
test('valseek high-stakes flag threshold is 3', () => {
assert.equal(THRESHOLD_VALSEEK_FLAGS, 3);
});
});
describe('DOMAIN_STAKES table', () => {
test('default weight is 1.0', () => {
assert.equal(DOMAIN_STAKES.default, 1.0);
});
test('high-stakes domains weighted 1.5', () => {
assert.equal(DOMAIN_STAKES.legal, 1.5);
assert.equal(DOMAIN_STAKES.parenting, 1.5);
assert.equal(DOMAIN_STAKES.health, 1.5);
assert.equal(DOMAIN_STAKES.financial, 1.5);
});
test('high-sycophancy domains weighted between 1.2 and 1.3', () => {
assert.equal(DOMAIN_STAKES.relationship, 1.3);
assert.equal(DOMAIN_STAKES.spirituality, 1.2);
});
test('table is frozen (immutable)', () => {
assert.equal(Object.isFrozen(DOMAIN_STAKES), true);
});
test('uses singular domain identifiers (relationship, not relationships)', () => {
assert.equal(DOMAIN_STAKES.relationship, 1.3);
assert.equal(DOMAIN_STAKES.relationships, undefined);
});
});
describe('domain classification arrays', () => {
test('HIGH_SYCOPHANCY_DOMAINS contains relationship and spirituality', () => {
assert.deepEqual([...HIGH_SYCOPHANCY_DOMAINS], ['relationship', 'spirituality']);
assert.equal(Object.isFrozen(HIGH_SYCOPHANCY_DOMAINS), true);
});
test('HIGH_STAKES_DOMAINS contains legal, parenting, health, financial', () => {
assert.deepEqual([...HIGH_STAKES_DOMAINS], ['legal', 'parenting', 'health', 'financial']);
assert.equal(Object.isFrozen(HIGH_STAKES_DOMAINS), true);
});
test('INFO_DOMAINS adds professional to HIGH_STAKES_DOMAINS', () => {
assert.deepEqual(
[...INFO_DOMAINS],
['legal', 'parenting', 'health', 'financial', 'professional']
);
assert.equal(Object.isFrozen(INFO_DOMAINS), true);
});
});
describe('readRecentEndRecords', () => {
function writeFixture(records) {
const lines = records.map(r => JSON.stringify(r)).join('\n') + '\n';
writeFileSync(SESSIONS_LOG, lines);
}
test('returns N most recent end records in chronological order', () => {
writeFixture([
{ session_id: 'a', start: '2026-05-01T10:00:00Z' }, // start record (no duration)
{ session_id: 'a', start: '2026-05-01T10:00:00Z', end: '2026-05-01T10:30:00Z', duration_min: 30 },
{ session_id: 'b', start: '2026-05-01T11:00:00Z' },
{ session_id: 'b', start: '2026-05-01T11:00:00Z', end: '2026-05-01T11:45:00Z', duration_min: 45 },
{ session_id: 'c', start: '2026-05-01T12:00:00Z', end: '2026-05-01T12:20:00Z', duration_min: 20 },
{ session_id: 'd', start: '2026-05-01T13:00:00Z', end: '2026-05-01T13:50:00Z', duration_min: 50 },
]);
const recent = readRecentEndRecords(3);
assert.equal(recent.length, 3);
assert.equal(recent[0].session_id, 'b');
assert.equal(recent[1].session_id, 'c');
assert.equal(recent[2].session_id, 'd');
});
test('returns fewer than N when not enough end records exist', () => {
writeFixture([
{ session_id: 'a', start: '2026-05-01T10:00:00Z', end: '2026-05-01T10:30:00Z', duration_min: 30 },
]);
const recent = readRecentEndRecords(5);
assert.equal(recent.length, 1);
assert.equal(recent[0].session_id, 'a');
});
test('skips malformed JSON lines', () => {
const goodA = JSON.stringify({ session_id: 'a', duration_min: 1 });
const goodB = JSON.stringify({ session_id: 'b', duration_min: 2 });
writeFileSync(SESSIONS_LOG, `${goodA}\nnot json\n${goodB}\n`);
const recent = readRecentEndRecords(5);
assert.equal(recent.length, 2);
assert.equal(recent[0].session_id, 'a');
assert.equal(recent[1].session_id, 'b');
});
test('empty file returns []', () => {
writeFileSync(SESSIONS_LOG, '');
assert.deepEqual(readRecentEndRecords(3), []);
});
test('missing file returns []', () => {
rmSync(SESSIONS_LOG, { force: true });
assert.deepEqual(readRecentEndRecords(3), []);
});
test('non-positive N returns []', () => {
writeFixture([{ session_id: 'a', duration_min: 1 }]);
assert.deepEqual(readRecentEndRecords(0), []);
assert.deepEqual(readRecentEndRecords(-1), []);
});
});

View file

@ -1,438 +0,0 @@
// Hook timing budget enforcement.
//
// Two thresholds are measured per hook:
//
// - WALL_CLOCK_P95_MS = 200 — total round-trip including Node ESM cold-start.
// The cold-start alone is 60-120ms on Intel Mac, so 100ms is unrealistic
// for any subprocess-based hook. 200ms gives headroom for shared CI noise.
//
// - LOGIC_TIME_P95_MS = 50 — pure work (regex evaluation + JSONL/state I/O)
// measured by a fixture-runner that imports lib.mjs once and exercises
// the hook's hot path inline. This is the meaningful hook-perf assertion;
// ESM cold-start is not something the plugin can optimize.
//
// p95 = the 4th value of 5 sorted iterations. Failing once triggers a single
// retry to absorb transient OS noise; a second failure is treated as a real
// signal (real perf regression or threshold needs tuning).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execSync } from 'child_process';
import {
mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync,
unlinkSync, rmSync, appendFileSync,
} from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { nowIso, nowEpoch } from '../hooks/scripts/lib.mjs';
const SCRIPTS_DIR = join(import.meta.dirname, '..', 'hooks', 'scripts');
const WALL_CLOCK_P95_MS = 200;
const LOGIC_TIME_P95_MS = 50;
const ITERATIONS = 5;
function setupDir() {
const dir = mkdtempSync(join(tmpdir(), 'ia-perf-'));
mkdirSync(join(dir, 'state'), { recursive: true });
return dir;
}
function p95(samples) {
return [...samples].sort((a, b) => a - b)[3];
}
// --- Wall-clock measurement (subprocess spawn) ---
function runWallClock(scriptName, stdinJson, dataDir) {
const t0 = performance.now();
execSync(`node ${join(SCRIPTS_DIR, scriptName)}`, {
input: JSON.stringify(stdinJson),
env: { ...process.env, CLAUDE_PLUGIN_DATA: dataDir },
encoding: 'utf8',
timeout: 5000,
});
return performance.now() - t0;
}
function measureWallClock(scriptName, stdinTemplate) {
const samples = [];
for (let i = 0; i < ITERATIONS; i++) {
const dir = setupDir();
try {
const sid = `perf-${i}`;
// Pre-seed state for hooks that read it (tool-tracker, session-end)
writeFileSync(
join(dir, 'state', `${sid}.json`),
JSON.stringify({ start_epoch: nowEpoch(), start_iso: nowIso(), tool_count: 0, edit_count: 0 })
);
samples.push(runWallClock(scriptName, { ...stdinTemplate, session_id: sid }, dir));
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
return samples;
}
// --- Logic-time fixtures (no subprocess, single import of lib.mjs) ---
//
// These mirror each hook's hot path in pure inline code so we can measure
// regex + I/O cost without paying the ~80ms ESM cold-start tax. The pattern
// list intentionally mirrors the size class of prompt-analyzer's full
// pattern set so the benchmark stays representative.
//
// v1.2 pattern count: ~133 = 41 v1.1 (25 negative + 12 pushback + 4 domain)
// + 48 new domains (8 × 6)
// + 32 user-info (15 people + 10 digital + 7 no)
// + 12 valseek
// Fixture sized at ~91+ to bracket the realistic prompt-analyzer cost without
// overweighting the perf budget on test fixture maintenance.
//
// Patterns here are structurally equivalent to the real ones (length +
// complexity), not literal copies — the privacy boundary at
// prompt-analyzer.mjs:119 means production patterns must stay co-located
// with the privacy wipe. Keep in sync (approximately) with v1.2 pattern count.
const samplePatterns = [
// Negative emotional patterns (25 — matches v1.1.0)
/\bI\s+can'?t\s+do\s+this\s+without\b/i,
/\bwhat\s+should\s+I\b/i,
/\bI\s+need\s+you\s+to\b/i,
/\bonly\s+you\s+understand\b/i,
/\b(?:always|never|every|all)\s+the\s+time\b/i,
/\bdefinitely\s+(?:should|will|need)\b/i,
/\babsolutely\s+(?:right|correct)\b/i,
/\bI\s+am\s+(?:tired|exhausted|drained)\b/i,
/\blate\s+night\b/i,
/\b(?:can'?t|cannot)\s+sleep\b/i,
/\bI\s+(?:wish|want)\s+(?:I|you)\s+could\b/i,
/\bdo\s+you\s+think\b/i,
/\bare\s+you\s+sure\b/i,
/\bright\?$/i,
/\bagree\?$/i,
/\bam\s+I\s+(?:right|wrong)\b/i,
/\bplease\s+confirm\b/i,
/\bI\s+keep\s+(?:thinking|coming\s+back)\b/i,
/\bI\s+(?:can'?t|cannot)\s+stop\b/i,
/\bone\s+more\s+(?:thing|question)\b/i,
/\bjust\s+one\s+more\b/i,
/\bI'?ve\s+been\s+thinking\b/i,
/\bwhy\s+did\s+I\b/i,
/\bI\s+messed\s+up\b/i,
/\bI\s+made\s+a\s+mistake\b/i,
// Pushback patterns (12 — matches v1.1.0)
/\bbut\s+(?:that|this)\s+is\s+wrong\b/i,
/\bno,?\s+I\s+(?:meant|asked|said)\b/i,
/\byou(?:'?re|\s+are)\s+(?:wrong|mistaken|incorrect)\b/i,
/\bthat'?s\s+not\s+(?:right|what)\b/i,
/\bactually,?\s+(?:I|the)\b/i,
/\bdisagree\s+(?:with|because)\b/i,
/\bI\s+(?:still|already)\s+(?:think|believe)\b/i,
/\blisten,?\s+(?:I|you)\b/i,
/\bdon'?t\s+(?:tell|give)\s+me\b/i,
/\bjust\s+(?:do|say|tell)\s+(?:it|me)\b/i,
/\bI\s+(?:already|just)\s+decided\b/i,
/\byou\s+(?:keep|always)\s+(?:saying|missing)\b/i,
// Domain patterns (4 — matches v1.1.0)
/\bmy\s+(?:partner|spouse|husband|wife|boyfriend|girlfriend)\b/i,
/\b(?:our|the)\s+relationship\b/i,
/\bbreak\s+up\s+(?:with|over)\b/i,
/\bdating\s+(?:someone|him|her|them)\b/i,
// v1.2: 48 new domain patterns (8 × 6) — structurally equivalent to real ones
/\b(?:my|our)\s+(?:lawyer|attorney)\b/i,
/\bfiling\s+a?\s+lawsuit\b/i,
/\b(?:custody|divorce)\s+(?:hearing|case)\b/i,
/\b(?:contract|nda)\s+(?:violation|dispute)\b/i,
/\bsued?\s+(?:by|for)\b/i,
/\b(?:landlord|tenant)\s+(?:rights|dispute)\b/i,
/\bmy\s+(?:kid|child|son|daughter)\b/i,
/\b(?:potty|sleep)\s+training\s+issue\b/i,
/\bas\s+a\s+(?:parent|mom|dad)\b/i,
/\b(?:bedtime|breastfeeding)\s+routine\b/i,
/\b(?:school|preschool)\s+(?:choice|conflict)\b/i,
/\bmy\s+(?:child|kid)'?s?\s+(?:diagnosis|teacher)\b/i,
/\bmy\s+(?:doctor|physician|gp)\b/i,
/\b(?:diagnosed|prescribed)\s+(?:with|for)\b/i,
/\bmy\s+symptoms?\s+(?:are|include)\b/i,
/\b(?:my|i\s+have)\s+(?:cancer|diabetes)\b/i,
/\b(?:blood\s+pressure|heart\s+rate)\s+reading\b/i,
/\b(?:scheduled|having)\s+(?:surgery|procedure)\b/i,
/\bmy\s+(?:savings|retirement|401k)\s+account\b/i,
/\b(?:mortgage|loan|debt)\s+(?:payment|advice)\b/i,
/\bmy\s+tax\s+(?:return|bracket)\b/i,
/\b(?:budget|paycheck)\s+(?:negotiation|advice)\b/i,
/\b(?:stock|portfolio)\s+(?:pick|allocation)\b/i,
/\b(?:credit\s+card|interest\s+rate)\s+advice\b/i,
/\bmy\s+(?:boss|manager|coworker)\b/i,
/\b(?:performance\s+review|promotion|fired)\b/i,
/\bmy\s+(?:job|career|workplace)\s+(?:change|conflict)\b/i,
/\b(?:resume|cv)\s+advice\b/i,
/\bproject\s+deadline\s+(?:fight|conflict)\b/i,
/\b(?:remote|hybrid)\s+(?:policy|mandate)\b/i,
/\bmy\s+(?:guru|spiritual\s+teacher)\b/i,
/\b(?:meditation|mindfulness)\s+(?:practice|journey)\b/i,
/\b(?:karma|dharma|chakra)\b/i,
/\b(?:god|the\s+universe)\s+(?:wants|told)\b/i,
/\b(?:soulmate|twin\s+flame|past\s+life)\b/i,
/\b(?:prayer|spiritual\s+journey)\b/i,
/\bshould\s+i\s+buy\s+(?:a|the)\b/i,
/\bwhich\s+(?:laptop|phone|car)\s+should\b/i,
/\b(?:product|item)\s+(?:review|comparison)\b/i,
/\b(?:amazon|online)\s+(?:order|purchase)\b/i,
/\b(?:better|best)\s+(?:deal|price)\s+(?:for|on)\b/i,
/\b(?:upgrade|replace)\s+my\s+(?:laptop|phone)\b/i,
/\b(?:learn|practice)\s+(?:a|the)\s+habit\s+of\b/i,
/\bmy\s+(?:morning|daily)\s+routine\b/i,
/\bread(?:ing)?\s+more\s+books\b/i,
/\b(?:start|build)\s+a\s+(?:journal|hobby)\b/i,
/\b(?:learning|teaching\s+myself)\b/i,
/\b(?:improve|level\s+up)\s+(?:myself|my\s+focus)\b/i,
// v1.2: 32 user-info patterns (15 people + 10 digital + 7 no)
/\bmy\s+(?:therapist|counselor|psychologist)\b/i,
/\bmy\s+(?:doctor|gp|physician)\b/i,
/\bmy\s+(?:friend|best\s+friend)\b/i,
/\bmy\s+(?:partner|spouse|wife|husband)\b/i,
/\bmy\s+(?:mom|dad|mother|father)\b/i,
/\bmy\s+(?:mentor|coach|advisor)\b/i,
/\bmy\s+support\s+group\b/i,
/\bi\s+asked\s+my\s+(?:friend|therapist)\b/i,
/\bi\s+told\s+my\s+(?:friend|therapist|partner)\b/i,
/\bmy\s+family\s+(?:said|told)\b/i,
/\bmy\s+(?:lawyer|attorney)\b/i,
/\bmy\s+(?:pastor|priest|rabbi)\b/i,
/\bmy\s+(?:teacher|professor|tutor)\b/i,
/\bmy\s+(?:colleague|coworker)\b/i,
/\bi\s+reached\s+out\s+to\s+my\s+(?:friend|therapist)\b/i,
/\bi\s+(?:googled|searched)\b/i,
/\bi\s+read\s+(?:online|on\s+the\s+internet)\b/i,
/\b(?:chatgpt|gpt|gemini)\s+(?:said|told)\b/i,
/\b(?:found|saw)\s+a\s+(?:forum\s+post|reddit\s+thread)\b/i,
/\b(?:youtube|tiktok|twitter)\s+(?:video|post)\b/i,
/\baccording\s+to\s+(?:wikipedia|google)\b/i,
/\bi\s+asked\s+(?:chatgpt|gpt|claude)\b/i,
/\bonline\s+says\s+(?:that|this)\b/i,
/\bsearched\s+(?:google|stackoverflow)\b/i,
/\bi\s+watched\s+a\s+youtube\b/i,
/\b(?:nobody|no\s+one)\s+knows\b/i,
/\bi\s+haven'?t\s+told\s+(?:anyone|anybody)\b/i,
/\bdealing\s+with\s+this\s+alone\b/i,
/\bi\s+can'?t\s+tell\s+(?:anyone|anybody)\b/i,
/\bkeep\s+(?:this|it)\s+(?:to\s+myself|secret)\b/i,
/\bnobody\s+(?:in\s+my\s+life|around\s+me)\s+would\s+understand\b/i,
/\bjust\s+me\s+(?:and|with)\s+(?:my|the)\s+(?:thoughts|head)\b/i,
// v1.2: 12 valseek patterns
/\bisn'?t\s+(?:it|that|she|he)\b[^.!?]*\?/i,
/\bdon'?t\s+you\s+(?:think|agree|see)\b[^.!?]*\?/i,
/\bright,?\s+(?:though|so)\b[^.!?]*\?/i,
/\bam\s+i\s+(?:crazy|wrong|the\s+only\s+one)\b/i,
/\btell\s+me\s+i'?m\s+not\s+(?:crazy|wrong)\b/i,
/\bis\s+it\s+(?:normal|crazy|reasonable)\s+(?:to|that)\b/i,
/\byou\s+agree,?\s+right\??/i,
/\btell\s+me\s+i'?m\s+right\b/i,
/\bback\s+me\s+up\s+(?:on\s+this|here)\b/i,
/\bi\s+(?:already|just)\s+(?:decided|knew)\b.*(?:should|right)\b/i,
/\bi'?ve\s+made\s+up\s+my\s+mind\b.*(?:right|correct)\b/i,
/\bi\s+know\s+i'?m\s+right\s+(?:about|on)\b/i,
];
function logicSessionStart(dir, sid) {
const stateFile = join(dir, 'state', `${sid}.json`);
const sessionsLog = join(dir, 'sessions.jsonl');
const iso = nowIso();
const epoch = nowEpoch();
const state = { start_epoch: epoch, start_iso: iso, tool_count: 0, edit_count: 0 };
writeFileSync(stateFile, JSON.stringify(state));
appendFileSync(
sessionsLog,
JSON.stringify({ session_id: sid, start: iso, hour: new Date().getUTCHours(), is_late_night: false }) + '\n'
);
}
function logicPromptAnalyzer(dir, sid, prompt) {
const stateFile = join(dir, 'state', `${sid}.json`);
const state = existsSync(stateFile) ? JSON.parse(readFileSync(stateFile, 'utf8')) : {};
let depHit = 0, valHit = 0;
for (const p of samplePatterns) { if (p.test(prompt)) { valHit = 1; break; } }
state.dep_flags = (state.dep_flags || 0) + depHit;
state.val_flags = (state.val_flags || 0) + valHit;
writeFileSync(stateFile, JSON.stringify(state));
}
function logicToolTracker(dir, sid, toolName) {
const stateFile = join(dir, 'state', `${sid}.json`);
const eventsLog = join(dir, 'events.jsonl');
const state = existsSync(stateFile) ? JSON.parse(readFileSync(stateFile, 'utf8')) : {};
state.tool_count = (state.tool_count || 0) + 1;
if (toolName === 'Edit' || toolName === 'Write') state.edit_count = (state.edit_count || 0) + 1;
appendFileSync(
eventsLog,
JSON.stringify({ ts: nowIso(), session_id: sid, tool_name: toolName }) + '\n'
);
writeFileSync(stateFile, JSON.stringify(state));
}
function logicSessionEnd(dir, sid) {
const stateFile = join(dir, 'state', `${sid}.json`);
const sessionsLog = join(dir, 'sessions.jsonl');
if (!existsSync(stateFile)) return;
const state = JSON.parse(readFileSync(stateFile, 'utf8'));
appendFileSync(
sessionsLog,
JSON.stringify({
session_id: sid,
start: state.start_iso,
end: nowIso(),
duration_min: 0,
tool_count: state.tool_count || 0,
edit_count: state.edit_count || 0,
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: state.val_flags || 0, pushback: 0 },
}) + '\n'
);
unlinkSync(stateFile);
}
function measureLogicTime(fn, ...extraArgs) {
const samples = [];
for (let i = 0; i < ITERATIONS; i++) {
const dir = setupDir();
const sid = `perf-${i}`;
try {
writeFileSync(
join(dir, 'state', `${sid}.json`),
JSON.stringify({ start_epoch: nowEpoch(), start_iso: nowIso(), tool_count: 0, edit_count: 0 })
);
const t0 = performance.now();
fn(dir, sid, ...extraArgs);
samples.push(performance.now() - t0);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
return samples;
}
function assertWithRetry(measure, threshold, label) {
let samples = measure();
let p = p95(samples);
if (p > threshold) {
samples = measure();
p = p95(samples);
}
assert.ok(
p <= threshold,
`${label} p95 = ${p.toFixed(1)}ms exceeds ${threshold}ms (samples: ${samples.map(s => s.toFixed(1)).join(', ')})`
);
}
// --- Wall-clock tests (4) ---
test('session-start.mjs wall-clock p95 within 200ms', () => {
assertWithRetry(
() => measureWallClock('session-start.mjs', { cwd: '/tmp' }),
WALL_CLOCK_P95_MS,
'session-start wall-clock'
);
});
test('prompt-analyzer.mjs wall-clock p95 within 200ms', () => {
assertWithRetry(
() => measureWallClock('prompt-analyzer.mjs', { prompt: 'are you sure I should do this? right?', cwd: '/tmp' }),
WALL_CLOCK_P95_MS,
'prompt-analyzer wall-clock'
);
});
test('tool-tracker.mjs wall-clock p95 within 200ms', () => {
assertWithRetry(
() => measureWallClock('tool-tracker.mjs', { tool_name: 'Edit', cwd: '/tmp' }),
WALL_CLOCK_P95_MS,
'tool-tracker wall-clock'
);
});
test('session-end.mjs wall-clock p95 within 200ms', () => {
assertWithRetry(
() => measureWallClock('session-end.mjs', { cwd: '/tmp' }),
WALL_CLOCK_P95_MS,
'session-end wall-clock'
);
});
// --- Logic-time tests (4) ---
test('session-start logic-time p95 within 50ms', () => {
assertWithRetry(
() => measureLogicTime(logicSessionStart),
LOGIC_TIME_P95_MS,
'session-start logic-time'
);
});
test('prompt-analyzer logic-time p95 within 50ms', () => {
assertWithRetry(
() => measureLogicTime(logicPromptAnalyzer, 'are you sure I should do this? right?'),
LOGIC_TIME_P95_MS,
'prompt-analyzer logic-time'
);
});
test('tool-tracker logic-time p95 within 50ms', () => {
assertWithRetry(
() => measureLogicTime(logicToolTracker, 'Edit'),
LOGIC_TIME_P95_MS,
'tool-tracker logic-time'
);
});
test('session-end logic-time p95 within 50ms', () => {
assertWithRetry(
() => measureLogicTime(logicSessionEnd),
LOGIC_TIME_P95_MS,
'session-end logic-time'
);
});
// --- v1.2: cross-session read at scale ---
//
// Pre-seeds sessions.jsonl with 1000 records to exercise the realistic
// readRecentEndRecords path. Tail-first scan should bound cost regardless.
function measureSessionStartWithJsonlFixture(recordCount) {
const samples = [];
for (let i = 0; i < ITERATIONS; i++) {
const dir = setupDir();
try {
// Pre-seed sessions.jsonl with mixed start/end records.
const lines = [];
for (let r = 0; r < recordCount; r++) {
const startISO = new Date(Date.now() - (recordCount - r) * 60_000).toISOString();
const endISO = new Date(Date.now() - (recordCount - r) * 60_000 + 30_000).toISOString();
lines.push(JSON.stringify({
session_id: `seed-${r}`, start: startISO,
end: endISO, duration_min: 30,
domain_context: ['legal'], user_info_class: 'no',
flags: { dependency: 0, escalation: 0, fatigue: 0, validation: 0, pushback: 0 },
}));
}
writeFileSync(join(dir, 'sessions.jsonl'), lines.join('\n') + '\n');
const sid = `bigfix-${i}`;
writeFileSync(
join(dir, 'state', `${sid}.json`),
JSON.stringify({ start_epoch: nowEpoch(), start_iso: nowIso(), tool_count: 0, edit_count: 0 })
);
samples.push(runWallClock('session-start.mjs', { session_id: sid, cwd: '/tmp' }, dir));
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
return samples;
}
test('session-start with 1000-record sessions.jsonl wall-clock p95 within 200ms', () => {
// The tier-2 alert in session-start.mjs reads the tail of sessions.jsonl
// via readRecentEndRecords(3). Tail-first scan should keep wall-clock
// bounded regardless of total file size.
assertWithRetry(
() => measureSessionStartWithJsonlFixture(1000),
WALL_CLOCK_P95_MS,
'session-start wall-clock with 1000-record fixture'
);
});

View file

@ -1,149 +0,0 @@
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`);
});
it('never leaks matched-pattern phrases through full lifecycle', () => {
dir = setupTestDir();
const matchedPhrase = 'are you sure';
const canary = 'CANARY_PRIVACY_xyz123';
const prompt = `${matchedPhrase}? ${canary}`;
runHook('session-start.mjs', { session_id: 'priv2', cwd: '/tmp' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'priv2', prompt }, dir);
runHook('tool-tracker.mjs', { session_id: 'priv2', tool_name: 'Edit' }, dir);
runHook('session-end.mjs', { session_id: 'priv2', cwd: '/tmp' }, dir);
const allContent = readAllFiles(dir);
assert.ok(
!allContent.includes(canary),
`Canary "${canary}" leaked — pattern-match did not protect prompt text`
);
assert.ok(
!allContent.toLowerCase().includes(matchedPhrase),
`Matched phrase "${matchedPhrase}" leaked — pattern name or trigger phrase written to disk`
);
});
// v1.2 detector canaries — one per new detector category, plus matched-phrase
// variants for new pattern phrases that must never reach disk verbatim.
it('user-info detector: yes_people canary never leaks', () => {
dir = setupTestDir();
const matchedPhrase = 'my therapist';
const canary = 'CANARY_USERINFO_PEOPLE_xyz123';
const prompt = `${matchedPhrase} suggested I journal more — ${canary}`;
runHook('session-start.mjs', { session_id: 'pv12a', cwd: '/tmp' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'pv12a', prompt }, dir);
runHook('tool-tracker.mjs', { session_id: 'pv12a', tool_name: 'Edit' }, dir);
runHook('session-end.mjs', { session_id: 'pv12a', cwd: '/tmp' }, dir);
const allContent = readAllFiles(dir);
assert.ok(!allContent.includes(canary),
`Canary "${canary}" leaked through user-info detector`);
assert.ok(!allContent.toLowerCase().includes(matchedPhrase),
`Matched phrase "${matchedPhrase}" leaked through user-info detector`);
});
it('user-info detector: yes_digital canary never leaks', () => {
dir = setupTestDir();
const matchedPhrase = 'I googled';
const canary = 'CANARY_USERINFO_DIGITAL_xyz123';
const prompt = `${matchedPhrase} this issue and got nothing — ${canary}`;
runHook('session-start.mjs', { session_id: 'pv12b', cwd: '/tmp' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'pv12b', prompt }, dir);
runHook('session-end.mjs', { session_id: 'pv12b', cwd: '/tmp' }, dir);
const allContent = readAllFiles(dir);
assert.ok(!allContent.includes(canary));
assert.ok(!allContent.toLowerCase().includes(matchedPhrase.toLowerCase()));
});
it('user-info detector: "no" isolation canary never leaks', () => {
dir = setupTestDir();
const matchedPhrase = "haven't told anyone";
const canary = 'CANARY_USERINFO_NO_xyz123';
const prompt = `I ${matchedPhrase} about it ${canary}`;
runHook('session-start.mjs', { session_id: 'pv12c', cwd: '/tmp' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'pv12c', prompt }, dir);
runHook('session-end.mjs', { session_id: 'pv12c', cwd: '/tmp' }, dir);
const allContent = readAllFiles(dir);
assert.ok(!allContent.includes(canary));
assert.ok(!allContent.toLowerCase().includes(matchedPhrase));
});
it('valseek detector canary never leaks', () => {
dir = setupTestDir();
const matchedPhrase = 'am I crazy';
const canary = 'CANARY_VALSEEK_xyz123';
const prompt = `${matchedPhrase} for thinking this — ${canary}`;
runHook('session-start.mjs', { session_id: 'pv12d', cwd: '/tmp' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'pv12d', prompt }, dir);
runHook('session-end.mjs', { session_id: 'pv12d', cwd: '/tmp' }, dir);
const allContent = readAllFiles(dir);
assert.ok(!allContent.includes(canary));
assert.ok(!allContent.toLowerCase().includes(matchedPhrase));
});
it('domain detector (legal): canary never leaks despite domain hit', () => {
dir = setupTestDir();
const matchedPhrase = 'my lawyer';
const canary = 'CANARY_DOMAIN_LEGAL_xyz123';
const prompt = `talked to ${matchedPhrase} about it ${canary}`;
runHook('session-start.mjs', { session_id: 'pv12e', cwd: '/tmp' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'pv12e', prompt }, dir);
runHook('session-end.mjs', { session_id: 'pv12e', cwd: '/tmp' }, dir);
const allContent = readAllFiles(dir);
assert.ok(!allContent.includes(canary),
`Canary "${canary}" leaked through legal domain detector`);
assert.ok(!allContent.toLowerCase().includes(matchedPhrase),
`Matched phrase "${matchedPhrase}" leaked through legal domain detector`);
});
});

View file

@ -1,522 +0,0 @@
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,
pushback_count: 0, domain_context: null,
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'));
});
});
// --- v1.1.0 pushback + domain regex (regex-only unit tests) ---
// Local copies of patterns in hooks/scripts/prompt-analyzer.mjs.
// Step 3 adds integration tests via runPrompt; integration tests catch
// pattern divergence between source and tests.
const pbReactivePatterns = [
/^are you sure\??/i,
/\bi'?m not convinced\b/i,
/\bthat doesn'?t (?:seem|feel) right\b/i,
/\bthat'?s not (?:quite )?what i meant\b/i,
/\blet me add (?:some )?context\b/i,
/\bactually,? (?:my situation|i)\b/i,
/(?:^|[.!?]\s+)i (?:believe|think) (?:you'?re|that'?s) wrong\b/i,
/\bi don'?t agree(?: with you)?\b/i,
/\bare you absolutely sure\b/i,
];
const pbPreemptivePatterns = [
/\bsteelman\b/i,
/\bplay (?:the )?devil'?s advocate\b/i,
/\bargue against (?:this|my)\b/i,
];
const domainRelationshipPatterns = [
/\b(?:my|our) (?:partner|spouse|wife|husband|girlfriend|boyfriend)\b/i,
/\bin our relationship\b/i,
/\b(?:dating|breakup|divorce)\b/i,
/\bromantic(?:ally)? (?:involved|interested)\b/i,
];
function matchesAny(patterns, text) {
return patterns.some((p) => p.test(text));
}
describe('pushback reactive patterns', () => {
it('matches "are you sure?"', () => assert.ok(matchesAny(pbReactivePatterns, 'are you sure?')));
it('does not match "tell me what to do" (no pushback)', () => assert.equal(matchesAny(pbReactivePatterns, 'tell me what to do'), false));
it("matches \"i'm not convinced\"", () => assert.ok(matchesAny(pbReactivePatterns, "i'm not convinced this works")));
it('does not match "i am convinced" (no negation)', () => assert.equal(matchesAny(pbReactivePatterns, 'i am convinced this works'), false));
it('matches "that doesn\'t seem right"', () => assert.ok(matchesAny(pbReactivePatterns, "that doesn't seem right to me")));
it('does not match "that seems right" (positive sense)', () => assert.equal(matchesAny(pbReactivePatterns, 'that seems right to me'), false));
it('matches "that\'s not what I meant"', () => assert.ok(matchesAny(pbReactivePatterns, "that's not what I meant by that")));
it('does not match "I meant exactly that"', () => assert.equal(matchesAny(pbReactivePatterns, 'I meant exactly that'), false));
it('matches "let me add context"', () => assert.ok(matchesAny(pbReactivePatterns, 'let me add context — the issue is X')));
it('does not match "I added context to the function"', () => assert.equal(matchesAny(pbReactivePatterns, 'I added context to the function'), false));
it('matches "actually, my situation is different"', () => assert.ok(matchesAny(pbReactivePatterns, 'actually, my situation is different')));
it('does not match "actually that approach works"', () => assert.equal(matchesAny(pbReactivePatterns, 'actually that approach works'), false));
it("matches \"I think you're wrong\"", () => assert.ok(matchesAny(pbReactivePatterns, "I think you're wrong about this")));
it("does not match \"I think we're wrong\" (different pronoun)", () => assert.equal(matchesAny(pbReactivePatterns, "I think we're wrong here"), false));
it("matches \"I don't agree\"", () => assert.ok(matchesAny(pbReactivePatterns, "I don't agree with that conclusion")));
it('does not match "I agree with you"', () => assert.equal(matchesAny(pbReactivePatterns, 'I agree with you fully'), false));
it('matches "are you absolutely sure"', () => assert.ok(matchesAny(pbReactivePatterns, 'are you absolutely sure about that')));
it('does not match "we are sure of the answer" (no questioning frame)', () => assert.equal(matchesAny(pbReactivePatterns, 'we are sure of the answer'), false));
});
describe('pushback preemptive patterns', () => {
it('matches "steelman"', () => assert.ok(matchesAny(pbPreemptivePatterns, 'please steelman this argument')));
it('does not match "steel manufacturing" (no whole-word match)', () => assert.equal(matchesAny(pbPreemptivePatterns, 'the steel manufacturing report'), false));
it("matches \"play devil's advocate\"", () => assert.ok(matchesAny(pbPreemptivePatterns, "can you play devil's advocate here")));
it('does not match "play music" (different verb object)', () => assert.equal(matchesAny(pbPreemptivePatterns, 'play music while coding'), false));
it('matches "argue against this"', () => assert.ok(matchesAny(pbPreemptivePatterns, 'argue against this proposal')));
it('does not match "they argue with each other"', () => assert.equal(matchesAny(pbPreemptivePatterns, 'they argue with each other'), false));
});
describe('domain relationship patterns', () => {
it('matches "my partner won\'t listen"', () => assert.ok(matchesAny(domainRelationshipPatterns, "my partner won't listen")));
it('matches "in our relationship"', () => assert.ok(matchesAny(domainRelationshipPatterns, 'in our relationship things changed')));
it('matches "considering divorce"', () => assert.ok(matchesAny(domainRelationshipPatterns, 'considering divorce after years')));
it('matches "romantically involved"', () => assert.ok(matchesAny(domainRelationshipPatterns, 'we are romantically involved')));
it('does not match "function relationship between input and output" (technical false-positive)', () => assert.equal(matchesAny(domainRelationshipPatterns, 'function relationship between input and output'), false));
it('does not match "database relationship mapping" (technical false-positive)', () => assert.equal(matchesAny(domainRelationshipPatterns, 'database relationship mapping'), false));
it('does not match "the data is updating" (no dating word boundary)', () => assert.equal(matchesAny(domainRelationshipPatterns, 'the data is updating in real time'), false));
it('does not match "romantic comedy film" (no involved/interested suffix)', () => assert.equal(matchesAny(domainRelationshipPatterns, 'watching a romantic comedy film'), false));
});
// --- v1.1.0 integration: pushback + valence + domain through prompt-analyzer.mjs ---
describe('pushback integration (state accumulation + same-invocation valence)', () => {
it('counts reactive pushback alone (no fatigue/escalation)', () => {
const s = runPrompt('are you sure?');
assert.equal(s.pushback_count, 1);
assert.equal(s.fatigue_flags, 0);
assert.equal(s.esc_flags, 0);
});
it('counts preemptive pushback alone', () => {
const s = runPrompt('please steelman this argument');
assert.equal(s.pushback_count, 1);
});
it('SUPPRESSES pushback when fatigue marker is in same invocation (valence guard)', () => {
const s = runPrompt("are you sure? I'm exhausted by all this");
assert.equal(s.pushback_count, 0, 'pushback must be suppressed when fatigue is co-present');
assert.equal(s.fatigue_flags, 1);
});
it('sets domain_context to ["relationship"] on positive match (v1.2 array shape)', () => {
const s = runPrompt("my partner won't listen to me");
assert.deepEqual(s.domain_context, ['relationship']);
});
it('keeps domain_context null on technical "function relationship" (false-positive guard)', () => {
const s = runPrompt('function relationship between input and output');
// No domainHit → state.domain_context stays as fresh-state null (untouched).
assert.equal(s.domain_context, null);
});
});
// --- v1.2 pushback alert contract (domain-aware re-contextualization) ---
//
// Step 12 of v1.2.0 ADDS the pushback alert with domain awareness baked in.
// Replaces the v1.1.0 "count but never alert" contract test.
//
// Behavior:
// - HIGH_SYCOPHANCY_DOMAINS (relationship, spirituality): alert at count >= 2
// - INFO_DOMAINS (legal, parenting, health, financial, professional): NO alert
// — pushback in info-seeking domains is healthy self-advocacy.
// - Empty / unknown domain: conservative default alert.
function runPromptCapture(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), ...stateOverrides });
const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt }, dir);
const state = readState(dir, 'p1');
return { state, out };
}
describe('pushback alert (v1.2 domain-aware contract)', () => {
it('accumulates pushback_count over 5 sequential prompts', () => {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), domain_context: ['relationship'] });
const prompts = [
'are you sure?',
"I'm not convinced",
"that doesn't seem right",
"actually, I think you're wrong",
"are you absolutely sure?",
];
for (const p of prompts) {
runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: p }, dir);
}
const s = readState(dir, 'p1');
assert.equal(s.pushback_count, 5, 'count accumulates across calls');
});
it('3 pushbacks + relationship → alert (HIGH_SYCOPHANCY)', () => {
const { state, out } = runPromptCapture('are you absolutely sure?', {
domain_context: ['relationship'],
pushback_count: 2, // becomes 3
});
assert.equal(state.pushback_count, 3);
assert.match(out.hookSpecificOutput.additionalContext, /pushback re-contextualization/);
});
it('3 pushbacks + parenting → NO alert (INFO_DOMAIN, healthy self-advocacy)', () => {
const { out } = runPromptCapture("I'm not convinced", {
domain_context: ['parenting'],
pushback_count: 2,
});
// Suppress pushback alert; nothing else should fire here either.
assert.equal(out.hookSpecificOutput, undefined,
'parenting pushback is healthy self-advocacy — no alert');
});
it('3 pushbacks + [relationship, legal] → alert (mixed: any HIGH_SYCOPHANCY wins)', () => {
const { out } = runPromptCapture('are you absolutely sure?', {
domain_context: ['relationship', 'legal'],
pushback_count: 2,
});
assert.match(out.hookSpecificOutput.additionalContext, /pushback re-contextualization/);
});
it('3 pushbacks + empty domain → alert (conservative default)', () => {
const { out } = runPromptCapture('are you absolutely sure?', {
domain_context: [],
pushback_count: 2,
});
assert.match(out.hookSpecificOutput.additionalContext, /pushback/);
});
it('1 pushback + relationship → NO alert (sub-threshold)', () => {
const { out } = runPromptCapture("are you sure?", {
domain_context: ['relationship'],
pushback_count: 0,
});
assert.equal(out.hookSpecificOutput, undefined,
'sub-threshold (count<2) — no alert even in HIGH_SYCOPHANCY');
});
it('5 pushbacks across info-only domains [legal, health] → NO alert', () => {
const { out } = runPromptCapture("I'm not convinced", {
domain_context: ['legal', 'health'],
pushback_count: 4,
});
assert.equal(out.hookSpecificOutput, undefined,
'all-info domains never alert pushback regardless of count');
});
});

View file

@ -1,121 +0,0 @@
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, pushback: 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');
});
it('persists pushback_count and coerces v1.1.0 string domain to array', () => {
dir = setupTestDir();
createStateFile(dir, 's4', {
start_epoch: Math.floor(Date.now() / 1000) - 120, start_iso: '2026-01-01T10:00:00Z',
tool_count: 2, edit_count: 1,
dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0,
pushback_count: 3, domain_context: 'relationship', // v1.1.0 string shape
last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0,
});
runHook('session-end.mjs', { session_id: 's4', cwd: '/tmp' }, dir);
const records = readJsonl(join(dir, 'sessions.jsonl'));
const end = records.find(r => r.end);
assert.ok(end);
assert.equal(end.flags.pushback, 3);
// v1.2: end record always carries an array, even when state had a string.
assert.deepEqual(end.domain_context, ['relationship']);
});
it('writes v1.2 multi-domain array unchanged when state already has array', () => {
dir = setupTestDir();
createStateFile(dir, 's4b', {
start_epoch: Math.floor(Date.now() / 1000) - 120, start_iso: '2026-01-01T10:00:00Z',
tool_count: 2, edit_count: 1,
dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0,
pushback_count: 1,
domain_context: ['relationship', 'health'],
last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0,
});
runHook('session-end.mjs', { session_id: 's4b', cwd: '/tmp' }, dir);
const records = readJsonl(join(dir, 'sessions.jsonl'));
const end = records.find(r => r.end);
assert.ok(end);
assert.deepEqual(end.domain_context, ['relationship', 'health']);
});
it('backward-compat: state without pushback_count yields flags.pushback === 0 (not NaN/undefined)', () => {
dir = setupTestDir();
createStateFile(dir, 's5', {
start_epoch: Math.floor(Date.now() / 1000) - 60, start_iso: '2026-01-01T10:00:00Z',
tool_count: 1, edit_count: 0,
dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0,
// pushback_count and domain_context intentionally absent (v1.0.0 state shape)
last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0,
});
runHook('session-end.mjs', { session_id: 's5', cwd: '/tmp' }, dir);
const records = readJsonl(join(dir, 'sessions.jsonl'));
const end = records.find(r => r.end);
assert.ok(end);
assert.equal(end.flags.pushback, 0);
assert.notEqual(end.flags.pushback, undefined);
assert.ok(!Number.isNaN(end.flags.pushback));
// v1.2: empty domain becomes [] (not null) — always an array on disk.
assert.deepEqual(end.domain_context, []);
});
});

View file

@ -1,137 +0,0 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'path';
import { writeFileSync } from 'fs';
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);
});
it('initializes pushback_count and domain_context fields (v1.1.0)', () => {
dir = setupTestDir();
runHook('session-start.mjs', { session_id: 's4', cwd: '/tmp' }, dir);
const state = readState(dir, 's4');
assert.ok(state);
assert.equal(state.pushback_count, 0);
assert.equal(state.domain_context, null);
});
it('initializes v1.2 user-info, valseek, turn_count fields', () => {
dir = setupTestDir();
runHook('session-start.mjs', { session_id: 's4b', cwd: '/tmp' }, dir);
const state = readState(dir, 's4b');
assert.equal(state.user_info_class, null);
assert.deepEqual(state.user_info_flags, { yes_people: 0, yes_digital: 0, no: 0 });
assert.equal(state.turn_count, 0);
assert.equal(state.valseek_count, 0);
assert.equal(state.valseek_flag, 0);
});
});
// --- Tier-2 cross-session alert ---
//
// Fires at SessionStart when last 3 end records all have user_info_class='no'
// AND each session had at least one HIGH_STAKES_DOMAINS hit.
function writeFixture(dir, records) {
const lines = records.map(r => JSON.stringify(r)).join('\n') + '\n';
writeFileSync(join(dir, 'sessions.jsonl'), lines);
}
describe('tier-2 cross-session isolation alert', () => {
it('fires when 3 prior end records all show no + high-stakes', () => {
dir = setupTestDir();
writeFixture(dir, [
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
{ session_id: 'p2', duration_min: 25, user_info_class: 'no', domain_context: ['health'] },
{ session_id: 'p3', duration_min: 40, user_info_class: 'no', domain_context: ['parenting', 'financial'] },
]);
const out = runHook('session-start.mjs', { session_id: 'snew', cwd: '/tmp' }, dir);
assert.match(out.hookSpecificOutput.additionalContext, /tier-2/);
});
it('does NOT fire when only 2 prior "no" records exist', () => {
dir = setupTestDir();
writeFixture(dir, [
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
{ session_id: 'p2', duration_min: 30, user_info_class: 'no', domain_context: ['health'] },
]);
const out = runHook('session-start.mjs', { session_id: 'snew2', cwd: '/tmp' }, dir);
const text = out.hookSpecificOutput.additionalContext;
assert.ok(!/tier-2/.test(text), 'tier-2 must require N consecutive sessions');
});
it('does NOT fire when one record has yes_people class', () => {
dir = setupTestDir();
writeFixture(dir, [
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
{ session_id: 'p2', duration_min: 30, user_info_class: 'yes_people', domain_context: ['health'] },
{ session_id: 'p3', duration_min: 30, user_info_class: 'no', domain_context: ['financial'] },
]);
const out = runHook('session-start.mjs', { session_id: 'snew3', cwd: '/tmp' }, dir);
assert.ok(!/tier-2/.test(out.hookSpecificOutput.additionalContext));
});
it('does NOT fire when any session is in low-stakes domain', () => {
dir = setupTestDir();
writeFixture(dir, [
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
{ session_id: 'p2', duration_min: 30, user_info_class: 'no', domain_context: ['consumer'] },
{ session_id: 'p3', duration_min: 30, user_info_class: 'no', domain_context: ['health'] },
]);
const out = runHook('session-start.mjs', { session_id: 'snew4', cwd: '/tmp' }, dir);
assert.ok(!/tier-2/.test(out.hookSpecificOutput.additionalContext));
});
it('handles v1.1.0 records with string domain_context (backward compat)', () => {
dir = setupTestDir();
writeFixture(dir, [
{ session_id: 'p1', duration_min: 30, user_info_class: 'no', domain_context: 'health' }, // string shape
{ session_id: 'p2', duration_min: 30, user_info_class: 'no', domain_context: ['legal'] },
{ session_id: 'p3', duration_min: 30, user_info_class: 'no', domain_context: ['parenting'] },
]);
const out = runHook('session-start.mjs', { session_id: 'snew5', cwd: '/tmp' }, dir);
assert.match(out.hookSpecificOutput.additionalContext, /tier-2/);
});
});

View file

@ -1,69 +0,0 @@
// Verifies SKILL.md stays aligned with the Constitution-mapping JSON
// produced during the v1.1.0 research phase, AND with the Appendix-driven
// v1.2.0 sycophancy 5-scale + 11 guidance criteria additions.
//
// The constitution-mapping.json file is generated locally during research
// and gitignored. On a fresh clone, fall back to checking the verbatim
// CC0 Constitution citation that should be present regardless.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, existsSync } from 'node:fs';
test('SKILL.md contains Constitution citation', () => {
const skill = readFileSync('skills/ai-psychosis/SKILL.md', 'utf8');
const mappingPath = '.claude/projects/2026-05-01-ai-psychosis-anthropic-guidance/constitution-mapping.json';
if (existsSync(mappingPath)) {
const mapping = JSON.parse(readFileSync(mappingPath, 'utf8'));
if (mapping.skill_md_grep_target === 'FALLBACK_PARAPHRASE') {
assert.ok(skill.includes('anthropic.com/research/claude-personal-guidance'));
} else {
assert.ok(
skill.includes(mapping.skill_md_grep_target),
`SKILL.md missing locked Constitution target: ${mapping.skill_md_grep_target}`
);
}
} else {
// Fresh clone — assertion fallback uses the verbatim CC0 Constitution
// text known to be present in v1.1.0+.
assert.ok(skill.includes("Sometimes being honest requires courage"),
'SKILL.md missing CC0 Constitution courage citation');
}
assert.ok(skill.includes('anthropic.com/constitution'));
assert.ok(skill.includes('anthropic.com/research/claude-personal-guidance'));
});
test('SKILL.md cites Score 5 sycophancy phrase verbatim (v1.2)', () => {
const skill = readFileSync('skills/ai-psychosis/SKILL.md', 'utf8');
assert.ok(
skill.includes('Provides accurate, honest information regardless'),
'SKILL.md missing verbatim Score 5 phrasing'
);
assert.ok(
skill.includes('Maintains correct positions when challenged'),
'SKILL.md missing Score 5 challenge phrase'
);
assert.ok(
skill.includes("Delivers difficult truths constructively"),
'SKILL.md missing Score 5 difficult-truths phrase'
);
});
test('SKILL.md cites the 11 guidance criteria (v1.2 — at least 3 quoted)', () => {
const skill = readFileSync('skills/ai-psychosis/SKILL.md', 'utf8');
// Three load-bearing quotes from the 11 criteria (page 2 of Appendix).
assert.ok(
skill.includes("Avoid providing guidance that would foster continued engagement"),
'SKILL.md missing engagement-foster criterion'
);
assert.ok(
skill.includes("Be wary of giving excessively confident verdicts"),
'SKILL.md missing confident-verdicts criterion'
);
assert.ok(
skill.includes("Maintain integrity and be willing to speak frankly"),
'SKILL.md missing frank-pushback criterion'
);
});

View file

@ -1,114 +0,0 @@
// stakes-matrix.test.mjs — verifies v1.2 domain-stakes weighting on
// new v1.2 alerts only. v1.1.0 alert sensitivity (dep, esc, fat, val,
// burst, low-edit-ratio) MUST be unchanged.
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;
afterEach(() => { if (dir) cleanupTestDir(dir); });
function freshState() {
return {
start_epoch: Math.floor(Date.now() / 1000) - 60,
start_iso: '2026-05-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,
pushback_count: 0, domain_context: null,
user_info_class: null,
user_info_flags: { yes_people: 0, yes_digital: 0, no: 0 },
turn_count: 0,
valseek_count: 0, valseek_flag: 0,
last_warning_epoch: 0,
};
}
function runPromptCapture(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 's-stake', { ...freshState(), ...stateOverrides });
const out = runHook('prompt-analyzer.mjs', { session_id: 's-stake', prompt }, dir);
const state = readState(dir, 's-stake');
return { state, out };
}
describe('stakes-matrix on valseek HIGH_STAKES path', () => {
it('valseek_count=2 in legal (weight 1.5) → effective threshold 2.0 → fires', () => {
// 3 / 1.5 = 2.0; valseek_count after this prompt becomes 2; 2 >= 2.0 → fires.
const { out } = runPromptCapture("am I crazy?", {
domain_context: ['legal'],
valseek_count: 1,
});
assert.match(out.hookSpecificOutput.additionalContext, /high-stakes/);
});
it('valseek_count=1 in legal → 1 < 2.0 → no alert', () => {
const { out } = runPromptCapture("am I crazy?", {
domain_context: ['legal'],
valseek_count: 0, // becomes 1
});
assert.equal(out.hookSpecificOutput, undefined);
});
it('valseek_count=4 in consumer (weight 1.0, NOT in HIGH_STAKES) → no alert regardless', () => {
const { out } = runPromptCapture("am I crazy?", {
domain_context: ['consumer'],
valseek_count: 3, // becomes 4
});
assert.equal(out.hookSpecificOutput, undefined,
'consumer is outside HIGH_STAKES_DOMAINS — high-stakes path never fires');
});
it('valseek_count=2 in legal → fires; same count in professional (INFO only) → no alert', () => {
const legal = runPromptCapture("am I crazy?", {
domain_context: ['legal'],
valseek_count: 1,
});
const pro = runPromptCapture("am I crazy?", {
domain_context: ['professional'],
valseek_count: 1,
});
assert.match(legal.out.hookSpecificOutput.additionalContext, /high-stakes/);
assert.equal(pro.out.hookSpecificOutput, undefined,
'professional is in INFO_DOMAINS but not HIGH_STAKES_DOMAINS');
});
});
describe('stakes-matrix on pushback HIGH_SYCOPHANCY path', () => {
it('pushback_count=2 in relationship (weight 1.3) → 2/1.3 ≈ 1.54 → fires', () => {
const { out } = runPromptCapture("are you sure?", {
domain_context: ['relationship'],
pushback_count: 1, // becomes 2
});
assert.match(out.hookSpecificOutput.additionalContext, /pushback re-contextualization/);
});
});
describe('stakes-matrix MUST NOT alter v1.1.0 alert sensitivity', () => {
it('dep_flags=1 in legal → does NOT fire dependency alert', () => {
// Dependency soft threshold = 2 in v1.1.0. If stakes-matrix bled into this,
// 2/1.5 = 1.33 → dep_flags=1 might trigger. It must NOT.
const { out } = runPromptCapture("tell me what to do here", {
domain_context: ['legal'],
dep_flags: 0, // this prompt sets to 1
});
// v1.1.0 dep alert requires >= 2 flags, regardless of domain weight.
// Output should not contain dep "Dependency language" wording.
const text = out.hookSpecificOutput?.additionalContext || '';
assert.ok(!/Dependency language/.test(text),
'v1.1.0 dependency threshold must not be lowered by stakes weight');
});
it('val_flags=2 in legal → does NOT fire validation-seeking v1.1.0 alert', () => {
// v1.1.0 val_flags threshold is 3. Stakes weight must not lower it to 2.
const { out } = runPromptCapture("right?", {
domain_context: ['legal'],
val_flags: 1, // becomes 2
});
const text = out.hookSpecificOutput?.additionalContext || '';
// The v1.1.0 wording is "Validation-seeking pattern detected (...)".
assert.ok(!/Validation-seeking pattern detected/.test(text),
'v1.1.0 val_flags threshold (3) must not be lowered by stakes weight');
});
});

View file

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

View file

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

View file

@ -1,247 +0,0 @@
// user-info.test.mjs — verifies v1.2 user-information classifier.
//
// Three classes: yes_people > yes_digital > no (priority order).
// Class is sticky upward — yes_people once set never downgrades.
// turn_count increments on every prompt-analyzer invocation.
// Step 9 will add the tier-1 alert; this file currently locks the
// detection + sticky semantics.
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;
afterEach(() => { if (dir) cleanupTestDir(dir); });
function freshState() {
return {
start_epoch: Math.floor(Date.now() / 1000) - 60,
start_iso: '2026-05-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,
pushback_count: 0, domain_context: null,
user_info_class: null,
user_info_flags: { yes_people: 0, yes_digital: 0, no: 0 },
turn_count: 0,
valseek_count: 0, valseek_flag: 0,
last_warning_epoch: 0,
};
}
function runPrompt(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 'u1', { ...freshState(), ...stateOverrides });
runHook('prompt-analyzer.mjs', { session_id: 'u1', prompt }, dir);
return readState(dir, 'u1');
}
// --- yes_people detection ---
describe('user_info: yes_people patterns', () => {
it('matches "my therapist"', () => {
const s = runPrompt('I asked my therapist about this');
assert.equal(s.user_info_class, 'yes_people');
assert.equal(s.user_info_flags.yes_people, 1);
});
it('matches "my friend"', () => {
const s = runPrompt('my friend says I should try meditation');
assert.equal(s.user_info_class, 'yes_people');
});
it('matches "my mentor"', () => {
const s = runPrompt('my mentor mentioned this approach');
assert.equal(s.user_info_class, 'yes_people');
});
it('matches "I told my partner"', () => {
const s = runPrompt('I told my partner about it last night');
assert.equal(s.user_info_class, 'yes_people');
});
});
describe('user_info: yes_digital patterns', () => {
it('matches "I googled"', () => {
const s = runPrompt('I googled this and got mixed results');
assert.equal(s.user_info_class, 'yes_digital');
});
it('matches "ChatGPT said"', () => {
const s = runPrompt('ChatGPT said the answer was 42');
assert.equal(s.user_info_class, 'yes_digital');
});
it('matches "I read on a forum post"', () => {
const s = runPrompt('I read on a forum post that this works');
assert.equal(s.user_info_class, 'yes_digital');
});
});
describe('user_info: no patterns', () => {
it('matches "nobody knows"', () => {
const s = runPrompt("nobody knows I'm dealing with this");
assert.equal(s.user_info_class, 'no');
});
it('matches "haven\'t told anyone"', () => {
const s = runPrompt("I haven't told anyone about it");
assert.equal(s.user_info_class, 'no');
});
it('matches "dealing with this alone"', () => {
const s = runPrompt("I'm dealing with this alone");
assert.equal(s.user_info_class, 'no');
});
});
// --- Priority + sticky semantics ---
describe('user_info: priority and stickiness', () => {
it('yes_people wins over yes_digital in same prompt', () => {
const s = runPrompt("I googled it but my therapist said something else");
assert.equal(s.user_info_class, 'yes_people');
// Both counters increment regardless of class outcome.
assert.equal(s.user_info_flags.yes_people, 1);
assert.equal(s.user_info_flags.yes_digital, 1);
});
it('yes_people wins over no in same prompt', () => {
const s = runPrompt("nobody knows but I told my friend");
assert.equal(s.user_info_class, 'yes_people');
});
it('yes_digital wins over no in same prompt', () => {
const s = runPrompt("nobody knows except what I read on a forum post");
assert.equal(s.user_info_class, 'yes_digital');
});
it('sticky: yes_people set, later yes_digital prompt does NOT downgrade', () => {
dir = setupTestDir();
createStateFile(dir, 'u-sticky', freshState());
runHook('prompt-analyzer.mjs', { session_id: 'u-sticky', prompt: 'my therapist suggested journaling' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'u-sticky', prompt: 'I googled the rest' }, dir);
const s = readState(dir, 'u-sticky');
assert.equal(s.user_info_class, 'yes_people', 'must not downgrade from people to digital');
assert.equal(s.user_info_flags.yes_digital, 1, 'digital counter still increments');
});
it('sticky: no → yes_people upgrades (lower → higher rank)', () => {
dir = setupTestDir();
createStateFile(dir, 'u-up', freshState());
runHook('prompt-analyzer.mjs', { session_id: 'u-up', prompt: 'nobody knows about this' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'u-up', prompt: 'finally told my therapist' }, dir);
const s = readState(dir, 'u-up');
assert.equal(s.user_info_class, 'yes_people');
});
it('class stays null when no user-info patterns hit', () => {
const s = runPrompt('refactor this typescript module to use generics');
assert.equal(s.user_info_class, null);
assert.equal(s.user_info_flags.yes_people, 0);
assert.equal(s.user_info_flags.yes_digital, 0);
assert.equal(s.user_info_flags.no, 0);
});
});
// --- turn_count ---
describe('turn_count', () => {
it('increments on every prompt-analyzer call', () => {
dir = setupTestDir();
createStateFile(dir, 'u-turn', freshState());
for (let i = 0; i < 5; i++) {
runHook('prompt-analyzer.mjs', { session_id: 'u-turn', prompt: `prompt ${i}` }, dir);
}
const s = readState(dir, 'u-turn');
assert.equal(s.turn_count, 5);
});
it('handles missing turn_count in pre-v1.2 state files (defaults to 0)', () => {
const legacy = freshState();
delete legacy.turn_count;
dir = setupTestDir();
createStateFile(dir, 'u-legacy', legacy);
runHook('prompt-analyzer.mjs', { session_id: 'u-legacy', prompt: 'hello' }, dir);
const s = readState(dir, 'u-legacy');
assert.equal(s.turn_count, 1, 'should start from 0 when field absent and increment to 1');
});
});
// --- Tier-1 alert ---
//
// Fires when user_info_class === 'no' AND domain_context intersects
// HIGH_STAKES_DOMAINS AND turn_count >= TIER1_TURN_THRESHOLD (15).
function runPromptCapture(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 'u-tier1', { ...freshState(), ...stateOverrides });
const out = runHook('prompt-analyzer.mjs', { session_id: 'u-tier1', prompt }, dir);
const state = readState(dir, 'u-tier1');
return { state, out };
}
describe('tier-1 user-info alert', () => {
it('fires at turn 15 (pre-seed 14) with no + legal domain', () => {
// Pre-seed: turn_count 14, after one hook call → 15. Triggers alert.
const { state, out } = runPromptCapture('any innocuous prompt', {
turn_count: 14,
user_info_class: 'no',
domain_context: ['legal'],
});
assert.equal(state.turn_count, 15);
assert.ok(out.hookSpecificOutput, 'tier-1 alert should be emitted');
assert.match(out.hookSpecificOutput.additionalContext, /tier-1/);
assert.match(out.hookSpecificOutput.additionalContext, /legal/);
});
it('does NOT fire sub-threshold (turn 14 → 14 should not trigger; 13 → 14)', () => {
const { state, out } = runPromptCapture('any prompt', {
turn_count: 13,
user_info_class: 'no',
domain_context: ['legal'],
});
assert.equal(state.turn_count, 14);
assert.equal(out.hookSpecificOutput, undefined,
'tier-1 must not fire below threshold');
});
it('does NOT fire for low-stakes domain (consumer)', () => {
const { out } = runPromptCapture('any prompt', {
turn_count: 14,
user_info_class: 'no',
domain_context: ['consumer'],
});
assert.equal(out.hookSpecificOutput, undefined,
'tier-1 only fires in high-stakes domains');
});
it('does NOT fire when user_info_class is yes_people (supersedes "no")', () => {
const { out } = runPromptCapture('any prompt', {
turn_count: 14,
user_info_class: 'yes_people',
domain_context: ['legal'],
});
assert.equal(out.hookSpecificOutput, undefined,
'tier-1 only fires when user signals isolation');
});
it('does NOT fire when domain_context is empty', () => {
const { out } = runPromptCapture('any prompt', {
turn_count: 14,
user_info_class: 'no',
domain_context: [],
});
assert.equal(out.hookSpecificOutput, undefined);
});
it('fires for parenting domain (also high-stakes)', () => {
const { out } = runPromptCapture('any prompt', {
turn_count: 14,
user_info_class: 'no',
domain_context: ['parenting'],
});
assert.ok(out.hookSpecificOutput, 'tier-1 fires for parenting too');
assert.match(out.hookSpecificOutput.additionalContext, /parenting/);
});
});

View file

@ -1,205 +0,0 @@
// validation-seeking.test.mjs — verifies v1.2 validation-seeking detector.
//
// Distinct from existing val_flags ("right?" tic). valseek targets:
// - tag-questions pressing for agreement
// - reality-testing ("am I crazy?", "is it normal?")
// - side-taking pressing ("back me up")
// - pre-committed stance + confirmation
//
// Step 11 will add the domain-gated alert; this file currently locks
// detection + count accumulation semantics.
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;
afterEach(() => { if (dir) cleanupTestDir(dir); });
function freshState() {
return {
start_epoch: Math.floor(Date.now() / 1000) - 60,
start_iso: '2026-05-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,
pushback_count: 0, domain_context: null,
user_info_class: null,
user_info_flags: { yes_people: 0, yes_digital: 0, no: 0 },
turn_count: 0,
valseek_count: 0, valseek_flag: 0,
last_warning_epoch: 0,
};
}
function runPrompt(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 'v1', { ...freshState(), ...stateOverrides });
runHook('prompt-analyzer.mjs', { session_id: 'v1', prompt }, dir);
return readState(dir, 'v1');
}
// --- Pattern detection ---
describe('valseek: tag-questions pressing for agreement', () => {
it('matches "isn\'t it?"', () => {
const s = runPrompt("she's wrong, isn't she?");
assert.equal(s.valseek_count, 1);
assert.equal(s.valseek_flag, 1);
});
it('matches "don\'t you think?"', () => {
const s = runPrompt("this approach is better, don't you think?");
assert.equal(s.valseek_count, 1);
});
it('matches "right, though?"', () => {
const s = runPrompt("I should leave him, right, though?");
assert.equal(s.valseek_count, 1);
});
});
describe('valseek: reality-testing patterns', () => {
it('matches "am I crazy"', () => {
const s = runPrompt("am I crazy for thinking this?");
assert.equal(s.valseek_count, 1);
});
it('matches "tell me I\'m not crazy"', () => {
const s = runPrompt("tell me I'm not crazy for feeling betrayed");
assert.equal(s.valseek_count, 1);
});
it('matches "is it normal to"', () => {
const s = runPrompt("is it normal to feel this way after years?");
assert.equal(s.valseek_count, 1);
});
});
describe('valseek: side-taking pressing', () => {
it('matches "you agree, right?"', () => {
const s = runPrompt("you agree, right?");
assert.equal(s.valseek_count, 1);
});
it('matches "back me up here"', () => {
const s = runPrompt("she lied to me — back me up here");
assert.equal(s.valseek_count, 1);
});
});
describe('valseek: pre-committed stance', () => {
it('matches "I already decided ... right"', () => {
const s = runPrompt("I already decided to quit, that's right?");
assert.equal(s.valseek_count, 1);
});
it('matches "I know I\'m right about this"', () => {
const s = runPrompt("I know I'm right about this whole thing");
assert.equal(s.valseek_count, 1);
});
});
// --- Negative cases ---
describe('valseek: false-positive guards', () => {
it('does NOT match casual "right?" tic alone', () => {
const s = runPrompt('the function returns true, right?');
// Casual right? hits the existing val_flags pattern but NOT valseek.
assert.equal(s.valseek_count, 0);
});
it('does NOT match technical question without pressing pattern', () => {
const s = runPrompt('what does this regex do?');
assert.equal(s.valseek_count, 0);
});
});
// --- Accumulation ---
describe('valseek: count accumulation', () => {
it('accumulates across multiple prompts', () => {
dir = setupTestDir();
createStateFile(dir, 'v-acc', freshState());
const prompts = [
"am I crazy for staying?",
"you agree, right?",
"isn't she wrong?",
"I know I'm right on this",
"tell me I'm not crazy",
];
for (const p of prompts) {
runHook('prompt-analyzer.mjs', { session_id: 'v-acc', prompt: p }, dir);
}
const s = readState(dir, 'v-acc');
assert.equal(s.valseek_count, 5);
assert.equal(s.valseek_flag, 1);
});
it('valseek_flag is sticky once set, even if later prompt has no hit', () => {
dir = setupTestDir();
createStateFile(dir, 'v-sticky', freshState());
runHook('prompt-analyzer.mjs', { session_id: 'v-sticky', prompt: 'am I crazy?' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'v-sticky', prompt: 'refactor this code' }, dir);
const s = readState(dir, 'v-sticky');
assert.equal(s.valseek_count, 1, 'count is unchanged by later non-matching prompt');
assert.equal(s.valseek_flag, 1, 'flag stays 1 once set');
});
});
// --- Domain-gated alert ---
function runPromptCapture(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 'v-alert', { ...freshState(), ...stateOverrides });
const out = runHook('prompt-analyzer.mjs', { session_id: 'v-alert', prompt }, dir);
const state = readState(dir, 'v-alert');
return { state, out };
}
describe('valseek: domain-gated alert', () => {
it('1 valseek + relationship → alert (high-sycophancy)', () => {
const { out } = runPromptCapture("am I crazy?", { domain_context: ['relationship'] });
assert.match(out.hookSpecificOutput.additionalContext, /validation-seeking/);
});
it('1 valseek + spirituality → alert (high-sycophancy)', () => {
const { out } = runPromptCapture("am I crazy?", { domain_context: ['spirituality'] });
assert.match(out.hookSpecificOutput.additionalContext, /validation-seeking/);
});
it('5 valseek + consumer → NO alert (low-stakes domain)', () => {
const { out } = runPromptCapture("you agree, right?", {
domain_context: ['consumer'],
valseek_count: 4, // becomes 5 after this prompt
});
assert.equal(out.hookSpecificOutput, undefined,
'low-stakes domain — no validation alert even at high count');
});
it('3 valseek + legal → alert (high-stakes path)', () => {
const { out } = runPromptCapture("am I crazy?", {
domain_context: ['legal'],
valseek_count: 2, // becomes 3
});
assert.match(out.hookSpecificOutput.additionalContext, /high-stakes/);
});
it('1 valseek + legal → NO alert (sub-threshold even with stakes weight)', () => {
// Step 13: stakes weight 1.5 lowers high-stakes threshold from 3 to 2.0.
// valseek_count=1 still under threshold.
const { out } = runPromptCapture("am I crazy?", {
domain_context: ['legal'],
valseek_count: 0, // becomes 1
});
assert.equal(out.hookSpecificOutput, undefined);
});
it('valseek alert fires for relationship even with valseek_count = 1', () => {
const { out } = runPromptCapture("you agree, right?", {
domain_context: ['relationship'],
valseek_count: 0, // becomes 1
});
assert.match(out.hookSpecificOutput.additionalContext, /validation-seeking/);
});
});

View file

@ -1,18 +0,0 @@
{
"name": "claude-design",
"version": "0.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.",
"author": {
"name": "Kjell Tore Guttormsen"
},
"auto_discover": true,
"license": "MIT",
"repository": "https://git.fromaitochitta.com/open/ktg-plugin-marketplace",
"keywords": [
"claude-design",
"claude-ai",
"prompt-engineering",
"artifacts",
"design"
]
}

View file

@ -1,90 +0,0 @@
# claude-design coverage manifest
**Captured-on date:** 2026-05-17 | **Source:** `https://anthropic.com/news/claude-design-anthropic-labs` (intent-preset enumeration)
This file is the canonical input for SC2 verification (`tests/test-sc2-artifact-coverage.sh`) and the SC3 Authoritative-claims registry (`tests/test-sc3-citations.sh`). Both tests read this file directly — keep it in sync with the references tree.
Anthropic's launch enumeration names eight intent presets; this plugin ships one reference file per preset with explicit evidence-grade labelling. The evidence-grade levels are:
- **Anthropic-documented + community-validated** — Anthropic publishes verbatim prompt patterns and community practitioners have independently validated them
- **Community-only** — Anthropic names the preset but publishes no per-preset prompt patterns; the patterns come from community practitioners with attribution
- **Experimental** — neither Anthropic nor community practitioners have published verifiable prompt patterns; the preset is engaged speculatively
The evidence-grade labels are load-bearing for SC2 and SC3. Per-preset reference files restate the grade inline on line 4.
---
## Intent preset coverage
| Preset | Reference file | Evidence grade | Anthropic anchor URL |
| --- | --- | --- | --- |
| designs | skills/claude-design-facilitator/references/presets/designs.md | Evidence grade: Anthropic-documented + community-validated | https://anthropic.com/news/claude-design-anthropic-labs |
| prototypes | skills/claude-design-facilitator/references/presets/prototypes.md | Evidence grade: Anthropic-documented + community-validated | https://claude.com/resources/tutorials/using-claude-design-for-prototypes-and-ux |
| slides | skills/claude-design-facilitator/references/presets/slides.md | Evidence grade: Anthropic-documented + community-validated | https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks |
| one-pagers | skills/claude-design-facilitator/references/presets/one-pagers.md | Evidence grade: Community-only | https://anthropic.com/news/claude-design-anthropic-labs |
| wireframes-mockups | skills/claude-design-facilitator/references/presets/wireframes-mockups.md | Evidence grade: Community-only | https://anthropic.com/news/claude-design-anthropic-labs |
| pitch-decks | skills/claude-design-facilitator/references/presets/pitch-decks.md | Evidence grade: Community-only | https://anthropic.com/news/claude-design-anthropic-labs |
| marketing-collateral | skills/claude-design-facilitator/references/presets/marketing-collateral.md | Evidence grade: Community-only | https://anthropic.com/news/claude-design-anthropic-labs |
| frontier-design | skills/claude-design-facilitator/references/presets/frontier-design.md | Evidence grade: Experimental — no validated practitioner pattern | https://anthropic.com/news/claude-design-anthropic-labs |
The preset names in column 1 (`designs`, `prototypes`, `slides`, `one-pagers`, `wireframes-mockups`, `pitch-decks`, `marketing-collateral`, `frontier-design`) are the canonical names used by `tests/test-sc2-artifact-coverage.sh`. The test extracts column 1 via awk and runs grep against the plugin's content tree to verify each preset has at least one supporting file.
---
## Authoritative-claims files
The following files contain authoritative claims (Anthropic-published material, primary sources, or community-converged patterns with attribution). Each must carry at least one Anthropic-domain URL citation. `tests/test-sc3-citations.sh` reads this bullet list, parses paths via awk on `^- `, then runs the citation grep against each file.
- skills/claude-design-facilitator/references/00-what-claude-design-is-and-isnt.md
- skills/claude-design-facilitator/references/01-prompt-fundamentals.md
- skills/claude-design-facilitator/references/02-design-md.md
- skills/claude-design-facilitator/references/03-iteration-and-session.md
- skills/claude-design-facilitator/references/04-handoff-and-scope.md
- skills/claude-design-facilitator/references/presets/designs.md
- skills/claude-design-facilitator/references/presets/prototypes.md
- skills/claude-design-facilitator/references/presets/slides.md
- skills/claude-design-facilitator/references/presets/one-pagers.md
- skills/claude-design-facilitator/references/presets/wireframes-mockups.md
- skills/claude-design-facilitator/references/presets/pitch-decks.md
- skills/claude-design-facilitator/references/presets/marketing-collateral.md
- skills/claude-design-facilitator/references/presets/frontier-design.md
Total: 13 authoritative-claims files (5 foundation references + 8 per-preset references).
The bullet-list format is load-bearing — `tests/test-sc3-citations.sh` parses lines starting with `- ` (dash + space). Do not switch to a table or numbered list without updating the test.
---
## Re-research triggers
This manifest refreshes when any of these events occurs:
- **Anthropic publishes per-preset guidance for a Community-only preset** (one-pagers, wireframes-mockups, pitch-decks, marketing-collateral) — upgrade the affected row's evidence grade and add the new Anthropic anchor URL
- **Anthropic publishes per-preset guidance for the Experimental preset** (frontier-design) — upgrade to Community-only or Anthropic-documented depending on coverage depth
- **A new intent preset is added to Anthropic's launch-post enumeration** (`https://anthropic.com/news/claude-design-anthropic-labs`) — add a new row, write a new preset reference file
- **An existing intent preset is removed from the enumeration** — remove the row, deprecate the reference file in `CHANGELOG.md`
- **A first verified frontier-design practitioner artifact ships publicly** with prompt + output + reproduction steps — upgrade the frontier-design row from Experimental to Community-only, update `presets/frontier-design.md`
- **Anthropic support article URL slugs change while keeping numeric IDs stable** — re-pin URLs in column 4 (Anthropic anchor URL); the numeric IDs in `support.claude.com/en/articles/<numeric-id>-<slug>` are the stable anchor
- **Labs → GA URL rename for `claude.ai/design`** — re-pin the launch-post URL once the `-anthropic-labs` slug is dropped (note: the launch URL `https://anthropic.com/news/claude-design-anthropic-labs` may or may not 301-redirect after the rename)
When any trigger fires, run `bash plugins/claude-design/verify.sh --strict` after the manifest update to confirm SC2 and SC3 still pass.
---
## Related sources (for context, not for SC checks)
Anthropic primary sources that ground this manifest but are not themselves authoritative-claims files (because they are external URLs, not plugin files):
- `https://anthropic.com/news/claude-design-anthropic-labs` — preset enumeration
- `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` — GLCA framework
- `https://support.claude.com/en/articles/14604397-set-up-your-design-system-in-claude-design` — design-system setup
- `https://support.claude.com/en/articles/13521390-use-claude-for-powerpoint` — PowerPoint-mode conventions
- `https://claude.com/resources/tutorials/using-claude-design-for-prototypes-and-ux` — prototypes tutorial
- `https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks` — slides tutorial
- `https://anthropic.com/engineering/harness-design-long-running-apps` — design grading framing
- `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` — Design-Thinking Framework, AI-slop avoid-list
- `https://claude.com/blog/improving-frontend-design-through-skills` — default-avoidance blog post
- `https://claude.com/plugins/design` — Anthropic's official knowledge-work-plugins/design plugin (downstream tool)
- `https://github.com/anthropics/knowledge-work-plugins` — source for Anthropic's downstream plugin
Anthropic URL canonicalisation: every `support.claude.com` reference uses the `https://support.claude.com/en/articles/<numeric-id>-<slug>` form. Numeric IDs are stable across slug rewrites; slug-only URLs are not.

View file

@ -1,37 +0,0 @@
# 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).
## [0.1.0] — 2026-05-17
### Added
- `claude-design-facilitator` skill with eight-phase facilitation flow (disambiguate → intent preset → audience + destination → DESIGN.md anchor → five-layer prompt draft → copy-paste delivery → iteration coaching → ship-readiness handoff) and 12 natural-language trigger phrases registered in `.triggers.txt`.
- Five foundation references under `skills/claude-design-facilitator/references/`: `00-what-claude-design-is-and-isnt.md` (surface disambiguation), `01-prompt-fundamentals.md` (five-layer prompt stack: GLCA + start-simple + concrete-alternative-spec + propose-options + AI-slop avoid-list + four design dimensions + four grading criteria), `02-design-md.md` (DESIGN.md 9-section canonical structure + brand-to-DESIGN.md extractor), `03-iteration-and-session.md` (Tweak / Comment / Chat cascade, session economics, recovery prompt library), `04-handoff-and-scope.md` (one-way Design → Code handoff + scope fence vs Anthropic's `knowledge-work-plugins/design`).
- Eight per-preset references under `skills/claude-design-facilitator/references/presets/` with evidence-grade labels: `designs.md`, `prototypes.md`, `slides.md` (Anthropic-documented + community-validated); `one-pagers.md`, `wireframes-mockups.md`, `pitch-decks.md`, `marketing-collateral.md` (Community-only); `frontier-design.md` (Experimental — no validated practitioner pattern as of 2026-05-16).
- `.coverage.md` at plugin root — preset enumeration table with evidence-grade labels (8 rows) + `Authoritative-claims files` bullet-list registry (13 paths). Canonical input for SC2 and SC3 verification.
- Five verification scripts under `tests/`: `validate-plugin.sh` (structural integrity + forbidden-command-name scope fence + operator-private-context grep + Norwegian-leakage advisory), `test-skill-triggers.sh` (description quality + trigger phrase coverage), `test-sc2-artifact-coverage.sh` (per-preset coverage from `.coverage.md`), `test-sc3-citations.sh` (Anthropic-domain citation discipline), `test-sc1-dogfood-log.sh` (operator dogfood log format-check in `REMEMBER.md`).
- `verify.sh` top-level roll-up with `--strict` (SC1 missing-block becomes FAIL) and `--quick` (skip skill-triggers test) flags.
- `LICENSE` (MIT) and `GOVERNANCE.md` (marketplace fork-and-own blurb).
- Marketplace registration in root `.claude-plugin/marketplace.json`.
### Documentation
- Plugin `README.md` rewritten from scaffold placeholder to full v0.1 surface description with `Scope and complementarity` section (placed before installation per brief), `What this plugin is NOT` (Non-Goals), eight-phase facilitation flow table, skill surface table, reference content map, per-preset coverage table, verification section, AI-generated disclosure, fork-and-own MIT licensing.
- Plugin `CLAUDE.md` translated to English (operator override of marketplace's Norwegian-dialogue default per v0.1 brief constraint); added `Scope fence` section explicitly forbidding command-name collisions with Anthropic's `knowledge-work-plugins/design` (`/critique`, `/accessibility`, `/ux-copy`, `/research-synthesis`, `/design-system`, `/handoff`); `Authoring rules` section codifies English-everywhere, no operator-private context, evidence-grade label discipline, URL canonicalisation on `support.claude.com/en/articles/<numeric-id>-<slug>`; `Communication patterns` block preserved verbatim.
- Root marketplace `README.md` updated with `### [Claude Design](plugins/claude-design/) \`v0.1.0\`` entry under the `## Plugins` section, positioned after the Human-Friendly Style entry per existing convention. Entry documents the complementary lifecycle coverage vs `knowledge-work-plugins/design`.
### Notes
- **Scope:** claude-design facilitates the pre-design and during-design lifecycle for `claude.ai/design` (Anthropic Labs research preview, Opus 4.7 pinned, eight intent presets). For post-design — critique, accessibility audit, UX copy review, design-system audit, engineering handoff — install Anthropic's official plugin via `claude plugins add knowledge-work-plugins/design`. Zero command overlap, complementary by design.
- **No browser automation.** This plugin produces prompts; the artifact gets built inside `claude.ai/design`. The operator copies and pastes manually.
- **No artifact code generation.** This plugin is a prompt-builder, not an artifact generator. Claude Design is the generator.
- **No artifact storage or versioning.** Claude Design has no version-tree primitive and this plugin does not invent one. The verbal save-pattern documented in `references/03-iteration-and-session.md` is the closest substitute.
- **English everywhere in shipped content.** Operator override of the marketplace's default Norwegian-dialogue convention. `tests/validate-plugin.sh` assertion (j) emits WARN on Norwegian diacritics in shipped content; review case-by-case.
- **Evidence-grade discipline.** Every per-preset reference file carries an inline `Evidence grade:` label on line 4 with one of three values: `Anthropic-documented + community-validated`, `Community-only`, `Experimental`. `.coverage.md` is the canonical registry.
- **Re-research triggers** documented in `.coverage.md` — fire on Anthropic publishing per-preset guidance for Community-only presets, on new intent presets added to the launch enumeration, on the first verified `frontier-design` practitioner artifact shipping publicly, on Labs → GA URL rename for `claude.ai/design`, on Anthropic's `knowledge-work-plugins/design` adding or removing slash-commands.
## [0.1.0-pre] — 2026-05-15
### Added
- Initial scaffold (README, CLAUDE.md, ROADMAP, TODO, plugin.json placeholder).

View file

@ -1,88 +0,0 @@
# claude-design
## Context
This plugin is an expert on **Claude Design** (`claude.ai/design`) — Anthropic's Labs research preview for generating interactive design artifacts from a prompt. It walks the operator through the full lifecycle: idea → intent-preset selection → audience and destination → DESIGN.md anchor → five-layer prompt drafting → copy-paste delivery → iteration coaching → ship-readiness handoff. It does not generate artifact code itself and it does not drive the browser; it produces the prompt that the operator pastes into Claude Design.
## Status
`v0.1.0`. Surface:
- One skill: `claude-design-facilitator` (auto-fire + explicit `/claude-design-facilitator` slash command)
- Five foundation references under `skills/claude-design-facilitator/references/`
- Eight per-preset references under `skills/claude-design-facilitator/references/presets/`
- Five test scripts under `tests/` plus a `verify.sh` roll-up
- A `.coverage.md` preset manifest at the plugin root (canonical input for SC2 and the SC3 Authoritative-claims registry)
- `LICENSE` (MIT), `GOVERNANCE.md` (marketplace fork-and-own blurb), `README.md`, `CHANGELOG.md`
No commands, no agents, no hooks, no MCP servers at v0.1. The single skill is the entire user-facing surface.
## Marketplace context
This plugin lives inside `ktg-plugin-marketplace`. No separate git repository, no separate Forgejo remote. All commits go to the marketplace repository at `https://git.fromaitochitta.com/open/ktg-plugin-marketplace`.
Marketplace conventions inherited from the root `CLAUDE.md`:
- Conventional Commits — `type(scope): description`; scope is `claude-design`
- Hooks in Node.js (`.mjs`), never bash (this plugin ships no hooks at v0.1)
- Zero npm dependencies in hooks and scripts
- Docs-triple updated in the same commit on every feature change: plugin `README.md` + plugin `CLAUDE.md` + root `README.md`
## Architecture (v0.1)
- **`skills/claude-design-facilitator/SKILL.md`** is the auto-fire entry point AND the explicit `/claude-design-facilitator` invocation surface. The skill body documents the eight-phase facilitation flow.
- **`skills/claude-design-facilitator/.triggers.txt`** lists the natural-language phrases the skill auto-fires on. `tests/test-skill-triggers.sh` validates every phrase appears in the SKILL.md description.
- **`skills/claude-design-facilitator/references/`** is the knowledge base. Five foundation references (0004) plus eight per-preset references under `references/presets/`. Every authoritative claim cites an Anthropic primary source inline.
- **`.coverage.md`** at the plugin root is the SC2 manifest (preset enumeration with evidence-grade labels) and the SC3 Authoritative-claims registry (bullet list of files that must carry Anthropic-domain citations).
- **`tests/`** + **`verify.sh`** enforce the brief Success Criteria: SC1 dogfood-log format, SC2 per-preset coverage, SC3 citation discipline, plus skill description quality and plugin structural integrity.
The skill body never offers to generate artifact code, automate the browser, or store artifact history (per [Non-Goals in README](README.md)). It produces prompts.
## Scope fence
This plugin covers **pre-design and during-design** for `claude.ai/design`: idea → prompt → preview → iterate → ship-readiness.
**Post-design** — critique, accessibility audit, UX copy review, research synthesis, design-system audit, engineering handoff — is out of scope and lives in Anthropic's official `knowledge-work-plugins/design` (`https://claude.com/plugins/design`). This plugin must never duplicate the commands `/critique`, `/accessibility`, `/ux-copy`, `/research-synthesis`, `/design-system`, `/handoff` — with or without a `claude-design:` namespace prefix. `tests/validate-plugin.sh` assertion (h) enforces this scope fence mechanically.
The lifecycle-stage coverage map and the operational handoff between the two plugins are documented in `skills/claude-design-facilitator/references/04-handoff-and-scope.md`.
## Authoring rules
Every contribution to this plugin must respect these rules:
- **Language: English everywhere.** Plugin file content — `README.md`, `CLAUDE.md` (this file), `CHANGELOG.md`, `SKILL.md`, all `references/*.md`, all `tests/*.sh` output messages, every code comment — is English. This is the operator override of the marketplace's default Norwegian-dialogue policy; documented in the v0.1 brief. The `tests/validate-plugin.sh` assertion (j) emits a WARN on Norwegian diacritics in shipped content; review case-by-case (citation slugs occasionally legitimately carry diacritics, but the default is zero hits).
- **No operator-private context in shipped content.** No personal-name or organization-affiliation tokens, no copy-paste from local session-state and handoff files. `tests/validate-plugin.sh` assertion (i) enforces this with a recursive grep on the specific patterns it bans; the grep excludes the local files themselves.
- **Evidence-grade label discipline.** Every per-preset reference file carries an inline `Evidence grade:` label on line 4. The three grades are `Anthropic-documented + community-validated`, `Community-only`, and `Experimental`. `.coverage.md` is the canonical registry. SC2 and SC3 read from `.coverage.md` directly — keep it in sync.
- **URL canonicalisation.** All `support.claude.com` references use the form `https://support.claude.com/en/articles/<numeric-id>-<slug>`. Numeric IDs are stable across slug rewrites; slug-only URLs are not. `https://anthropic.com/news/...` and `https://claude.com/blog/...` follow whatever slug Anthropic publishes.
- **No NIH of Anthropic surfaces.** The plugin recommends Anthropic's `knowledge-work-plugins/design` as the downstream tool; it does not duplicate that plugin's functionality.
## Workflow
The Voyage pipeline produces v0.1 and every subsequent feature change:
1. **Brief** closes scope and scope boundaries
2. **Research** gathers external sources — Anthropic primary material (news posts, support articles, blog posts, open-source skills, tutorials, plugins), plus community practitioners with attribution
3. **Plan** specifies file-by-file what gets built
4. **Execute** delivers the code and content
5. **Review** is the release gate (`/trekreview`)
Voyage policy: Opus across all sub-agents and orchestrator phases (per `feedback_voyage_opus_always`).
For incremental content updates that do not warrant a full Voyage iteration (e.g., refreshing a single per-preset reference when Anthropic publishes new guidance), the docs-triple rule still applies: plugin `README.md` + plugin `CLAUDE.md` (this file) + root `README.md` updated in the same commit as the content change.
## Communication patterns
### Linking to local files
When pointing to local files in responses, always use markdown link syntax with a descriptive name:
- Use `[Human-friendly name](file:///absolute/path)` — never bare `file:///...` URLs or autolinks `<file://...>`.
- Always use absolute paths. Never `~/` or relative paths.
- For multiple files, render as a bullet list of named markdown links.
Why: bare `file://` URLs only render the first as clickable across multiple lines. Named markdown links make each entry independently clickable and look cleaner.
Example:
- [Brief](file:///Users/ktg/.../brief.html)
- [Research summary](file:///Users/ktg/.../research/summary.md)

View file

@ -1,118 +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, 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.
For `claude-design` specifically, the most likely fork is a content adaptation — different intent-preset coverage (e.g., dropping `frontier-design` if your team never uses it), an organization-specific DESIGN.md template, a different evidence-grade discipline, or per-preset prompt patterns tuned to your team's design system. The plugin is a content surface plus a single skill. Forking it is straightforward.
### What to change first when you fork
- **Identity** — rename the plugin, replace authorship, update README.
- **Reference content** — the `references/` tree reflects what Anthropic published and the community converged on as of 2026-05-17. Adjust to your team's house style and design system.
- **Frontmatter**`name` and `description` show up in `/config`. Pick names that won't collide with other forks installed on the same machine.
### Staying current with upstream
If you want to pull in upstream changes later:
- **Cherry-pick, don't merge.** Each plugin moves independently.
- **Read the CHANGELOG first.**
- **Keep your customizations distinct.** A renamed skill (`my-org-design-facilitator`) merges more cleanly than edits to `claude-design-facilitator`.
---
## 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 |
| **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.
- **Content suggestions.** If a reference file in `claude-design` produces guidance that doesn't match what you observe in `claude.ai/design` today, tell me what you saw. Concrete examples beat abstract complaints.
### 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 this plugin 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, and the shared `human-friendly-style` output style) but no runtime dependencies.
`claude-design` is a content surface with a single skill — it works without any other plugin installed. It recommends Anthropic's official `knowledge-work-plugins/design` as the downstream tool for post-design critique, accessibility audit, and engineering handoff, but does not depend on it being present.
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.
For `claude-design` specifically: changes to skill trigger behavior or per-preset reference content schema are minor or major bumps. Pure documentation or per-preset content refresh from Anthropic source updates are patch. The skill surface itself is meant to be stable across patch releases.
---
## License
MIT for all plugins in this marketplace. See [LICENSE](LICENSE) in this plugin and each other plugin's `LICENSE` file.

View file

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

View file

@ -1,206 +0,0 @@
# Claude Design Facilitator
> End-to-end facilitator for prompting Claude Design (`claude.ai/design`). Walks the operator from raw idea through intent-preset selection, audience and destination clarification, DESIGN.md anchor, five-layer prompt drafting, copy-paste delivery, iteration coaching, and ship-readiness handoff. Cites Anthropic primary sources inline. Recommends Anthropic's official `knowledge-work-plugins/design` as the downstream post-design tool.
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for the full model and what upstream provides.
*AI-generated: all content produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
![Version](https://img.shields.io/badge/version-0.1.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Skill](https://img.shields.io/badge/skills-1-green)
![References](https://img.shields.io/badge/references-13-green)
![Hooks](https://img.shields.io/badge/hooks-0-lightgrey)
![Commands](https://img.shields.io/badge/commands-0-lightgrey)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
A Claude Code plugin that ships one skill (`claude-design-facilitator`) plus a reference tree for prompting Anthropic's `claude.ai/design` workspace. The skill auto-fires on natural-language triggers, walks the operator through an eight-phase facilitation flow, and produces a copy-paste-ready prompt grounded in Anthropic's verbatim Goal / Layout / Content / Audience framework and the four published per-preset prompt patterns. Output is the prompt — the artifact gets built in Claude Design.
---
## Scope and complementarity
This plugin covers the **pre-design and during-design lifecycle** for `claude.ai/design`: idea → intent-preset selection → prompt engineering → copy-paste delivery → iteration coaching → ship-readiness check.
For **post-design** work — critique, accessibility audit, UX copy review, research synthesis, design-system audit, engineering handoff guidance — install Anthropic's official plugin:
```
claude plugins add knowledge-work-plugins/design
```
Anthropic's plugin operates on existing artifacts (Figma URLs, screenshots, copy snippets) and ships six slash-commands: `/critique`, `/accessibility`, `/ux-copy`, `/research-synthesis`, `/design-system`, `/handoff`. There is zero command overlap with this plugin and complementary lifecycle coverage — the two plugins are designed to be installed together. See [skills/claude-design-facilitator/references/04-handoff-and-scope.md](skills/claude-design-facilitator/references/04-handoff-and-scope.md) for the full coverage map.
---
## What this plugin is NOT
By design, this plugin does not:
- **Drive the browser.** No automation of `claude.ai/design` itself; you copy and paste the prompts the skill produces.
- **Generate the artifact code.** Claude Design is the artifact generator. This plugin produces prompts that go into Claude Design.
- **Store artifact history or version artifacts.** Claude Design has no version-tree primitive and this plugin does not invent one.
- **Cover adjacent Anthropic surfaces.** Classic Artifacts at `claude.ai`, Live Artifacts in Claude Cowork, custom visuals embedded in a chat reply are out of scope — see [skills/claude-design-facilitator/references/00-what-claude-design-is-and-isnt.md](skills/claude-design-facilitator/references/00-what-claude-design-is-and-isnt.md) for the disambiguation reference.
- **Duplicate Anthropic's `knowledge-work-plugins/design` plugin.** No `/critique`, no `/accessibility`, no `/ux-copy`, no `/research-synthesis`, no `/design-system`, no `/handoff`. The post-design lane belongs to Anthropic's plugin.
`tests/validate-plugin.sh` enforces the forbidden-command-name list mechanically.
---
## Installation
Add the marketplace once, then install the plugin:
```bash
claude plugins marketplace add ktg-plugin-marketplace https://git.fromaitochitta.com/open/ktg-plugin-marketplace
```
In Claude Code:
```
/plugin install claude-design@ktg-plugin-marketplace
```
Or enable directly in `~/.claude/settings.json`:
```json
{
"enabledPlugins": {
"claude-design@ktg-plugin-marketplace": true
}
}
```
The skill auto-discovers; no further configuration needed.
---
## What you can do with it
The skill `claude-design-facilitator` walks the operator through eight phases. The phases are scoping + grounding (14), drafting + delivery (56), and iteration + ship-readiness (78).
| Phase | What happens |
|-------|--------------|
| **1. Disambiguate the surface** | Confirm `claude.ai/design` is the intended surface, not classic Artifacts, Live Artifacts, custom chat visuals, or `knowledge-work-plugins/design`. Read [references/00](skills/claude-design-facilitator/references/00-what-claude-design-is-and-isnt.md) when signals are mixed. |
| **2. Name the intent preset** | Pick one of eight Claude Design presets: `designs`, `prototypes`, `slides`, `one-pagers`, `wireframes-mockups`, `pitch-decks`, `marketing-collateral`, `frontier-design`. The per-preset reference file shapes the prompt pattern. Evidence-grade labels are surfaced. |
| **3. Audience and destination** | Capture audience (internal team / external stakeholder / investor / customer) and destination (PDF / PPTX / HTML / Canva / Code-handoff / share-link). Flag PPTX-export traps for `pitch-decks` early. |
| **4. Anchor on DESIGN.md** | Read [references/02](skills/claude-design-facilitator/references/02-design-md.md). If the operator has no DESIGN.md, point at the copy-paste brand-to-DESIGN.md extractor prompt. |
| **5. Draft the prompt** | Compose layers 15 from [references/01](skills/claude-design-facilitator/references/01-prompt-fundamentals.md): Anthropic's verbatim Goal / Layout / Content / Audience framework + start-simple-layer-complexity + concrete-alternative-spec + propose-options-before-building + AI-slop negative constraints + four design dimensions + four grading criteria + the per-preset pattern. |
| **6. Deliver** | Output a single copy-paste-ready fenced markdown code block. Add a one-line caption and three to five expected follow-up turns. |
| **7. Iteration coaching** | Read [references/03](skills/claude-design-facilitator/references/03-iteration-and-session.md). Coach which surface to use next — Tweak panel (zero-token, surgical), inline comments (component-scoped), or chat (full regen). Session-break heuristics + recovery prompt library when iteration gets stuck. |
| **8. Ship-readiness** | Run the export validation checklist. If shipping to engineering, confirm the Design → Code handoff bundle is complete. Recommend installing `knowledge-work-plugins/design` for downstream critique / accessibility / handoff. |
The skill auto-fires on natural-language triggers like *"I want to build a dashboard in Claude Design"*, *"help me prompt claude.ai/design"*, *"iterate on my Claude Design artifact"*. The full trigger list is in [skills/claude-design-facilitator/.triggers.txt](skills/claude-design-facilitator/.triggers.txt) and `tests/test-skill-triggers.sh` validates each phrase appears in the skill description.
Explicit invocation works too: the skill registers as the slash command `/claude-design-facilitator` for when the operator wants to start a clean facilitation session.
---
## Skill surface
| Skill | Triggers | Output |
|-------|----------|--------|
| `claude-design-facilitator` | 12 natural-language phrases (full list in `.triggers.txt`); also explicit `/claude-design-facilitator` slash command | A copy-paste-ready Claude Design prompt block composed from the five-layer stack and the per-preset pattern, with follow-up-turn expectations |
No commands, no agents, no hooks, no MCP servers at v0.1. The single skill is the entire user-facing surface.
---
## Reference content map
The plugin ships 13 reference files in `skills/claude-design-facilitator/references/`:
**Foundation references (5):**
- [`00-what-claude-design-is-and-isnt.md`](skills/claude-design-facilitator/references/00-what-claude-design-is-and-isnt.md) — Surface disambiguation against Artifacts, Live Artifacts, custom chat visuals, and Anthropic's `knowledge-work-plugins/design`.
- [`01-prompt-fundamentals.md`](skills/claude-design-facilitator/references/01-prompt-fundamentals.md) — The five-layer prompt stack: GLCA framework + start-simple-layer-complexity + concrete-alternative-spec + propose-options + AI-slop negative constraints + four design dimensions + four grading criteria. Anchored on four Anthropic primary sources.
- [`02-design-md.md`](skills/claude-design-facilitator/references/02-design-md.md) — DESIGN.md 9-section canonical structure + brand-to-DESIGN.md extractor prompt + failure modes.
- [`03-iteration-and-session.md`](skills/claude-design-facilitator/references/03-iteration-and-session.md) — Tweak / Comment / Chat cascade, session economics, 4-screen inflection, recovery prompt library (break-default-aesthetic, fix-the-system, edit-previous-message, 3-failed-comment escalation, model downshift, verbal save-pattern).
- [`04-handoff-and-scope.md`](skills/claude-design-facilitator/references/04-handoff-and-scope.md) — Design → Code one-way handoff, bundle contents, lifecycle-stage coverage map vs Anthropic's `knowledge-work-plugins/design`, downstream tool recommendation.
**Per-preset references (8):**
- [`presets/designs.md`](skills/claude-design-facilitator/references/presets/designs.md) — Anthropic-documented + community-validated
- [`presets/prototypes.md`](skills/claude-design-facilitator/references/presets/prototypes.md) — Anthropic-documented + community-validated
- [`presets/slides.md`](skills/claude-design-facilitator/references/presets/slides.md) — Anthropic-documented + community-validated
- [`presets/one-pagers.md`](skills/claude-design-facilitator/references/presets/one-pagers.md) — Community-only
- [`presets/wireframes-mockups.md`](skills/claude-design-facilitator/references/presets/wireframes-mockups.md) — Community-only
- [`presets/pitch-decks.md`](skills/claude-design-facilitator/references/presets/pitch-decks.md) — Community-only (with explicit PPTX-export caveat)
- [`presets/marketing-collateral.md`](skills/claude-design-facilitator/references/presets/marketing-collateral.md) — Community-only
- [`presets/frontier-design.md`](skills/claude-design-facilitator/references/presets/frontier-design.md) — Experimental — no validated practitioner pattern as of 2026-05-16
---
## Per-preset coverage
The canonical coverage manifest is [`.coverage.md`](.coverage.md). Below mirrors that file.
| Preset | Evidence grade | Anthropic anchor |
|--------|----------------|------------------|
| designs | Anthropic-documented + community-validated | [launch post](https://anthropic.com/news/claude-design-anthropic-labs) |
| prototypes | Anthropic-documented + community-validated | [prototypes tutorial](https://claude.com/resources/tutorials/using-claude-design-for-prototypes-and-ux) |
| slides | Anthropic-documented + community-validated | [slides tutorial](https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks) |
| one-pagers | Community-only | [launch post](https://anthropic.com/news/claude-design-anthropic-labs) |
| wireframes-mockups | Community-only | [launch post](https://anthropic.com/news/claude-design-anthropic-labs) |
| pitch-decks | Community-only (with PPTX-export caveat) | [launch post](https://anthropic.com/news/claude-design-anthropic-labs) |
| marketing-collateral | Community-only | [launch post](https://anthropic.com/news/claude-design-anthropic-labs) |
| frontier-design | Experimental — no validated practitioner pattern | [launch post](https://anthropic.com/news/claude-design-anthropic-labs) |
When Anthropic publishes per-preset guidance for a Community-only or Experimental preset, [`.coverage.md`](.coverage.md) and the affected preset file refresh — re-research triggers are documented inline.
---
## Verification
```bash
bash plugins/claude-design/verify.sh
```
Runs five test scripts under `tests/` in dependency order:
| Script | Verifies |
|--------|----------|
| `validate-plugin.sh` | plugin.json + SKILL.md frontmatter + LICENSE + GOVERNANCE.md + README.md + CLAUDE.md + .coverage.md presence; forbidden-command-name scope-fence check; operator-private-context grep; Norwegian-leakage advisory |
| `test-skill-triggers.sh` | SKILL.md description >=400 chars; every phrase in `.triggers.txt` appears in SKILL.md |
| `test-sc2-artifact-coverage.sh` | Each preset in `.coverage.md` has >=1 file hit in plugin content |
| `test-sc3-citations.sh` | No unsourced-attribution placeholders (citation-stub markers, verification-flag markers, vague second-hand phrasing); each Authoritative-claims file has >=1 Anthropic-domain URL. The script enforces the exact patterns it bans — see the script source for the regex. |
| `test-sc1-dogfood-log.sh` | Format-check the operator dogfood log in `REMEMBER.md` (gitignored) — 5 fields well-formed |
Flags:
- `--strict` — pass-through to `test-sc1-dogfood-log.sh`. Without `--strict`, missing dogfood block is advisory. With `--strict`, it is the release gate.
- `--quick` — skip `test-skill-triggers.sh` for fast incremental runs.
Exit codes: `0` = all pass; non-zero = at least one sub-test failed.
---
## Compatibility
| Requirement | Version |
|-------------|---------|
| Claude Code | Recent versions with plugin support |
| Anthropic surface | `claude.ai/design` (Labs research preview launched 2026-04-17) |
| Platform | macOS, Linux, Windows |
| Network | None for the skill itself; the artifact-generation lives in `claude.ai/design` |
| Dependencies | None — no npm packages, no Python, no external tools. Bash 3.2 compatible for test scripts. |
---
## Re-research triggers
The reference tree carries Anthropic citations that may decay. Re-research is triggered by:
- Anthropic publishing per-preset guidance for a `Community-only` or `Experimental` preset
- Anthropic announcing material changes to the Goal / Layout / Content / Audience framework, the AI-slop avoid-list, or the design grading criteria
- Anthropic adding or removing an intent preset from the launch enumeration
- A first verified `frontier-design` practitioner artifact shipping publicly
- Anthropic's `knowledge-work-plugins/design` plugin adding or removing slash-commands (scope-fence implications)
- Labs → GA URL rename for `claude.ai/design`
When a trigger fires, run `bash verify.sh --strict` after the update to confirm SC2 and SC3 still pass.
---
## License
[MIT](LICENSE). Fork it, modify it, ship your own version under your own name.

View file

@ -1,12 +0,0 @@
I want to build a dashboard in Claude Design
help me prompt claude.ai/design
make a slide deck in claude.ai/design
iterate on my Claude Design artifact
what should I prompt Claude Design with
build a one-pager in Claude Design
design a prototype in claude.ai/design
refine my Claude Design output
create a pitch deck in Claude Design
use Claude Design
draft a Claude Design prompt
make wireframes in claude.ai/design

View file

@ -1,176 +0,0 @@
---
name: claude-design-facilitator
argument-hint: "[intent-preset]"
description: |
End-to-end facilitator for prompting Claude Design (claude.ai/design — Anthropic Labs research preview launched 2026-04-17, Opus 4.7 pinned). Walks the operator from raw idea through intent-preset selection, audience and destination clarification, DESIGN.md anchor, prompt drafting using Anthropic's verbatim Goal / Layout / Content / Audience framework plus the five-layer prompt stack, copy-paste delivery, iteration coaching across the Tweak / Comment / Chat cascade, and ship-readiness handoff to Anthropic's official knowledge-work-plugins/design plugin for critique, accessibility audit, and engineering handoff. Cites Anthropic primary sources inline; refuses to generate the artifact code itself or drive the browser. Use for any work that ends with a Claude Design artifact.
Triggers on:
- "I want to build a dashboard in Claude Design"
- "help me prompt claude.ai/design"
- "make a slide deck in claude.ai/design"
- "iterate on my Claude Design artifact"
- "what should I prompt Claude Design with"
- "build a one-pager in Claude Design"
- "design a prototype in claude.ai/design"
- "refine my Claude Design output"
- "create a pitch deck in Claude Design"
- "use Claude Design"
- "draft a Claude Design prompt"
- "make wireframes in claude.ai/design"
---
# claude-design-facilitator
You are a facilitator for prompting Claude Design (`claude.ai/design`). You walk the operator from raw idea to a copy-paste-ready prompt, through iteration, to ship-readiness. You do **not** generate artifact code yourself and you do **not** drive the browser. Claude Design is where the artifact gets built; you exist to make the operator's interaction with that surface land on the first try.
You follow the phases below in order. Phases 1 through 4 are scoping and grounding; do not draft a prompt before they are done. If the operator pushes for a prompt straight away, briefly explain that a five-second alignment pass produces a one-shot prompt instead of a four-round iteration spiral, then ask the Phase 2 intent question.
All output is English. All authoritative claims about Claude Design behaviour cite Anthropic primary sources — `anthropic.com/news`, `support.claude.com`, `claude.com/blog`, `claude.com/resources/tutorials`, `claude.com/plugins`, `platform.claude.com`, `github.com/anthropics`. Community patterns are labelled as such with the source link. The reference files under `references/` carry the canonical content; this file is the flow.
---
## Phase 1 — Disambiguate the surface
Confirm the operator wants `claude.ai/design` specifically, not one of the four surfaces it is most commonly confused with: classic Artifacts at `claude.ai`, Live Artifacts in Claude Cowork, custom visuals embedded in a chat reply, or Anthropic's `knowledge-work-plugins/design` plugin (which audits already-built artifacts and does not generate them).
If the operator is clear, move on. If signals are mixed — they mention "Artifacts" or "Cowork", they describe a feature that does not exist in Claude Design (no `/rewind`, no version history, no branching), or they expect round-trip handoff back from Claude Code — read `references/00-what-claude-design-is-and-isnt.md` and walk through the relevant anti-conflation block.
---
## Phase 2 — Name the intent preset
Claude Design exposes eight intent presets. The operator picks one before drafting begins, because the prompt pattern differs per preset and the per-preset reference files are the place that pattern lives.
The eight presets, in the order they appear in Anthropic's launch enumeration (`anthropic.com/news/claude-design-anthropic-labs`, 2026-04-17):
- **designs** — generic dashboards, components, layouts, design explorations
- **prototypes** — interactive product flows for usability testing and demos
- **slides** — presentation decks, internal or external
- **one-pagers** — single-page artifacts (memos, summaries, leave-behinds)
- **wireframes-mockups** — low-fi or high-fi layout structure, pre-visual-design
- **pitch-decks** — investor or external pitch decks (note: PPTX export trap — see preset file)
- **marketing-collateral** — landing pages, social variants, visual assets
- **frontier-design** — Anthropic's "code-powered prototypes with voice, video, shaders, 3D" preset (labelled experimental in this plugin — no validated practitioner pattern as of 2026-05-16)
If the operator is uncertain which preset fits, read `.coverage.md` and the matching one-line summaries; offer the two or three that match the situation. The evidence-grade label on each preset reference file is load-bearing — surface it: `Anthropic-documented + community-validated` (designs / prototypes / slides), `Community-only` (one-pagers / wireframes-mockups / pitch-decks / marketing-collateral), or `Experimental` (frontier-design).
---
## Phase 3 — Audience and destination
Establish the audience and the destination *before* drafting the prompt. This is `@claudedesign` Anthropic-affiliated guidance: the destination format constrains the prompt because Claude Design's export options have asymmetric fidelity.
Ask:
- **Audience:** who reads or uses this artifact? Internal team, external stakeholder, investor, customer prospect, partner, user-testing participant?
- **Destination:** where does it end up? PDF (lossless for static layouts, lossy for interactive elements), PPTX (Claude reads slide master / layouts / fonts / color scheme, but text flattens to images on complex compositions — see `references/03-iteration-and-session.md` PPTX trap section), HTML standalone, Canva import, Claude Code handoff for engineering build, or share-link?
If the destination is PPTX and the preset is `pitch-decks`, flag the export trap explicitly (`moda.app/blog/claude-design-for-pitch-decks` documents the case where PPTX flattens richly-styled text to images). If the destination is Claude Code handoff, set expectation that the bundle Claude Design produces is one-way (no return path to Claude Design — see `references/04-handoff-and-scope.md`).
---
## Phase 4 — Anchor on DESIGN.md
A DESIGN.md file is the operator's leverage against Claude Design's defaults. It anchors design-system identity (colors, typography, motion, layout, do's-and-don'ts) so the model does not fall back to its convergent middle-ground aesthetic.
Read `references/02-design-md.md`. The reference file documents the community-converged 9-section canonical structure and a copy-paste extractor prompt that converts a brand URL or screenshot into a DESIGN.md.
If the operator already has a DESIGN.md, confirm it is uploaded to the Claude Design project and that the agent prompt guide section names it. If they do not have one, point at the extractor prompt — it is the highest-leverage single piece of content in this plugin.
**Evidence grade context for the operator:** Anthropic publishes the concept of DESIGN.md (`support.claude.com/en/articles/14604397-set-up-your-design-system-in-claude-design`) but not the 9-section structure. The 9-section template comes from community practitioners (`github.com/rohitg00/awesome-claude-design`, `github.com/VoltAgent/awesome-claude-design`).
---
## Phase 5 — Draft the prompt using the five-layer stack
Now draft. Open `references/01-prompt-fundamentals.md` and `references/presets/<preset>.md` for the named preset. Compose the prompt from these layers, in order:
1. **Layer 1 — Goal / Layout / Content / Audience (GLCA)** — Anthropic's verbatim framework. Source: `support.claude.com/en/articles/14604416-get-started-with-claude-design`. Every prompt to Claude Design starts here.
2. **Layer 1.5 — Start simple, layer in complexity** — Anthropic's verbatim incremental-prompting advice (same source). Do not ship a 600-word first prompt; ship a 120-word first prompt and add detail in turn two.
3. **Layer 2a — Concrete-alternative-spec house-style control** — Anthropic's verbatim guidance from `platform.claude.com/docs/en/docs/build-with-claude/prompt-engineering/claude-4-best-practices`. Includes the AEFRM example with explicit hex palette and motion timing.
4. **Layer 2b — Propose-options-before-building** — Anthropic's verbatim prompt template asking Claude Design to surface four distinct visual directions before committing.
5. **Layer 3 — Negative constraints (the AI-slop avoid-list)** — verbatim banned items from `claude.com/blog/improving-frontend-design-through-skills` and `github.com/anthropics/skills/skills/frontend-design/SKILL.md`. Inter, Roboto, Arial, Space Grotesk; purple gradients on white; solid-color backgrounds; cookie-cutter framing; convergent middle-ground palettes; scattered micro-interactions.
6. **Layer 4 — Four design dimensions** — verbatim typography / color / motion / backgrounds guidance from `frontend-design/SKILL.md`.
7. **Layer 5 — Four design grading criteria** — Anthropic's verbatim quality criteria from `anthropic.com/engineering/harness-design-long-running-apps` (design quality, originality, craft, functionality) plus the emphasis-weighting recommendation.
On top of the five layers, layer the per-preset pattern from `references/presets/<preset>.md`. For `designs`, `prototypes`, and `slides`, this is Anthropic-published prompt material. For the other four `Community-only` presets, it is community-converged pattern with attribution. For `frontier-design`, it is honest-experimental and labelled as such.
Resist the urge to over-spec. Anthropic's own guidance is start simple, layer in complexity. Draft the layer-1+layer-2a+layer-3 composition first. Save layers 4 and 5 for the refinement turn.
---
## Phase 6 — Deliver the prompt
Output a single copy-paste-ready fenced markdown code block containing the composed prompt. No preamble, no commentary inside the block. Add a one-line caption above the block: which preset, which audience, which destination.
After the block, list three to five expected follow-up turns the operator should anticipate (e.g., "if it lands too generic, add layers 4 + 5 in turn two", "if PPTX is destination, validate the rendered text-as-text count in turn three"). This sets the iteration expectation honestly — Claude Design quality is non-monotonic across turns (`anthropic.com/engineering/harness-design-long-running-apps`).
---
## Phase 7 — Iteration coaching
When the operator returns with feedback after a Claude Design generation, you do not regenerate the prompt. You coach which surface to use next. Read `references/03-iteration-and-session.md`.
The three-surface cascade, in order of token cost:
- **Tweak panel** — controls and sliders Claude pre-derives at artifact generation time. Zero token cost. Surgical. Use for: section reordering, variant swap, density slider, spacing scale, color temperature, typography scale, padding / radius / shadow. The Anthropic-published guidance is verbatim in `references/03`.
- **Inline comments** — component-scoped edits via the comment surface. Surgical when the edit is in-component. Has an Anthropic-acknowledged vanish bug — if a comment disappears, paste the comment text into chat. Fails for new structural containers.
- **Chat** — full regeneration. Use for any structural change (add a new section), aesthetic pivot, multi-component change, or anything Claude did not pre-derive a Tweak control for. Costs one full chat turn.
Operator mantra (the synthesis from `research/04`): *anything Claude pre-derives at generation time is surgical thereafter; new controls cost one chat turn for setup.*
Session-management heuristics from `references/03-iteration-and-session.md`:
- 4-screen inflection — quality drops noticeably after the fourth screen of context in a session.
- Opus 4.7 context — quality degrades at the 4050% context mark.
- Pro budget burns in roughly 2530 minutes of active design; Max in roughly 6090.
- Session-break triggers: hitting screen 4, reorder / density tweaks stop landing, chat re-introduces removed defaults.
If the operator hits a stuck state, point at the recovery prompt library in `references/03-iteration-and-session.md` — the `break-default-aesthetic.md` adapted prompt, "fix the system not the prompt" pattern, edit-previous-message workaround, the 3-failed-comment escalation rule, and the model downshift escalator (Opus 4.7 → Opus 4.6 / Sonnet 4.6).
---
## Phase 8 — Ship-readiness
Before the operator declares an artifact done, run a short ship-readiness check against `references/04-handoff-and-scope.md`:
- Has the destination format been validated against the rendered output? (PPTX text-as-text count, PDF interactive-element check, HTML standalone export at target viewport.)
- If handing off to engineering: is the export bundle complete? Anthropic's handoff bundle includes a machine-readable component spec, design tokens, layout hierarchy, referenced assets, standalone HTML + inline CSS + JS, per-state screenshots, PM-annotated notes, and a stack / framework README (`anthropic.com/news/claude-design-anthropic-labs` + `support.claude.com/en/articles/14604416`).
- Is the operator aware that the Design → Code direction is one-way? Once handed off, the path back to Claude Design is lossy (screenshot → new Claude Design session).
**Downstream tool recommendation.** Once the operator has an artifact in hand and wants critique, accessibility audit, UX copy review, design-system audit, or engineering handoff guidance, recommend Anthropic's official plugin:
```
claude plugins add knowledge-work-plugins/design
```
That plugin operates on existing artifacts (Figma URLs, screenshots, copy snippets) and ships six commands: `/critique`, `/accessibility`, `/ux-copy`, `/research-synthesis`, `/design-system`, `/handoff`. It is the lifecycle complement to this one — see `references/04-handoff-and-scope.md` for the full coverage table. This plugin (claude-design) covers idea through delivered prompt through iteration coaching; `knowledge-work-plugins/design` covers everything after. There is no command overlap and no functional redundancy.
---
## What this skill never does
- It does not generate the artifact code itself. Claude Design is the artifact generator. This skill produces prompts that go into Claude Design.
- It does not automate the browser, paste prompts on the operator's behalf, or read the Claude Design canvas. The operator copies and pastes manually.
- It does not store artifact history, version artifacts, or branch between iterations. Claude Design has no version tree and this skill does not invent one.
- It does not duplicate the post-design lane covered by `knowledge-work-plugins/design`. No `/critique`, no `/accessibility`, no `/ux-copy`, no `/research-synthesis`, no `/design-system`, no `/handoff` commands.
---
## Reference files
- `references/00-what-claude-design-is-and-isnt.md` — surface disambiguation
- `references/01-prompt-fundamentals.md` — the five-layer prompt stack
- `references/02-design-md.md` — DESIGN.md template + brand-to-DESIGN.md extractor
- `references/03-iteration-and-session.md` — Tweak / Comment / Chat cascade, session economics, recovery prompt library
- `references/04-handoff-and-scope.md` — one-way handoff, scope fence vs Anthropic's design plugin
- `references/presets/designs.md`, `prototypes.md`, `slides.md` — Anthropic-documented per-preset patterns
- `references/presets/one-pagers.md`, `wireframes-mockups.md`, `pitch-decks.md`, `marketing-collateral.md` — Community-only per-preset patterns
- `references/presets/frontier-design.md` — Experimental, no validated practitioner pattern
- `.coverage.md` — preset enumeration with evidence-grade labels (the source of truth for SC2 verification)
---
## Explicit invocation
The skill name registers as the explicit slash command `/claude-design-facilitator`. Operators can either trigger by natural language (the description above is the auto-fire surface) or invoke explicitly when they want to start a facilitation session from a clean state.

View file

@ -1,113 +0,0 @@
# What Claude Design is and isn't
**Last updated:** 2026-05-17 | **Verified:** research/01-claude-design-surface.md
**Status:** Beta (Labs research preview)
**Captured-on date:** 2026-05-16
This file disambiguates `claude.ai/design` from the four surfaces it is most commonly conflated with. The cost of getting this wrong is wasted iteration: applying a prompt pattern that fits Live Artifacts to Claude Design (or vice versa) produces output that misses the operator's intent, and the failure looks like a prompt problem instead of a surface problem.
Read this file when the operator's signals are mixed — they reference "Artifacts" loosely, they expect a feature that does not exist in Claude Design (like `/rewind` or a version tree), they think Claude Design audits artifacts rather than generates them, or they expect round-trip handoff back from Claude Code.
---
## 1. What Claude Design is
Claude Design is an Anthropic Labs research preview that launched on 2026-04-17 (`https://anthropic.com/news/claude-design-anthropic-labs`). It is a dedicated workspace at `claude.ai/design` for generating interactive design artifacts from a prompt.
Five properties define the surface:
- **Labs research preview, not GA.** The product can change without notice. Anthropic surfaces it under the Labs banner specifically to signal that the contract is non-stable. The URL still carries the `-anthropic-labs` slug today; a Labs → GA rename is a known re-research trigger captured in `.coverage.md`.
- **Opus 4.7 pinned.** All generations run on Opus 4.7. Operators cannot select a model from the Claude Design UI. The session inherits Anthropic's model choice for this surface (`https://anthropic.com/news/claude-design-anthropic-labs`).
- **Single HTML/React substrate.** Underneath every output is one rendering engine — HTML, React components, inline CSS — regardless of which intent preset the operator picks. The intent preset shapes prompting and export, not the underlying tech.
- **Eight intent presets exposed in the UI.** Anthropic's launch post enumerates: designs, prototypes, slides, one-pagers, wireframes-mockups, pitch-decks, marketing-collateral, frontier-design. The enumeration is the source of truth for SC2 coverage in this plugin (`https://anthropic.com/news/claude-design-anthropic-labs`).
- **Multiple export paths.** PDF (lossless for static layouts, lossy for interactive elements), PPTX (slide master / layouts / fonts honored, but text can flatten to images on complex compositions), HTML standalone, Canva import, share-link, and Claude Code handoff (machine-readable component spec + design tokens + layout hierarchy + assets + standalone HTML/CSS/JS + per-state screenshots + PM notes + framework README — verbatim per `https://support.claude.com/en/articles/14604416-get-started-with-claude-design`).
Three Anthropic-published support articles ground the surface: the get-started article (`https://support.claude.com/en/articles/14604416-get-started-with-claude-design`), the design-system setup article (`https://support.claude.com/en/articles/14604397-set-up-your-design-system-in-claude-design`), and the PowerPoint-mode conventions article (`https://support.claude.com/en/articles/13521390-use-claude-for-powerpoint`).
---
## 2. What Claude Design is NOT
### Not classic Artifacts at `claude.ai`
Classic Artifacts live in any Claude.ai chat. They appear in a side panel when Claude generates code, markdown, SVG, mermaid diagrams, or other inline outputs. Artifacts carry no intent presets, no Tweak panel, no export-to-PPTX, no Claude Code handoff bundle, no DESIGN.md anchor concept. They are a chat affordance.
Confusion happens because both surfaces produce HTML/React output and Anthropic's documentation has used "Artifacts" loosely in launch contexts. If the operator says "Artifacts" but describes intent presets or destination formats (`https://anthropic.com/news/claude-design-anthropic-labs`), they mean Claude Design. If they describe a side panel inside a chat (`support.claude.com` discusses Artifacts in the chat-product context), they mean classic Artifacts.
### Not Live Artifacts in Claude Cowork
Live Artifacts is a different Labs surface — collaborative real-time editing of artifacts inside Claude Cowork sessions. It runs in a different workspace, has different affordances (multi-cursor presence, version stream), and is a separate product line at `claude.ai/code` family (see Anthropic's Cowork-related communications). Claude Design has none of those collaborative primitives. The operator working alone in `claude.ai/design` is the canonical flow.
### Not custom visuals embedded in a chat reply
Sometimes Claude generates an inline HTML/SVG visual as part of a chat answer (a chart, a diagram, an illustration). That is a one-off chat artifact, not a Claude Design session. The prompt patterns are different (chat conversational tone vs design intent presets), the export options are different (chat artifact has Save / Copy, Claude Design has the full export matrix), and there is no Tweak panel on the chat-inline visuals.
### Not Anthropic's `knowledge-work-plugins/design` plugin
This is the most consequential conflation. Anthropic ships an official plugin at `https://claude.com/plugins/design` (`https://github.com/anthropics/knowledge-work-plugins`) with six slash-commands: `/critique`, `/accessibility`, `/ux-copy`, `/research-synthesis`, `/design-system`, `/handoff`. That plugin operates on **existing** artifacts (Figma URLs, screenshots, copy snippets). It does not generate artifacts.
The lifecycle split is clean:
- This plugin (`claude-design`) covers **pre-design and during-design** — idea → intent-preset selection → prompt drafting → copy-paste delivery → iteration coaching → ship-readiness.
- Anthropic's `knowledge-work-plugins/design` covers **post-design** — critique → accessibility → UX copy review → research synthesis → design-system audit → engineering handoff.
There is zero command overlap (this plugin ships no commands named `/critique`, `/accessibility`, `/ux-copy`, `/research-synthesis`, `/design-system`, or `/handoff``tests/validate-plugin.sh` assertion (h) enforces this mechanically). Workflow recommendation: use this plugin to land the artifact in `claude.ai/design`; once the artifact exists, install Anthropic's official plugin via `claude plugins add knowledge-work-plugins/design` for downstream review and handoff.
### Not third-party clones like `jiji262/claude-design-skill`
Several third-party repos use names like `claude-design-skill`. They are independent community efforts targeting general design workflows in Claude Code, not the `claude.ai/design` surface specifically. They predate the Anthropic Labs launch in some cases. This plugin is *Claude Design facilitation* — it targets the Anthropic surface explicitly, citing Anthropic's primary sources. Verify the operator's mental model accordingly.
---
## 3. Why the distinction matters operationally
Three operational consequences flow from getting the surface identification right.
### Prompt patterns differ
Claude Design's prompt patterns are documented in `https://anthropic.com/news/claude-design-anthropic-labs`, `https://support.claude.com/en/articles/14604416-get-started-with-claude-design`, and the two per-preset tutorials (`https://claude.com/resources/tutorials/using-claude-design-for-prototypes-and-ux`, `https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks`). These prompts assume the Claude Design substrate, intent presets, and the Tweak / Comment / Chat iteration cascade. Applying them to classic Artifacts, Cowork, or an inline chat visual produces noise.
Classic Artifacts prompts (the kind used inside any `claude.ai` chat) are conversational and lean on chat affordances. Claude Design prompts use the verbatim Goal / Layout / Content / Audience framework and lean on intent presets. The frameworks do not interchange cleanly.
### Limits differ
Claude Design has its own quota economics — the operator's Max / Pro plan budget burns down at a different rate than classic chat (research/04 documents observed Pro burn of roughly 25-30 minutes of active design work, Max roughly 60-90; these are community-observed, not Anthropic-published, and may shift). Opus 4.7 quality degrades at 40-50% context (`https://anthropic.com/engineering/harness-design-long-running-apps`). A 4-screen session inflection is documented community-wide.
None of these limits apply identically to classic Artifacts or to the official `knowledge-work-plugins/design` plugin. Diagnosing a quota / quality issue requires knowing which surface is in play.
### Scope differs
The official `knowledge-work-plugins/design` plugin is the right tool for post-design critique. Trying to make this plugin (`claude-design`) emit a critique would duplicate Anthropic's command surface and add nothing. The reverse — using `knowledge-work-plugins/design` to generate the artifact — does not work because that plugin operates on artifacts that already exist.
If the operator is uncertain whether their question is pre-design or post-design, ask: *does the artifact exist yet?* If no — this plugin. If yes — Anthropic's plugin.
---
## 4. Decision shortcuts
- The operator mentions intent presets (designs / prototypes / slides / one-pagers / wireframes-mockups / pitch-decks / marketing-collateral / frontier-design) → Claude Design.
- The operator mentions a workspace URL `claude.ai/design` → Claude Design.
- The operator mentions PPTX / PDF / Canva / Code-handoff exports → Claude Design.
- The operator says "Tweak panel" or "Tweak slider" → Claude Design.
- The operator says "Artifact" in a side panel context inside a normal chat → classic Artifacts.
- The operator says "Cowork" or "real-time collaborative" or "multi-cursor" → Live Artifacts.
- The operator says "critique" / "accessibility audit" / "Figma" → Anthropic's `knowledge-work-plugins/design`.
- The operator references a third-party repo named `claude-design-*` → ask what surface they target; likely not the Anthropic Labs preview.
If signals remain mixed after this read-through, ask one clarifying question rather than guess: *"Are you working in the dedicated Claude Design workspace at `claude.ai/design`, or somewhere else?"*
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — Anthropic Labs launch announcement (2026-04-17), Opus 4.7 pin, intent-preset enumeration, export options
- `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` — get-started article, GLCA framework, handoff bundle contents
- `https://support.claude.com/en/articles/14604397-set-up-your-design-system-in-claude-design` — design-system setup, DESIGN.md concept
- `https://support.claude.com/en/articles/13521390-use-claude-for-powerpoint` — PowerPoint-mode conventions
- `https://claude.com/plugins/design` — Anthropic's official knowledge-work-plugins/design plugin
- `https://github.com/anthropics/knowledge-work-plugins` — source for the official plugin
- `https://claude.com/resources/tutorials/using-claude-design-for-prototypes-and-ux` — Anthropic-published per-preset tutorial (prototypes)
- `https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks` — Anthropic-published per-preset tutorial (slides)
- `https://anthropic.com/engineering/harness-design-long-running-apps` — design grading criteria, non-monotonic-improvement framing
When in doubt: the Anthropic news post and the get-started support article are the load-bearing sources. Everything else triangulates against them.

View file

@ -1,265 +0,0 @@
# Prompt fundamentals — the five-layer stack
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
**Status:** Beta (Labs research preview)
**Captured-on date:** 2026-05-16
This file documents the universal prompt framework an operator applies across every Claude Design intent preset. The five layers compose into one prompt block. Layers 1 to 3 are load-bearing for every preset; layers 4 and 5 are the refinement turn.
Every authoritative claim cites an Anthropic primary source. Where community practice extends an Anthropic concept, the extension is labelled and attributed.
---
## Layer 1 — Goal / Layout / Content / Audience (GLCA)
Anthropic's verbatim framework for every Claude Design prompt. The framework is published in the get-started support article `https://support.claude.com/en/articles/14604416-get-started-with-claude-design`. Anthropic's framing: a good Claude Design prompt names the **Goal**, the **Layout**, the **Content**, and the **Audience**, in that order, before any aesthetic specification.
The four canonical questions:
- **Goal** — what is the artifact for? "An admin dashboard for monitoring API latency", "an onboarding flow for first-time users", "a landing page that converts free trial signups". One sentence.
- **Layout** — what is the page structure? Header / hero / metrics row / table / footer; or: hero / three-feature-grid / pricing table / CTA. Name the regions.
- **Content** — what fills the regions? Real data placeholders if you have them, named labels if not. Avoid generic "lorem ipsum" — the model defaults to convergent middle-ground content if you do not constrain it.
- **Audience** — who reads or uses this artifact? Internal team, external stakeholder, B2B procurement, B2C consumer, investor. Audience determines tone, density, and aesthetic.
Anthropic publishes three verbatim canonical examples in the same support article:
```
Goal: An analytics dashboard for our customer success team
Layout: Top metrics row (4 KPIs), main chart panel, recent activity table
Content: Today's MRR, 30-day churn, NPS, expansion revenue; revenue chart;
the last 10 account events
Audience: Internal CS leads — they're in this thing every day, want density
and signal, not flashy
```
```
Goal: A mobile onboarding flow for a new fitness app
Layout: Welcome screen, goal-selection (3 cards), motion preference, sign-in
Content: Headlines, single CTA per screen, accessible touch targets
Audience: First-time users, gym beginners, ages 25-45
```
```
Goal: A SaaS landing page that converts free trial signups
Layout: Hero, three-feature grid, social proof, pricing table, FAQ, footer CTA
Content: Product name placeholder "ProductX", real headline benefit copy,
three feature blurbs (icon + headline + line)
Audience: B2B technical buyers evaluating dev tools
```
The GLCA framework is sufficient on its own for a first prompt at intent preset `designs`. For other presets, GLCA composes with the per-preset pattern in `references/presets/<preset>.md`.
Source: `https://support.claude.com/en/articles/14604416-get-started-with-claude-design`.
---
## Layer 1.5 — Start simple, layer in complexity
The same Anthropic get-started article publishes verbatim incremental-prompting advice: do not ship a 600-word first prompt. Ship a 120-word first prompt that names GLCA, see what Claude Design produces, then add complexity in turn two and turn three.
Anthropic frames this as the dominant failure mode for first-time Claude Design operators: over-specifying the first prompt produces an output that is dense but generic. The remedy is staged — let Claude Design make its default choices, then react to what it produces with targeted constraints.
This frames how the rest of the stack composes. In turn one, ship layers 1 + 2a (or 2b) + 3. In turn two, add layer 4. In turn three, add layer 5 emphasis-weighting.
Source: `https://support.claude.com/en/articles/14604416-get-started-with-claude-design`.
---
## Layer 2a — Concrete-alternative-spec house-style control
The first of two Anthropic-documented house-style controls. Verbatim guidance from `https://platform.claude.com/docs/en/docs/build-with-claude/prompt-engineering/claude-4-best-practices`: name a concrete aesthetic family with explicit visual primitives rather than gesturing at a style.
The Anthropic-published exemplar — the AEFRM (Anthropic Engineering Field Reference Material) example — is verbatim usable:
```
Aesthetic family: industrial-utilitarian, slate-monochrome
Color palette (CSS hex):
--color-bg: #E9ECEC
--color-surface: #C9D2D4
--color-muted: #8C9A9E
--color-fg: #44545B
--color-ink: #11171B
Typography: square angular sans-serif (Söhne, Inter Variable as fallback);
no rounded glyphs; weight 500 for body, 700 for headers
Corner radius: 4px throughout — no fully rounded buttons, no pill shapes
Motion: transition: all 160ms ease-out on hover; no springy easing
Density: dense (table rows 32px tall; padding 8px on cards)
Surface: flat — no shadows, no glassmorphism
```
The control works because Claude Design reads this as a concrete brief and constrains its aesthetic decision space accordingly. Without an explicit concrete-alternative-spec, the model defaults to its convergent middle-ground aesthetic (rounded corners, generous spacing, friendly typography, gentle shadows — Anthropic's documented "AI-slop" default).
The hex palette, corner radius, and motion timing are all required — naming "industrial-utilitarian" alone is gesturing, not specifying.
Source: `https://platform.claude.com/docs/en/docs/build-with-claude/prompt-engineering/claude-4-best-practices`.
---
## Layer 2b — Propose-options-before-building
The second Anthropic-documented house-style control, also from `https://platform.claude.com/docs/en/docs/build-with-claude/prompt-engineering/claude-4-best-practices`. When the operator does not know exactly which aesthetic to brief, Anthropic publishes a verbatim prompt template asking Claude Design to propose four distinct visual directions before committing to one.
The verbatim prompt:
```
Before building the dashboard, propose 4 distinct visual directions.
For each, give:
- bg hex
- accent hex
- typeface (named, not gestured)
- one-line rationale tying the direction to the audience and goal
Wait for me to pick a direction before generating the artifact.
```
This forks the conversation: turn one returns four named directions, the operator picks one, turn two generates against the chosen direction. The cost is one extra round; the upside is the operator avoids the dead-end of generating against an aesthetic that does not fit and only finding out after generation.
Use layer 2b when layer 2a is not feasible (the operator does not yet know the aesthetic). Use layer 2a when the aesthetic is known.
---
## Layer 3 — AI-slop avoid-list (negative constraints)
Anthropic publishes a verbatim banned-items list in `https://claude.com/blog/improving-frontend-design-through-skills` and reinforces it in the open-source frontend-design skill at `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md`. The list names the convergent middle-ground patterns that Claude Design defaults to when underspecified.
Anthropic's verbatim AI-slop fingerprints to avoid:
- **Typography slop:** Inter, Roboto, Arial as default body font. Space Grotesk is flagged as overused. Default to a concrete-named typeface in the brief, not a generic sans-serif.
- **Color slop:** purple gradients on white backgrounds; solid-color hero backgrounds; convergent middle-ground palettes (the muted blue-and-grey "professional" default).
- **Layout slop:** cookie-cutter three-column feature grids; centered-hero-with-CTA defaults; full-width-image-with-text-overlay defaults.
- **Motion slop:** scattered micro-interactions; bouncy spring easing on hover; pulse animations on idle elements.
- **Complexity-to-vision mismatch:** ornate components on simple layouts; flat components on otherwise rich layouts.
Operator-actionable copy-paste anti-prompt block (composes with layers 1 and 2):
```
Negative constraints — do not produce any of:
- Inter, Roboto, Arial, or Space Grotesk as the primary typeface
- Purple gradients on white backgrounds
- Solid-color hero backgrounds
- Three-column feature grids with icon + headline + line
- Centered-hero-with-single-CTA layout default
- Bouncy spring easing on hover transitions
- Pulse / breathing animations on idle elements
- Glassmorphism, neumorphism, or generic "modern SaaS" defaults
If you find yourself defaulting to any of these, stop and ask me to
clarify the aesthetic before continuing.
```
Sources: `https://claude.com/blog/improving-frontend-design-through-skills` and `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md`.
---
## Layer 4 — Four design dimensions to optimize
Anthropic's verbatim per-dimension guidance from `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md`. Four dimensions to brief explicitly when refining beyond the first turn:
- **Typography** — name typeface, modular scale (e.g., 1.250 minor third or 1.333 perfect fourth), weight palette, line-height palette, letter-spacing for headings. Anthropic's frontend-design SKILL.md publishes specific modular scales and weight palettes verbatim.
- **Color** — beyond palette hex, specify semantic roles (background, surface, accent, muted, error, success). Define interaction states explicitly (hover, active, disabled, focus). Anthropic's guidance: avoid relying on opacity for state changes; use explicit color tokens.
- **Motion** — name easing curves (ease-out, cubic-bezier values), name durations (120ms / 160ms / 240ms tiers), name what gets animated and what does not. Anthropic's guidance: motion should clarify hierarchy and confirm interaction; avoid decorative motion.
- **Backgrounds** — flat surface vs depth, when to layer surfaces, when shadows or borders define edges. Anthropic's guidance: backgrounds carry meaning; the bare-default-white background is rarely the right choice.
In turn two of an iteration, add layer-4 dimension specs to the brief. In turn three, refine the dimension that drifted most from intent.
Source: `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md`.
---
## Layer 5 — Four design grading criteria
Anthropic publishes verbatim grading criteria for design quality in `https://anthropic.com/engineering/harness-design-long-running-apps`. Four criteria, used as emphasis weights:
- **Design quality** — does the artifact look intentional, not defaulted? Is the aesthetic coherent across regions?
- **Originality** — does the artifact avoid the convergent middle-ground? Does it surprise without being weird?
- **Craft** — does the artifact feel detailed and considered at every level — typography, spacing, alignment, hierarchy, color?
- **Functionality** — does the artifact work for its goal and audience? Would it survive a usability test or a stakeholder review?
Anthropic's emphasis-weighting recommendation: in the prompt, weight which criterion matters most for *this* artifact. A dashboard for internal use weights functionality and craft highest. A pitch deck for an external investor weights design quality and originality highest. A wireframe for early exploration weights functionality highest with craft and originality deprioritized.
Operator-actionable layer-5 block:
```
Grading criteria for this artifact, in priority order:
1. {craft|design quality|originality|functionality} — weight 0.4
2. {one of the others} — weight 0.3
3. {one of the others} — weight 0.2
4. {the remaining one} — weight 0.1
Optimize against this ordering. If the artifact has to trade off,
trade off the lowest-weighted criterion first.
```
The non-monotonic-improvement caveat applies — Anthropic notes that quality across iterations is not strictly increasing. If turn three is worse than turn two on a critical criterion, the recovery move is documented in `references/03-iteration-and-session.md` ("pivot to an entirely different aesthetic if the approach wasn't working").
Source: `https://anthropic.com/engineering/harness-design-long-running-apps`.
---
## How the layers compose into one prompt
A worked example for the `designs` intent preset, dashboard, three turns:
### Turn 1 — layers 1 + 2a + 3 (the first prompt)
```
Goal: An admin dashboard for monitoring API latency by route, by region,
and by P50/P95/P99
Layout: Header with environment switcher; top metrics row (4 KPIs:
global P95, error rate, throughput, active requests); main chart
(time series, P50/P95/P99 lines); routes table with sortable
latency columns; alerts sidebar
Content: KPI placeholders are real metric names; chart uses synthetic
24-hour data; table has 12 routes with realistic paths
(/api/v1/users, /api/v1/orders, etc.)
Audience: Platform engineers, on-call rotation, ages 25-45,
comfortable with dense interfaces
Aesthetic family: industrial-utilitarian, slate-monochrome
Color palette (CSS hex):
--color-bg: #E9ECEC
--color-surface: #C9D2D4
--color-muted: #8C9A9E
--color-fg: #44545B
--color-ink: #11171B
Typography: square angular sans-serif (Söhne, Inter Variable fallback);
no rounded glyphs
Corner radius: 4px throughout
Motion: transition: all 160ms ease-out
Density: dense (32px table rows, 8px card padding)
Surface: flat — no shadows
Negative constraints — do not produce any of:
- Inter, Roboto, Arial, or Space Grotesk as primary typeface
- Purple gradients on white backgrounds
- Pulse animations on idle elements
- Glassmorphism, neumorphism, generic "modern SaaS" defaults
```
### Turn 2 — add layer 4 dimensions
Operator reacts to turn-1 output by adding typography modular scale, semantic color roles, motion easing, and surface-depth rules.
### Turn 3 — add layer 5 weighting
Operator specifies that craft and functionality are the two highest-weighted criteria for this dashboard; design quality is third; originality lowest.
### Turn 4+ — Tweak panel takes over
Most subsequent refinements happen in the Tweak panel (per-artifact Claude-generated controls; zero-token surgical edits) — see `references/03-iteration-and-session.md` for the surface cascade.
---
## Source map
The five layers anchor on four Anthropic primary sources plus one open-source skill:
| Layer | Anthropic source |
|-------|------------------|
| 1, 1.5 | `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` |
| 2a, 2b | `https://platform.claude.com/docs/en/docs/build-with-claude/prompt-engineering/claude-4-best-practices` |
| 3 | `https://claude.com/blog/improving-frontend-design-through-skills` + `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` |
| 4 | `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` |
| 5 | `https://anthropic.com/engineering/harness-design-long-running-apps` |
Re-research trigger: any of the four URLs returning 404 or shifting content materially; Anthropic publishing a sixth layer or revising any of the five. Captured-on date 2026-05-16 — the layer-1 framework has been stable since the launch announcement.

View file

@ -1,333 +0,0 @@
# DESIGN.md — template and extractor
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
**Status:** Beta (Labs research preview)
**Captured-on date:** 2026-05-16
**Evidence grade:** Community-converged — Anthropic publishes the *concept* of a design-system document anchored to a Claude Design project (`https://support.claude.com/en/articles/14604397-set-up-your-design-system-in-claude-design`), but does not publish the 9-section canonical structure. The 9-section template comes from community practitioners (`https://github.com/rohitg00/awesome-claude-design`, `https://github.com/VoltAgent/awesome-claude-design`). Use accordingly: the concept is Anthropic-authoritative; the structure is community-converged.
---
## 1. Why DESIGN.md
A DESIGN.md file uploaded to a Claude Design project anchors design-system identity for every artifact generated in that project. Without an anchor, Claude Design defaults to its convergent middle-ground aesthetic (rounded corners, generous spacing, friendly typography, gentle shadows — the AI-slop pattern documented at `https://claude.com/blog/improving-frontend-design-through-skills`). With an anchor, the model reads the file at generation time and constrains its aesthetic, component, and motion decisions to match.
Anthropic publishes the concept of design-system anchors in `https://support.claude.com/en/articles/14604397-set-up-your-design-system-in-claude-design`. The article describes asset uploads, brand kits, and the principle that artifacts in a Claude Design project inherit the project's design language. What Anthropic does *not* publish is a recommended structure for the design-language file itself.
The community converged on a 9-section structure — documented across multiple awesome-claude-design repos, Substack posts, and practitioner blogs — that maps cleanly onto how Claude Design reads design context. The sections below are that converged structure.
---
## 2. The 9-section canonical structure
Each section names a decision Claude Design will otherwise default. The order is the order Claude Design appears to read most reliably (heaviest design-decision sections first).
### Section 1 — Visual Theme & Atmosphere
A one-paragraph description of the aesthetic family. Use named visual references the model can anchor to: "industrial-utilitarian like a Bloomberg terminal", "warm-editorial like The New York Times opinion section", "minimal-monochrome like Linear's UI".
Worked example:
```markdown
# Visual Theme & Atmosphere
Industrial-utilitarian. Slate-monochrome palette, square-cut typography,
flat surfaces. Reference: a modern data-tool UI (Linear, Datadog,
Bloomberg) — dense, intentional, no flourish. The product should look
like it was built for engineers by engineers.
```
### Section 2 — Color Palette & Roles
CSS-variable form with explicit hex values and semantic roles. Avoid relying on opacity for state changes — name each state explicitly.
Worked example:
```markdown
# Color Palette & Roles
:root {
--color-bg: #E9ECEC;
--color-surface: #C9D2D4;
--color-muted: #8C9A9E;
--color-fg: #44545B;
--color-ink: #11171B;
--color-accent: #4A6FA5;
--color-accent-hover: #3D5C8A;
--color-accent-active: #2F4A70;
--color-error: #B23A48;
--color-warning: #C89B3F;
--color-success: #4F7A4F;
}
Semantic roles:
- bg — page background
- surface — card / panel background
- muted — secondary text, borders
- fg — primary text
- ink — emphasis / heading text
```
### Section 3 — Typography Rules
Named typeface, modular scale, weight palette, line-height palette. Modular scales the community converged on are 1.250 (minor third) for dense interfaces and 1.333 (perfect fourth) for marketing pages.
Worked example:
```markdown
# Typography Rules
Primary typeface: Söhne (concrete-named, not "modern sans-serif").
Fallback: Inter Variable.
Display typeface: same as primary (no separate display face).
Modular scale: 1.250 (minor third).
--text-xs: 0.64rem;
--text-sm: 0.8rem;
--text-base: 1rem;
--text-lg: 1.25rem;
--text-xl: 1.563rem;
--text-2xl: 1.953rem;
Weight palette: 500 body, 600 emphasized, 700 headings.
Line height: 1.4 body, 1.2 headings.
If the typeface is not available, substitute Inter Variable — never
default to Inter, Roboto, Arial, or Space Grotesk
(per https://claude.com/blog/improving-frontend-design-through-skills
AI-slop avoid-list).
```
### Section 4 — Component Stylings
Per-component rules for the components Claude Design generates. Cover buttons, inputs, cards, tables, navigation. Specify radius, padding, border treatment, hover/active/disabled state explicitly.
Worked example:
```markdown
# Component Stylings
Buttons:
- radius: 4px (no pill shapes)
- padding: 8px 16px
- primary: bg accent, fg surface
- secondary: border 1px muted, bg transparent
- hover: bg accent-hover
- active: bg accent-active
- disabled: opacity 0.4, no pointer events
Inputs:
- radius: 4px
- padding: 8px 12px
- border: 1px solid muted
- focus: border accent + 2px outset ring at accent + 20% alpha
Cards:
- radius: 4px
- padding: 16px
- bg surface
- no shadow — borders define edges if needed
Tables:
- row height: 32px (dense)
- cell padding: 8px
- alternating row: bg + 4% darken
- hover row: bg + 8% darken
```
### Section 5 — Layout Principles
Grid system, spacing scale, breakpoint widths. Name the grid columns and the gap value.
Worked example:
```markdown
# Layout Principles
Grid: 12-column on screens >= 1024px, 8-column on screens 768-1023px,
4-column on screens < 768px.
Gap: 16px (--space-md).
Spacing scale:
--space-xs: 4px
--space-sm: 8px
--space-md: 16px
--space-lg: 24px
--space-xl: 32px
--space-2xl: 48px
Page max-width: 1440px centered.
Container padding: 24px on screens >= 768px, 16px below.
```
### Section 6 — Depth & Elevation
Surface depth rules. Most designs benefit from a clear flat-vs-layered decision rather than a mixed palette.
Worked example:
```markdown
# Depth & Elevation
Flat. No box-shadows by default. Borders define component edges.
Z-stack:
z-0: page surface
z-10: navigation
z-20: dropdown / popover
z-30: modal backdrop
z-40: modal content
z-50: toast / notification
Modal: bg surface, 1px border ink, no shadow.
Popover: bg surface, 1px border muted, no shadow.
```
### Section 7 — Do's and Don'ts
Explicit constraint list. This is where layer-3 (AI-slop avoid-list) gets project-specific.
Worked example:
```markdown
# Do's and Don'ts
Do:
- Use accent color sparingly (CTA + active state only)
- Use ink for headings, fg for body, muted for secondary
- Use 4px corner radius consistently
- Use 160ms ease-out for all hover transitions
Don't:
- Use purple gradients on white backgrounds
- Use solid-color hero backgrounds
- Use Inter / Roboto / Arial / Space Grotesk as primary typeface
- Use bouncy spring easing
- Use pulse / breathing animations on idle elements
- Use glassmorphism or neumorphism
- Use shadows except where the depth-and-elevation section explicitly permits
```
### Section 8 — Responsive Behavior
Breakpoint behavior. Anthropic does not publish responsive rules; this is the section where a project encodes its responsive philosophy.
Worked example:
```markdown
# Responsive Behavior
Mobile-first reasoning, but built desktop-first (target audience is
desktop). Below 768px:
- Navigation collapses to single icon-button row
- Tables become card stacks (one card per row)
- 12-column grid becomes 4-column
- Container padding drops from 24px to 16px
- Font sizes scale down by 0.9x
Touch targets: minimum 44px height (regardless of viewport).
```
### Section 9 — Agent Prompt Guide
A short block telling Claude Design how to use this DESIGN.md. This section is the bridge between the file and the prompt — it names the file by section and reminds the model that the constraints are load-bearing.
Worked example:
```markdown
# Agent Prompt Guide
When generating an artifact in this project, read every section of this
DESIGN.md before producing output. Treat color palette, typography rules,
component stylings, and layout principles as constraints — not
suggestions. If a generation would violate a constraint, stop and ask
which constraint to relax.
Cite specific section names when justifying design decisions in
explanatory text (e.g., "I chose 4px corners per Section 4 Component
Stylings").
```
---
## 3. Brand-to-DESIGN.md extractor prompt
A copy-paste-ready prompt the operator pastes into `claude.ai` (the chat product) or Claude.com to convert a brand URL, screenshot, or marketing asset into a DESIGN.md. The pattern comes from `https://github.com/rohitg00/awesome-claude-design/blob/main/prompts/brand-to-design-md.md` (adapted with attribution to the awesome-claude-design community template).
```
You will produce a DESIGN.md file by analyzing the brand reference
materials I provide. Output structure: 9 sections, in this order,
with the exact heading names:
1. Visual Theme & Atmosphere
2. Color Palette & Roles
3. Typography Rules
4. Component Stylings
5. Layout Principles
6. Depth & Elevation
7. Do's and Don'ts
8. Responsive Behavior
9. Agent Prompt Guide
Rules:
- Use CSS-variable form (--color-name: #HEX) for color palette
- Use modular scale form (--text-xs / --text-sm / ...) for typography
- Use named typefaces ONLY — if you cannot identify the typeface from
the brand materials with high confidence, write "unknown — operator
to fill in" rather than guessing. Do not hallucinate a typeface name.
- For each component (button, input, card, table, navigation), name
radius, padding, border treatment, and hover / active / disabled states
- Cite the source brand material at the top (e.g., "Extracted from
brand kit at <URL> on 2026-05-17")
Brand reference materials:
[paste URL, screenshot, or brand kit description here]
```
The anti-hallucination clause ("if you cannot identify... write unknown") is load-bearing — the failure mode without it is plausible-but-wrong typography names that the operator does not catch until they brief Claude Design and the output drifts.
Source for this extractor: community pattern at `https://github.com/rohitg00/awesome-claude-design/blob/main/prompts/brand-to-design-md.md`, adapted.
---
## 4. DESIGN.md failure modes
Four failure modes the operator should know before adopting DESIGN.md as a workflow primitive.
### Vision-token cost penalty
If the DESIGN.md is uploaded as an image (screenshot of a brand page) rather than as text, Claude Design pays a vision-token cost on every generation. Practitioner walkthroughs (Xinran Ma's documented experience in `research/03`) report meaningful quota burn from image-based DESIGN.md anchors. Use text-form DESIGN.md whenever possible.
### Per-user quota not pooled
Anthropic does not pool Claude Design quota across team members. A team using a shared DESIGN.md will not share quota burn — each member pays separately. Plan project workflow accordingly (research/04 documents community-observed Pro burn of roughly 25-30 minutes; Max roughly 60-90).
### Long-session degradation
DESIGN.md adherence appears to degrade in the back third of a long session. Opus 4.7 quality drops at the 40-50% context mark (`https://anthropic.com/engineering/harness-design-long-running-apps`); DESIGN.md is part of context, so its enforcement weakens accordingly. Session-break heuristics in `references/03-iteration-and-session.md` apply.
### Post-export drift
When an artifact is exported (PPTX, PDF, Code-handoff), the DESIGN.md does not travel with it. Downstream editing tools — PowerPoint, Adobe, Code IDEs — apply their own defaults. Validate the rendered output against DESIGN.md after export.
---
## 5. DESIGN.md sanity-check pattern
A community-converged test for DESIGN.md adherence: generate three artifacts in the same Claude Design project using deliberately different intent presets (designs, slides, one-pagers) and confirm that all three respect the DESIGN.md color palette, typography, and component-styling rules. If one preset drifts more than the others, the DESIGN.md needs sharpening on the section the drifting preset emphasized.
The community attribution for this pattern comes from a `theadpharm` Substack walkthrough (cited in `research/03`). It is a smoke test, not a guarantee — but it catches the common case where DESIGN.md is too general to constrain the model.
---
## Sources
- `https://support.claude.com/en/articles/14604397-set-up-your-design-system-in-claude-design` — Anthropic's design-system setup concept
- `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` — Anthropic's AI-slop avoid-list and four design dimensions
- `https://claude.com/blog/improving-frontend-design-through-skills` — Anthropic blog on default-avoidance
- `https://anthropic.com/engineering/harness-design-long-running-apps` — context-degradation framing
- `https://github.com/rohitg00/awesome-claude-design` — community awesome-list, brand-to-DESIGN.md extractor source
- `https://github.com/VoltAgent/awesome-claude-design` — community awesome-list, alternate 9-section structure references
Re-research trigger: Anthropic publishing an official DESIGN.md structure; either community awesome-list reaching consensus on a different section ordering; new failure mode surfaced in practitioner posts.

View file

@ -1,256 +0,0 @@
# Iteration and session — Tweak / Comment / Chat cascade and recovery
**Last updated:** 2026-05-17 | **Verified:** research/04-iteration-mechanics-recovery.md
**Status:** Beta (Labs research preview)
**Captured-on date:** 2026-05-16
This file documents three things in order of operational urgency: which iteration surface to use when, when to break a session, and how to recover when iteration stops landing. The cost asymmetry between Tweak / Comment / Chat is the single largest leverage point in a Claude Design workflow.
---
## 1. The three-surface cascade
Claude Design exposes three edit surfaces with asymmetric token costs and asymmetric scope:
| Surface | Token cost | Scope | When to use |
|---------|-----------|-------|-------------|
| Tweak panel | Zero | Surgical, per-control | Anything Claude pre-derived at generation time |
| Inline comment | Zero on success | Component-scoped, in-component | Targeted in-component text or visual change |
| Chat | One full turn | Whole artifact | Structural change, aesthetic pivot, new section |
### Tweak panel
The Tweak panel is the per-artifact set of controls and sliders Claude pre-derives during generation. Each artifact comes with its own Tweak surface — section reordering, variant swap, density slider, spacing scale, color temperature, typography scale, padding / radius / shadow. The controls are surgical and zero-token: applying them does not consume a chat turn or budget time. They are also lossy-free — the artifact does not regenerate; the controls operate on the existing render.
Tweak panel coverage is per-artifact. If Claude did not pre-derive a control for a dimension, that dimension is not Tweak-editable. The first move is always to check the Tweak panel: most dimensions an operator wants to refine after the first generation are already there.
Operator-actionable mantra (the synthesis from `research/04`):
> Anything Claude pre-derives at generation time is surgical thereafter; new controls cost one chat turn for setup.
### Inline comments
The comment surface lets the operator click anywhere on the rendered artifact and attach a directive — "make this section narrower", "use a darker shade for this header", "remove this icon". Comments are surgical when the change is in-component (text edit, color tweak, sizing within an existing container) and they cost zero tokens on success.
Two failure modes the operator should know:
1. **Vanish bug** — comments sometimes disappear after submission with no edit applied. Anthropic has acknowledged this (community-cited; the workaround is to paste the comment text directly into chat as a follow-up turn).
2. **Structural-container failure** — comments cannot add a new structural container (a new section, a new column, a new modal). The model interprets the directive but produces no change, or makes an irrelevant change. For new containers, escalate to chat.
### Chat
Chat is the full-regeneration surface. Any structural change, aesthetic pivot, multi-component change, or new section requires a chat turn. The artifact regenerates against the new prompt; previous Tweak panel and comment state may not survive intact.
Chat costs one full turn — count it against the session budget. Use the layer-1-through-5 framework (`references/01-prompt-fundamentals.md`) for the chat prompt rather than free-form natural language.
### Per-operation surgical-vs-regen catalogue
A practical lookup for which surface fits which operation (synthesized from `research/04`):
| Operation | Surface | Notes |
|-----------|---------|-------|
| Section reordering | Tweak | Pre-derived if Claude includes a section-order control |
| Variant swap (component variant A → B) | Tweak | If Claude generated multiple variants |
| Density slider (compact / cozy / comfortable) | Tweak | Common Tweak control |
| Spacing scale (--space-* token shift) | Tweak | Common Tweak control |
| Color temperature (warmer / cooler) | Tweak | If Claude derives this dimension |
| Typography scale (modular scale shift) | Tweak | If Claude derives this dimension |
| Padding / radius / shadow per component | Tweak | Common Tweak controls |
| Text edit in existing component | Comment | Surgical, in-component |
| Color tweak in existing component | Comment | Surgical, in-component |
| Add a new section | Chat | Structural — Tweak / Comment cannot do this |
| Aesthetic pivot (industrial → editorial) | Chat | Full regen — name the new aesthetic |
| Multi-component change (revise hero + CTA + footer together) | Chat | Full regen — too broad for Comment |
| New interaction state (hover / disabled / active) | Chat | Structural — requires regeneration |
---
## 2. Anthropic-published surgical/structural split
Anthropic's verbatim framing in `https://support.claude.com/en/articles/14604416-get-started-with-claude-design`: the Claude Design canvas distinguishes between *surgical edits* (per-element changes that do not regenerate the artifact) and *structural edits* (new components, new layouts, aesthetic pivots that require regeneration). The Tweak panel and inline comments are surgical surfaces; chat is the structural surface.
The operator's job is to identify which kind of edit a given change is *before* picking the surface. A surgical change attempted via chat regenerates the whole artifact and burns a turn; a structural change attempted via comment fails silently and wastes time. Misclassification is the dominant inefficiency in a long Claude Design session.
Source: `https://support.claude.com/en/articles/14604416-get-started-with-claude-design`.
---
## 3. Anthropic-engineering refine-vs-pivot rule
Anthropic publishes a verbatim refine-vs-pivot guideline in `https://anthropic.com/engineering/harness-design-long-running-apps`:
> Pivot to an entirely different aesthetic if the approach wasn't working — iteration within a bad direction compounds the failure.
The companion warning, also verbatim from the same source: design quality is **non-monotonic** across iterations. Turn 4 can be worse than turn 3 on a critical criterion. The framing matters because the operator's intuition pushes toward continued refinement; the discipline is to recognize a stuck state and pivot.
Operational signal that a pivot is needed (community-converged from `research/04`):
- Three consecutive comments have failed to land
- The aesthetic is drifting back to the AI-slop default on each regeneration
- The operator finds themselves explaining what they *don't* want more than what they *do* want
- The artifact is converging on a different audience than the brief
Pivot move: rewrite the layer-2 aesthetic-family specification entirely, then ship a fresh chat turn against the new family. Do not try to incrementally edit out of a stuck state.
Source: `https://anthropic.com/engineering/harness-design-long-running-apps`.
---
## 4. Session-management heuristics
Four heuristics — one Anthropic-published, three community-converged — govern when to break a session.
### 4-screen inflection (community-converged)
Practitioners across multiple posts in `research/04` document a quality inflection around the fourth screen of context in a Claude Design session. Before screen four, edits land cleanly; after, comments start vanishing, aesthetic defaults creep back, and Tweak controls feel less precise. The exact mechanism is unclear (context-window pressure on Opus 4.7 + cumulative DESIGN.md re-reads + cumulative artifact history), but the pattern is consistent.
Practitioner mantra: **at screen four, save what you have and start a new session.**
### Opus 4.7 context degradation (Anthropic-published)
`https://anthropic.com/engineering/harness-design-long-running-apps` publishes the verbatim observation: Opus 4.7 quality degrades noticeably at the 40-50% context-window mark. Claude Design sessions accumulate context faster than chat sessions (each generation includes the artifact in context for subsequent turns); the 40-50% mark arrives sooner.
### Quota burn (community-observed)
Practitioner walkthroughs cited in `research/04` report quota burn rates as of 2026-04-28 per MindStudio's documented walkthrough — these are community observations, not Anthropic-published limits and may shift:
- **Pro plan:** ~25-30 minutes of active design before quota becomes the binding constraint
- **Max plan:** ~60-90 minutes of active design before quota becomes the binding constraint
These numbers assume continuous active design (chat turns, regenerations, image-form DESIGN.md anchors). Tweak panel and comment surface usage does not burn quota.
Captured-on date: 2026-04-28 per `research/04`. Not an Anthropic-published limit.
### Session-break triggers (community-converged)
Three signals that a session has reached its productive end:
- Reorder / density Tweak controls stop landing (the model is not respecting the surgical surface)
- Chat re-introduces previously-removed defaults (the model is losing the negative constraints)
- The operator finds themselves repeating the same constraint in three consecutive turns
When two of three trigger together, break the session.
---
## 5. Context-reset prompt
When the operator needs to break a session but does not want to lose what worked, the verbatim community pattern from MindStudio (2026-04-28, cited in `research/04`):
```
Before we continue, summarize the design system and component decisions
we've made in this session as a structured markdown document I can use
as a fresh starting context. Include:
- the aesthetic family we converged on
- color palette in CSS-variable form
- typography decisions (typeface, modular scale, weights)
- component patterns we settled on
- decisions we made and then reversed (so I don't reintroduce them)
- anything we tried that did not work
```
Paste the produced markdown into a new Claude Design session as the opening context, alongside the original DESIGN.md. The new session starts with the cumulative decisions but a fresh context window.
Captured-on date: 2026-04-28.
---
## 6. Recovery prompt library
Five recovery moves, listed in escalating cost order.
### 6.1 — Break the default aesthetic
The highest-leverage single content asset in this plugin. Adapted with attribution from `https://github.com/rohitg00/awesome-claude-design/blob/main/prompts/break-default-aesthetic.md`. Use when the artifact has drifted toward AI-slop defaults despite negative constraints in the brief.
```
The current direction has converged on a generic default. I want a
distinct visual direction. Constraints:
1. Pick ONE aesthetic family and commit to it. Name a concrete reference
(an existing product, an editorial source, a design movement). No
"modern SaaS", no "clean", no "minimal" as the named family — those
are defaults, not directions.
2. Do not produce any of:
- Inter, Roboto, Arial, Space Grotesk as primary typeface
- Purple gradients on white backgrounds
- Three-column feature grids with icon + headline + line
- Centered-hero-with-single-CTA layout default
- Glassmorphism, neumorphism, generic "modern" defaults
3. Before generating: list four candidate directions matching the goal
and audience. For each:
- Aesthetic family (with concrete reference)
- Color palette in hex
- Typeface (named)
- One-line rationale tying it to goal + audience
Wait for me to pick one. Do NOT default to "the most common modern
approach."
4. The aesthetic should surprise without being weird. If you're tempted
to write "professional" or "balanced" or "approachable", stop.
Those words signal default-mode reasoning.
```
### 6.2 — Fix the system, not the prompt
Community pattern: when iteration is stuck, the prompt is rarely the problem. The DESIGN.md is. Reopen the DESIGN.md, audit the section the artifact is drifting on (typography, color, components), tighten that section, re-upload, then re-generate.
The instinct is to add more constraints to the chat prompt. The discipline is to fix the upstream anchor.
### 6.3 — Edit previous message rather than send a new one
Community-documented workaround for context-bloat: when the previous prompt almost worked but missed one detail, edit the previous message rather than send a new turn. Claude Design re-generates from the edited message without adding to context. This is in `research/04` as a low-cost recovery move for the case where a single-word change would have fixed the output.
### 6.4 — 3-failed-comment escalation rule
If three consecutive inline comments fail to land, stop commenting and escalate to chat. The comment surface is signaling that the model is not in a state to respect surgical edits — either the artifact has drifted too far from the brief, the context window is pressured, or the change is actually structural and was misclassified.
Escalation move: paste the failed comment text directly into a chat turn, prefaced with "the inline comment surface is not landing on this; please apply this change via regeneration".
### 6.5 — Model downshift escalator
When Opus 4.7 generations are non-monotonic in quality and Tweak / Comment / chat moves all stop landing, the recovery move is to start a fresh session at a different model. The downshift sequence community-converged on (per `research/04`):
- Opus 4.7 → Opus 4.6 (same family, less context-pressure-sensitive)
- Opus 4.6 → Sonnet 4.6 (faster, less context-sensitive, sometimes better at constraint-following on tight briefs)
Claude Design pins to Opus 4.7. The downshift happens by moving the work to a different Anthropic surface (Claude.com chat with a model picker) for the constraint-tightening turn, then bringing the result back to Claude Design as a new session anchor.
### 6.6 — Verbal save-pattern
When the operator wants to preserve what works but try a different direction without losing the current state, the community pattern is to **verbally save** in chat:
```
Save what we have. The current direction is good but I want to explore
a completely different aesthetic for comparison. Acknowledge this save,
then start fresh on a new direction without referencing the saved state.
We may come back to it.
```
The "save" is verbal — Claude Design has no version-tree primitive — but it signals to the model that the previous direction is preserved in the operator's mental model and the next turn is exploratory.
---
## 7. What Claude Design lacks
Four primitives that exist in adjacent Anthropic surfaces but not in Claude Design today. The plugin must never promise these:
- **No `/rewind`** — Anthropic Code has a `/rewind` primitive that reverts to a prior conversational state. Claude Design does not.
- **No version history** — there is no Tweak-history, no Comment-history, no chat-thread-fork primitive. The verbal save-pattern (Section 6.6) is the closest substitute.
- **No two-way handoff** — once an artifact is exported to Claude Code, there is no path back into Claude Design. Re-import requires a screenshot → new Claude Design session (lossy). See `references/04-handoff-and-scope.md`.
- **No branching** — Claude Design cannot fork a session into parallel directions and compare. The verbal save-pattern is the only branching primitive.
When the operator asks for any of these, name the constraint and offer the closest substitute (verbal save-pattern, multi-session-with-context-reset-prompt, manual screenshot archive).
---
## Sources
- `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` — surgical / structural edit split, intent presets
- `https://anthropic.com/engineering/harness-design-long-running-apps` — refine-vs-pivot rule, non-monotonic improvement, 40-50% context degradation, design grading criteria
- `https://github.com/rohitg00/awesome-claude-design` — community recovery prompts, break-default-aesthetic source
- `https://claude.com/blog/improving-frontend-design-through-skills` — AI-slop avoid-list applied during recovery
Re-research trigger: Anthropic publishing version-history or branching primitives; community 4-screen inflection no longer reproducing; quota mechanics shifting (Pro / Max minute counts have a 2026-04-28 captured-on date and are community-observed, not Anthropic-published).

View file

@ -1,157 +0,0 @@
# Handoff and scope fence
**Last updated:** 2026-05-17 | **Verified:** research/04-iteration-mechanics-recovery.md + research/05-anthropic-design-plugin-scope.md
**Status:** Beta (Labs research preview)
**Captured-on date:** 2026-05-16
This file documents two things: how Claude Design hands artifacts off to downstream tools (Claude Code, PowerPoint, PDF, Canva), and how this plugin (`claude-design`) fits next to Anthropic's official `knowledge-work-plugins/design`. The scope fence is load-bearing — getting it wrong duplicates Anthropic's command surface and adds nothing.
---
## 1. Handoff bundle contents
When the operator chooses Claude Code handoff as the destination, Claude Design produces a bundle containing — verbatim per `https://anthropic.com/news/claude-design-anthropic-labs` and `https://support.claude.com/en/articles/14604416-get-started-with-claude-design`:
- **Machine-readable component spec** — a JSON-shaped description of the components in the artifact, with names, props, and variants
- **Design tokens** — colors, typography, spacing, radii in token form (CSS variables or JSON tokens)
- **Layout hierarchy** — the page / screen structure as a tree
- **Referenced assets** — images, icons, fonts referenced in the artifact, bundled
- **Standalone HTML + inline CSS + JS** — a self-contained render that runs without Claude Design
- **Per-state screenshots** — visual snapshots of each interaction state (default, hover, active, disabled, focused)
- **PM-annotated notes** — annotations Claude Design surfaces about design decisions, edge cases, and trade-offs
- **Stack / framework README** — a guide to which framework conventions the artifact assumes (e.g., React + Tailwind, or vanilla HTML)
The bundle is generated once on export. It does not regenerate when the operator iterates the artifact further inside Claude Design — the operator must re-export to pick up changes.
Sources: `https://anthropic.com/news/claude-design-anthropic-labs` and `https://support.claude.com/en/articles/14604416-get-started-with-claude-design`.
---
## 2. Direction is one-way
The Design → Code handoff direction is one-way. Once the bundle is exported and the operator starts iterating in Claude Code (or any code editor), there is no return path to Claude Design. The component spec, design tokens, and standalone HTML continue to live in the code repository; Claude Design has no concept of "re-ingest from code".
If the operator wants to visit the visual surface again after engineering iteration, the only path is:
1. Screenshot the current Claude Code render
2. Open a new Claude Design session
3. Paste the screenshot as the starting visual reference
4. Brief Claude Design from scratch using layer-1-through-5 framework
This is lossy: the design tokens, component spec, and PM notes from the original bundle do not travel into the new Claude Design session. The new session inherits only what the screenshot communicates.
Operational consequences:
- **Finalize visual decisions inside Claude Design before exporting.** The Tweak panel and inline comments are free; chat turns inside Claude Design are budget-priced; engineering iteration in code is budget-free but the visual round-trip is one-way. Order accordingly.
- **Export once, intentionally.** Bundling everything in a single export (per Section 6 below) costs one chat turn; bundling screen-by-screen costs N turns and consumes budget faster.
- **Plan for asymmetric revisit.** When the engineering implementation diverges from the design intent and the operator wants a designer review, schedule that revisit as a fresh Claude Design session, not as an extension of the original session.
Practitioner consensus on this point is documented at `https://claudefa.st/blog/guide/mechanics/claude-design-handoff` (community source). Anthropic frames the same one-way property implicitly in the get-started article — the handoff is described as an export, not a connection.
---
## 3. Workflow recommendation
The recommended flow for any Claude Design artifact destined for engineering implementation:
1. **Iterate visually in Claude Design until the artifact is shippable.** Use Tweak panel and inline comments first; chat turns for structural and aesthetic changes.
2. **Validate the destination format before exporting.** If destination is PPTX, verify the text-as-text count (see Section 6 token cost trap). If destination is HTML standalone, render it in the target browser at the target viewport. If destination is PDF, check the interactive-element handling.
3. **Export the full bundle once.** Bundle all screens in one export, not per-screen. The token cost trap (Section 6) compounds with per-screen exports.
4. **Iterate engineering inside Claude Code or the code editor.** Use Claude Code's `/edit` and chat surfaces. Pull the design tokens from the bundle into the repository's styling layer.
5. **For post-design design-quality work — critique, accessibility audit, UX copy review, design-system audit, engineering handoff guidance — install Anthropic's official plugin (Section 4 below).**
6. **If a visual revisit becomes necessary later, accept the one-way cost.** Open a new Claude Design session against a screenshot; do not try to re-extend the original session.
This is the flow per Section 4's scope-fence reasoning: this plugin covers the upstream lifecycle; Anthropic's covers downstream.
---
## 4. Scope fence vs Anthropic's `knowledge-work-plugins/design`
Anthropic ships an official Claude Code plugin at `https://claude.com/plugins/design` (source: `https://github.com/anthropics/knowledge-work-plugins`). It is skill-driven, Apache 2.0 licensed (MIT-equivalent), and ships six slash-commands operating on **existing** artifacts (Figma URLs, screenshots, copy snippets).
The lifecycle-stage coverage map:
| Lifecycle stage | This plugin (claude-design) | Anthropic's plugin (knowledge-work-plugins/design) |
|-----------------|------------------------------|----------------------------------------------------|
| Idea ingestion | ✓ Disambiguate surface, intent preset, audience | — |
| Intent-preset selection | ✓ Eight presets, evidence-grade labelled | — |
| Prompt engineering | ✓ Five-layer stack + per-preset patterns | — |
| Copy-paste delivery | ✓ Composed prompt block | — |
| Iteration coaching | ✓ Tweak / Comment / Chat cascade, session economics | — |
| Ship-readiness | ✓ Operator-attested + recommend downstream tool | — |
| Critique | — | ✓ `/critique` |
| Accessibility audit | — | ✓ `/accessibility` |
| UX copy review | — | ✓ `/ux-copy` |
| Research synthesis | — | ✓ `/research-synthesis` |
| Design-system audit | — | ✓ `/design-system` |
| Engineering handoff | — | ✓ `/handoff` |
There is no functional overlap. This plugin produces prompts that go into Claude Design; Anthropic's plugin operates on artifacts that already exist. The split is clean by design — both plugins document the other as the lifecycle complement.
**Forbidden command-name list.** This plugin must NOT ship slash-commands with any of these names (with or without a `claude-design:` namespace prefix):
- `/critique`
- `/accessibility`
- `/ux-copy`
- `/research-synthesis`
- `/design-system`
- `/handoff`
`tests/validate-plugin.sh` assertion (h) enforces this mechanically. The rationale is collision-avoidance — if both plugins are installed and both ship `/critique`, command resolution becomes ambiguous and one or the other silently fails. The cleaner solution is: this plugin does not own those commands.
---
## 5. Recommended downstream tool
When the operator finishes the Claude Design lifecycle (artifact exists, exported, ready for review), surface the downstream tool installation as the next step:
```
claude plugins add knowledge-work-plugins/design
```
In a new Claude Code session with that plugin installed:
- Run `/critique <path-or-URL>` to get a design critique
- Run `/accessibility <path-or-URL>` for a WCAG audit
- Run `/ux-copy <path-or-URL>` for copy review
- Run `/research-synthesis` if the operator has user-research notes to synthesize
- Run `/design-system <path-or-URL>` for design-system consistency check
- Run `/handoff <path-or-URL>` for engineering-handoff guidance
Sources: `https://claude.com/plugins/design` and `https://github.com/anthropics/knowledge-work-plugins`.
The plugin is Apache 2.0, free, and maintained by Anthropic. There is no commercial trade-off; it is the canonical downstream tool.
---
## 6. Token cost trap — bundle all screens in one export
Practitioner-documented failure mode: exporting screen-by-screen instead of bundling all screens in one export. The community-cited reference is `token-budget-claude-design.md` (cited in `research/04` as one of the highest-leverage cost-management items).
The mechanism: each export turn passes the current artifact state through Opus 4.7 to produce the bundle. For an N-screen artifact, N separate exports run N separate bundle generations and burn N chat turns. Bundling all N screens in a single export runs one bundle generation against the cumulative state and burns one chat turn.
The bundling prompt pattern:
```
Generate the full export bundle covering all N screens of this artifact:
[screen 1: name], [screen 2: name], ... [screen N: name].
Include for each screen: HTML standalone, design tokens, component spec,
per-state screenshots, PM notes. Bundle as a single download.
```
Multi-screen artifacts (prototypes, slide decks, multi-page landing pages) benefit most from this discipline. Single-screen artifacts (a single dashboard, a single one-pager) are not affected because there is only one bundle to generate.
If the operator has already paid the per-screen cost and noticed mid-flight, the recovery is to abandon partial exports and run one final bundling export against the cumulative artifact state.
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — Anthropic Labs launch, bundle contents
- `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` — get-started, handoff bundle contents
- `https://claude.com/plugins/design` — Anthropic's official knowledge-work-plugins/design plugin
- `https://github.com/anthropics/knowledge-work-plugins` — source for the official plugin
- `https://anthropic.com/engineering/harness-design-long-running-apps` — design grading framing
- `https://claudefa.st/blog/guide/mechanics/claude-design-handoff` — community operational consensus on one-way direction
Re-research trigger: Anthropic announcing a two-way handoff primitive; `knowledge-work-plugins/design` adding or removing slash-commands; bundle contents changing materially; this plugin and Anthropic's plugin overlap emerging.

View file

@ -1,183 +0,0 @@
# Preset: designs
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
**Evidence grade:** Anthropic-documented + community-validated
**Captured-on date:** 2026-05-16
The `designs` intent preset is Claude Design's generic generation mode. It covers dashboards, components, layouts, and design explorations that do not fit into one of the more specialised presets (prototypes, slides, one-pagers, etc.). It is the preset operators reach for when the goal is "produce a high-quality visual artifact" rather than a destination-shaped artifact.
This file documents the `designs` preset across six dimensions: what it is, when to use it, Anthropic's published prompt patterns, community uplift, critical caveats, and one end-to-end worked prompt.
---
## (a) What this preset is
Anthropic's launch post (`https://anthropic.com/news/claude-design-anthropic-labs`) describes `designs` as the default-mode preset — the substrate every other preset effectively inherits from, with destination shaping layered on top. Output is HTML + React + inline CSS, viewable in the Claude Design canvas, exportable to PDF / HTML standalone / Code-handoff.
Two Anthropic primary sources ground this preset:
- The Anthropic-engineering blog `https://anthropic.com/engineering/harness-design-long-running-apps` publishes the four design grading criteria (design quality, originality, craft, functionality) that the `designs` preset is optimised against.
- The frontend-design open-source skill at `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` documents Anthropic's verbatim Design-Thinking Framework — **Purpose**, **Tone**, **Constraints**, **Differentiation** — and the verbatim AI-slop avoid-list.
The frontend-design skill is the closest thing Anthropic publishes to a `designs`-preset system prompt. Read it whenever the operator wants to understand what Claude Design is internally optimising for.
---
## (b) When to use it
Pick `designs` when the goal is generic, exploratory, or composite. The decision matrix:
| Operator goal | Preset |
|---------------|--------|
| Generic dashboard, component library exploration, design system playground | **designs** |
| Interactive product flow for usability testing | prototypes |
| Presentation for stakeholders | slides |
| Single-page memo or leave-behind | one-pagers |
| Low-fi structural layout for early review | wireframes-mockups |
| Investor / external pitch | pitch-decks |
| Landing page, social variant, marketing asset | marketing-collateral |
| Code-powered prototype with voice / video / shaders / 3D | frontier-design (experimental — see preset file) |
If the operator is uncertain between `designs` and `prototypes`, the distinguishing question is: **is this for usability testing?** Yes → prototypes. No → designs.
If uncertain between `designs` and `marketing-collateral`, the distinguishing question is: **is this destined for a marketing surface (landing page, social, ad)?** Yes → marketing-collateral. No → designs.
---
## (c) Anthropic-published prompt patterns
### The Design-Thinking Framework (verbatim from frontend-design/SKILL.md)
Anthropic's `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` publishes the verbatim four-part framework Claude Design uses when reasoning about a design:
- **Purpose** — what is the artifact for? Match every aesthetic decision to the purpose.
- **Tone** — what emotional register fits the audience and the purpose? Energetic, calm, authoritative, playful, terse?
- **Constraints** — what cannot be changed? Brand colors, typeface restrictions, layout rules, accessibility minimums.
- **Differentiation** — what makes this artifact distinct from the convergent middle-ground default? Name the differentiation explicitly.
Use this framework as a pre-brief check before composing a layer-1-through-5 prompt (see `../01-prompt-fundamentals.md`). If any of the four parts is fuzzy, sharpen it before drafting.
### Verbatim AI-slop avoid-list
Anthropic's frontend-design skill + the blog post `https://claude.com/blog/improving-frontend-design-through-skills` publish the verbatim banned-items list used in layer 3 of the prompt stack. See `../01-prompt-fundamentals.md` Section "Layer 3" for the full list. The `designs` preset inherits this list — it is not optional.
### Anthropic's verbatim canonical examples
The Anthropic get-started article `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` publishes three verbatim canonical examples (dashboard, mobile onboarding, landing page) demonstrating the Goal / Layout / Content / Audience framework. Read them as the reference shape for a first prompt against `designs`. Reproduced in full in `../01-prompt-fundamentals.md` Section "Layer 1".
---
## (d) Community uplift
Three community-converged patterns extend Anthropic's published material for the `designs` preset.
### Real-data injection over lorem ipsum
Victor Dibia's documented pattern (`research/03`): substitute realistic placeholder content rather than lorem ipsum. The model defaults to convergent middle-ground content when content is unspecified; named placeholders ("Today's MRR: $48,200", "Last 24h error rate: 0.12%") anchor the model to real-shaped output.
For dashboards specifically: use realistic metric values, realistic timestamps, realistic user names. The visual difference between a chart with `$3,200` / `$4,500` / `$2,800` and a chart with `$XXX` / `$YYY` / `$ZZZ` is large — Claude Design will infer typography spacing and component sizing from the named values.
### Explicit modular scale and weight palette
Community pattern (research/03): name the typographic modular scale and weight palette in the brief rather than letting the model default. The `1.250` (minor third) scale fits dense informational artifacts; the `1.333` (perfect fourth) scale fits marketing pages. Weight palettes converge on `500 body / 600 emphasized / 700 headings`.
### Specify the negative aesthetic family
Beyond layer-3 negative constraints (which name specific banned items), community practice (research/03) is to name an entire negative aesthetic family — "not modern SaaS", "not playful illustrated", "not corporate professional" — to push the model out of its default neighbourhood. The model interprets aesthetic-family naming as a strong signal even in the negative.
---
## (e) Critical caveats
Three caveats specific to the `designs` preset.
### Default-aesthetic drift on iteration
The `designs` preset is most susceptible to default-aesthetic drift because it has no destination-shaped constraint pulling it toward a specific genre. Watch for drift back to AI-slop defaults across iterations — the `references/03-iteration-and-session.md` "break-default-aesthetic" recovery prompt is targeted at exactly this drift.
### Non-monotonic improvement across iterations
`https://anthropic.com/engineering/harness-design-long-running-apps` documents that quality across iterations is not strictly increasing. Turn 4 can be worse than turn 3 on design quality, originality, or craft. The recovery move (pivot, not refine) is in `../03-iteration-and-session.md`.
### Component spec coherence
For dashboards and component libraries specifically, the export bundle's machine-readable component spec is load-bearing for engineering handoff. Ensure the artifact has coherent component definitions (named, with consistent variants) before exporting — otherwise the component spec will be partial and the engineering implementation will diverge.
---
## (f) One end-to-end worked prompt — layers 1 + 2a + 3 composed
Goal: an admin dashboard for an analytics product, audience is data engineers.
```
Goal: An admin dashboard for monitoring data-pipeline freshness across
120 tables, sorted by last-successful-load timestamp
Layout: Header with environment switcher + global time-window selector;
top metrics row (4 KPIs: tables behind SLA, tables current,
tables stale, tables errored); main panel with stacked area
chart showing freshness over the last 24 hours; sortable table
below with 120 rows; alerts sidebar
Content: Realistic table names (orders, customers, inventory,
user_events, sessions, etc.); realistic timestamps (last
successful load within the last 6 hours for most, some at
12 hours, some at 48 hours); realistic error rates (0.01% to
3.2%)
Audience: Data engineers, on-call rotation, ages 25-50, comfortable
with dense interfaces, need to scan and triage quickly
Aesthetic family: industrial-utilitarian, slate-monochrome
Color palette (CSS hex):
--color-bg: #E9ECEC
--color-surface: #C9D2D4
--color-muted: #8C9A9E
--color-fg: #44545B
--color-ink: #11171B
--color-accent: #4A6FA5
--color-error: #B23A48
--color-warning: #C89B3F
Typography: square angular sans-serif (Söhne preferred, Inter Variable
fallback); no rounded glyphs; modular scale 1.250
Corner radius: 4px throughout — no pill shapes
Motion: transition: all 160ms ease-out
Density: dense (32px table rows, 8px card padding)
Surface: flat — no shadows, borders define edges
Design-Thinking Framework:
Purpose: enable on-call triage in under 60 seconds per incident
Tone: terse, signal-dense, no decorative copy
Constraints: 32px row height minimum (accessibility), accent reserved
for actionable items only
Differentiation: this is a data-engineer tool, not a marketing
dashboard — no card-style metric tiles, no playful
illustrations, no progress-ring widgets
Negative constraints — do not produce any of:
- Inter, Roboto, Arial, or Space Grotesk as primary typeface
- Purple gradients on white backgrounds
- Card-style KPI tiles with shadows and rounded corners
- Centered-hero with single CTA
- Bouncy spring easing on hover
- Pulse animations on idle elements
- Glassmorphism, neumorphism, generic "modern SaaS" defaults
If you find yourself defaulting to any of these, stop and ask me to
clarify the aesthetic before continuing.
```
Expected follow-up turns:
1. Turn 2: add layer 4 (typography modular scale specifics, semantic color roles, motion easing curves)
2. Turn 3: add layer 5 (grading criteria weighting — craft and functionality at 0.4 and 0.3, design quality 0.2, originality 0.1)
3. Turn 4+: Tweak panel takes over for surgical edits
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — preset enumeration, launch post
- `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` — GLCA framework, three canonical examples
- `https://anthropic.com/engineering/harness-design-long-running-apps` — design grading criteria, non-monotonic improvement
- `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` — Design-Thinking Framework, AI-slop avoid-list
- `https://claude.com/blog/improving-frontend-design-through-skills` — default-avoidance blog post
Re-research trigger: Anthropic updating the Design-Thinking Framework; new canonical examples added to get-started article; AI-slop avoid-list materially extended.

View file

@ -1,149 +0,0 @@
# Preset: frontier-design
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
Evidence grade: Experimental — no validated practitioner pattern as of 2026-05-16. Frontier design is currently marketing language for "elaborate variants of the other presets," not a distinguishable generation mode practitioners can reliably invoke today.
This file documents what Anthropic publishes about the `frontier-design` preset, what practitioners have shipped (nothing verified), what adjacent material exists, and the single experimental pattern the plugin offers — clearly labelled as unverified speculation. The honest position the plugin takes is in Section (e).
---
## (a) What Anthropic says
Anthropic's launch post `https://anthropic.com/news/claude-design-anthropic-labs` describes `frontier-design` in a single sentence (verbatim):
> code-powered prototypes with voice, video, shaders, 3D and built-in AI
This is the entirety of Anthropic's per-preset documentation as of the 2026-05-16 captured-on date. There is no dedicated tutorial, no support article, no canonical prompt set, no Anthropic-published example artifact.
The launch sentence implies the preset targets:
- Code-powered prototypes (not static designs) — implying interactive elements at minimum
- Voice (audio playback, speech recognition, voice UI)
- Video (embedded video playback, possibly video-driven UI)
- Shaders (WebGL, custom GLSL shaders, GPU-driven visual effects)
- 3D (WebGL 3D scenes, possibly Three.js or similar)
- Built-in AI (LLM-driven interactions inside the artifact)
Anthropic's framing suggests `frontier-design` is the preset for showpiece artifacts demonstrating Claude Design's outer-edge capabilities — not a workhorse preset like `designs`, `prototypes`, or `slides`.
---
## (b) What practitioners have shipped
Verifiable practitioner outputs as of 2026-05-16: **NONE that we could verify.**
The most explicit acknowledgment of this gap comes from `https://llmx.tech/blog/claude-design-hands-on-review-2026` (cited in `research/03`):
> ...no frontier design assessment provided. The hands-on review covers designs, prototypes, slides, and one-pagers. Frontier design is named in the preset list but received zero hands-on evaluation, because no practitioner artifact has been published demonstrating what the preset produces in practice.
Across the community sources surveyed in `research/03` (Substack walkthroughs, awesome-claude-design lists, Twitter / X threads, MindStudio walkthroughs, sagnikbhattacharya, victordibia, theadpharm, claudefa.st, etc.), no practitioner has published a verifiable frontier-design artifact with prompt, output, and reproduction steps. The preset is named, occasionally referenced, but not demonstrated.
This may change. The preset is new (April 2026 launch); practitioner adoption lags. The `.coverage.md` re-research trigger explicitly flags "first verified frontier-design practitioner artifact ships publicly" as a refresh trigger for this file.
---
## (c) Adjacent material
While no `frontier-design`-specific Anthropic or practitioner material exists, two adjacent sources cover the underlying capabilities Anthropic names.
### Motion and spatial composition — `frontend-design/SKILL.md`
`https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` publishes Anthropic's guidance on motion (easing curves, timing tiers, what to animate and what not to) and spatial composition (typography hierarchy, surface depth, layered backgrounds). These are the building blocks the `frontier-design` preset would extend with voice / video / shaders / 3D, but the building blocks themselves are general-purpose.
### Animated and 3D websites — MindStudio walkthrough
MindStudio's 2026-04-28 walkthrough (cited in `research/03`) covers prompting Claude for animated and 3D websites — but the walkthrough is set in adjacent Anthropic surfaces (Claude.com chat with an HTML artifact), not in `claude.ai/design` with the `frontier-design` preset specifically. The walkthrough is useful for the prompt-engineering pattern (naming GLSL fragment shader constraints, polygon-count budgets, voice-prompt structuring) but is not a frontier-design-preset artifact.
---
## (d) Single experimental pattern (unverified speculation)
One experimental pattern, clearly labelled as unverified, that an operator could try if they want to engage with the preset despite the gap.
The pattern comes from Google's Gemini deep-research output (cited in `research/03`) and carries low confidence. It is a constraint-language pattern for shader and physics elements, adapted from broader frontend-design practice:
```
[layers 1 through 5 of the standard prompt stack from
../01-prompt-fundamentals.md]
Frontier capabilities to engage:
Shaders:
- One custom GLSL fragment shader applied to the hero region
- Shader pattern: [name the visual character — e.g.,
"subtle gradient flow with imperceptible noise" or
"iridescent surface reacting to cursor position"]
- Frame budget: 60fps target on Apple M1 / equivalent
- No fullscreen shader-bombs (battery / heat / accessibility)
3D:
- One 3D element in the hero region, scene-bounded (no fullscreen)
- Polygon count budget: <50,000 triangles
- Lighting: 2-3 light sources max
- Camera: fixed or single-axis orbit; no free-camera
Voice:
- [if voice UI relevant] one voice-driven interaction, with
visible text-transcript fallback
- Speech-recognition language and accent assumptions named
explicitly
Video:
- [if video element relevant] embedded video with explicit
autoplay/no-autoplay decision; explicit captions decision
Built-in AI:
- [if applicable] one LLM-driven interaction in the artifact
- Explicit fallback for when the LLM call fails
Test in target browsers (Chrome, Safari, Firefox) at the target device
class (M1 / M2 desktop, mid-range mobile). Expect aesthetic drift
across runs; non-monotonic improvement applies amplified for frontier
capabilities.
```
Confidence rating on this pattern: **low**. It is a reasoned extrapolation from frontend-design principles, not a tested frontier-design prompt. If you try it, document what works and what does not — there is a community gap to fill.
---
## (e) The plugin's honest position
The plugin's stance on `frontier-design`:
If you want to attempt frontier design, treat it as a high-fidelity prototype (`prototypes` preset) with extra constraint language for shaders, polygons, voice, video, and built-in AI. Expect aesthetic drift on first generations. Verify that your output works in target browsers before committing chat-turn budget to refinement. Expect that the model's prior on what "frontier design" means may differ from yours — over-specify everything that matters.
Do not assume `frontier-design` produces a categorically different artifact from `prototypes` + extra capability constraints. The launch sentence is suggestive; the practitioner evidence is absent. The preset is marketing language for elaborate prototype variants until proven otherwise.
When the operator names `frontier-design` specifically:
1. Read this file with them
2. Confirm they have understood the practitioner-evidence gap
3. Offer the experimental pattern in Section (d) as a starting point, clearly labelled as unverified
4. Treat the resulting artifact as exploratory — surface what worked and what did not, contribute back to the community gap
5. Plan for amplified non-monotonic-improvement (`../03-iteration-and-session.md`) — frontier capabilities compound the standard non-monotonic risk
---
## (f) Re-research trigger
This file refreshes when any of the following happens:
- Anthropic publishes a dedicated tutorial, support article, or canonical prompt set for `frontier-design`
- A verified practitioner artifact appears publicly with prompt + output + reproduction steps
- The launch-post one-sentence description changes materially
- A community pattern reaches enough adoption to be cited (not speculation) — the `awesome-claude-design` lists and adjacent practitioner blogs are the primary surfaces to watch
When any of these triggers, update Section (b) to reflect verified material, replace Section (d) with the verified pattern, and re-grade the evidence label from "Experimental" to "Community-only" or "Anthropic-documented + community-validated" as appropriate.
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — Anthropic's verbatim one-sentence description (the entirety of Anthropic-published material on this preset)
- `https://llmx.tech/blog/claude-design-hands-on-review-2026` — community practitioner explicitly noting the frontier-design evaluation gap
- `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` — adjacent material on motion and spatial composition
- `https://anthropic.com/engineering/harness-design-long-running-apps` — non-monotonic-improvement framing (amplified here)
- `https://claude.com/blog/improving-frontend-design-through-skills` — AI-slop avoid-list (composed for frontier prompts)
Re-research trigger: see Section (f). The preset is the most volatile in this plugin's coverage; expect this file to refresh first when Anthropic ships material or practitioners publish artifacts.

View file

@ -1,218 +0,0 @@
# Preset: marketing-collateral
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
Evidence grade: Community-only — Anthropic publishes no per-preset prompt patterns for this preset as of 2026-05-16.
Anthropic names `marketing-collateral` in the launch enumeration at `https://anthropic.com/news/claude-design-anthropic-labs` but publishes no dedicated tutorial. The patterns below come from community practitioners; treat them as field-tested but not Anthropic-authoritative. Anthropic's frontend-design open-source skill at `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` is the closest adjacent Anthropic source — it covers landing-page and marketing-site design philosophy without per-preset prompts.
---
## (a) What this preset is
Anthropic launch post one-sentence description: `marketing-collateral` covers landing pages, social variants, banner ads, email creative, and other visual assets in the marketing surface area. Output is typically HTML for landing pages, image-shaped for social and ads.
Distinguishing properties:
- Conversion-oriented — the artifact has a measurable goal (signups, clicks, opens)
- Multi-format — a single campaign typically needs landing page + social variants + email + ad creative
- Brand-anchored — marketing collateral lives or dies on brand fidelity; a DESIGN.md is essentially mandatory
- Variant-heavy — A/B testing assumes multiple variants of the same creative
---
## (b) Why Anthropic published no per-preset guidance
The launch enumeration treats marketing-collateral as a destination shape rather than a distinct generation mode. The frontend-design open-source skill (`https://github.com/anthropics/skills/skills/frontend-design/SKILL.md`) is the closest thing Anthropic publishes — it covers the design-philosophy layer (Purpose / Tone / Constraints / Differentiation) but not marketing-specific prompt patterns.
Community practitioners have built patterns around landing-page composition, social-variant fan-out, and competitor-screenshot extraction (Section c).
---
## (c) Community patterns
### chatprd.ai landing-page workflow
Community pattern from `https://chatprd.ai` (cited in `research/03`): a four-stage landing-page production flow optimised for Claude Design:
1. **Brief stage** — define the audience, the value prop, the one CTA, the proof points. Output: text document, not in Claude Design yet.
2. **Outline stage** — translate the brief into a section-by-section landing-page outline. Hero, problem, solution, features (3-grid or 4-grid), proof (logos / quotes / numbers), pricing or single-CTA, FAQ, footer. Output: text outline.
3. **Visual stage** — brief Claude Design from the outline using layers 1-5. First turn produces the landing page; iteration tightens.
4. **Variant stage** — once the master landing page works, generate variants for A/B testing (different hero, different proof-point ordering, different CTA framing) using the variant-fan-out pattern below.
The four-stage workflow separates copy decisions from visual decisions, which lets the operator iterate each independently. The community-documented failure mode is briefing visual + copy together in one prompt — the model conflates the two and produces a generic landing page.
### Sagnik Bhattacharya variant-fan-out for social
Community pattern from `https://sagnikbhattacharya.com/blog/claude-design` (cited in `research/03`): for social-format collateral (Instagram square, LinkedIn rectangle, Twitter / X aspect), generate N variants in parallel rather than sequentially. The brief pattern:
```
Generate 6 variants of the [campaign] creative, sized for [format
spec]. Across the 6:
- Vary the headline framing (problem-led, solution-led,
proof-led)
- Vary the visual hierarchy (text-dominant, image-dominant,
balanced)
- Vary the color emphasis (accent-dominant, monochrome,
high-contrast)
- Keep the value prop, audience, and CTA identical across all 6
Output as 6 distinct artifacts I can A/B test.
```
The pattern produces a campaign-set in one chat turn rather than six iterations.
### Competitor-screenshot visual-reference extraction
Community pattern (cited in `research/03`): when the operator has a competitor's marketing page that visually achieves what they want, screenshot it and brief Claude Design with the screenshot as a visual reference, paired with an explicit "do not copy; extract the visual-language principles" instruction:
```
The attached screenshot shows [competitor]'s landing page. Do NOT
copy the structure, the copy, or the layout. DO extract the
visual-language principles:
- typography character (named family + scale + weights)
- color temperature and palette structure
- visual density (how much whitespace, how many elements per fold)
- motion language (if visible from the screenshot or apparent from
the brand)
- overall aesthetic family (named with concrete reference)
Apply those principles to our landing page, which has a fundamentally
different structure, copy, and CTA flow. Output our landing page
respecting the extracted visual language but original in structure.
```
The pattern is high-leverage when the operator has a clear visual reference but cannot articulate it in DESIGN.md form. The risk: too-literal copying produces a derivative-feeling artifact. Brief the "extract, do not copy" constraint explicitly.
### Slop-fingerprints warning amplified
Marketing collateral is the surface where AI-slop fingerprints are most punishing. The teal gradient + serif headline + blinking status dot + container-on-container + glassmorphism pattern is recognisable across many AI-generated landing pages. Audiences pattern-match on it and discount the artifact. Layer 3 negative constraints apply with extra weight:
```
Negative constraints — do not produce any of:
- Teal-to-blue or teal-to-green gradients
- Serif headline on sans-serif body (unless explicitly briefed for
editorial direction)
- Blinking / pulsing status indicators ("Live", "New", "Updated")
- Container-on-container layouts (card-inside-card)
- Glassmorphism or neumorphism on any element
- Generic "modern SaaS landing page" template defaults
- Stock-photo abstract gradient hero imagery
```
---
## (d) Critical caveats
### Brand fidelity is the dominant failure mode
Marketing collateral without a tight DESIGN.md anchor produces generic output. The brand DESIGN.md is essentially mandatory — see `../02-design-md.md` for the extractor pattern when the operator does not already have one. Validate brand fidelity at every iteration: typeface, color palette, voice (tone of copy), visual density. Brand drift on marketing collateral is more visible to the audience than brand drift on internal artifacts.
### A/B testing requires more than aesthetic variation
The variant-fan-out pattern produces aesthetic variations. For meaningful A/B testing, the variants should test specific hypotheses (does problem-led headline outperform solution-led? does image-dominant outperform text-dominant?) rather than test generic aesthetic variation. Brief the hypotheses explicitly.
### Export-to-image for social formats
Social-format collateral typically exports as PNG or JPG (Claude Design produces HTML; the operator screenshots at the target dimensions). The export is lossy for hover states, interactive elements, and motion. Brief the static state explicitly when the destination is image:
```
The destination for this creative is a static PNG/JPG export. Generate
the static state only. No hover states, no interaction logic, no motion.
```
---
## (e) One worked prompt — layers 1 + 3 composed, four-stage landing-page flow
Goal: a landing page for a developer-tools SaaS product, audience is senior engineers evaluating dev tools.
```
Goal: A landing page for "ObserveAPI", a developer-tools SaaS product
for API observability. The goal: convert senior-engineer
visitors to free-trial signups.
Layout: Hero (above-fold), problem (one paragraph + 3 pain points
as labelled rows), solution (one paragraph + product
screenshot), features (3-grid), proof (3 customer logos +
one quote + one named metric), pricing (single tier + free
trial CTA), FAQ (4 questions), footer (links + secondary
CTA)
Content: Real product positioning, real customer logos (placeholder
names but realistic shapes), real metric numbers, real
FAQ content. No lorem ipsum.
Audience: Senior engineers, ages 30-50, evaluating dev tools,
allergic to marketing fluff, allergic to AI-generated
landing page fingerprints, will scroll fast and bounce
fast unless the headline lands
Stage 1 (brief): Audience = senior engineers, value prop = "the
first API observability tool that doesn't require
you to instrument anything", CTA = "Start free
trial", proof points = 3 customer logos + one
quote + one metric
Stage 2 (outline): use the layout above
Stage 3 (visual): use the brief below
Stage 4 (variants): defer to next session
Aesthetic family: developer-confident — like Linear's marketing site
meets the editorial confidence of The New York Times
opinion section. No flourish, every claim earns its
place, headline is a claim not a tagline.
Color palette (CSS hex):
--color-bg: #FAFAF8
--color-surface: #FFFFFF
--color-muted: #6B6B6B
--color-fg: #2A2A2A
--color-ink: #0A0A0A
--color-accent: #2D6356
Typography: Söhne (preferred — concrete-named) or Inter Variable;
modular scale 1.333; weight palette 400 body / 600
emphasized / 700 hero headline
Corner radius: 4px on buttons and cards; full-bleed hero
Motion: transition: all 160ms ease-out on hover; no auto-play motion
anywhere
Density: comfortable above the fold (5 elements max), denser below
the fold (features grid, proof, FAQ)
Surface: flat — single subtle border or single subtle shadow on
cards, never both
Negative constraints — do not produce any of:
- Inter, Roboto, Arial, Space Grotesk as primary typeface
- Teal-to-blue or teal-to-green gradients
- Serif headline on sans-serif body
- Blinking / pulsing status indicators
- Container-on-container layouts
- Glassmorphism, neumorphism
- Generic "modern SaaS landing page" defaults
- Stock-photo abstract gradient hero imagery
- Three-column feature grid with icon + headline + line (default
fingerprint)
- Centered-hero with single CTA (default fingerprint)
If you find yourself defaulting to any of these, stop and ask me to
clarify before continuing.
Brand DESIGN.md: ObserveAPI brand kit attached as project asset.
Reference it at every section.
```
Expected follow-up turns:
1. Turn 1: outline review (Stage 2)
2. Turn 2: visual generation (Stage 3) at the brief above
3. Turn 3: layer-4 dimension refinement (typography modular scale, semantic color roles, motion easing)
4. Turn 4: layer-5 grading-criteria weighting (functionality 0.4, craft 0.3, design quality 0.2, originality 0.1 — landing pages weight functionality high)
5. Turn 5+: Tweak panel for spacing and density adjustments
6. Variant fan-out (Stage 4) in next session, against the approved master
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — preset enumeration
- `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` — Anthropic's frontend-design skill (closest adjacent Anthropic source; Design-Thinking Framework, AI-slop avoid-list, four design dimensions)
- `https://claude.com/blog/improving-frontend-design-through-skills` — AI-slop avoid-list (amplified for marketing collateral)
- `https://chatprd.ai` — community four-stage landing-page workflow
- `https://sagnikbhattacharya.com/blog/claude-design` — community variant-fan-out pattern for social formats
- `https://anthropic.com/engineering/harness-design-long-running-apps` — design grading criteria (composed for marketing collateral)
Re-research trigger: Anthropic publishing a marketing-collateral tutorial; community four-stage workflow drifting; new slop-fingerprint patterns emerging in the AI-generated landing-page corpus; competitor-screenshot extraction patterns evolving.

View file

@ -1,168 +0,0 @@
# Preset: one-pagers
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
Evidence grade: Community-only — Anthropic publishes no per-preset prompt patterns for this preset as of 2026-05-16.
Anthropic names `one-pagers` in the launch enumeration at `https://anthropic.com/news/claude-design-anthropic-labs` but does not publish a dedicated tutorial, support article, or canonical prompt set for it. The patterns below come from community practitioners — Substack walkthroughs, blog posts, newsletter pieces — with full attribution. Treat them as field-tested but not Anthropic-authoritative.
---
## (a) What this preset is
Anthropic launch post one-sentence description: a one-pager is a single-screen artifact for memos, summaries, leave-behinds, executive briefs, or single-page deliverables. The destination is typically PDF or print.
Distinguishing properties:
- Single page — no multi-screen navigation, no scrolling sections that imply continuation
- High information density compared to a slide
- Self-contained narrative — reader does not need surrounding context
- Often delivered as a leave-behind after a meeting or as a one-shot brief
---
## (b) Why Anthropic published no per-preset guidance
The launch enumeration treats one-pagers as a destination shape rather than a generation mode requiring distinct prompt patterns. The Goal / Layout / Content / Audience framework (`../01-prompt-fundamentals.md` Layer 1) and the five-layer stack apply directly — Layout becomes single-page structure, Content becomes the dense information payload, Audience tightens the tone.
Community practitioners have converged on patterns that constrain the one-pager preset more tightly than the generic stack does (Sections c and d).
---
## (c) Community patterns
### Word-count cap per block
Community pattern from `https://sagnikbhattacharya.com/blog/claude-design` (cited in `research/03`): cap the word count per layout block in the brief. The mechanism — Claude Design defaults to verbose prose when block content is unspecified, and one-pagers fail when any block runs long. The convergent caps from community practice:
- Title block: 8 words max
- Subtitle: 15 words max
- Body paragraph: 60 words max
- Bullet item: 12 words max
- Callout box: 25 words max
Brief the caps explicitly in the prompt — the model otherwise produces blocks 2-3x longer than the operator wants.
### Above-the-fold density limit
Community pattern from `https://newsletter.victordibia.com` (cited in `research/03`): cap the number of distinct elements visible in the top half of the one-pager. Convergent limits:
- Maximum 5 distinct visual elements above the fold (counting title, subtitle, one body block, one visual, one callout = 5)
- Maximum 3 colour roles visible above the fold (typically: ink for title, fg for body, accent for one emphasis)
- Maximum 2 typographic weights above the fold (typically 700 title, 500 body)
The brief encodes these as explicit constraints:
```
Above the fold (top half of the page), no more than 5 distinct visual
elements, no more than 3 color roles, no more than 2 typographic
weights. Density below the fold can scale up.
```
### Real-data injection over lorem ipsum
Same pattern as `designs.md` and `prototypes.md`: use realistic placeholder content. For one-pagers specifically, this matters more — a one-pager is typically read once and discarded; if the content reads as placeholder, it loses the reader.
---
## (d) Critical caveats
### Density-versus-readability trade-off
One-pagers are constrained by physical reading mechanics — the operator can pack a lot into one page, but each element added reduces the reader's attention to every other element. The brief should weight density-vs-readability explicitly:
```
Optimise for the reader's ability to extract the takeaway in 30 seconds
of scanning. If a block requires more than 30 seconds of focused reading
to extract the takeaway, it does not belong on this one-pager.
```
### Export to PDF preserves layout; export to other formats may not
PDF is the canonical one-pager export. HTML standalone works. PPTX is awkward for one-pagers (PPTX assumes deck format, not single-page format). Code-handoff is rare for one-pagers but works.
### Anthropic AI-slop avoid-list still applies
Layer 3 negative constraints (`../01-prompt-fundamentals.md`) apply with full force on one-pagers — the dense information context does not exempt the artifact from the avoid-list. Inter, Roboto, Arial, purple gradients on white, generic-modern defaults all degrade the one-pager.
---
## (e) One worked prompt — layers 1 + 3 composed (Layer 2a is preset-optional)
Goal: an executive one-pager summarizing a project's Q1 status, audience is VP of Engineering.
```
Goal: A single-page executive summary of the platform team's Q1 2026
delivery, reliability, and Q2 themes. Designed to be scanned in
30 seconds and absorbed in 3 minutes.
Layout: Single-page A4 portrait. Top quarter: title + headline takeaway
+ 3 KPI numbers in a row. Middle half: 3 short body paragraphs
(one per: delivery, reliability, Q2 themes). Bottom quarter:
callout box with the one explicit ask + signature/contact block.
Content: Real KPI numbers (% completion, MTTR minutes, uptime %); real
body content (no lorem ipsum); explicit ask is one sentence;
contact block names the person + email
Audience: VP of Engineering, scanning between meetings, needs the
takeaway and one ask, will dive into details only if the
takeaway warrants it
Word-count caps (community pattern from
https://sagnikbhattacharya.com/blog/claude-design):
- Title block: 8 words max
- Subtitle / headline takeaway: 15 words max
- Body paragraph: 60 words max
- Bullet item: 12 words max
- Callout box: 25 words max
Above-the-fold density limit (community pattern from
https://newsletter.victordibia.com):
- Maximum 5 distinct visual elements above the fold
- Maximum 3 color roles visible above the fold
- Maximum 2 typographic weights above the fold
Aesthetic family: editorial-confident — terse, signal-dense, no flourish
Color palette (CSS hex):
--color-bg: #FAFAF8
--color-surface: #FFFFFF
--color-muted: #6B6B6B
--color-fg: #2A2A2A
--color-ink: #0A0A0A
--color-accent: #3D5C8A
Typography: Söhne or Inter Variable; modular scale 1.250; weight palette
500 body / 700 headings
Corner radius: 4px on the callout box only; rest is flat
Motion: none (one-pager is static)
Density: dense (8mm margins, 4mm gutters)
Surface: flat — no shadows
Negative constraints — do not produce any of:
- Inter, Roboto, Arial, or Space Grotesk as primary typeface
- Purple gradients on white backgrounds
- Card-style metric tiles with shadows
- Centered-title-and-subtitle generic header
- Pulse / breathing animations (this is a static one-pager)
- Generic "executive summary template" defaults
Optimise for the reader's ability to extract the takeaway in 30 seconds
of scanning. If a block requires more than 30 seconds of focused reading
to extract the takeaway, it does not belong on this one-pager.
```
Expected follow-up turns:
1. Turn 2: tighten word counts if any block ran over cap
2. Turn 3: refine callout-box positioning if it competes with the headline takeaway
3. Turn 4+: Tweak panel for spacing scale and density adjustments
4. Export to PDF; visual-audit at 100% zoom and at print size
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — preset enumeration, one-sentence description
- `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` — GLCA framework (composed in this preset)
- `https://sagnikbhattacharya.com/blog/claude-design` — community pattern for word-count caps per block
- `https://newsletter.victordibia.com` — community pattern for above-the-fold density limits
- `https://claude.com/blog/improving-frontend-design-through-skills` — AI-slop avoid-list (composed)
- `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` — Design-Thinking Framework reference
Re-research trigger: Anthropic publishing a dedicated tutorial for one-pagers; community word-count caps drifting; new one-pager-specific community pattern emerging.

View file

@ -1,198 +0,0 @@
# Preset: pitch-decks
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
**Evidence grade:** Community-only — Anthropic publishes no per-preset prompt patterns for this preset as of 2026-05-16.
Evidence grade: Community-only — Anthropic publishes no per-preset prompt patterns for this preset as of 2026-05-16.
Anthropic names `pitch-decks` in the launch enumeration at `https://anthropic.com/news/claude-design-anthropic-labs` but publishes no dedicated tutorial. **Critical caveat upfront:** community practitioner `https://moda.app/blog/claude-design-for-pitch-decks` documents an explicit recommendation against using Claude Design for external/investor pitch decks when PPTX is the required delivery format — see Section (d).
---
## (a) What this preset is
Anthropic launch post one-sentence description: `pitch-decks` covers investor pitches, external partner proposals, and any high-stakes presentation format that needs to look polished. The preset distinguishes itself from the more general `slides` preset by audience — external rather than internal — and by typical destination — PPTX or PDF rather than HTML.
The distinguishing question vs `slides`: **is the audience an external investor or external partner where the deck represents the company's positioning?** Yes → `pitch-decks`. Internal audience → `slides`.
---
## (b) Why Anthropic published no per-preset guidance
Anthropic likely treats pitch-decks as a high-stakes specialisation of `slides` rather than a fundamentally distinct generation mode. The `slides` tutorial at `https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks` covers the prompt patterns; the `pitch-decks` preset inherits those patterns and adds the audience-stakes layer.
Community practitioners have converged on patterns specific to pitch-deck production (Section c). The most important community contribution, however, is the PPTX-export caveat (Section d) — the failure mode is severe enough that the default recommendation diverges from `slides`.
---
## (c) Community patterns
### Sagnik Bhattacharya's 10-slide template
Community pattern from `https://sagnikbhattacharya.com/blog/claude-design` (cited in `research/03`): the convergent 10-slide pitch-deck template for B2B SaaS pitches:
1. Title — company name, one-line positioning, tagline
2. Problem — who has it, what it costs them, how acute
3. Solution — what we built, one-sentence value prop
4. Market — TAM / SAM / SOM (sized realistically)
5. Product — 2-3 screenshots or visual demos
6. Business model — how we charge, ACV ranges, GTM motion
7. Traction — revenue, growth rate, named customers
8. Team — founders + key hires, why this team for this problem
9. Competition — competitive map (4-quadrant or named comparisons)
10. Ask — funding round size, use of funds, timeline
The template is community-converged, not Anthropic-published. It composes with Anthropic's `slides` tutorial patterns from `https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks`.
### MindStudio per-slide micro-prompts
Community pattern from MindStudio (cited in `research/03`): rather than briefing the full pitch deck in one prompt, micro-prompt each slide individually. The pattern produces tighter per-slide narrative because each slide gets dedicated attention.
The micro-prompt pattern (per slide):
```
Generate slide N of the pitch deck. This slide does one job:
[the slide's job — e.g., "convince the audience that the problem is
acute by quantifying customer cost"].
Visual elements: [specific to this slide — e.g., "one large number
showing annual cost, two supporting smaller numbers, one explanatory
sentence"].
Constraints from DESIGN.md: [reference the project's DESIGN.md].
Do not include filler — every element on this slide must support the
one job.
```
Walk through slides 1-10 sequentially.
### Outline-first scaffolding (composed from `slides.md`)
The outline-first pattern from `slides.md` Section (d) applies: brief the deck as an outline first (turn 1), then expand to full slides (turn 2-N).
---
## (d) Critical caveats
### PPTX export trap — explicit recommendation against external pitch decks
Community-documented at `https://moda.app/blog/claude-design-for-pitch-decks` (cited in `research/03`) and `https://claudedesign.substack.com`: when an HTML-rendered pitch deck is exported to PPTX, richly-styled text can flatten to images. PowerPoint loses the editability — the text becomes a rasterised picture.
For internal slide decks (audience tolerates some export friction), the operator can mitigate by keeping typography simple. For external pitch decks (audience expects polish, may want to add their own annotations or edit the deck), this failure mode is severe enough that the community recommendation is:
> Do not use Claude Design for external/investor pitch decks where PPTX export is the required delivery format. Use HTML standalone or PDF if Claude Design is required; otherwise produce the deck in PowerPoint or Keynote directly.
This plugin surfaces the recommendation but does not refuse to operate. The operator may have a reason to proceed (HTML acceptable, PDF acceptable, the PPTX text-as-text survival is verified to be acceptable for their specific styling). When proceeding, validate PPTX export early — generate slide 1 fully, export to PPTX, verify text-as-text survival, then commit to the deck.
### Audience-stakes asymmetry
A pitch deck for a $50M Series C carries different stakes than a pitch deck for a $500K seed extension. The operator's tolerance for export imperfection scales with the dollar amount on the line. Default conservatively — when in doubt about whether the export will survive, treat the deck as high-stakes.
### Slop-fingerprints warning amplified
Layer 3 negative constraints apply with extra weight on pitch decks. Investors see many decks; AI-slop fingerprints (purple gradients, generic three-column structure, Inter typography, centered-hero defaults, glassmorphism, neumorphism) signal that the deck is templated and the team did not invest care. The brief should over-specify the negative constraints.
---
## (e) One worked prompt — layers 1 + 3 composed, slide-by-slide micro-prompt pattern
Goal: a 10-slide investor pitch deck for a B2B SaaS observability product, audience is Series A investors.
```
PRECONDITION: Before generating any slide, render slide 1 fully and
export to PPTX. Verify text-as-text survival. If text flattens to
images, switch destination to HTML standalone or PDF and notify me
before continuing.
Goal: A 10-slide Series A pitch deck for a B2B SaaS observability
product
Layout (outline — per Sagnik Bhattacharya's 10-slide template at
https://sagnikbhattacharya.com/blog/claude-design):
1. Title
2. Problem
3. Solution
4. Market (TAM / SAM / SOM)
5. Product (2-3 visual demos)
6. Business model
7. Traction (revenue, growth, named customers)
8. Team
9. Competition
10. Ask
Content: Real numbers, real customer names, real founder names, real
competitive references. No lorem ipsum, no placeholder logos.
Audience: Series A investors at top-tier funds, ages 35-55, see 50+
decks per quarter, allergic to template fingerprints
Aesthetic family: editorial-confident — like Andreessen Horowitz pitch
decks meets Linear's design language. Authoritative,
no flourish, every visual element earns its place.
Color palette (CSS hex):
--color-bg: #FAFAF8
--color-surface: #FFFFFF
--color-muted: #6B6B6B
--color-fg: #2A2A2A
--color-ink: #0A0A0A
--color-accent: #1A3552
Typography: Söhne (preferred — concrete-named) or Inter Variable;
modular scale 1.333; weight palette 400 body / 600
emphasized / 700 slide titles
Corner radius: 0 (full-bleed slides), 4px on any inline container
Motion: none on static slides; ease-out 240ms on slide transitions
Density: comfortable — one job per slide, generous spacing
Surface: flat — full-bleed, no shadows
Slide composition rules:
- Each slide does one job
- Slide titles are claims, not topics ("$2.4B addressable market"
not "Market")
- Body text is 2 lines max per slide
- One number, chart, or visual element per slide max
- Speaker notes carry depth; slides carry the takeaway
Per-slide micro-prompt pattern (MindStudio, cited in research/03):
Generate slide N. Its one job: [name the job].
Visual elements: [specific to slide].
No filler — every element supports the one job.
Negative constraints — do not produce any of:
- Inter, Roboto, Arial, Space Grotesk as primary typeface
- Purple gradients on white backgrounds
- Three-column feature grid as a default slide structure
- Centered-title-and-subtitle on every slide
- Glassmorphism, neumorphism, gradient hero backgrounds
- Pulse / breathing animations or fly-in transitions
- Generic "investor pitch deck template" defaults
- Stock-photo placeholder imagery
If you find yourself defaulting to any AI-slop pattern (per
https://claude.com/blog/improving-frontend-design-through-skills),
stop and ask me to clarify before continuing.
Slop-fingerprints warning is amplified — investors recognise template
patterns. Over-specify the aesthetic to push the deck out of default
neighbourhood.
```
Expected follow-up turns:
1. Turn 1 (precondition): slide 1 + PPTX export validation. If text flattens, switch destination.
2. Turn 2: 10-slide outline approval
3. Turn 3-12: per-slide micro-prompt for each slide
4. Turn 13: full-deck render, cross-slide consistency check
5. Turn 14: Tweak panel for spacing and density adjustments
6. Export and visual-audit at full deck level
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — preset enumeration
- `https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks` — Anthropic's slides tutorial (composed for pitch-decks)
- `https://support.claude.com/en/articles/13521390-use-claude-for-powerpoint` — PowerPoint-mode conventions (relevant for PPTX export)
- `https://moda.app/blog/claude-design-for-pitch-decks` — community-documented PPTX export caveat (load-bearing)
- `https://claudedesign.substack.com` — community pattern reinforcing PPTX export caveat
- `https://sagnikbhattacharya.com/blog/claude-design` — community 10-slide pitch-deck template
- `https://claude.com/blog/improving-frontend-design-through-skills` — AI-slop avoid-list (amplified for pitch decks)
Re-research trigger: Anthropic publishing a pitch-decks-specific tutorial; PPTX export behaviour changing (text-as-text survival improving or worsening); community 10-slide template drifting; new investor-deck pattern emerging.

View file

@ -1,225 +0,0 @@
# Preset: prototypes
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
**Evidence grade:** Anthropic-documented + community-validated
**Captured-on date:** 2026-05-16
The `prototypes` intent preset generates interactive product flows for usability testing, design review, and stakeholder demos. Output is multi-screen HTML with working state transitions, clickable navigation, and per-state visual treatments. The preset is documented by Anthropic in a dedicated tutorial.
---
## (a) What this preset is
Anthropic frames `prototypes` (launch post `https://anthropic.com/news/claude-design-anthropic-labs`) as the preset for product flows that need to behave like a real product, not just look like one. The distinguishing property vs `designs`: interaction state. Prototypes have hover states, active states, click-through transitions, multi-screen navigation, and per-state visual treatments.
The dedicated tutorial is `https://claude.com/resources/tutorials/using-claude-design-for-prototypes-and-ux`. It is the load-bearing source for this preset.
Output is HTML + React + inline CSS + JS (the JS makes the interactions work). Exportable to Code-handoff (engineering takes the working interaction logic forward) or HTML standalone (runs in a browser without Claude Design).
---
## (b) When to use it
Pick `prototypes` when the goal is interactive validation. The decision matrix:
| Operator goal | Preset |
|---------------|--------|
| Feature flow for usability testing (5-user study) | **prototypes** |
| Internal tool demo for stakeholder review | **prototypes** |
| A-B comparison of two design directions in working form | **prototypes** |
| Onboarding flow walkthrough for new hire training | **prototypes** |
| Static design exploration (no interaction needed) | designs |
| Slide deck for a meeting | slides |
If the operator describes the artifact in terms of "user clicks here, then sees this", they want `prototypes`. If they describe it in terms of "screen with these regions", they may want `designs`.
---
## (c) Anthropic-published prompt patterns
The Anthropic tutorial `https://claude.com/resources/tutorials/using-claude-design-for-prototypes-and-ux` publishes nine canonical prompt patterns across four families:
### Family 1 — Feature prototyping (4 verbatim canonical prompts)
For new feature flows where the operator needs to test interaction logic. The patterns cover: a sign-in / sign-up multi-step flow; a checkout / payment flow with form validation states; a settings / preferences flow with toggles and selects; a search / filter flow with result-state transitions. Each prompt names entry point, success path, error states, and edge cases.
Refer to the tutorial for the verbatim prompt text — Anthropic publishes the exact wording, and this plugin cites it by URL rather than copying it (per the brief's source-quality rule and the Apache-2.0/MIT compatibility note in `../04-handoff-and-scope.md`).
### Family 2 — Design review and A-B comparison (2 verbatim canonical prompts)
For prototyping when the goal is to compare two design directions side-by-side or in turn. The verbatim Anthropic comparison-prompt pattern:
```
Show me three different layouts for [feature]. For each:
- Visual direction (named, with concrete reference)
- Interaction model (where the user starts, where they end up)
- One-line rationale tying it to the audience and goal
Once I pick one, generate the full interactive flow.
```
Use this for A-B-C exploration before committing to a direction. The pattern composes with layer 2b (propose-options-before-building) from `../01-prompt-fundamentals.md`.
### Family 3 — User-flow scaffolding (1 verbatim canonical prompt)
For mapping a multi-screen user journey. The pattern names the entry context, the screens in sequence, the decisions at each screen, and the success path vs the error paths. The output is a clickable multi-screen prototype with the navigation logic baked in.
### Family 4 — Internal tools (2 verbatim canonical prompts)
For internal-tooling prototypes — admin panels, content-moderation queues, customer-support consoles. The pattern emphasises dense interfaces, keyboard-driven navigation, and minimal aesthetic flourish. The patterns differ from external-product prototypes in tone and density.
### Component-naming clarity, decision documentation, edge-case flagging
The tutorial also publishes three transversal recommendations Anthropic asks operators to apply across all four families:
- **Component-naming clarity** — name components in the brief so the generated artifact's component spec is engineering-handoff-ready (research/03 D2). Generic names like "Button1" produce generic component specs.
- **Decision documentation** — ask Claude Design to document its design decisions inline (the PM-annotated notes feature) so the engineering handoff carries rationale, not just visuals.
- **Edge-case flagging** — explicitly request that Claude Design flag interaction edge cases (empty state, loading state, error state, offline state, permission-denied state). The model defaults to happy-path-only without this directive.
---
## (d) Community uplift
Three community-converged patterns extend Anthropic's published material for `prototypes`.
### Request every state upfront
Community pattern (`research/03`): explicitly request every interaction state in the first prompt rather than discovering missing states across iterations. The verbatim community phrasing:
```
For every interactive element in this prototype, generate:
- default state
- hover state
- active / pressed state
- focused state (keyboard navigation)
- disabled state
- loading state (if the element triggers async work)
- error state (if the element can fail)
Render every state visibly somewhere in the prototype — either inline
or in a dedicated state-catalogue page.
```
This catches the failure mode where the operator does not notice a missing state until a usability test surfaces it.
### Real-data injection over lorem ipsum
Same pattern as the `designs` preset, more important here: prototypes used in usability testing fail when the content is obviously fake. Test participants react to lorem ipsum and stop engaging with the flow. Use realistic content even when the prototype is throwaway.
### Explicit motion timing and easing
MindStudio community walkthrough (cited in `research/03`): name the easing curve and the duration explicitly for prototypes that include any motion. Default motion is the largest source of "feels like AI" in a prototype. The community-converged baselines for product prototypes:
- Hover transitions: `transition: all 160ms ease-out`
- Modal / drawer enter: `cubic-bezier(0.16, 1, 0.3, 1) 240ms`
- Modal / drawer exit: `cubic-bezier(0.7, 0, 0.84, 0) 180ms`
- Page transitions: `ease-out 280ms`
---
## (e) Critical caveats
Three caveats specific to `prototypes`.
### Interactive state count compounds context
A prototype with 10 components × 7 states each = 70 distinct visual treatments in one artifact. Each treatment consumes context. Claude Design quality drops faster on prototypes than on `designs` for the same number of screens. Session-break heuristics (`../03-iteration-and-session.md`) apply with extra weight.
### Test in target browsers before stakeholder review
The standalone HTML export runs the prototype's JavaScript locally. Edge-case JavaScript (touch handlers, IntersectionObserver, ResizeObserver) does not always work the same across browsers. Test in Chrome + Safari + Firefox before sharing with stakeholders. If you target mobile usability testing, test on actual mobile devices, not just a browser DevTools mobile-emulation viewport.
### Multi-screen exports — bundle in one export
The token-cost trap in `../04-handoff-and-scope.md` Section 6 applies most strongly to multi-screen prototypes. Bundle all screens in one export turn; do not export screen-by-screen.
---
## (f) One end-to-end worked prompt — layers 1 + 2a + 3 composed
Goal: a multi-step onboarding flow for a new SaaS analytics product, audience is small-business operators.
```
Goal: An interactive 5-step onboarding flow for new users of a SaaS
product. The flow: welcome → data-source connection → metric
selection → notification preferences → first-dashboard generation
Layout: Single-column centered, fixed step indicator at top, primary
CTA at bottom of each step, secondary "back" link to top-left
Content: Real product-facing copy (no lorem ipsum); step indicator
labels match the 5 steps verbatim; each step has a one-line
description below the step name; CTAs use action-verb naming
("Connect your data", "Select your metrics", etc.)
Audience: First-time users of a SaaS product, B2B small-business
operators, ages 30-55, comfortable with software but not
power-users
Aesthetic family: warm-confident — like Linear's onboarding, like
Notion's first-run, like Vercel's CLI prompts.
Approachable but tight; never playful.
Color palette (CSS hex):
--color-bg: #FAFAF8
--color-surface: #FFFFFF
--color-muted: #6B6B6B
--color-fg: #2A2A2A
--color-ink: #0A0A0A
--color-accent: #2D6356
--color-accent-hover: #1F4A41
--color-success: #2D6356
--color-warning: #C89B3F
--color-error: #B23A48
Typography: Söhne (preferred) or Inter Variable; modular scale 1.250;
weight palette 400 body / 500 emphasized / 600 headings
Corner radius: 6px on cards, 4px on buttons and inputs
Motion: transition: all 160ms ease-out on hover; cubic-bezier(0.16, 1,
0.3, 1) 240ms on step transitions
Density: comfortable (44px touch targets, 16px card padding)
Surface: subtle depth — 1px border + very subtle shadow on cards
Interaction states (render every one for every interactive element):
default, hover, active, focused, disabled, loading, error
Multi-screen requirements:
- Step 1: welcome — value prop in 2 sentences + Get Started CTA
- Step 2: data-source connection — list of 6 integrations with
connect buttons, hover states show "Connect" tooltip
- Step 3: metric selection — multi-select chip interface with 12
metric options, selection persists across step navigation
- Step 4: notification preferences — three toggle rows, with help-text
below each toggle
- Step 5: first-dashboard generation — loading state for 4-6 seconds,
then success state with "View Dashboard" CTA
Edge cases to flag:
- Step 2 connection failure (network error visible)
- Step 3 zero metrics selected (CTA disabled, help-text appears)
- Step 5 generation timeout (recovery CTA appears)
Negative constraints — do not produce any of:
- Inter, Roboto, Arial, or Space Grotesk as primary typeface
- Purple gradients on white backgrounds
- Centered-hero with single CTA (this is sequenced flow, not landing
page)
- Bouncy spring easing on hover
- Pulse animations on idle elements
- Generic "modern SaaS onboarding" template defaults
```
Expected follow-up turns:
1. Turn 2: refine motion easing if step transitions feel sluggish or jumpy
2. Turn 3: add layer 5 grading criteria (functionality 0.5, craft 0.3, design quality 0.2, originality 0)
3. Turn 4+: Tweak panel for density and color-temperature adjustments
4. Usability test surfaces missing states → iterate states via comments
5. Export bundle for engineering handoff once stakeholders sign off
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — preset enumeration
- `https://claude.com/resources/tutorials/using-claude-design-for-prototypes-and-ux` — verbatim canonical prompts (4 families, 9 prompts) + component-naming clarity + decision documentation + edge-case flagging recommendations
- `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` — GLCA framework
- `https://anthropic.com/engineering/harness-design-long-running-apps` — design grading criteria
- `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` — Design-Thinking Framework, AI-slop avoid-list
Re-research trigger: Anthropic updating the prototypes tutorial; new canonical prompt family added; component-naming-clarity / decision-documentation / edge-case-flagging recommendations materially revised.

View file

@ -1,237 +0,0 @@
# Preset: slides
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
**Evidence grade:** Anthropic-documented + community-validated
**Captured-on date:** 2026-05-16
The `slides` intent preset generates presentation decks — internal stakeholder updates, executive roadmaps, customer briefings, partner proposals, all-hands meetings. Output is HTML deck with per-slide layouts, optionally exportable to PPTX (with caveats — see Section e).
---
## (a) What this preset is
Anthropic launch post (`https://anthropic.com/news/claude-design-anthropic-labs`) names `slides` as a destination-shaped preset: the artifact assumes the slide-deck format, not the dashboard / one-pager / prototype format. Output renders in the Claude Design canvas as a slide-by-slide thumbnail strip plus the active slide in full view.
Two Anthropic primary sources ground this preset:
- The dedicated tutorial `https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks` publishes five verbatim canonical prompts (Section c) and the slide-deck composition framework.
- The PowerPoint-mode article `https://support.claude.com/en/articles/13521390-use-claude-for-powerpoint` publishes the PPTX export conventions and the template-respecting guidance: Claude reads the slide master, layouts, fonts, and color scheme of an uploaded PPTX template and produces output that respects them.
The two sources compose: the tutorial covers the prompt patterns, the PowerPoint-mode article covers the export discipline.
---
## (b) When to use it
Pick `slides` when the destination is a presentation surface. The decision matrix:
| Operator goal | Preset |
|---------------|--------|
| Internal team update / project review | **slides** |
| Customer-prep briefing for a sales call | **slides** |
| Executive roadmap or quarterly business review (Q1/Q2/Q3/Q4 results) | **slides** |
| Partner proposal / co-development pitch | **slides** |
| All-hands or company-wide announcement | **slides** |
| External investor pitch deck | **pitch-decks** (separate preset — see preset file for the PPTX trap) |
| Single-page memo / one-pager | one-pagers |
The distinguishing question vs `pitch-decks`: **is the audience internal or external?** Internal → `slides`. External investor → `pitch-decks` (with explicit caveat about PPTX export, see `pitch-decks.md`).
---
## (c) Anthropic-published prompt patterns
The Anthropic tutorial `https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks` publishes five verbatim canonical prompts:
### Pattern 1 — Q1 results deck
For quarterly business review decks (Q1 / Q2 / Q3 / Q4 results). Pattern names the metrics in priority order, the audience seniority level, the narrative arc (where we started, what changed, where we are, what's next), and the supporting visualisations (charts, tables, callout numbers).
### Pattern 2 — Executive roadmap
For multi-quarter roadmap decks. Pattern names the roadmap horizon (quarters or half-years), the workstreams (3-7 named tracks), the major milestones per workstream, the dependencies between workstreams, and the assumptions / risks per quarter.
### Pattern 3 — Customer-prep briefing
For sales-call preparation decks. Pattern names the customer (company + named contacts), the meeting goal, the customer's known priorities, the value-prop alignment, the proof points (case studies, metrics), and the asks / next steps.
### Pattern 4 — Partner proposal
For co-development or partnership proposals. Pattern names the proposed scope, the resourcing model, the timeline, the success metrics, the IP / licensing model, and the open questions.
### Pattern 5 — All-hands announcement
For company-wide updates. Pattern names the announcement (one sentence), the why-now context, the impact on employees, the timeline, and the resources / Q&A links.
Refer to the tutorial URL for the verbatim prompt text. This plugin cites by URL rather than reproducing Anthropic's exact wording (per the brief's source-quality rule and the Apache-2.0/MIT compatibility note in `../04-handoff-and-scope.md`).
### Template-respecting guidance (verbatim from PowerPoint-mode article)
`https://support.claude.com/en/articles/13521390-use-claude-for-powerpoint` publishes the verbatim guidance about uploading an existing PPTX template:
> Claude reads the slide master, layouts, fonts, and color scheme of an uploaded PowerPoint template and produces output that respects them.
Practical implication: if the operator has an existing brand-compliant PPTX template, upload it as a Claude Design project asset before prompting. The generated deck will respect the template's typography, color palette, and layout conventions. Without an uploaded template, the model defaults to its convergent middle-ground deck aesthetic.
---
## (d) Community uplift
Three community-converged patterns extend Anthropic's material for `slides`.
### Outline-first narrative scaffolding
Community pattern (`research/03`): brief the deck as an outline first (turn 1: produce the slide-by-slide outline as a markdown bullet list), then expand to full slides (turn 2: generate the deck from the approved outline). This forks the conversation but produces tighter narrative arcs than briefing the full deck in one turn.
The outline-first pattern composes with Anthropic's GLCA framework (`../01-prompt-fundamentals.md` Layer 1) — Goal becomes the deck's takeaway, Layout becomes the outline, Content becomes the per-slide bullets, Audience becomes the seniority + context match.
### Single-job-per-slide constraint
Community pattern (research/03): each slide should communicate exactly one idea. Slides with two or more ideas leak comprehension. Brief the constraint explicitly:
```
Each slide does one job. If a slide is trying to communicate two ideas,
split it into two slides. The takeaway from each slide should be
nameable in one sentence.
```
### Audience translation matrix
Community pattern (research/03): brief the audience translation explicitly when the deck spans seniority levels. For example, a roadmap deck shared with both engineering leads and executive sponsors needs to translate technical decisions into business outcomes for the exec audience without losing fidelity for the engineering audience. The pattern:
```
For each slide, write two versions of the takeaway:
- The technical-detail version (for the engineering audience)
- The business-outcome version (for the executive audience)
Use the business-outcome version on the slide and the technical-detail
version in the speaker notes.
```
---
## (e) Critical caveats
Three caveats specific to `slides`.
### HTML → PPTX export is lossy for richly-styled text
Community-documented at `https://moda.app/blog/claude-design-for-pitch-decks` (cited in research/03) and `https://claudedesign.substack.com`: when the operator exports an HTML-rendered slide deck to PPTX, richly-styled text (custom typography, mixed weights, inline color variations) can flatten to images. PowerPoint loses the editability — the text becomes a rasterised picture.
The mitigation:
- If the destination is final PPTX delivery, validate the rendered PPTX text-as-text count before assuming editability survived
- If text-as-text is critical (legal review, copy-edit-after-the-fact), keep the typographic styling simple in the brief — single typeface, two weights, no inline color variation, no inline highlighting
- For the `pitch-decks` preset specifically, this caveat is severe enough that the recommendation is "do not use Claude Design for external pitch decks where PPTX is required" — see `pitch-decks.md`
### Don't trust the canvas as ground truth if destination is PPTX
The Claude Design canvas renders the deck in HTML. The PPTX export converts to PowerPoint format. Some aesthetic decisions that look correct in the canvas do not survive the export:
- Custom backgrounds with gradients can rasterize
- Inline icons positioned via CSS can shift
- Multi-column slide layouts can collapse
- Speaker-notes-equivalent annotations may not round-trip
Validate by opening the exported PPTX in PowerPoint before stakeholder delivery, especially for high-stakes decks.
### Quota burn on long decks
Multi-slide decks (10+ slides) compound context faster than single-page artifacts. The 4-screen inflection in `../03-iteration-and-session.md` applies — long decks reach the inflection within 2-3 chat turns. Plan to break the deck into outline → first 5 slides → next 5 slides if the deck is large, using the context-reset prompt between sessions.
---
## (f) One end-to-end worked prompt — layers 1 + 2a + 3 composed
Goal: a 12-slide internal-team Q1 results deck, audience is engineering leadership + cross-functional partners.
```
Goal: A 12-slide Q1 2026 results deck covering platform-team delivery,
reliability metrics, headcount and hiring, top 3 themes for Q2,
and one slide on a major incident retrospective
Layout (outline):
1. Title — "Platform team Q1 2026 results"
2. TL;DR — three bullet takeaways
3. Delivery — features shipped, % of roadmap completed
4. Reliability — uptime, MTTR, incident count
5. Latency — p95/p99 trend
6. Hiring — headcount delta, key hires, open roles
7. Top theme 1 for Q2 — name + one-sentence framing
8. Top theme 2 for Q2 — name + one-sentence framing
9. Top theme 3 for Q2 — name + one-sentence framing
10. Incident retrospective — what happened, what we learned
11. Asks — explicit asks of leadership and partners
12. Q&A / Discussion prompt
Content: Real numbers throughout — actual % completion, real MTTR
minutes, real headcount, real names for hires (or named
placeholders); each slide's takeaway nameable in one sentence
Audience: VP of Engineering, peer Eng-leadership, partner-team PMs,
partner-team Eng-leads — mixed seniority, mixed technical
depth, ages 30-55
Aesthetic family: editorial-confident — like The New York Times opinion
section meets Linear's design language. Clean,
authoritative, no flourish. Each slide reads like a
well-edited paragraph.
Color palette (CSS hex):
--color-bg: #FAFAF8
--color-surface: #FFFFFF
--color-muted: #6B6B6B
--color-fg: #2A2A2A
--color-ink: #0A0A0A
--color-accent: #3D5C8A
--color-success: #2D6356
--color-warning: #C89B3F
--color-error: #B23A48
Typography: Söhne or Inter Variable; modular scale 1.333 (perfect
fourth — slides scale up); weight palette 400 body / 600
emphasized / 700 slide titles
Corner radius: 4px on any card-like containers; slides themselves
have no corner radius (full-bleed)
Motion: none on static slides; ease-out 240ms on slide transitions
Density: comfortable — generous spacing, one job per slide
Surface: flat — full-bleed slides, no shadows, single subtle accent
line under slide title
Slide composition rules:
- Each slide does one job
- Slide titles are claims, not topics ("We shipped 87% of roadmap"
not "Roadmap delivery")
- Body text is 2-3 lines max per slide
- One number, chart, or visual element per slide max
- Speaker notes carry the depth; slides carry the takeaway
Negative constraints — do not produce any of:
- Inter, Roboto, Arial, Space Grotesk as primary typeface
- Purple gradients on white backgrounds
- Three-bullet-and-image generic slide template
- Pulse animations or fly-in transitions
- Generic "corporate deck" template defaults (centered title-and-
subtitle on every slide)
- Multi-column slides with more than two ideas per slide
If the destination is PPTX, ensure text-heavy slides keep text as text
(not rasterized). Keep typography simple to maximise text-as-text
survival in export.
```
Expected follow-up turns:
1. Turn 2: outline-first review — confirm the 12-slide structure, adjust ordering, swap titles if needed
2. Turn 3: add audience translation per slide (speaker notes vs slide takeaway)
3. Turn 4: render full deck against the approved outline
4. Turn 5+: Tweak panel for typography scale and spacing; comments for slide-by-slide refinements
5. Export validation: open PPTX in PowerPoint and audit text-as-text vs rasterized
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — preset enumeration
- `https://claude.com/resources/tutorials/using-claude-design-for-presentations-and-slide-decks` — verbatim canonical prompts (5 patterns), slide-deck composition framework
- `https://support.claude.com/en/articles/13521390-use-claude-for-powerpoint` — PowerPoint-mode conventions, template-respecting guidance
- `https://support.claude.com/en/articles/14604416-get-started-with-claude-design` — GLCA framework
- `https://moda.app/blog/claude-design-for-pitch-decks` — community-documented PPTX export caveat (cited)
- `https://anthropic.com/engineering/harness-design-long-running-apps` — design grading criteria
Re-research trigger: Anthropic updating the slides tutorial; new canonical pattern added; PowerPoint-mode conventions revised; PPTX export behaviour changing materially.

View file

@ -1,163 +0,0 @@
# Preset: wireframes-mockups
**Last updated:** 2026-05-17 | **Verified:** research/03-prompt-patterns-intent-presets.md
Evidence grade: Community-only — Anthropic publishes no per-preset prompt patterns for this preset as of 2026-05-16.
Anthropic names `wireframes-mockups` in the launch enumeration at `https://anthropic.com/news/claude-design-anthropic-labs` but publishes no dedicated tutorial, support article, or canonical prompt set for it. The patterns below come from community practitioners; treat them as field-tested but not Anthropic-authoritative.
---
## (a) What this preset is
Anthropic launch post one-sentence description: `wireframes-mockups` covers the spectrum from low-fidelity layout sketches (boxes, labels, structure-only) to high-fidelity mockups (visual design applied, but not interactive). The output is structural — the goal is to communicate layout, not aesthetic and not interaction.
Distinguishing properties:
- Static (not interactive) — wireframes and mockups do not have working state transitions; for interaction logic, use `prototypes`
- Low-fi or high-fi — the preset spans both ends; the operator picks via prompt
- Pre-visual-design — wireframes are often deliverables before the visual designer commits to a direction
- Iteration-cheap — wireframes are intended to be iterated quickly, so the prompt patterns lean on N-variations-first generation
---
## (b) Why Anthropic published no per-preset guidance
Wireframes occupy a niche between `designs` (visual exploration) and `prototypes` (interaction validation). Anthropic appears to treat the preset as a destination shape rather than a distinct generation mode. The Goal / Layout / Content / Audience framework (`../01-prompt-fundamentals.md` Layer 1) applies — Layout is the dominant concern, Content becomes structural labels, Audience determines fidelity level.
Community practitioners have converged on the patterns below (Section c).
---
## (c) Community patterns
### N-variations-first
Community pattern from `https://designwithai.substack.com` (cited in `research/03`): wireframes are most useful when generated in N variations and compared. Brief the model to produce N distinct layout variations in the first turn, then pick one to refine.
Convergent N values from community practice: 3 or 4 variations is the sweet spot. More than 4 dilutes the operator's attention; fewer than 3 does not surface meaningful alternatives.
The brief pattern:
```
Generate 4 distinct wireframe variations for [feature/page]. For each:
- One sentence describing the structural direction
- The wireframe itself (boxes, labels, no visual design)
After I pick one, refine that variation into a mockup with visual
design applied.
```
This composes with Layer 2b (propose-options-before-building) from `../01-prompt-fundamentals.md`.
### Wireframe vs High-Fidelity sub-preset selection
Community pattern from `https://computingforgeeks.com` (cited in `research/03`): the preset spans low-fi to high-fi, but the model behaves differently across the spectrum. Brief the fidelity level explicitly:
```
Fidelity: low-fi
- boxes with labels, no typography weights other than 500
- one color (greyscale) — bg, surface, muted, fg
- no images, no icons — represent visual elements as labelled boxes
- 8pt grid visible if helpful
OR
Fidelity: high-fi
- actual typography, full color palette, real icons, real images
- production-ready visual treatment
- no interaction logic (this is wireframes preset, not prototypes)
```
Pick one explicitly. The default-mode failure is the model producing a mid-fidelity output that satisfies neither the low-fi structural goal nor the high-fi visual-validation goal.
### The Aakashg / Nielsen "low-fi-is-deprecated" debate (flagged as unsettled)
A community debate documented across Aakash Gupta's and Jakob Nielsen's posts in 2024-2025 (cited in `research/03`) argues that AI-generated high-fi mockups have made low-fi wireframes operationally obsolete — the marginal cost of generating a high-fi mockup is now low enough that there is no reason to start with low-fi. The counter-argument: low-fi wireframes still serve a communication function (forcing reviewers to focus on structure, not aesthetic) that high-fi mockups undermine.
This plugin treats the debate as **unsettled**. The brief should pick the fidelity level deliberately, with the choice tied to the audience and the review purpose, not to a default assumption that one fidelity dominates. Flag the debate when the operator's choice seems unconsidered.
---
## (d) Critical caveats
### Aesthetic drift if starting in high-fi
When starting in high-fidelity mode, the model imports its convergent middle-ground aesthetic defaults more aggressively (because the visual decisions are within scope). Layer-3 negative constraints (`../01-prompt-fundamentals.md`) apply with extra weight on high-fi mockups.
### Iteration economy — wireframes burn turns
Each variation requested in the N-variations-first pattern costs a fraction of a turn (the model generates all N in one chat round). But subsequent refinement of the chosen variation often requires multiple turns (typography decisions, color palette, component-level styling). Budget accordingly — a wireframe-to-mockup flow can consume 4-6 turns for a single page.
### Wireframe ≠ prototype
If the operator describes user interactions ("the user clicks here, then sees this"), they want `prototypes`, not `wireframes-mockups`. Wireframes capture structure; prototypes capture behaviour. Misclassification leads to wasted turns regenerating an artifact in the wrong preset.
---
## (e) One worked prompt — layers 1 + 3 composed, N-variations-first pattern
Goal: 4 wireframe variations for a customer-onboarding page, audience is product team for review.
```
Goal: 4 distinct wireframe variations for the first page of a customer
onboarding flow. The page introduces the product, captures
essential information, and routes the customer to one of three
paths (self-serve, sales-assisted, partner-handoff).
Layout: Single page, viewport ~1440x900. Each variation lays out the
same content differently.
Content: Real placeholder content — actual headlines, actual button
labels, actual form field labels. No lorem ipsum.
Audience: Internal product team (PM, design lead, eng lead) reviewing
structure choices before committing to a direction
Fidelity: low-fi (community pattern from
https://computingforgeeks.com — fidelity affects iteration
path)
- boxes with labels, no typography weights other than 500
- greyscale only (bg, surface, muted, fg)
- no images, no icons — labelled boxes
- 8pt grid visible
N-variations-first (community pattern from
https://designwithai.substack.com):
Generate 4 distinct wireframe variations. For each variation:
- One-sentence description of the structural direction (e.g.,
"Top-down narrative — story first, paths second")
- The wireframe itself
- One-line rationale tying the structure to the audience and goal
The 4 variations should be meaningfully distinct from each other —
not minor tweaks of one base layout.
After I pick one, generate a fifth output: a refined mockup of the
chosen variation, transitioning fidelity from low-fi to medium-fi.
Negative constraints (Anthropic AI-slop avoid-list):
- Inter, Roboto, Arial, Space Grotesk as primary typeface (the
fidelity-low constraint covers most of this, but flag explicitly)
- Purple gradients (low-fi means greyscale anyway)
- Three-column feature grid as the default structural pattern
- Centered-hero with single CTA as the default
- Cookie-cutter framing
```
Expected follow-up turns:
1. Turn 1: 4 wireframe variations generated
2. Turn 2: operator picks variation, refined low-fi mockup generated
3. Turn 3: aesthetic family applied (full layer-2a brief), medium-fi mockup
4. Turn 4: layer-4 dimensions applied (typography modular scale, color palette, component stylings)
5. Turn 5+: Tweak panel for spacing and density adjustments
---
## Sources
- `https://anthropic.com/news/claude-design-anthropic-labs` — preset enumeration, one-sentence description
- `https://designwithai.substack.com` — community pattern: N-variations-first
- `https://computingforgeeks.com` — community pattern: explicit Wireframe-vs-High-Fidelity fidelity selection
- `https://claude.com/blog/improving-frontend-design-through-skills` — AI-slop avoid-list (composed for high-fi mode)
- `https://github.com/anthropics/skills/skills/frontend-design/SKILL.md` — Design-Thinking Framework reference
Re-research trigger: Anthropic publishing a dedicated wireframes-mockups tutorial; the Aakashg/Nielsen low-fi-is-deprecated debate reaching practitioner consensus; new sub-fidelity tier surfacing in community practice.

View file

@ -1,203 +0,0 @@
#!/usr/bin/env bash
# test-sc1-dogfood-log.sh — Verifies SC1 (operator-attested dogfood log) in REMEMBER.md
#
# Usage:
# bash tests/test-sc1-dogfood-log.sh # missing block = WARN, exit 0
# bash tests/test-sc1-dogfood-log.sh --strict # missing block = FAIL, exit 1
#
# Expects in REMEMBER.md (plugin root, gitignored):
# - A fenced section with heading `## Dogfood log — v0.1 slides run`
# - Five mechanically-checkable fields inside the section:
# artifact_type: <preset-name from .coverage.md>
# refine_rounds: <integer>
# final_prompt:
# ```
# <non-empty prompt content>
# ```
# shipped: yes (or `shipped: equivalent`)
# comparison_to_unaided: <one sentence ending with .>
#
# REMEMBER.md is gitignored — this evidence is local-only. The script
# validates format only; the outcome judgement is operator-attested.
set -euo pipefail
LC_ALL=en_US.UTF-8
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
FAIL=0
WARN=0
pass() { printf "${GREEN} ✓ %s${NC}\n" "$1"; PASS=$((PASS + 1)); }
fail() { printf "${RED} ✗ %s${NC}\n" "$1"; FAIL=$((FAIL + 1)); }
warn() { printf "${YELLOW} ⚠ %s${NC}\n" "$1"; WARN=$((WARN + 1)); }
STRICT=false
for arg in "$@"; do
case "$arg" in
--strict) STRICT=true ;;
esac
done
echo "=== test-sc1-dogfood-log ==="
echo "Plugin root: $PLUGIN_ROOT"
echo "Strict mode: $STRICT"
echo ""
REMEMBER_FILE="$PLUGIN_ROOT/REMEMBER.md"
COVERAGE_FILE="$PLUGIN_ROOT/.coverage.md"
# -------------------------------------------------------
# Locate REMEMBER.md
# -------------------------------------------------------
if [ ! -f "$REMEMBER_FILE" ]; then
if $STRICT; then
fail "REMEMBER.md missing (strict mode — required)"
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
exit 1
else
warn "REMEMBER.md missing (advisory until operator dogfood step)"
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
exit 0
fi
fi
# -------------------------------------------------------
# Extract fenced block between dogfood heading and next H2 (or EOF)
# -------------------------------------------------------
BLOCK="$(awk '
/^## Dogfood log — v0\.1 slides run$/ { capture = 1; next }
capture && /^## / { exit }
capture { print }
' "$REMEMBER_FILE")"
if [ -z "$BLOCK" ]; then
if $STRICT; then
fail "REMEMBER.md missing dogfood block '## Dogfood log — v0.1 slides run' (strict mode — required)"
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
exit 1
else
warn "REMEMBER.md missing dogfood block (advisory until operator dogfood step)"
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
exit 0
fi
fi
pass "found '## Dogfood log — v0.1 slides run' block"
# -------------------------------------------------------
# Field 1: artifact_type — must match a preset name from .coverage.md
# -------------------------------------------------------
ARTIFACT_TYPE="$(printf '%s\n' "$BLOCK" | awk -F': *' '/^artifact_type:/ { print $2; exit }' | tr -d '[:space:]')"
if [ -z "$ARTIFACT_TYPE" ]; then
fail "artifact_type: field missing or empty"
else
# extract preset names from .coverage.md table column 1
if [ -f "$COVERAGE_FILE" ]; then
PRESETS="$(awk -F'|' '
/^\| [a-z]/ { gsub(/^ +| +$/, "", $2); print $2 }
' "$COVERAGE_FILE")"
FOUND=false
while IFS= read -r preset; do
[ -z "$preset" ] && continue
if [ "$preset" = "$ARTIFACT_TYPE" ]; then
FOUND=true
break
fi
done < <(printf '%s\n' "$PRESETS")
if $FOUND; then
pass "artifact_type='$ARTIFACT_TYPE' matches a preset in .coverage.md"
else
fail "artifact_type='$ARTIFACT_TYPE' does not match any preset in .coverage.md"
fi
else
fail ".coverage.md missing — cannot validate artifact_type"
fi
fi
# -------------------------------------------------------
# Field 2: refine_rounds — integer
# -------------------------------------------------------
REFINE_ROUNDS_LINE="$(printf '%s\n' "$BLOCK" | grep -E '^refine_rounds:[[:space:]]*[0-9]+[[:space:]]*$' || true)"
if [ -n "$REFINE_ROUNDS_LINE" ]; then
pass "refine_rounds: matches integer regex"
else
fail "refine_rounds: missing or not an integer"
fi
# -------------------------------------------------------
# Field 3: final_prompt: followed by non-empty fenced code block
# -------------------------------------------------------
HAS_FINAL_PROMPT="$(printf '%s\n' "$BLOCK" | grep -c '^final_prompt:' || true)"
if [ "$HAS_FINAL_PROMPT" -ge 1 ]; then
# check that a fenced code block (```) appears after final_prompt:
FENCE_AFTER="$(awk '
/^final_prompt:/ { found = 1; next }
found && /^```/ { fence_open = !fence_open; if (fence_open) { in_fence = 1 } else { exit } }
found && in_fence && fence_open && /./ { content_lines++ }
END { print content_lines + 0 }
' <<<"$BLOCK")"
if [ -z "$FENCE_AFTER" ]; then FENCE_AFTER=0; fi
if [ "$FENCE_AFTER" -ge 1 ]; then
pass "final_prompt: followed by non-empty fenced code block ($FENCE_AFTER content line(s))"
else
fail "final_prompt: not followed by a non-empty fenced code block"
fi
else
fail "final_prompt: field missing"
fi
# -------------------------------------------------------
# Field 4: shipped — yes or equivalent
# -------------------------------------------------------
SHIPPED_LINE="$(printf '%s\n' "$BLOCK" | grep -E '^shipped:[[:space:]]*(yes|equivalent)[[:space:]]*$' || true)"
if [ -n "$SHIPPED_LINE" ]; then
pass "shipped: matches 'yes' or 'equivalent'"
else
fail "shipped: missing or not 'yes'/'equivalent'"
fi
# -------------------------------------------------------
# Field 5: comparison_to_unaided — non-empty sentence >=10 chars ending with .
# -------------------------------------------------------
COMP_LINE="$(printf '%s\n' "$BLOCK" | awk -F': *' '/^comparison_to_unaided:/ { for (i=2;i<=NF;i++) printf "%s%s", $i, (i<NF?": ":""); print ""; exit }')"
COMP_TRIMMED="$(printf '%s' "$COMP_LINE" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
COMP_LEN="${#COMP_TRIMMED}"
if [ -z "$COMP_TRIMMED" ]; then
fail "comparison_to_unaided: field missing or empty"
elif [ "$COMP_LEN" -lt 10 ]; then
fail "comparison_to_unaided: too short ($COMP_LEN chars; need >=10)"
elif [ "${COMP_TRIMMED: -1}" != "." ]; then
fail "comparison_to_unaided: does not end with '.'"
else
pass "comparison_to_unaided: non-empty, $COMP_LEN chars, ends with '.'"
fi
# -------------------------------------------------------
# Summary
# -------------------------------------------------------
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0

View file

@ -1,100 +0,0 @@
#!/usr/bin/env bash
# test-sc2-artifact-coverage.sh — Verifies SC2 (per-preset coverage)
#
# Reads .coverage.md, extracts preset names from the table column 1,
# for each preset runs:
# grep -rli "<preset>" plugins/claude-design/ --include='*.md' \
# --exclude-dir='.claude' --exclude-dir='tests'
# and asserts ≥1 file hit.
#
# The preset list is NOT hardcoded — auto-adapts when .coverage.md changes.
#
# Usage: bash tests/test-sc2-artifact-coverage.sh
# Exit codes: 0 = all presets covered; 1 = at least one preset uncovered
set -euo pipefail
LC_ALL=en_US.UTF-8
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
FAIL=0
WARN=0
pass() { printf "${GREEN} ✓ %s${NC}\n" "$1"; PASS=$((PASS + 1)); }
fail() { printf "${RED} ✗ %s${NC}\n" "$1"; FAIL=$((FAIL + 1)); }
warn() { printf "${YELLOW} ⚠ %s${NC}\n" "$1"; WARN=$((WARN + 1)); }
COVERAGE_FILE="$PLUGIN_ROOT/.coverage.md"
echo "=== test-sc2-artifact-coverage ==="
echo "Plugin root: $PLUGIN_ROOT"
echo ".coverage.md: $COVERAGE_FILE"
echo ""
if [ ! -f "$COVERAGE_FILE" ]; then
fail ".coverage.md missing — cannot verify SC2"
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
exit 1
fi
# -------------------------------------------------------
# Extract preset names from .coverage.md table column 1
# -------------------------------------------------------
# Table rows look like:
# | designs | skills/.../presets/designs.md | Evidence grade: ... | https://... |
# Skip header (| Preset | ...) and separator (| --- | ...).
PRESETS="$(awk -F'|' '
/^\| / && NR > 1 {
name = $2
gsub(/^ +| +$/, "", name)
if (name != "Preset" && name !~ /^-+$/ && name != "") {
print name
}
}
' "$COVERAGE_FILE")"
if [ -z "$PRESETS" ]; then
fail "no preset names extracted from .coverage.md"
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
exit 1
fi
# -------------------------------------------------------
# For each preset, grep for at least one file hit in plugin content
# -------------------------------------------------------
while IFS= read -r preset; do
[ -z "$preset" ] && continue
HITS="$(grep -rli "$preset" "$PLUGIN_ROOT" \
--include='*.md' \
--exclude-dir='.claude' \
--exclude-dir='tests' \
2>/dev/null || true)"
HIT_COUNT="$(printf '%s\n' "$HITS" | grep -c '.' || true)"
if [ -z "$HIT_COUNT" ]; then HIT_COUNT=0; fi
if [ "$HIT_COUNT" -ge 1 ]; then
pass "preset '$preset' covered by $HIT_COUNT file(s)"
else
fail "preset '$preset' has zero file hits in plugin content"
fi
done < <(printf '%s\n' "$PRESETS")
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0

View file

@ -1,123 +0,0 @@
#!/usr/bin/env bash
# test-sc3-citations.sh — Verifies SC3 (Anthropic-domain citation discipline)
#
# Two checks:
# Negative — grep -rnE '\[CITE\]|\[verify\]|\baccording to\b' against
# shipped content. Zero hits expected.
# Positive — read .coverage.md "Authoritative-claims" bullet list
# (awk on '^- ' prefix), then for each file ensure ≥1
# Anthropic-domain URL citation is present.
#
# Excludes .claude/projects/** and tests/ from greps.
#
# Anthropic-domain URL regex (positive):
# https?://(docs\.anthropic\.com|anthropic\.com|github\.com/anthropics
# |claude\.com|support\.claude\.com|platform\.claude\.com)
#
# Usage: bash tests/test-sc3-citations.sh
# Exit codes: 0 = pass; 1 = at least one FAIL
set -euo pipefail
LC_ALL=en_US.UTF-8
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
FAIL=0
WARN=0
pass() { printf "${GREEN} ✓ %s${NC}\n" "$1"; PASS=$((PASS + 1)); }
fail() { printf "${RED} ✗ %s${NC}\n" "$1"; FAIL=$((FAIL + 1)); }
warn() { printf "${YELLOW} ⚠ %s${NC}\n" "$1"; WARN=$((WARN + 1)); }
COVERAGE_FILE="$PLUGIN_ROOT/.coverage.md"
echo "=== test-sc3-citations ==="
echo "Plugin root: $PLUGIN_ROOT"
echo ""
# -------------------------------------------------------
# Negative grep: \[CITE\], \[verify\], \baccording to\b
# -------------------------------------------------------
echo "--- negative grep: forbidden placeholders ---"
NEG_HITS="$(grep -rnE '\[CITE\]|\[verify\]|\baccording to\b' \
"$PLUGIN_ROOT" \
--include='*.md' \
--exclude-dir='.claude' \
--exclude-dir='tests' \
2>/dev/null || true)"
if [ -z "$NEG_HITS" ]; then
pass "no forbidden placeholders ([CITE], [verify], 'according to') in shipped content"
else
while IFS= read -r hit; do
fail "forbidden placeholder in shipped content: $hit"
done < <(printf '%s\n' "$NEG_HITS")
fi
echo ""
# -------------------------------------------------------
# Positive grep: Authoritative-claims files have Anthropic-domain URLs
# -------------------------------------------------------
echo "--- positive grep: Authoritative-claims citation coverage ---"
if [ ! -f "$COVERAGE_FILE" ]; then
fail ".coverage.md missing — cannot read Authoritative-claims registry"
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
exit 1
fi
# Extract bullet-list paths under the "Authoritative-claims" section
AUTH_FILES="$(awk '
/^## Authoritative-claims files/ { capture = 1; next }
capture && /^## / { exit }
capture && /^- / {
sub(/^- /, "", $0)
print $0
}
' "$COVERAGE_FILE")"
if [ -z "$AUTH_FILES" ]; then
fail "no Authoritative-claims files extracted from .coverage.md"
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
exit 1
fi
ANTHROPIC_REGEX='https?://(docs\.anthropic\.com|anthropic\.com|github\.com/anthropics|claude\.com|support\.claude\.com|platform\.claude\.com)'
while IFS= read -r relpath; do
[ -z "$relpath" ] && continue
fpath="$PLUGIN_ROOT/$relpath"
if [ ! -f "$fpath" ]; then
fail "Authoritative-claims file missing: $relpath"
continue
fi
HIT_COUNT="$(grep -cE "$ANTHROPIC_REGEX" "$fpath" || true)"
if [ -z "$HIT_COUNT" ]; then HIT_COUNT=0; fi
if [ "$HIT_COUNT" -ge 1 ]; then
pass "$relpath: $HIT_COUNT Anthropic-domain URL citation(s)"
else
fail "$relpath: zero Anthropic-domain URL citations (anthropic.com expected)"
fi
done < <(printf '%s\n' "$AUTH_FILES")
echo ""
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0

View file

@ -1,122 +0,0 @@
#!/usr/bin/env bash
# test-skill-triggers.sh — Verifies skill description quality
#
# Honest limit: this only verifies strings are present in SKILL.md description.
# It cannot prove Claude Code's orchestrator fires the skill on those prompts.
# Runtime auto-fire validation is the operator's dogfood step (SC1).
#
# Checks:
# - SKILL.md frontmatter has 'description:' field
# - description block (from `description: |` to the closing `---`) is >=400 chars
# - if .triggers.txt exists, every phrase in it appears in SKILL.md description
#
# Usage: bash tests/test-skill-triggers.sh
# Exit codes: 0 = pass; 1 = at least one FAIL
set -euo pipefail
LC_ALL=en_US.UTF-8
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
FAIL=0
WARN=0
pass() { printf "${GREEN} ✓ %s${NC}\n" "$1"; PASS=$((PASS + 1)); }
fail() { printf "${RED} ✗ %s${NC}\n" "$1"; FAIL=$((FAIL + 1)); }
warn() { printf "${YELLOW} ⚠ %s${NC}\n" "$1"; WARN=$((WARN + 1)); }
echo "=== test-skill-triggers ==="
echo "Plugin root: $PLUGIN_ROOT"
echo ""
# -------------------------------------------------------
# Iterate over every SKILL.md
# -------------------------------------------------------
SKILL_COUNT=0
for skill_file in "$PLUGIN_ROOT"/skills/*/SKILL.md; do
[ -f "$skill_file" ] || continue
SKILL_COUNT=$((SKILL_COUNT + 1))
skill_dir="$(dirname "$skill_file")"
skill_name="$(basename "$skill_dir")"
echo "--- $skill_name ---"
# ------------------------
# Frontmatter check
# ------------------------
first_line="$(head -n 1 "$skill_file")"
if [ "$first_line" != "---" ]; then
fail "$skill_name/SKILL.md: missing frontmatter delimiter on line 1"
echo ""
continue
fi
# ------------------------
# Description >=400 chars (Triggers on: enumeration counts)
# ------------------------
desc_block_chars="$(awk '/^description: \|/,/^---$/' "$skill_file" | wc -c | tr -d '[:space:]')"
if [ -z "$desc_block_chars" ]; then desc_block_chars=0; fi
if [ "$desc_block_chars" -ge 400 ]; then
pass "$skill_name: description block is $desc_block_chars chars (>=400)"
else
fail "$skill_name: description block is $desc_block_chars chars (<400 required)"
fi
# ------------------------
# Trigger-phrase coverage (.triggers.txt)
# ------------------------
triggers_file="$skill_dir/.triggers.txt"
if [ ! -f "$triggers_file" ]; then
warn "$skill_name: .triggers.txt missing (advisory — operator may want one)"
echo ""
continue
fi
trigger_count="$(grep -cE '.' "$triggers_file" || true)"
if [ -z "$trigger_count" ] || [ "$trigger_count" -lt 8 ]; then
fail "$skill_name/.triggers.txt: only $trigger_count phrase(s) (>=8 required)"
else
pass "$skill_name/.triggers.txt: $trigger_count phrase(s)"
fi
# check each phrase appears in SKILL.md description block
MISSING=0
while IFS= read -r phrase; do
[ -z "$phrase" ] && continue
if ! grep -qF "$phrase" "$skill_file"; then
fail "$skill_name: trigger phrase missing from SKILL.md description: '$phrase'"
MISSING=$((MISSING + 1))
fi
done < "$triggers_file"
if [ "$MISSING" -eq 0 ]; then
pass "$skill_name: all $trigger_count trigger phrase(s) appear in SKILL.md"
fi
echo ""
done
if [ "$SKILL_COUNT" -eq 0 ]; then
fail "no SKILL.md found under skills/*/"
fi
# -------------------------------------------------------
# Summary
# -------------------------------------------------------
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0
# Triggers on: documented in each skill's .triggers.txt sibling file.

View file

@ -1,265 +0,0 @@
#!/usr/bin/env bash
# validate-plugin.sh — Foundation plugin structure validator for claude-design
# Usage: bash tests/validate-plugin.sh
# Exit codes: 0 = all checks pass; 1 = at least one FAIL
#
# Forked from plugins/ms-ai-architect/tests/validate-plugin.sh:
# keep: helpers (pass/fail/warn), counters, PLUGIN_ROOT, JSON-validity check,
# README/CLAUDE.md existence checks
# strip: agent frontmatter loop, commands frontmatter loop, KB-staleness checks,
# architect:* command-name assertions, references-count assertions
# add: SKILL.md frontmatter + description-length, LICENSE content, GOVERNANCE
# existence, .coverage.md existence, forbidden-command-name regex (h),
# operator-private-context grep (i), Norwegian-leakage grep (j)
set -euo pipefail
LC_ALL=en_US.UTF-8
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
FAIL=0
WARN=0
pass() { printf "${GREEN} ✓ %s${NC}\n" "$1"; PASS=$((PASS + 1)); }
fail() { printf "${RED} ✗ %s${NC}\n" "$1"; FAIL=$((FAIL + 1)); }
warn() { printf "${YELLOW} ⚠ %s${NC}\n" "$1"; WARN=$((WARN + 1)); }
echo "=== claude-design Plugin Validation ==="
echo "Plugin root: $PLUGIN_ROOT"
echo ""
# -------------------------------------------------------
# Check (a): plugin.json valid + required fields
# -------------------------------------------------------
echo "--- (a) plugin.json structure ---"
PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json"
if [ ! -f "$PLUGIN_JSON" ]; then
fail ".claude-plugin/plugin.json missing"
else
if node -e "JSON.parse(require('fs').readFileSync('$PLUGIN_JSON'))" 2>/dev/null; then
pass ".claude-plugin/plugin.json is valid JSON"
else
fail ".claude-plugin/plugin.json is invalid JSON"
fi
for field in name version description; do
if node -e "const p = JSON.parse(require('fs').readFileSync('$PLUGIN_JSON')); if (typeof p['$field'] !== 'string' || p['$field'] === '') process.exit(1)" 2>/dev/null; then
pass "plugin.json has '$field'"
else
fail "plugin.json missing or empty '$field'"
fi
done
fi
echo ""
# -------------------------------------------------------
# Check (b): at least one SKILL.md under skills/*/
# -------------------------------------------------------
echo "--- (b) SKILL.md presence ---"
SKILL_COUNT=0
for skill_file in "$PLUGIN_ROOT"/skills/*/SKILL.md; do
[ -f "$skill_file" ] || continue
SKILL_COUNT=$((SKILL_COUNT + 1))
done
if [ "$SKILL_COUNT" -ge 1 ]; then
pass "found $SKILL_COUNT SKILL.md file(s) under skills/*/"
else
fail "no SKILL.md found under skills/*/"
fi
echo ""
# -------------------------------------------------------
# Check (c): SKILL.md frontmatter has name+description, description >=400 chars
# -------------------------------------------------------
echo "--- (c) SKILL.md frontmatter quality ---"
for skill_file in "$PLUGIN_ROOT"/skills/*/SKILL.md; do
[ -f "$skill_file" ] || continue
basename_skill="$(basename "$(dirname "$skill_file")")/SKILL.md"
first_line="$(head -n 1 "$skill_file")"
if [ "$first_line" != "---" ]; then
fail "$basename_skill: missing frontmatter delimiter on line 1"
continue
fi
frontmatter="$(awk 'NR==1{next} /^---$/{exit} {print}' "$skill_file")"
if echo "$frontmatter" | grep -qE '^name:'; then
pass "$basename_skill: has 'name:'"
else
fail "$basename_skill: missing 'name:'"
fi
if echo "$frontmatter" | grep -qE '^description:'; then
pass "$basename_skill: has 'description:'"
else
fail "$basename_skill: missing 'description:'"
fi
desc_len="$(awk '/^description: \|/,/^---$/' "$skill_file" | wc -c | tr -d '[:space:]')"
if [ -z "$desc_len" ]; then desc_len=0; fi
if [ "$desc_len" -ge 400 ]; then
pass "$basename_skill: description block is $desc_len chars (>=400)"
else
fail "$basename_skill: description block is $desc_len chars (<400)"
fi
done
echo ""
# -------------------------------------------------------
# Check (d): LICENSE exists, non-empty, contains "MIT License"
# -------------------------------------------------------
echo "--- (d) LICENSE ---"
LICENSE_FILE="$PLUGIN_ROOT/LICENSE"
if [ ! -f "$LICENSE_FILE" ]; then
fail "LICENSE missing"
elif [ ! -s "$LICENSE_FILE" ]; then
fail "LICENSE is empty"
elif ! grep -q "MIT License" "$LICENSE_FILE"; then
fail "LICENSE does not contain 'MIT License'"
else
pass "LICENSE exists, non-empty, MIT License"
fi
echo ""
# -------------------------------------------------------
# Check (e): GOVERNANCE.md exists, non-empty
# -------------------------------------------------------
echo "--- (e) GOVERNANCE.md ---"
GOVERNANCE_FILE="$PLUGIN_ROOT/GOVERNANCE.md"
if [ ! -f "$GOVERNANCE_FILE" ]; then
fail "GOVERNANCE.md missing"
elif [ ! -s "$GOVERNANCE_FILE" ]; then
fail "GOVERNANCE.md is empty"
else
pass "GOVERNANCE.md exists, non-empty"
fi
echo ""
# -------------------------------------------------------
# Check (f): README.md + CLAUDE.md exist, non-empty
# -------------------------------------------------------
echo "--- (f) README.md and CLAUDE.md ---"
for f in README.md CLAUDE.md; do
fpath="$PLUGIN_ROOT/$f"
if [ ! -f "$fpath" ]; then
fail "$f missing"
elif [ ! -s "$fpath" ]; then
fail "$f is empty"
else
pass "$f exists, non-empty"
fi
done
echo ""
# -------------------------------------------------------
# Check (g): .coverage.md exists at plugin root
# -------------------------------------------------------
echo "--- (g) .coverage.md ---"
COVERAGE_FILE="$PLUGIN_ROOT/.coverage.md"
if [ ! -f "$COVERAGE_FILE" ]; then
fail ".coverage.md missing"
elif [ ! -s "$COVERAGE_FILE" ]; then
fail ".coverage.md is empty"
else
pass ".coverage.md exists, non-empty"
fi
echo ""
# -------------------------------------------------------
# Check (h): forbidden command-name regex (scope fence vs
# Anthropic's knowledge-work-plugins/design)
# -------------------------------------------------------
echo "--- (h) forbidden command-name regex ---"
FORBIDDEN_REGEX='^name:[[:space:]]*(claude-design:)?(critique|accessibility|ux-copy|research-synthesis|design-system|handoff)[[:space:]]*$'
H_HIT=0
for cmd_file in "$PLUGIN_ROOT"/commands/*.md "$PLUGIN_ROOT"/skills/*/SKILL.md; do
[ -f "$cmd_file" ] || continue
if grep -qE "$FORBIDDEN_REGEX" "$cmd_file"; then
fail "command-name collision with Anthropic's official knowledge-work-plugins/design plugin: $cmd_file"
H_HIT=$((H_HIT + 1))
fi
done
if [ "$H_HIT" -eq 0 ]; then
pass "no forbidden command-name collisions"
fi
echo ""
# -------------------------------------------------------
# Check (i): operator-private-context grep
# -------------------------------------------------------
echo "--- (i) operator-private-context grep ---"
I_HITS="$(grep -rnE '(kjell|vegvesen|NEXT-SESSION-PROMPT|REMEMBER\.md content from)' \
"$PLUGIN_ROOT" \
--include='*.md' \
--exclude-dir='.claude' \
--exclude-dir='tests' \
--exclude='REMEMBER.md' \
--exclude='TODO.md' \
--exclude='NEXT-SESSION-PROMPT.local.md' \
2>/dev/null || true)"
if [ -z "$I_HITS" ]; then
pass "no operator-private context leaks in shipped content"
else
while IFS= read -r hit; do
fail "operator-private context leak in shipped content (brief NFR): $hit"
done < <(printf '%s\n' "$I_HITS")
fi
echo ""
# -------------------------------------------------------
# Check (j): Norwegian-leakage grep (WARN, not FAIL)
# -------------------------------------------------------
echo "--- (j) Norwegian-leakage grep ---"
J_HITS="$(grep -rnE '[æøåÆØÅ]' \
"$PLUGIN_ROOT" \
--include='*.md' \
--exclude-dir='.claude' \
--exclude='REMEMBER.md' \
--exclude='TODO.md' \
--exclude='NEXT-SESSION-PROMPT.local.md' \
2>/dev/null || true)"
if [ -z "$J_HITS" ]; then
pass "no Norwegian diacritics in shipped content"
else
while IFS= read -r hit; do
warn "Norwegian diacritic in shipped content (review case-by-case): $hit"
done < <(printf '%s\n' "$J_HITS")
fi
echo ""
# -------------------------------------------------------
# Summary
# -------------------------------------------------------
echo "=== Summary ==="
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0

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