@erclx/canon 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/claude/.claude-plugin/plugin.json +19 -0
- package/claude/skills/bash-cli-script/REQUIREMENT.md +42 -0
- package/claude/skills/bash-cli-script/SKILL.md +48 -0
- package/claude/skills/bash-cli-script/references/template.md +43 -0
- package/claude/skills/bash-script/REQUIREMENT.md +36 -0
- package/claude/skills/bash-script/SKILL.md +100 -0
- package/claude/skills/bash-script/references/patterns.md +349 -0
- package/claude/skills/canon-cli/REQUIREMENT.md +41 -0
- package/claude/skills/canon-cli/SKILL.md +103 -0
- package/claude/skills/canon-feedback-file/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-file/SKILL.md +80 -0
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +63 -0
- package/claude/skills/canon-operator/REQUIREMENT.md +61 -0
- package/claude/skills/canon-operator/SKILL.md +108 -0
- package/claude/skills/canon-rollout/REQUIREMENT.md +59 -0
- package/claude/skills/canon-rollout/SKILL.md +147 -0
- package/claude/skills/canon-screencast/REQUIREMENT.md +39 -0
- package/claude/skills/canon-screencast/SKILL.md +167 -0
- package/claude/skills/canon-slides-draft/REQUIREMENT.md +39 -0
- package/claude/skills/canon-slides-draft/SKILL.md +62 -0
- package/claude/skills/ci-workflow/REQUIREMENT.md +40 -0
- package/claude/skills/ci-workflow/SKILL.md +65 -0
- package/claude/skills/ci-workflow/references/workflows.md +98 -0
- package/claude/skills/claude-address-review/REQUIREMENT.md +57 -0
- package/claude/skills/claude-address-review/SKILL.md +212 -0
- package/claude/skills/claude-address-review/references/rebase-conflicts.md +39 -0
- package/claude/skills/claude-autoship/REQUIREMENT.md +50 -0
- package/claude/skills/claude-autoship/SKILL.md +207 -0
- package/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
- package/claude/skills/claude-design-extract/SKILL.md +102 -0
- package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
- package/claude/skills/claude-diagram/SKILL.md +177 -0
- package/claude/skills/claude-docs/REQUIREMENT.md +60 -0
- package/claude/skills/claude-docs/SKILL.md +287 -0
- package/claude/skills/claude-docs/references/anchor-sweep.md +58 -0
- package/claude/skills/claude-docs/references/wireframe-sweep.md +45 -0
- package/claude/skills/claude-feature/REQUIREMENT.md +36 -0
- package/claude/skills/claude-feature/SKILL.md +115 -0
- package/claude/skills/claude-groundwork/REQUIREMENT.md +48 -0
- package/claude/skills/claude-groundwork/SKILL.md +142 -0
- package/claude/skills/claude-intake/REQUIREMENT.md +49 -0
- package/claude/skills/claude-intake/SKILL.md +114 -0
- package/claude/skills/claude-intake-answer/REQUIREMENT.md +48 -0
- package/claude/skills/claude-intake-answer/SKILL.md +90 -0
- package/claude/skills/claude-markdown-propose/REQUIREMENT.md +48 -0
- package/claude/skills/claude-markdown-propose/SKILL.md +118 -0
- package/claude/skills/claude-markdown-propose/references/format.md +107 -0
- package/claude/skills/claude-memory-capture/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-capture/SKILL.md +101 -0
- package/claude/skills/claude-memory-review/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-review/SKILL.md +210 -0
- package/claude/skills/claude-memory-review/references/receipt-format.md +48 -0
- package/claude/skills/claude-orchestrate/REQUIREMENT.md +121 -0
- package/claude/skills/claude-orchestrate/SKILL.md +241 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +132 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +32 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +72 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +87 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +30 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +21 -0
- package/claude/skills/claude-orchestrate/scripts/poll.sh +373 -0
- package/claude/skills/claude-orchestrate/scripts/watch.sh +181 -0
- package/claude/skills/claude-pr-review/REQUIREMENT.md +47 -0
- package/claude/skills/claude-pr-review/SKILL.md +295 -0
- package/claude/skills/claude-review/REQUIREMENT.md +39 -0
- package/claude/skills/claude-review/SKILL.md +142 -0
- package/claude/skills/claude-seed-sync/REQUIREMENT.md +45 -0
- package/claude/skills/claude-seed-sync/SKILL.md +156 -0
- package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
- package/claude/skills/claude-standards-audit/SKILL.md +99 -0
- package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
- package/claude/skills/claude-tasks/SKILL.md +161 -0
- package/claude/skills/claude-teach/REQUIREMENT.md +56 -0
- package/claude/skills/claude-teach/SKILL.md +196 -0
- package/claude/skills/claude-teach/references/lesson-craft.md +59 -0
- package/claude/skills/claude-teach/references/pedagogy.md +67 -0
- package/claude/skills/claude-teach/references/promotion.md +54 -0
- package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ui-test/SKILL.md +77 -0
- package/claude/skills/claude-ux-audit/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ux-audit/SKILL.md +79 -0
- package/claude/skills/claude-ux-measure/REQUIREMENT.md +48 -0
- package/claude/skills/claude-ux-measure/SKILL.md +122 -0
- package/claude/skills/claude-worker/REQUIREMENT.md +54 -0
- package/claude/skills/claude-worker/SKILL.md +96 -0
- package/claude/skills/claude-worktree/REQUIREMENT.md +58 -0
- package/claude/skills/claude-worktree/SKILL.md +136 -0
- package/claude/skills/create-rule/REQUIREMENT.md +45 -0
- package/claude/skills/create-rule/SKILL.md +68 -0
- package/claude/skills/create-skill/REQUIREMENT.md +38 -0
- package/claude/skills/create-skill/SKILL.md +32 -0
- package/claude/skills/create-snippet/REQUIREMENT.md +39 -0
- package/claude/skills/create-snippet/SKILL.md +30 -0
- package/claude/skills/create-standard/REQUIREMENT.md +35 -0
- package/claude/skills/create-standard/SKILL.md +29 -0
- package/claude/skills/decision-escalate/REQUIREMENT.md +45 -0
- package/claude/skills/decision-escalate/SKILL.md +79 -0
- package/claude/skills/docs-sync/REQUIREMENT.md +41 -0
- package/claude/skills/docs-sync/SKILL.md +95 -0
- package/claude/skills/git-branch/REQUIREMENT.md +38 -0
- package/claude/skills/git-branch/SKILL.md +60 -0
- package/claude/skills/git-commit/REQUIREMENT.md +36 -0
- package/claude/skills/git-commit/SKILL.md +49 -0
- package/claude/skills/git-followup/REQUIREMENT.md +43 -0
- package/claude/skills/git-followup/SKILL.md +48 -0
- package/claude/skills/git-issue/REQUIREMENT.md +38 -0
- package/claude/skills/git-issue/SKILL.md +65 -0
- package/claude/skills/git-pr/REQUIREMENT.md +50 -0
- package/claude/skills/git-pr/SKILL.md +164 -0
- package/claude/skills/git-pr/references/labels.md +95 -0
- package/claude/skills/git-ship/REQUIREMENT.md +42 -0
- package/claude/skills/git-ship/SKILL.md +55 -0
- package/claude/skills/git-split/REQUIREMENT.md +39 -0
- package/claude/skills/git-split/SKILL.md +162 -0
- package/claude/skills/git-stage/REQUIREMENT.md +39 -0
- package/claude/skills/git-stage/SKILL.md +73 -0
- package/claude/skills/git-worktree/REQUIREMENT.md +38 -0
- package/claude/skills/git-worktree/SKILL.md +130 -0
- package/claude/skills/migration-claude-md/REQUIREMENT.md +40 -0
- package/claude/skills/migration-claude-md/SKILL.md +76 -0
- package/claude/skills/migration-context/REQUIREMENT.md +36 -0
- package/claude/skills/migration-context/SKILL.md +95 -0
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +55 -0
- package/claude/skills/migration-standards-drop/SKILL.md +113 -0
- package/claude/skills/migration-superseded/REQUIREMENT.md +44 -0
- package/claude/skills/migration-superseded/SKILL.md +115 -0
- package/claude/skills/project-commands/REQUIREMENT.md +42 -0
- package/claude/skills/project-commands/SKILL.md +85 -0
- package/claude/skills/restate-plainly/REQUIREMENT.md +41 -0
- package/claude/skills/restate-plainly/SKILL.md +39 -0
- package/claude/skills/session-map/REQUIREMENT.md +57 -0
- package/claude/skills/session-map/SKILL.md +70 -0
- package/claude/skills/session-resume/REQUIREMENT.md +49 -0
- package/claude/skills/session-resume/SKILL.md +51 -0
- package/claude/skills/setup-gov/REQUIREMENT.md +37 -0
- package/claude/skills/setup-gov/SKILL.md +77 -0
- package/claude/skills/setup-indexes/REQUIREMENT.md +45 -0
- package/claude/skills/setup-indexes/SKILL.md +153 -0
- package/claude/skills/setup-init/REQUIREMENT.md +45 -0
- package/claude/skills/setup-init/SKILL.md +127 -0
- package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
- package/claude/skills/setup-plugins/SKILL.md +81 -0
- package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
- package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
- package/claude/skills/setup-verify/SKILL.md +51 -0
- package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
- package/claude/skills/systematic-debugging/SKILL.md +70 -0
- package/claude/skills/write-human/REQUIREMENT.md +46 -0
- package/claude/skills/write-human/SKILL.md +68 -0
- package/claude/skills/write-human/references/density.md +38 -0
- package/claude/skills/write-human/references/machine-tells.md +107 -0
- package/claude/skills/write-human/references/source-material.md +37 -0
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
- package/claude/skills/youtube-transcripts/SKILL.md +34 -0
- package/docs/agents/audits.md +98 -0
- package/docs/agents/capture.md +37 -0
- package/docs/agents/census.md +23 -0
- package/docs/agents/commands.md +146 -0
- package/docs/agents/comments.md +34 -0
- package/docs/agents/context-audit-checks.md +120 -0
- package/docs/agents/context-audit.md +83 -0
- package/docs/agents/counts.md +76 -0
- package/docs/agents/demo.md +86 -0
- package/docs/agents/docs.md +17 -0
- package/docs/agents/gate.md +84 -0
- package/docs/agents/index.md +47 -0
- package/docs/agents/indexes.md +35 -0
- package/docs/agents/install-and-sync.md +385 -0
- package/docs/agents/intake.md +81 -0
- package/docs/agents/key-changes.md +103 -0
- package/docs/agents/label-coverage.md +73 -0
- package/docs/agents/markdown-audit.md +197 -0
- package/docs/agents/output-shape.md +70 -0
- package/docs/agents/overview.md +26 -0
- package/docs/agents/records.md +170 -0
- package/docs/agents/restated.md +81 -0
- package/docs/agents/review-classification.md +77 -0
- package/docs/agents/routing.md +61 -0
- package/docs/agents/rule-citations.md +98 -0
- package/docs/agents/sandbox.md +71 -0
- package/docs/agents/scripting.md +149 -0
- package/docs/agents/sessions.md +120 -0
- package/docs/agents/skills-audit.md +94 -0
- package/docs/agents/skills-reach.md +64 -0
- package/docs/agents/standards-audit.md +38 -0
- package/docs/agents/state-scoped-risk.md +105 -0
- package/docs/agents/superseded.md +85 -0
- package/docs/agents/targets.md +83 -0
- package/docs/agents/tasks.md +200 -0
- package/docs/agents/teach.md +158 -0
- package/docs/agents/test-order.md +56 -0
- package/docs/agents/worktrees.md +62 -0
- package/docs/ai-workflow.md +317 -0
- package/docs/index.md +23 -0
- package/docs/operating-model.md +223 -0
- package/docs/target-projects.md +258 -0
- package/docs/visual-design-workflow.md +151 -0
- package/docs/zshrc-aliases.md +65 -0
- package/governance/rules/ci/700-ci-workflow.md +44 -0
- package/governance/rules/claude/500-prose.md +15 -0
- package/governance/rules/claude/501-markdown.md +14 -0
- package/governance/rules/claude/510-context.md +28 -0
- package/governance/rules/claude/511-indexes.md +14 -0
- package/governance/rules/claude/520-wireframes.md +12 -0
- package/governance/rules/claude/530-requirements.md +11 -0
- package/governance/rules/claude/540-architecture.md +11 -0
- package/governance/rules/claude/550-design.md +11 -0
- package/governance/rules/claude/555-tasks.md +12 -0
- package/governance/rules/claude/556-groundwork.md +11 -0
- package/governance/rules/claude/557-intake.md +11 -0
- package/governance/rules/claude/558-plan.md +22 -0
- package/governance/rules/claude/559-memory.md +11 -0
- package/governance/rules/claude/560-diagrams.md +18 -0
- package/governance/rules/claude/561-teach.md +13 -0
- package/governance/rules/claude/562-session.md +15 -0
- package/governance/rules/claude/570-skill.md +24 -0
- package/governance/rules/claude/575-hooks.md +17 -0
- package/governance/rules/claude/576-settings.md +14 -0
- package/governance/rules/claude/580-readme.md +11 -0
- package/governance/rules/claude/590-rule-authoring.md +12 -0
- package/governance/rules/claude/591-standard-authoring.md +12 -0
- package/governance/rules/claude/592-claude-md.md +19 -0
- package/governance/rules/core/000-constitution.md +30 -0
- package/governance/rules/core/005-behavior.md +27 -0
- package/governance/rules/core/010-testing.md +35 -0
- package/governance/rules/core/015-output.md +20 -0
- package/governance/rules/core/020-concurrency.md +22 -0
- package/governance/rules/core/025-indexes.md +9 -0
- package/governance/rules/core/030-error-handling.md +31 -0
- package/governance/rules/core/035-tasks.md +13 -0
- package/governance/rules/core/040-performance.md +20 -0
- package/governance/rules/core/045-memory.md +12 -0
- package/governance/rules/core/050-logging.md +20 -0
- package/governance/rules/core/055-scratch.md +9 -0
- package/governance/rules/core/060-naming.md +19 -0
- package/governance/rules/core/065-spelling.md +19 -0
- package/governance/rules/core/070-planning.md +18 -0
- package/governance/rules/core/075-dependencies.md +25 -0
- package/governance/rules/core/080-config-comments.md +22 -0
- package/governance/rules/core/085-worktrees.md +17 -0
- package/governance/rules/core/087-git.md +11 -0
- package/governance/rules/core/090-code-comments.md +39 -0
- package/governance/rules/framework/200-react.md +51 -0
- package/governance/rules/framework/210-astro.md +41 -0
- package/governance/rules/framework/220-fastapi.md +43 -0
- package/governance/rules/framework/230-nextjs.md +48 -0
- package/governance/rules/framework/250-tailwind.md +32 -0
- package/governance/rules/framework/260-shadcn.md +34 -0
- package/governance/rules/lang/100-typescript.md +40 -0
- package/governance/rules/lang/110-python.md +42 -0
- package/governance/rules/lang/120-bash.md +19 -0
- package/governance/rules/lib/300-testing-ts.md +39 -0
- package/governance/rules/lib/305-e2e-reliability.md +34 -0
- package/governance/rules/lib/306-test-scope.md +25 -0
- package/governance/rules/lib/310-zod.md +25 -0
- package/governance/rules/lib/320-tanstack-query.md +32 -0
- package/governance/rules/lib/330-testing-py.md +44 -0
- package/governance/rules/lib/340-pydantic.md +38 -0
- package/governance/rules/lib/350-security-web.md +32 -0
- package/governance/rules/lib/360-security-server.md +39 -0
- package/governance/rules/lib/370-database.md +35 -0
- package/governance/rules/snippets/505-at-references.md +9 -0
- package/governance/rules/ui/400-ui.md +36 -0
- package/governance/rules/ui/410-a11y.md +48 -0
- package/governance/rules/ui/420-forms.md +36 -0
- package/governance/rules/ui/430-ux-completeness.md +65 -0
- package/governance/rules/ui/440-surface-capture.md +34 -0
- package/governance/rules/ui/450-link-behavior.md +19 -0
- package/governance/stacks/astro.toml +2 -0
- package/governance/stacks/base.toml +8 -0
- package/governance/stacks/node-server.toml +2 -0
- package/governance/stacks/node.toml +2 -0
- package/governance/stacks/python-fastapi.toml +2 -0
- package/governance/stacks/python.toml +2 -0
- package/governance/stacks/react.toml +2 -0
- package/package.json +69 -0
- package/scripts/config.sh +11 -0
- package/scripts/core/bootstrap.sh +81 -0
- package/scripts/core/check-color-source.sh +41 -0
- package/scripts/core/check-ignore-parity.sh +162 -0
- package/scripts/core/check-plugin-boundary.sh +45 -0
- package/scripts/core/check-seed-independence.sh +59 -0
- package/scripts/core/check-skill-paths.sh +24 -0
- package/scripts/core/clean.sh +36 -0
- package/scripts/core/install-check.sh +101 -0
- package/scripts/core/list-seed-roots.sh +18 -0
- package/scripts/core/regen-claude-copies.sh +10 -0
- package/scripts/core/regen-hero.sh +217 -0
- package/scripts/core/regen-indexes.sh +10 -0
- package/scripts/core/regen-tooling-paths.sh +61 -0
- package/scripts/core/repair-bare-flag.sh +19 -0
- package/scripts/core/snapshot.sh +134 -0
- package/scripts/core/update.sh +35 -0
- package/scripts/docs/list.sh +165 -0
- package/scripts/lib/frontmatter.sh +30 -0
- package/scripts/lib/gov.sh +14 -0
- package/scripts/lib/sandbox-fixtures.sh +191 -0
- package/scripts/lib/sandbox-git.sh +125 -0
- package/scripts/lib/sandbox-path.sh +206 -0
- package/scripts/lib/tooling.sh +35 -0
- package/scripts/lib/ui.sh +266 -0
- package/scripts/lib/worktree.sh +20 -0
- package/scripts/manage-sandbox.sh +466 -0
- package/scripts/snippets/create.sh +156 -0
- package/scripts/standards/list.sh +115 -0
- package/scripts/tooling/create.sh +109 -0
- package/scripts/tooling/verify.sh +179 -0
- package/snippets/align.md +12 -0
- package/snippets/claude/decision-memo.md +39 -0
- package/snippets/claude/feature-recap.md +19 -0
- package/snippets/claude/figma-steps.md +24 -0
- package/snippets/compact-summary.md +5 -0
- package/snippets/decision-help.md +6 -0
- package/snippets/meta-prompt.md +14 -0
- package/snippets/research-prompt.md +7 -0
- package/snippets/session-notes.md +11 -0
- package/snippets/snippets.toml +5 -0
- package/snippets/step-by-step.md +10 -0
- package/snippets/web-research.md +21 -0
- package/src/audits/baseline.ts +201 -0
- package/src/audits/catalog.ts +876 -0
- package/src/audits/run.ts +204 -0
- package/src/autoship/classify.ts +75 -0
- package/src/autoship/paths.ts +51 -0
- package/src/binary.ts +16 -0
- package/src/browser/engine.ts +40 -0
- package/src/census/count.ts +113 -0
- package/src/claude/cases/all.ts +24 -0
- package/src/claude/cases/authoring.ts +53 -0
- package/src/claude/cases/claude-workflow.ts +158 -0
- package/src/claude/cases/git.ts +44 -0
- package/src/claude/cases/misc.ts +27 -0
- package/src/claude/cases/setup.ts +94 -0
- package/src/claude/gitignore.ts +51 -0
- package/src/claude/routing.ts +283 -0
- package/src/claude/seeds-list.ts +47 -0
- package/src/claude/seeds.ts +150 -0
- package/src/claude/settings.ts +151 -0
- package/src/claude/skills-audit.ts +228 -0
- package/src/claude/skills-drift.ts +156 -0
- package/src/claude/skills-list.ts +99 -0
- package/src/claude/skills-rank.ts +320 -0
- package/src/claude/skills-reach.ts +227 -0
- package/src/cli-run.ts +43 -0
- package/src/cli.ts +200 -0
- package/src/commands/audits.ts +350 -0
- package/src/commands/autoship.ts +129 -0
- package/src/commands/capture.ts +133 -0
- package/src/commands/census.ts +105 -0
- package/src/commands/claude.ts +1286 -0
- package/src/commands/comments.ts +240 -0
- package/src/commands/context.ts +857 -0
- package/src/commands/demo.ts +389 -0
- package/src/commands/deps.ts +173 -0
- package/src/commands/design.ts +36 -0
- package/src/commands/docs.ts +60 -0
- package/src/commands/feedback-format.ts +23 -0
- package/src/commands/feedback.ts +112 -0
- package/src/commands/gate.ts +189 -0
- package/src/commands/gov.ts +1265 -0
- package/src/commands/indexes.ts +184 -0
- package/src/commands/init.ts +113 -0
- package/src/commands/intake.ts +406 -0
- package/src/commands/inventory.ts +256 -0
- package/src/commands/labels.ts +361 -0
- package/src/commands/markdown.ts +544 -0
- package/src/commands/migrate.ts +175 -0
- package/src/commands/pass-through.ts +39 -0
- package/src/commands/pr.ts +411 -0
- package/src/commands/records.ts +728 -0
- package/src/commands/sandbox.ts +468 -0
- package/src/commands/secrets.ts +132 -0
- package/src/commands/serve.ts +159 -0
- package/src/commands/sessions.ts +408 -0
- package/src/commands/slides.ts +126 -0
- package/src/commands/snippets.ts +84 -0
- package/src/commands/standards.ts +247 -0
- package/src/commands/sync.ts +428 -0
- package/src/commands/targets.ts +319 -0
- package/src/commands/tasks.ts +743 -0
- package/src/commands/teach.ts +786 -0
- package/src/commands/tooling.ts +573 -0
- package/src/commands/transcripts.ts +44 -0
- package/src/commands/upgrade.ts +231 -0
- package/src/commands/wiki.ts +100 -0
- package/src/commands/worktrees.ts +191 -0
- package/src/comments/scan.ts +338 -0
- package/src/comments/trend.ts +207 -0
- package/src/comments/vocabulary.ts +85 -0
- package/src/context/architecture.ts +364 -0
- package/src/context/audit.ts +790 -0
- package/src/context/citations.ts +196 -0
- package/src/context/folders.ts +186 -0
- package/src/context/gate.ts +57 -0
- package/src/context/index-drift.ts +64 -0
- package/src/context/narration.ts +99 -0
- package/src/copy.ts +30 -0
- package/src/counts/catalogs.ts +96 -0
- package/src/counts/numbers.ts +79 -0
- package/src/counts/scan.ts +314 -0
- package/src/demo/beats.ts +135 -0
- package/src/demo/compile.ts +326 -0
- package/src/demo/container.ts +63 -0
- package/src/demo/cursors.ts +55 -0
- package/src/demo/drive.ts +357 -0
- package/src/demo/pointer.ts +178 -0
- package/src/demo/theme.ts +112 -0
- package/src/deps/audit.ts +153 -0
- package/src/design/parse.ts +116 -0
- package/src/design/render.ts +249 -0
- package/src/docs/read.ts +77 -0
- package/src/exec.ts +16 -0
- package/src/exempt-marker.ts +43 -0
- package/src/frontmatter.ts +13 -0
- package/src/gate/measures.ts +682 -0
- package/src/gate/sequencer.ts +386 -0
- package/src/gate/stages.ts +412 -0
- package/src/git-env.ts +36 -0
- package/src/git-files.ts +105 -0
- package/src/git-ignore.ts +46 -0
- package/src/github-format.ts +13 -0
- package/src/github.ts +24 -0
- package/src/gov/adapter.ts +103 -0
- package/src/gov/citations.ts +514 -0
- package/src/gov/consumed.ts +129 -0
- package/src/gov/install.ts +132 -0
- package/src/gov/list.ts +106 -0
- package/src/gov/payload.ts +39 -0
- package/src/gov/restated.ts +814 -0
- package/src/gov/stacks.ts +205 -0
- package/src/gov/superseded.ts +415 -0
- package/src/gov/test-order.ts +407 -0
- package/src/indexes/frontmatter.ts +46 -0
- package/src/indexes/regen.ts +84 -0
- package/src/indexes/render.ts +201 -0
- package/src/indexes/walk.ts +83 -0
- package/src/init/flags.ts +60 -0
- package/src/init/plan.ts +114 -0
- package/src/init/run.ts +46 -0
- package/src/init/steps.ts +77 -0
- package/src/intake/folder.ts +320 -0
- package/src/intake/items.ts +174 -0
- package/src/inventory/config.ts +117 -0
- package/src/inventory/group.ts +76 -0
- package/src/inventory/subjects.ts +114 -0
- package/src/inventory/walk.ts +129 -0
- package/src/labels/audit.ts +82 -0
- package/src/labels/coverage.ts +79 -0
- package/src/labels/map.ts +101 -0
- package/src/labels/phase.ts +95 -0
- package/src/markdown/bans.ts +94 -0
- package/src/markdown/files.ts +102 -0
- package/src/markdown/gate.ts +28 -0
- package/src/markdown/scan.ts +295 -0
- package/src/markdown/structure.ts +730 -0
- package/src/migrate/apply.ts +115 -0
- package/src/migrate/plan.ts +103 -0
- package/src/migrate/rename.ts +183 -0
- package/src/pr/bijection.ts +145 -0
- package/src/pr/paths.ts +335 -0
- package/src/process/harness.ts +167 -0
- package/src/project-root.ts +19 -0
- package/src/records/backup.ts +455 -0
- package/src/records/migrate.ts +78 -0
- package/src/records/size.ts +260 -0
- package/src/records/validate.ts +1162 -0
- package/src/sandbox/census.ts +228 -0
- package/src/sandbox/coverage.ts +115 -0
- package/src/sandbox/expect.ts +629 -0
- package/src/sandbox/tree.ts +47 -0
- package/src/secrets/marker.ts +30 -0
- package/src/secrets/patterns.ts +142 -0
- package/src/secrets/scan.ts +123 -0
- package/src/secrets/shipped.ts +111 -0
- package/src/seed-marker.ts +74 -0
- package/src/serve/static.ts +322 -0
- package/src/sessions/claim.ts +85 -0
- package/src/sessions/live.ts +79 -0
- package/src/sessions/registry.ts +137 -0
- package/src/sessions/resolve.ts +333 -0
- package/src/slides/layouts.ts +391 -0
- package/src/slides/open.ts +18 -0
- package/src/slides/parse.ts +84 -0
- package/src/slides/render.ts +88 -0
- package/src/slides/styles.ts +44 -0
- package/src/snippets/categories.ts +66 -0
- package/src/snippets/list.ts +32 -0
- package/src/snippets/presets.ts +50 -0
- package/src/standards/audit.ts +132 -0
- package/src/standards/read.ts +100 -0
- package/src/sync/check.ts +692 -0
- package/src/sync/engine.ts +576 -0
- package/src/sync/git.ts +225 -0
- package/src/sync/history.ts +123 -0
- package/src/sync/layout.ts +140 -0
- package/src/sync/reverse.ts +268 -0
- package/src/sync/seeds-report.ts +126 -0
- package/src/sync/stamp.ts +358 -0
- package/src/sync/target.ts +70 -0
- package/src/sync/workflow.ts +200 -0
- package/src/target.ts +43 -0
- package/src/targets/pulls.ts +250 -0
- package/src/targets/registry.ts +203 -0
- package/src/targets/resolve.ts +145 -0
- package/src/targets/sweep.ts +246 -0
- package/src/tasks/archive.ts +506 -0
- package/src/tasks/record.ts +311 -0
- package/src/tasks/trunk.ts +89 -0
- package/src/tasks/validate.ts +1001 -0
- package/src/teach/lesson.ts +180 -0
- package/src/teach/workspace.ts +842 -0
- package/src/tooling/gitignore.ts +122 -0
- package/src/tooling/inject.ts +193 -0
- package/src/tooling/list.ts +39 -0
- package/src/tooling/manifest.ts +176 -0
- package/src/tooling/package.ts +166 -0
- package/src/tooling/read.ts +65 -0
- package/src/tooling/scan.ts +147 -0
- package/src/tooling/stamp.ts +44 -0
- package/src/transcripts/fetch.ts +156 -0
- package/src/transcripts/metadata.ts +54 -0
- package/src/transcripts/vtt.ts +114 -0
- package/src/ui.ts +266 -0
- package/src/version/compare.ts +53 -0
- package/src/version/installed.ts +40 -0
- package/src/version/manager.ts +67 -0
- package/src/version/skew.ts +192 -0
- package/src/wiki/init.ts +85 -0
- package/src/worktree.ts +143 -0
- package/src/worktrees/reclaim.ts +306 -0
- package/standards/architecture.md +72 -0
- package/standards/branch.md +59 -0
- package/standards/commit.md +72 -0
- package/standards/context.md +151 -0
- package/standards/design.md +93 -0
- package/standards/diagrams.md +152 -0
- package/standards/glossary.md +75 -0
- package/standards/groundwork.md +211 -0
- package/standards/index.md +36 -0
- package/standards/intake.md +192 -0
- package/standards/issue.md +94 -0
- package/standards/markdown.md +137 -0
- package/standards/memory.md +144 -0
- package/standards/plan.md +172 -0
- package/standards/pr.md +139 -0
- package/standards/publish.md +51 -0
- package/standards/readme.md +208 -0
- package/standards/requirements.md +70 -0
- package/standards/rule.md +118 -0
- package/standards/session.md +109 -0
- package/standards/skill.md +300 -0
- package/standards/slug.md +39 -0
- package/standards/snippets.md +76 -0
- package/standards/standard.md +170 -0
- package/standards/tasks.md +254 -0
- package/standards/teach.md +153 -0
- package/standards/versioning.md +71 -0
- package/standards/wireframes.md +113 -0
- package/tooling/astro/configs/astro.config.mjs +31 -0
- package/tooling/astro/configs/eslint.config.js +79 -0
- package/tooling/astro/configs/playwright.config.ts +26 -0
- package/tooling/astro/configs/tsconfig.json +12 -0
- package/tooling/astro/configs/vitest.config.ts +22 -0
- package/tooling/astro/manifest.toml +34 -0
- package/tooling/astro/reference.md +60 -0
- package/tooling/base/configs/.editorconfig +5 -0
- package/tooling/base/configs/.github/pull_request_template.md +18 -0
- package/tooling/base/configs/.github/workflows/verify.yml +35 -0
- package/tooling/base/configs/.husky/commit-msg +1 -0
- package/tooling/base/configs/.husky/post-merge +61 -0
- package/tooling/base/configs/.husky/post-rewrite +21 -0
- package/tooling/base/configs/.husky/pre-commit +1 -0
- package/tooling/base/configs/.husky/pre-push +1 -0
- package/tooling/base/configs/.prettierrc +12 -0
- package/tooling/base/configs/.shellcheckrc +1 -0
- package/tooling/base/configs/.vscode/extensions.json +9 -0
- package/tooling/base/configs/.vscode/settings.json +3 -0
- package/tooling/base/configs/commitlint.config.js +11 -0
- package/tooling/base/configs/scripts/verify.sh +64 -0
- package/tooling/base/manifest.toml +30 -0
- package/tooling/base/reference.md +98 -0
- package/tooling/base/seeds/.claude/context/ci.md +33 -0
- package/tooling/base/seeds/.claude/context/development.md +37 -0
- package/tooling/base/seeds/.claude/context/index.md +11 -0
- package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
- package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/base/seeds/.lintstagedrc +8 -0
- package/tooling/base/seeds/.prettierignore +0 -0
- package/tooling/base/seeds/cspell.json +20 -0
- package/tooling/claude/manifest.toml +14 -0
- package/tooling/claude/reference.md +79 -0
- package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
- package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
- package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
- package/tooling/claude/seeds/.claude/diagrams/index.md +8 -0
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +50 -0
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +68 -0
- package/tooling/claude/seeds/.claude/hooks/path-form.sh +57 -0
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +52 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +86 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +71 -0
- package/tooling/claude/seeds/.claude/memory/index.md +8 -0
- package/tooling/claude/seeds/.claude/settings.json +47 -0
- package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
- package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
- package/tooling/claude/seeds/CLAUDE.md +29 -0
- package/tooling/claude/user/settings.template.json +10 -0
- package/tooling/claude/user/statusline-command.sh +53 -0
- package/tooling/python/configs/.coveragerc +14 -0
- package/tooling/python/configs/.python-version +1 -0
- package/tooling/python/configs/mypy.ini +6 -0
- package/tooling/python/configs/pytest.ini +4 -0
- package/tooling/python/configs/ruff.toml +15 -0
- package/tooling/python/configs/scripts/verify.sh +77 -0
- package/tooling/python/manifest.toml +16 -0
- package/tooling/python/reference.md +66 -0
- package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/python/seeds/tests/test_smoke.py +2 -0
- package/tooling/vite-react/configs/playwright.config.ts +26 -0
- package/tooling/vite-react/configs/tsconfig.json +35 -0
- package/tooling/vite-react/configs/vite.config.ts +24 -0
- package/tooling/vite-react/configs/vitest.config.ts +26 -0
- package/tooling/vite-react/manifest.toml +23 -0
- package/tooling/vite-react/reference.md +55 -0
- package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
- package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
- package/tooling/web/configs/.github/workflows/verify.yml +134 -0
- package/tooling/web/configs/.vscode/extensions.json +13 -0
- package/tooling/web/configs/.vscode/settings.json +10 -0
- package/tooling/web/configs/e2e/home.spec.ts +6 -0
- package/tooling/web/configs/e2e/screenshot.ts +53 -0
- package/tooling/web/configs/eslint.config.js +82 -0
- package/tooling/web/configs/scripts/screenshot.sh +28 -0
- package/tooling/web/configs/scripts/verify.sh +80 -0
- package/tooling/web/configs/scripts/worktree-port.sh +74 -0
- package/tooling/web/configs/src/test/setup.ts +8 -0
- package/tooling/web/manifest.toml +58 -0
- package/tooling/web/reference.md +115 -0
- package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { copyPreservingMode } from '@/copy'
|
|
5
|
+
import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
|
|
6
|
+
|
|
7
|
+
const SEEDS_DIR = join('tooling', 'claude', 'seeds')
|
|
8
|
+
const CLAUDE_DIR = '.claude'
|
|
9
|
+
const CLAUDE_MD = 'CLAUDE.md'
|
|
10
|
+
const HOOKS = 'hooks'
|
|
11
|
+
/**
|
|
12
|
+
* Seed subdirectories under `.claude/`. Exported because each one replaced a
|
|
13
|
+
* single file of the same stem in an older layout, which is what
|
|
14
|
+
* `@/sync/layout` pairs a target against to find a superseded artifact.
|
|
15
|
+
*/
|
|
16
|
+
export const SUBDIRS: readonly string[] = [
|
|
17
|
+
HOOKS,
|
|
18
|
+
'context',
|
|
19
|
+
'diagrams',
|
|
20
|
+
'memory',
|
|
21
|
+
'tasks',
|
|
22
|
+
'wireframes',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export type SeedScope = 'claude' | 'root'
|
|
26
|
+
|
|
27
|
+
export interface Seed {
|
|
28
|
+
readonly src: string
|
|
29
|
+
readonly dest: string
|
|
30
|
+
readonly scanLabel: string
|
|
31
|
+
readonly applyLabel: string
|
|
32
|
+
readonly scope: SeedScope
|
|
33
|
+
readonly executable: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SeedEntry {
|
|
37
|
+
readonly seed: Seed
|
|
38
|
+
readonly present: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SeedCounts {
|
|
42
|
+
readonly claude: number
|
|
43
|
+
readonly root: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function seedsRoot(root: string): string {
|
|
47
|
+
return join(root, SEEDS_DIR)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lists a single directory level, sorted the way `find -maxdepth 1 -type f |
|
|
52
|
+
* sort` was. Bun.Glob skips dotfiles without `dot`, and every path here sits
|
|
53
|
+
* under `.claude`, so omitting it would match nothing.
|
|
54
|
+
*/
|
|
55
|
+
function listLevel(dir: string): string[] {
|
|
56
|
+
if (!existsSync(dir)) return []
|
|
57
|
+
return [
|
|
58
|
+
...new Bun.Glob('*').scanSync({ cwd: dir, onlyFiles: true, dot: true }),
|
|
59
|
+
].sort()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds the seed list in the order the bash scanned it: the `.claude` root
|
|
64
|
+
* level, then each subdirectory, then the project-level `CLAUDE.md`. Order is
|
|
65
|
+
* load-bearing because it is also the order the timeline prints.
|
|
66
|
+
*/
|
|
67
|
+
export function planSeeds(root: string, target: string): SeedEntry[] {
|
|
68
|
+
const source = join(seedsRoot(root), CLAUDE_DIR)
|
|
69
|
+
const destDir = join(target, CLAUDE_DIR)
|
|
70
|
+
const seeds: Seed[] = []
|
|
71
|
+
|
|
72
|
+
for (const name of listLevel(source)) {
|
|
73
|
+
seeds.push({
|
|
74
|
+
src: join(source, name),
|
|
75
|
+
dest: join(destDir, name),
|
|
76
|
+
scanLabel: name,
|
|
77
|
+
applyLabel: join(CLAUDE_DIR, name),
|
|
78
|
+
scope: 'claude',
|
|
79
|
+
executable: false,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const subdir of SUBDIRS) {
|
|
84
|
+
for (const name of listLevel(join(source, subdir))) {
|
|
85
|
+
const rel = `${subdir}/${name}`
|
|
86
|
+
seeds.push({
|
|
87
|
+
src: join(source, subdir, name),
|
|
88
|
+
dest: join(destDir, subdir, name),
|
|
89
|
+
scanLabel: rel,
|
|
90
|
+
applyLabel: join(CLAUDE_DIR, subdir, name),
|
|
91
|
+
scope: 'claude',
|
|
92
|
+
executable: subdir === HOOKS,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const claudeMd = join(seedsRoot(root), CLAUDE_MD)
|
|
98
|
+
if (existsSync(claudeMd)) {
|
|
99
|
+
seeds.push({
|
|
100
|
+
src: claudeMd,
|
|
101
|
+
dest: join(target, CLAUDE_MD),
|
|
102
|
+
scanLabel: CLAUDE_MD,
|
|
103
|
+
applyLabel: CLAUDE_MD,
|
|
104
|
+
scope: 'root',
|
|
105
|
+
executable: false,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return seeds.map((seed) => ({ seed, present: existsSync(seed.dest) }))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function pendingSeeds(entries: readonly SeedEntry[]): Seed[] {
|
|
113
|
+
return entries.filter((entry) => !entry.present).map((entry) => entry.seed)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function countByScope(seeds: readonly Seed[]): SeedCounts {
|
|
117
|
+
return {
|
|
118
|
+
claude: seeds.filter((seed) => seed.scope === 'claude').length,
|
|
119
|
+
root: seeds.filter((seed) => seed.scope === 'root').length,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Copies each pending seed. Hooks get the executable bit the way `chmod +x`
|
|
125
|
+
* granted it, added on top of whatever mode the destination already carried.
|
|
126
|
+
*
|
|
127
|
+
* A markdown seed is rewritten rather than copied, so the stub marker the seed
|
|
128
|
+
* gate reads does not reach the target. Every other seed copies byte for byte,
|
|
129
|
+
* which is what keeps the hook scripts and `settings.json` untouched.
|
|
130
|
+
*/
|
|
131
|
+
export async function applySeeds(seeds: readonly Seed[]): Promise<string[]> {
|
|
132
|
+
const applied: string[] = []
|
|
133
|
+
|
|
134
|
+
for (const seed of seeds) {
|
|
135
|
+
if (rewritesOnInstall(seed.src)) {
|
|
136
|
+
await mkdir(dirname(seed.dest), { recursive: true })
|
|
137
|
+
await writeFile(
|
|
138
|
+
seed.dest,
|
|
139
|
+
stripSeedMarker(await readFile(seed.src, 'utf8')),
|
|
140
|
+
)
|
|
141
|
+
} else {
|
|
142
|
+
await copyPreservingMode(seed.src, seed.dest)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (seed.executable) await chmod(seed.dest, 0o755)
|
|
146
|
+
applied.push(seed.applyLabel)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return applied
|
|
150
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_INDENT = ' '
|
|
6
|
+
const INDENT = /^([ \t]+)\S/m
|
|
7
|
+
|
|
8
|
+
export type SettingsRecord = Record<string, unknown>
|
|
9
|
+
|
|
10
|
+
export interface SettingsKey {
|
|
11
|
+
readonly label: string
|
|
12
|
+
readonly changed: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SettingsPlan {
|
|
16
|
+
readonly keys: readonly SettingsKey[]
|
|
17
|
+
readonly next: SettingsRecord
|
|
18
|
+
readonly changed: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reads the indentation of the first indented line. Rewriting an operator's
|
|
23
|
+
* settings file at a different width is a diff they did not ask for, and this
|
|
24
|
+
* verb is the only one in the toolkit that writes outside a target.
|
|
25
|
+
*/
|
|
26
|
+
export function detectIndent(source: string): string {
|
|
27
|
+
return INDENT.exec(source)?.[1] ?? DEFAULT_INDENT
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function serializeSettings(
|
|
31
|
+
value: SettingsRecord,
|
|
32
|
+
indent: string,
|
|
33
|
+
): string {
|
|
34
|
+
return `${JSON.stringify(value, null, indent)}\n`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parses a settings file, treating an absent one as empty. A malformed one
|
|
39
|
+
* throws rather than resolving to empty, because the alternative silently
|
|
40
|
+
* replaces a file the operator owns with a generated one.
|
|
41
|
+
*/
|
|
42
|
+
export async function readSettings(
|
|
43
|
+
path: string,
|
|
44
|
+
): Promise<{ value: SettingsRecord; indent: string }> {
|
|
45
|
+
if (!existsSync(path)) return { value: {}, indent: DEFAULT_INDENT }
|
|
46
|
+
|
|
47
|
+
const source = await readFile(path, 'utf8')
|
|
48
|
+
const indent = detectIndent(source)
|
|
49
|
+
if (source.trim() === '') return { value: {}, indent }
|
|
50
|
+
|
|
51
|
+
let parsed: unknown
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(source)
|
|
54
|
+
} catch (cause) {
|
|
55
|
+
throw new Error(`${path} is not valid JSON`, { cause })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
59
|
+
throw new Error(`${path} is not a JSON object`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { value: parsed as SettingsRecord, indent }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function asRecord(value: unknown): SettingsRecord {
|
|
66
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
67
|
+
return {}
|
|
68
|
+
}
|
|
69
|
+
return value as SettingsRecord
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function asStrings(value: unknown): string[] {
|
|
73
|
+
if (!Array.isArray(value)) return []
|
|
74
|
+
return value.filter((entry): entry is string => typeof entry === 'string')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function equals(left: unknown, right: unknown): boolean {
|
|
78
|
+
return Bun.deepEquals(left, right)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Unions two permission lists into the sorted, deduplicated shape `jq unique`
|
|
83
|
+
* produced. The sort is load-bearing, since the comparison that decides
|
|
84
|
+
* whether to write runs against the sorted result.
|
|
85
|
+
*/
|
|
86
|
+
function mergeList(current: unknown, incoming: unknown): string[] {
|
|
87
|
+
return [...new Set([...asStrings(current), ...asStrings(incoming)])].sort()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Applies the four settings the toolkit owns, leaving every other key in
|
|
92
|
+
* place. Each key reports whether it changed so the caller prints `+` or `✓`
|
|
93
|
+
* per key, matching the four separate `jq` passes this replaced.
|
|
94
|
+
*/
|
|
95
|
+
export function planSettings(
|
|
96
|
+
current: SettingsRecord,
|
|
97
|
+
template: SettingsRecord,
|
|
98
|
+
statusLineCommand: string,
|
|
99
|
+
): SettingsPlan {
|
|
100
|
+
const next: SettingsRecord = { ...current }
|
|
101
|
+
const keys: SettingsKey[] = []
|
|
102
|
+
|
|
103
|
+
const statusLine = asRecord(next.statusLine)
|
|
104
|
+
const statusLineChanged = statusLine.command !== statusLineCommand
|
|
105
|
+
if (statusLineChanged) {
|
|
106
|
+
next.statusLine = { type: 'command', command: statusLineCommand }
|
|
107
|
+
}
|
|
108
|
+
keys.push({ label: 'statusLine', changed: statusLineChanged })
|
|
109
|
+
|
|
110
|
+
const attribution = template.attribution
|
|
111
|
+
const attributionChanged = !equals(next.attribution, attribution)
|
|
112
|
+
if (attributionChanged) next.attribution = attribution
|
|
113
|
+
keys.push({ label: 'attribution', changed: attributionChanged })
|
|
114
|
+
|
|
115
|
+
const permissions = asRecord(next.permissions)
|
|
116
|
+
const merged: SettingsRecord = { ...permissions }
|
|
117
|
+
const templatePermissions = asRecord(template.permissions)
|
|
118
|
+
|
|
119
|
+
for (const field of ['allow', 'deny'] as const) {
|
|
120
|
+
const union = mergeList(permissions[field], templatePermissions[field])
|
|
121
|
+
const changed = !equals(asStrings(permissions[field]), union)
|
|
122
|
+
if (changed) merged[field] = union
|
|
123
|
+
keys.push({ label: `permissions.${field}`, changed })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (keys.some((key) => key.label.startsWith('permissions.') && key.changed)) {
|
|
127
|
+
next.permissions = merged
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { keys, next, changed: keys.some((key) => key.changed) }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Writes in place, restoring the file's existing mode. The bash wrote through
|
|
135
|
+
* `mktemp` and `mv`, which handed the destination the temp file's 600 and
|
|
136
|
+
* silently tightened a settings file the operator had left at 644.
|
|
137
|
+
*/
|
|
138
|
+
export async function writeSettings(
|
|
139
|
+
path: string,
|
|
140
|
+
content: string,
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
await mkdir(dirname(path), { recursive: true })
|
|
143
|
+
|
|
144
|
+
const mode = await stat(path)
|
|
145
|
+
.then((info) => info.mode)
|
|
146
|
+
.catch(() => undefined)
|
|
147
|
+
|
|
148
|
+
await writeFile(path, content)
|
|
149
|
+
|
|
150
|
+
if (mode !== undefined) await chmod(path, mode)
|
|
151
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
type Frontmatter,
|
|
5
|
+
parseFrontmatter,
|
|
6
|
+
readField,
|
|
7
|
+
} from '@/indexes/frontmatter'
|
|
8
|
+
|
|
9
|
+
/** Returned when a skill folder carries no `REQUIREMENT.md`, the gating check. */
|
|
10
|
+
export const EXIT_MISSING_REQUIREMENT = 2
|
|
11
|
+
|
|
12
|
+
/** The description ceiling stated in the frontmatter rules of `standards/skill.md`. */
|
|
13
|
+
export const DESCRIPTION_LIMIT = 1024
|
|
14
|
+
|
|
15
|
+
/** The two headings the requirement template declares, matched at any level. */
|
|
16
|
+
export const REQUIREMENT_SECTIONS: readonly string[] = ['Gap', 'Must']
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Both trees the standard governs. `claude/skills/` ships to a target and
|
|
20
|
+
* `.claude/skills/` stays here, and every rule measured below applies to each,
|
|
21
|
+
* so a corpus reading one of them reports a pass over half the subject.
|
|
22
|
+
*/
|
|
23
|
+
export const CORPORA: readonly string[] = [
|
|
24
|
+
join('claude', 'skills'),
|
|
25
|
+
join('.claude', 'skills'),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The one reason this audit refuses. A project carrying neither corpus is the
|
|
30
|
+
* ordinary state of a target that has not adopted either skills convention,
|
|
31
|
+
* the same absence `no-skills` reads for the shipped citation reach check.
|
|
32
|
+
*/
|
|
33
|
+
export type SkillsAuditRefusal = 'no-corpus'
|
|
34
|
+
|
|
35
|
+
/** Kebab-case, which the standard states as no spaces, capitals, or underscores. */
|
|
36
|
+
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
37
|
+
|
|
38
|
+
const HEADING = /^#{1,6}\s+(.+?)\s*$/
|
|
39
|
+
|
|
40
|
+
export interface SkillFinding {
|
|
41
|
+
readonly rel: string
|
|
42
|
+
readonly detail: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CorpusReport {
|
|
46
|
+
/**
|
|
47
|
+
* Left as `join` produced it, where `SkillsCorpus.rel` in `skills-list.ts`
|
|
48
|
+
* normalizes the same spelling to POSIX. This one is an existing JSON field
|
|
49
|
+
* a caller already reads, so the split holds until a branch reading this verb
|
|
50
|
+
* is the one to close it.
|
|
51
|
+
*/
|
|
52
|
+
readonly rel: string
|
|
53
|
+
readonly skills: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SkillsAudit {
|
|
57
|
+
readonly corpora: readonly CorpusReport[]
|
|
58
|
+
readonly skills: number
|
|
59
|
+
readonly missingRequirement: readonly string[]
|
|
60
|
+
readonly readme: readonly string[]
|
|
61
|
+
readonly folderName: readonly string[]
|
|
62
|
+
readonly missingDescription: readonly string[]
|
|
63
|
+
readonly nameMismatch: readonly SkillFinding[]
|
|
64
|
+
readonly longDescription: readonly SkillFinding[]
|
|
65
|
+
readonly requirementSections: readonly SkillFinding[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface SkillSource {
|
|
69
|
+
readonly rel: string
|
|
70
|
+
readonly folder: string
|
|
71
|
+
readonly hasReadme: boolean
|
|
72
|
+
readonly name: string | undefined
|
|
73
|
+
readonly description: string | undefined
|
|
74
|
+
/** Undefined when the folder carries no `REQUIREMENT.md` at all. */
|
|
75
|
+
readonly requirementHeadings: readonly string[] | undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Measures both skill corpora against the rules `standards/skill.md` states
|
|
80
|
+
* mechanically. A corpus the project does not carry is skipped rather than
|
|
81
|
+
* reported, so a target holding only `.claude/skills/` reads as in scope.
|
|
82
|
+
*
|
|
83
|
+
* Reads raw frontmatter rather than `listSkills`, which prefers the folder name
|
|
84
|
+
* over the declared one and so can never surface a disagreement between them.
|
|
85
|
+
*/
|
|
86
|
+
export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
87
|
+
const present = CORPORA.map((rel) => ({ rel, dir: join(root, rel) })).filter(
|
|
88
|
+
(corpus) => existsSync(corpus.dir),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const perCorpus = await Promise.all(
|
|
92
|
+
present.map((corpus) => readCorpus(corpus.rel, corpus.dir)),
|
|
93
|
+
)
|
|
94
|
+
const sources = perCorpus.flat()
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
corpora: present.map((corpus, index) => ({
|
|
98
|
+
rel: corpus.rel,
|
|
99
|
+
skills: perCorpus[index]?.length ?? 0,
|
|
100
|
+
})),
|
|
101
|
+
skills: sources.length,
|
|
102
|
+
missingRequirement: sources
|
|
103
|
+
.filter((source) => source.requirementHeadings === undefined)
|
|
104
|
+
.map((source) => source.rel),
|
|
105
|
+
readme: sources
|
|
106
|
+
.filter((source) => source.hasReadme)
|
|
107
|
+
.map((source) => source.rel),
|
|
108
|
+
folderName: sources
|
|
109
|
+
.filter((source) => !KEBAB_CASE.test(source.folder))
|
|
110
|
+
.map((source) => source.rel),
|
|
111
|
+
missingDescription: sources
|
|
112
|
+
.filter((source) => source.description === undefined)
|
|
113
|
+
.map((source) => source.rel),
|
|
114
|
+
nameMismatch: sources.flatMap(nameFindings),
|
|
115
|
+
longDescription: sources.flatMap(lengthFindings),
|
|
116
|
+
requirementSections: sources.flatMap(sectionFindings),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Only a required file that is absent sets a failing code, which is a fact.
|
|
122
|
+
* Every other measure is a judgment a reader settles, and failing a push on one
|
|
123
|
+
* teaches contributors to route around the stage.
|
|
124
|
+
*/
|
|
125
|
+
export function auditExitCode(report: SkillsAudit): number {
|
|
126
|
+
return report.missingRequirement.length > 0 ? EXIT_MISSING_REQUIREMENT : 0
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readCorpus(rel: string, dir: string): Promise<SkillSource[]> {
|
|
130
|
+
const paths = [
|
|
131
|
+
...new Bun.Glob('*/SKILL.md').scanSync({ cwd: dir, onlyFiles: true }),
|
|
132
|
+
].sort()
|
|
133
|
+
|
|
134
|
+
return Promise.all(paths.map((path) => readSkill(rel, dir, dirname(path))))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function readSkill(
|
|
138
|
+
rel: string,
|
|
139
|
+
dir: string,
|
|
140
|
+
folder: string,
|
|
141
|
+
): Promise<SkillSource> {
|
|
142
|
+
const skillDir = join(dir, folder)
|
|
143
|
+
const requirementPath = join(skillDir, 'REQUIREMENT.md')
|
|
144
|
+
|
|
145
|
+
const [body, requirement] = await Promise.all([
|
|
146
|
+
Bun.file(join(skillDir, 'SKILL.md')).text(),
|
|
147
|
+
existsSync(requirementPath)
|
|
148
|
+
? Bun.file(requirementPath).text()
|
|
149
|
+
: Promise.resolve(undefined),
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
const fields = readFields(body)
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
rel: join(rel, folder),
|
|
156
|
+
folder,
|
|
157
|
+
hasReadme: existsSync(join(skillDir, 'README.md')),
|
|
158
|
+
name: declared(readField(fields, 'name')),
|
|
159
|
+
description: declared(readField(fields, 'description')),
|
|
160
|
+
requirementHeadings:
|
|
161
|
+
requirement === undefined ? undefined : headings(requirement),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Returns undefined rather than throwing on unparseable YAML, so one malformed
|
|
167
|
+
* body reports as missing its required fields instead of ending the audit
|
|
168
|
+
* before it reaches the rest of the corpus.
|
|
169
|
+
*/
|
|
170
|
+
function readFields(body: string): Frontmatter | undefined {
|
|
171
|
+
try {
|
|
172
|
+
return parseFrontmatter(body)
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A key carrying an empty value declares nothing, so blank reads as absent
|
|
180
|
+
* rather than as a name that disagrees with every folder.
|
|
181
|
+
*/
|
|
182
|
+
function declared(value: string | undefined): string | undefined {
|
|
183
|
+
const trimmed = value?.trim()
|
|
184
|
+
return trimmed === undefined || trimmed === '' ? undefined : trimmed
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function headings(source: string): string[] {
|
|
188
|
+
return source
|
|
189
|
+
.split('\n')
|
|
190
|
+
.map((line) => HEADING.exec(line)?.[1])
|
|
191
|
+
.filter((text): text is string => text !== undefined)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function nameFindings(source: SkillSource): SkillFinding[] {
|
|
195
|
+
if (source.name === undefined) {
|
|
196
|
+
return [{ rel: source.rel, detail: 'frontmatter declares no name' }]
|
|
197
|
+
}
|
|
198
|
+
if (source.name === source.folder) return []
|
|
199
|
+
return [{ rel: source.rel, detail: `frontmatter name: ${source.name}` }]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function lengthFindings(source: SkillSource): SkillFinding[] {
|
|
203
|
+
const { description } = source
|
|
204
|
+
if (description === undefined) return []
|
|
205
|
+
if (description.length <= DESCRIPTION_LIMIT) return []
|
|
206
|
+
return [{ rel: source.rel, detail: `${description.length} characters` }]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Stays silent on a folder carrying no requirement at all, which the presence
|
|
211
|
+
* check already reports. Counting the same skill twice reads as two defects.
|
|
212
|
+
*/
|
|
213
|
+
function sectionFindings(source: SkillSource): SkillFinding[] {
|
|
214
|
+
const { requirementHeadings } = source
|
|
215
|
+
if (requirementHeadings === undefined) return []
|
|
216
|
+
|
|
217
|
+
const missing = REQUIREMENT_SECTIONS.filter(
|
|
218
|
+
(section) => !requirementHeadings.includes(section),
|
|
219
|
+
)
|
|
220
|
+
if (missing.length === 0) return []
|
|
221
|
+
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
rel: join(source.rel, 'REQUIREMENT.md'),
|
|
225
|
+
detail: `missing: ${missing.join(', ')}`,
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { execaSync } from 'execa'
|
|
4
|
+
import { gitEnv } from '@/git-env'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shipped corpus alone. A session loads a body from the plugin root, which
|
|
8
|
+
* resolves to `claude/skills/`, so the internal tree carries no held copy that
|
|
9
|
+
* could go stale in one.
|
|
10
|
+
*
|
|
11
|
+
* Held with posix separators because it is passed to git as a pathspec. Git
|
|
12
|
+
* emits and accepts posix separators on every platform, so a `join()` form would
|
|
13
|
+
* reach a Windows checkout as `claude\skills`, match no path, and return an
|
|
14
|
+
* empty log that reads as a tree where nothing moved.
|
|
15
|
+
*/
|
|
16
|
+
const SHIPPED_SKILLS = 'claude/skills'
|
|
17
|
+
|
|
18
|
+
/** A `%H` line, which is 40 hex characters under sha1 and 64 under sha256. */
|
|
19
|
+
const COMMIT = /^[0-9a-f]{40,64}$/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A skill body, which is the only file in the folder a session loads. A
|
|
23
|
+
* reference or a requirement beside it is read by whoever opens it, so it holds
|
|
24
|
+
* no copy that outlives the read.
|
|
25
|
+
*/
|
|
26
|
+
const BODY = /^claude\/skills\/([^/]+)\/SKILL\.md$/
|
|
27
|
+
|
|
28
|
+
export interface MovedBody {
|
|
29
|
+
readonly name: string
|
|
30
|
+
/** The newest commit that rewrote this body, not the first. */
|
|
31
|
+
readonly commit: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DriftReport =
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: 'measured'
|
|
37
|
+
readonly base: string
|
|
38
|
+
readonly head: string
|
|
39
|
+
readonly moved: readonly MovedBody[]
|
|
40
|
+
}
|
|
41
|
+
| { readonly kind: 'unreadable'; readonly reason: string }
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads `--name-only` log output into one entry per skill whose body moved.
|
|
45
|
+
*
|
|
46
|
+
* Reverse-chronological input means the first commit naming a body is the
|
|
47
|
+
* newest that rewrote it, so an existing name is never overwritten. A body
|
|
48
|
+
* rewritten twice therefore reports the recent commit, which is the one a
|
|
49
|
+
* session can read to see what it is missing.
|
|
50
|
+
*/
|
|
51
|
+
export function parseMovedBodies(output: string): MovedBody[] {
|
|
52
|
+
const moved = new Map<string, string>()
|
|
53
|
+
let commit = ''
|
|
54
|
+
|
|
55
|
+
for (const line of output.split('\n')) {
|
|
56
|
+
const trimmed = line.trim()
|
|
57
|
+
if (trimmed === '') continue
|
|
58
|
+
|
|
59
|
+
if (COMMIT.test(trimmed)) {
|
|
60
|
+
commit = trimmed
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const match = BODY.exec(trimmed)
|
|
65
|
+
if (match === null || commit === '') continue
|
|
66
|
+
if (!moved.has(match[1])) moved.set(match[1], commit)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [...moved]
|
|
70
|
+
.map(([name, sha]) => ({ name, commit: sha }))
|
|
71
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Which shipped skill bodies were rewritten between `ref` and the current
|
|
76
|
+
* `HEAD` of the tree at `root`.
|
|
77
|
+
*
|
|
78
|
+
* This measures a file moving rather than a held copy differing. A session
|
|
79
|
+
* cannot read its own loaded body as bytes, so the comparison runs against
|
|
80
|
+
* history and a session passing a ref older than its oldest load over-reports.
|
|
81
|
+
* That is the safe direction, since confirming a name costs one read of the
|
|
82
|
+
* body while the failure this answers is silence.
|
|
83
|
+
*
|
|
84
|
+
* Every path that cannot answer returns `unreadable` with its own reason. A
|
|
85
|
+
* target project loads the plugin from a marketplace cache with no history
|
|
86
|
+
* behind it, where reporting an empty result would read as a clean tree.
|
|
87
|
+
*/
|
|
88
|
+
export function readDrift(root: string, ref: string): DriftReport {
|
|
89
|
+
if (!existsSync(join(root, SHIPPED_SKILLS))) {
|
|
90
|
+
return {
|
|
91
|
+
kind: 'unreadable',
|
|
92
|
+
reason: `No shipped skill corpus under ${root}. Looked for ${SHIPPED_SKILLS}.`,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const head = revParse(root, 'HEAD')
|
|
97
|
+
if (head === undefined) {
|
|
98
|
+
return {
|
|
99
|
+
kind: 'unreadable',
|
|
100
|
+
reason: `No git history under ${root}. A plugin loaded from a marketplace cache carries none, so a ref cannot be resolved there.`,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const base = revParse(root, ref)
|
|
105
|
+
if (base === undefined) {
|
|
106
|
+
return {
|
|
107
|
+
kind: 'unreadable',
|
|
108
|
+
reason: `Ref ${ref} resolves to no commit in ${root}. Pass a commit this tree carries.`,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = execaSync(
|
|
113
|
+
'git',
|
|
114
|
+
[
|
|
115
|
+
'-C',
|
|
116
|
+
root,
|
|
117
|
+
'log',
|
|
118
|
+
'--name-only',
|
|
119
|
+
'--no-renames',
|
|
120
|
+
'--format=%H',
|
|
121
|
+
`${base}..${head}`,
|
|
122
|
+
'--',
|
|
123
|
+
SHIPPED_SKILLS,
|
|
124
|
+
],
|
|
125
|
+
{ reject: false, env: gitEnv(), extendEnv: false },
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if (result.exitCode !== 0) {
|
|
129
|
+
return {
|
|
130
|
+
kind: 'unreadable',
|
|
131
|
+
reason: `Reading history between ${ref} and HEAD failed: ${result.stderr || result.stdout}`,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
kind: 'measured',
|
|
137
|
+
base,
|
|
138
|
+
head,
|
|
139
|
+
moved: parseMovedBodies(result.stdout),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The commit a ref names, or `undefined` when the tree carries no repository or
|
|
145
|
+
* no such ref. `^{commit}` is what turns a tag or a tree into the commit behind
|
|
146
|
+
* it, so a caller never compares a ref against an object of another type.
|
|
147
|
+
*/
|
|
148
|
+
function revParse(root: string, ref: string): string | undefined {
|
|
149
|
+
const result = execaSync(
|
|
150
|
+
'git',
|
|
151
|
+
['-C', root, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`],
|
|
152
|
+
{ reject: false, env: gitEnv(), extendEnv: false },
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return result.exitCode === 0 ? result.stdout.trim() : undefined
|
|
156
|
+
}
|