@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,358 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join, sep } from 'node:path'
|
|
5
|
+
import { execa } from 'execa'
|
|
6
|
+
import { recordTarget } from '@/targets/registry'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Domains the stamp can record. Governance attributes file by file through the
|
|
10
|
+
* sync engine. Tooling runs its own inject and manifest machinery, so it
|
|
11
|
+
* records the stack chain it resolved instead and carries no file hashes.
|
|
12
|
+
*
|
|
13
|
+
* A stamp written before the standards or snippets install channel closed
|
|
14
|
+
* still carries a `standards` or `snippets` record. `isStamp` ignores the key
|
|
15
|
+
* and `sortDomains` drops it on the next write, so the target loses a domain
|
|
16
|
+
* nothing can refresh rather than losing the whole file.
|
|
17
|
+
*/
|
|
18
|
+
export const STAMP_DOMAINS = ['governance', 'tooling'] as const
|
|
19
|
+
|
|
20
|
+
export type StampDomain = (typeof STAMP_DOMAINS)[number]
|
|
21
|
+
|
|
22
|
+
/** What a domain needs to stamp itself: its key, and the clone to date it against. */
|
|
23
|
+
export interface StampSource {
|
|
24
|
+
readonly domain: StampDomain
|
|
25
|
+
readonly toolkitRoot: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Target-relative posix path to content hash, for one domain. */
|
|
29
|
+
export type DomainHashes = Readonly<Record<string, string>>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One domain's record. The anchor sits here rather than at the top level
|
|
33
|
+
* because domains sync independently, and a shared anchor would let a gov sync
|
|
34
|
+
* advance the revision standards measures its upstream range from.
|
|
35
|
+
*/
|
|
36
|
+
export interface DomainStamp {
|
|
37
|
+
/** Toolkit revision this domain last synced from. Absent outside a git clone. */
|
|
38
|
+
readonly commit?: string
|
|
39
|
+
readonly syncedAt: string
|
|
40
|
+
readonly files: DomainHashes
|
|
41
|
+
/**
|
|
42
|
+
* Stack names the install resolved. Tooling records the full ancestor chain,
|
|
43
|
+
* nearest stack first, because a stack that extends another cannot be
|
|
44
|
+
* reinstalled from its leaf alone, and a `--skip` run installs fewer layers
|
|
45
|
+
* than the leaf's own chain would reproduce. Governance records the single
|
|
46
|
+
* stack `canon gov install` was given, since `resolveRules` walks its
|
|
47
|
+
* ancestors internally and a reader needs only the leaf to ask it again.
|
|
48
|
+
*/
|
|
49
|
+
readonly chain?: readonly string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Stamp {
|
|
53
|
+
/** Domains actually stamped in this target, so an unstamped one is legible. */
|
|
54
|
+
readonly covers: readonly StampDomain[]
|
|
55
|
+
readonly domains: Readonly<Partial<Record<StampDomain, DomainStamp>>>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function stampPath(target: string): string {
|
|
59
|
+
return join(target, '.claude', 'canon', 'config.json')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where `106115ba` moved the stamp from. No migration shipped with that move,
|
|
64
|
+
* so a target stamped before it still carries its config here, and `readStamp`
|
|
65
|
+
* falls back to this path when the current one is absent.
|
|
66
|
+
*
|
|
67
|
+
* The old tool name is deliberate and this path never renames. It names what a
|
|
68
|
+
* target already has on disk, so rewriting it to the current spelling would
|
|
69
|
+
* point the fallback at a file that has never existed anywhere.
|
|
70
|
+
*/
|
|
71
|
+
export function legacyStampPath(target: string): string {
|
|
72
|
+
// canon-keep-retired
|
|
73
|
+
return join(target, '.claude', 'aitk.json')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The stamp folder under the retired tool name, which is where every target
|
|
78
|
+
* stamped between `106115ba` and the rename carries its config. Retired for
|
|
79
|
+
* the same reason as the path above and kept readable on the same terms.
|
|
80
|
+
*/
|
|
81
|
+
export function retiredNameStampPath(target: string): string {
|
|
82
|
+
// canon-keep-retired
|
|
83
|
+
return join(target, '.claude', 'aitk', 'config.json')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every spelling a stamp has been written under, current first. The order is
|
|
88
|
+
* the read order, so a target carrying more than one resolves to the newest.
|
|
89
|
+
*
|
|
90
|
+
* The fallback carries no end date. It costs two path reads on a command that
|
|
91
|
+
* already touches the filesystem, and dropping it later is a second breaking
|
|
92
|
+
* change aimed at exactly the targets that were slowest to migrate the first
|
|
93
|
+
* time.
|
|
94
|
+
*/
|
|
95
|
+
export function stampPaths(target: string): readonly string[] {
|
|
96
|
+
return [
|
|
97
|
+
stampPath(target),
|
|
98
|
+
retiredNameStampPath(target),
|
|
99
|
+
legacyStampPath(target),
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether `readStamp` would resolve to a retired path, so a caller can report
|
|
105
|
+
* that a target's config still sits at one. False when no path exists, since
|
|
106
|
+
* there is nothing to migrate off of.
|
|
107
|
+
*/
|
|
108
|
+
export function isLegacyStamped(target: string): boolean {
|
|
109
|
+
if (existsSync(stampPath(target))) return false
|
|
110
|
+
return (
|
|
111
|
+
existsSync(retiredNameStampPath(target)) ||
|
|
112
|
+
existsSync(legacyStampPath(target))
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function hashContent(content: Buffer | string): string {
|
|
117
|
+
return `sha256:${createHash('sha256').update(content).digest('hex')}`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function hashFile(path: string): string {
|
|
121
|
+
return hashContent(readFileSync(path))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Keys are stored posix-style so a stamp written on one platform still
|
|
126
|
+
* resolves on another.
|
|
127
|
+
*/
|
|
128
|
+
export function toStampKey(rel: string): string {
|
|
129
|
+
return rel.split(sep).join('/')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A missing or corrupt stamp reads as absent rather than failing, which is what
|
|
134
|
+
* keeps every unstamped target on the existing unattributed path.
|
|
135
|
+
*
|
|
136
|
+
* Falls back to the retired path only when the current one does not exist,
|
|
137
|
+
* read only: nothing here migrates a target's config as a side effect of a
|
|
138
|
+
* report. The check is existence rather than a successful parse, so a
|
|
139
|
+
* corrupt current stamp reads as absent rather than silently serving the
|
|
140
|
+
* retired one beside it. `isLegacyStamped` tests the same existence check,
|
|
141
|
+
* which is what keeps the two agreeing on which path a corrupt current file
|
|
142
|
+
* was read from.
|
|
143
|
+
*/
|
|
144
|
+
export function readStamp(target: string): Stamp | undefined {
|
|
145
|
+
const found = stampPaths(target).find((path) => existsSync(path))
|
|
146
|
+
return found === undefined ? undefined : readStampFile(found)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function readStampFile(path: string): Stamp | undefined {
|
|
150
|
+
if (!existsSync(path)) return undefined
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
154
|
+
return isStamp(parsed) ? parsed : undefined
|
|
155
|
+
} catch {
|
|
156
|
+
return undefined
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The revision this domain last synced from, which bounds its upstream range. */
|
|
161
|
+
export function stampedCommit(
|
|
162
|
+
stamp: Stamp | undefined,
|
|
163
|
+
domain: StampDomain,
|
|
164
|
+
): string | undefined {
|
|
165
|
+
return stamp?.domains[domain]?.commit
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function stampedHashes(
|
|
169
|
+
stamp: Stamp | undefined,
|
|
170
|
+
domain: StampDomain | undefined,
|
|
171
|
+
): DomainHashes {
|
|
172
|
+
if (stamp === undefined || domain === undefined) return {}
|
|
173
|
+
return stamp.domains[domain]?.files ?? {}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The stack chain a domain's install last recorded. An empty result is the
|
|
178
|
+
* state every target predating that domain's chain recording sits in, and a
|
|
179
|
+
* reader treats it as unmeasured rather than as clean.
|
|
180
|
+
*/
|
|
181
|
+
export function stampedChain(
|
|
182
|
+
stamp: Stamp | undefined,
|
|
183
|
+
domain: StampDomain,
|
|
184
|
+
): readonly string[] {
|
|
185
|
+
return stamp?.domains[domain]?.chain ?? []
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Replaces one domain's file hashes and leaves the others, including that
|
|
190
|
+
* domain's own chain, untouched. Domains install and sync independently but
|
|
191
|
+
* share the one file, and a chain an install recorded is a separate fact a
|
|
192
|
+
* later file-only sync must not erase.
|
|
193
|
+
*/
|
|
194
|
+
export async function writeStamp(
|
|
195
|
+
target: string,
|
|
196
|
+
source: StampSource,
|
|
197
|
+
hashes: DomainHashes,
|
|
198
|
+
now: Date,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
await putDomain(target, source, { files: sortKeys(hashes) }, now)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Records the stack chain an install resolved and leaves that domain's file
|
|
205
|
+
* hashes untouched. Tooling calls this with no files ever recorded, since
|
|
206
|
+
* `src/tooling/` never runs the sync engine and the chain is its whole record.
|
|
207
|
+
* Governance calls it alongside `writeStamp`, since it records both.
|
|
208
|
+
*/
|
|
209
|
+
export async function writeChainStamp(
|
|
210
|
+
target: string,
|
|
211
|
+
source: StampSource,
|
|
212
|
+
chain: readonly string[],
|
|
213
|
+
now: Date,
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
await putDomain(target, source, { chain: [...chain] }, now)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function putDomain(
|
|
219
|
+
target: string,
|
|
220
|
+
source: StampSource,
|
|
221
|
+
payload: Partial<Pick<DomainStamp, 'files' | 'chain'>>,
|
|
222
|
+
now: Date,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
const previous = readStamp(target)
|
|
225
|
+
const previousRecord = previous?.domains[source.domain]
|
|
226
|
+
const commit = await toolkitCommit(source.toolkitRoot)
|
|
227
|
+
|
|
228
|
+
const record: DomainStamp = {
|
|
229
|
+
...(commit === undefined ? {} : { commit }),
|
|
230
|
+
syncedAt: now.toISOString(),
|
|
231
|
+
files: payload.files ?? previousRecord?.files ?? {},
|
|
232
|
+
...resolveChainField(payload.chain, previousRecord?.chain),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const domains = sortDomains({
|
|
236
|
+
...previous?.domains,
|
|
237
|
+
[source.domain]: record,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const stamp: Stamp = {
|
|
241
|
+
covers: STAMP_DOMAINS.filter((domain) => domains[domain] !== undefined),
|
|
242
|
+
domains,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const path = stampPath(target)
|
|
246
|
+
await mkdir(dirname(path), { recursive: true })
|
|
247
|
+
await writeFile(path, `${JSON.stringify(stamp, null, 2)}\n`)
|
|
248
|
+
|
|
249
|
+
// Every install and sync that stamps a target passes through here, which is
|
|
250
|
+
// what makes this the one place the machine-level index can be kept without
|
|
251
|
+
// each command remembering to. Its outcome is dropped rather than reported:
|
|
252
|
+
// the stamp just written is the authoritative record of this install, the
|
|
253
|
+
// index is a cache over every such stamp, and a state folder nobody can
|
|
254
|
+
// write is not a reason to fail a sync that already landed its files.
|
|
255
|
+
recordTarget(target, now)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const commitCache = new Map<string, Promise<string | undefined>>()
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Resolved once per clone per process. A toolkit reached outside a git clone
|
|
262
|
+
* yields no commit, which costs the report its upstream range but not its
|
|
263
|
+
* per-file attribution.
|
|
264
|
+
*/
|
|
265
|
+
export function toolkitCommit(root: string): Promise<string | undefined> {
|
|
266
|
+
const cached = commitCache.get(root)
|
|
267
|
+
if (cached !== undefined) return cached
|
|
268
|
+
|
|
269
|
+
const pending = readCommit(root)
|
|
270
|
+
commitCache.set(root, pending)
|
|
271
|
+
return pending
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function readCommit(root: string): Promise<string | undefined> {
|
|
275
|
+
const result = await execa(
|
|
276
|
+
'git',
|
|
277
|
+
['-C', root, 'rev-parse', '--short', 'HEAD'],
|
|
278
|
+
{ reject: false },
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return result.exitCode === 0 && result.stdout.trim() !== ''
|
|
282
|
+
? result.stdout.trim()
|
|
283
|
+
: undefined
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* A write naming no chain keeps the domain's previous one rather than dropping
|
|
288
|
+
* it, since `writeStamp` and `writeChainStamp` each touch one half of a
|
|
289
|
+
* governance record and neither should erase what the other wrote.
|
|
290
|
+
*/
|
|
291
|
+
function resolveChainField(
|
|
292
|
+
chain: readonly string[] | undefined,
|
|
293
|
+
previous: readonly string[] | undefined,
|
|
294
|
+
): Pick<DomainStamp, 'chain'> {
|
|
295
|
+
const resolved = chain ?? previous
|
|
296
|
+
return resolved === undefined ? {} : { chain: resolved }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Deterministic key order keeps a re-sync diff empty and a merge conflict local. */
|
|
300
|
+
function sortKeys(hashes: DomainHashes): DomainHashes {
|
|
301
|
+
return Object.fromEntries(
|
|
302
|
+
Object.entries(hashes).sort(([left], [right]) => left.localeCompare(right)),
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function sortDomains(
|
|
307
|
+
domains: Partial<Record<StampDomain, DomainStamp>>,
|
|
308
|
+
): Partial<Record<StampDomain, DomainStamp>> {
|
|
309
|
+
const sorted: Partial<Record<StampDomain, DomainStamp>> = {}
|
|
310
|
+
for (const domain of STAMP_DOMAINS) {
|
|
311
|
+
const record = domains[domain]
|
|
312
|
+
if (record !== undefined) sorted[domain] = record
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return sorted
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Validates every recognized domain record rather than only the container. A
|
|
320
|
+
* stamp whose domain value is the wrong shape would otherwise reach
|
|
321
|
+
* `attribute`, where a bad hash lookup silently reads as a customization.
|
|
322
|
+
*
|
|
323
|
+
* A key naming no current domain is ignored rather than failing the parse. A
|
|
324
|
+
* target stamped before the standards install channel closed carries one, and
|
|
325
|
+
* rejecting the file would read as unstamped and then drop the three records it
|
|
326
|
+
* does carry on the next write. `sortDomains` retires the key at that write.
|
|
327
|
+
*/
|
|
328
|
+
function isStamp(value: unknown): value is Stamp {
|
|
329
|
+
if (!isRecord(value) || !isRecord(value.domains)) return false
|
|
330
|
+
|
|
331
|
+
return Object.entries(value.domains)
|
|
332
|
+
.filter(([domain]) => (STAMP_DOMAINS as readonly string[]).includes(domain))
|
|
333
|
+
.every(([, record]) => isDomainStamp(record))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* `chain` is optional, which is what keeps a stamp written before tooling
|
|
338
|
+
* joined the domains readable rather than parsing as corrupt and discarding
|
|
339
|
+
* the three records it does carry.
|
|
340
|
+
*/
|
|
341
|
+
function isDomainStamp(value: unknown): value is DomainStamp {
|
|
342
|
+
if (!isRecord(value) || !isRecord(value.files)) return false
|
|
343
|
+
if (typeof value.syncedAt !== 'string') return false
|
|
344
|
+
if (value.commit !== undefined && typeof value.commit !== 'string') {
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
if (value.chain !== undefined && !isStringArray(value.chain)) return false
|
|
348
|
+
|
|
349
|
+
return Object.values(value.files).every((hash) => typeof hash === 'string')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isStringArray(value: unknown): value is string[] {
|
|
353
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
357
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
358
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { isDirectory } from '@/target'
|
|
4
|
+
|
|
5
|
+
export const SYNC_DOMAINS = ['governance', 'claude'] as const
|
|
6
|
+
|
|
7
|
+
export type SyncDomain = (typeof SYNC_DOMAINS)[number]
|
|
8
|
+
|
|
9
|
+
export interface DomainState {
|
|
10
|
+
readonly domain: SyncDomain
|
|
11
|
+
readonly installed: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DOMAIN_MARKERS: Record<SyncDomain, string> = {
|
|
15
|
+
governance: join('.claude', 'rules'),
|
|
16
|
+
claude: '.claude',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Paths whose git status decides whether a domain contributed to the sync
|
|
21
|
+
* commit. Governance carries the retired `.claude/GOV.md` because a sync that
|
|
22
|
+
* removes it is still a governance change, and `claude` watches `.gitignore`
|
|
23
|
+
* because that is the only file `canon claude sync` writes.
|
|
24
|
+
*/
|
|
25
|
+
const DOMAIN_PATHS: Record<SyncDomain, readonly string[]> = {
|
|
26
|
+
governance: ['.claude/rules/', '.claude/GOV.md'],
|
|
27
|
+
claude: ['.gitignore'],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function domainPaths(domain: SyncDomain): readonly string[] {
|
|
31
|
+
return DOMAIN_PATHS[domain]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function detectDomains(target: string): DomainState[] {
|
|
35
|
+
return SYNC_DOMAINS.map((domain) => ({
|
|
36
|
+
domain,
|
|
37
|
+
installed: isDirectory(join(target, DOMAIN_MARKERS[domain])),
|
|
38
|
+
}))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function installedDomains(states: readonly DomainState[]): SyncDomain[] {
|
|
42
|
+
return states.filter((state) => state.installed).map((state) => state.domain)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a domain's sync command should run, which is not the same question
|
|
47
|
+
* `detectDomains` answers. Governance reports as absent without
|
|
48
|
+
* `.claude/rules/`, yet still syncs when only the retired `.claude/GOV.md`
|
|
49
|
+
* remains, so that the sync can delete it. The bash drew the same distinction
|
|
50
|
+
* between `detect_domains` and `run_syncs`.
|
|
51
|
+
*/
|
|
52
|
+
export function shouldSync(target: string, domain: SyncDomain): boolean {
|
|
53
|
+
if (domain === 'governance') {
|
|
54
|
+
return (
|
|
55
|
+
isDirectory(join(target, '.claude', 'rules')) ||
|
|
56
|
+
existsSync(join(target, '.claude', 'GOV.md'))
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return isDirectory(join(target, DOMAIN_MARKERS[domain]))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A target with no `.git/` reports clean, because `git status` fails there and
|
|
65
|
+
* the bash swallowed that failure. The guard exists to make the sync commit
|
|
66
|
+
* survivable, and a directory outside git has no commit to protect.
|
|
67
|
+
*/
|
|
68
|
+
export function isTreeClean(status: string): boolean {
|
|
69
|
+
return status.trim() === ''
|
|
70
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import {
|
|
3
|
+
changedNames,
|
|
4
|
+
changedPaths,
|
|
5
|
+
classifyStatus,
|
|
6
|
+
commitMessage,
|
|
7
|
+
type DomainChange,
|
|
8
|
+
type GitRunner,
|
|
9
|
+
type PullRequestOpener,
|
|
10
|
+
pullRequestBody,
|
|
11
|
+
syncBranchName,
|
|
12
|
+
} from '@/sync/git'
|
|
13
|
+
import { domainPaths, SYNC_DOMAINS } from '@/sync/target'
|
|
14
|
+
import { isDirectory } from '@/target'
|
|
15
|
+
import {
|
|
16
|
+
intro,
|
|
17
|
+
logError,
|
|
18
|
+
logInfo,
|
|
19
|
+
logStep,
|
|
20
|
+
logWarn,
|
|
21
|
+
outro,
|
|
22
|
+
palette,
|
|
23
|
+
pipeOutput,
|
|
24
|
+
select,
|
|
25
|
+
} from '@/ui'
|
|
26
|
+
|
|
27
|
+
const PROTECTED_BRANCHES: readonly string[] = ['main', 'master']
|
|
28
|
+
|
|
29
|
+
export type WorkflowChoice = 'pr' | 'commit' | 'cancel'
|
|
30
|
+
|
|
31
|
+
export interface WorkflowDeps {
|
|
32
|
+
readonly git: GitRunner
|
|
33
|
+
/** Absent when `gh` is not installed, which hides the pull request option. */
|
|
34
|
+
readonly pullRequests: PullRequestOpener | undefined
|
|
35
|
+
readonly now: Date
|
|
36
|
+
readonly nonInteractive: boolean
|
|
37
|
+
/**
|
|
38
|
+
* Defaults to the interactive prompt. A test injects it to reach the commit
|
|
39
|
+
* and pull request branches, which `select` otherwise gates behind a TTY.
|
|
40
|
+
*/
|
|
41
|
+
readonly choose?: (canOpenPullRequest: boolean) => Promise<WorkflowChoice>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Groups the target's pending changes by the domain that produced them. Each
|
|
46
|
+
* domain is read independently, so the four status calls run together.
|
|
47
|
+
*/
|
|
48
|
+
export async function collectChanges(git: GitRunner): Promise<DomainChange[]> {
|
|
49
|
+
const results = await Promise.all(
|
|
50
|
+
SYNC_DOMAINS.map(async (domain) => {
|
|
51
|
+
const status = await git.status(domainPaths(domain))
|
|
52
|
+
return { domain, status }
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const changes: DomainChange[] = []
|
|
57
|
+
|
|
58
|
+
for (const { domain, status } of results) {
|
|
59
|
+
const names = changedNames(status)
|
|
60
|
+
if (names.length === 0) continue
|
|
61
|
+
|
|
62
|
+
changes.push({
|
|
63
|
+
domain,
|
|
64
|
+
verb: classifyStatus(status),
|
|
65
|
+
names,
|
|
66
|
+
paths: changedPaths(status),
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return changes
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Commits what the domain syncs wrote and optionally opens a pull request.
|
|
75
|
+
*
|
|
76
|
+
* This is the only toolkit code that writes to a git remote, which is why a
|
|
77
|
+
* headless run refuses it outright. `select_option` resolved to its first
|
|
78
|
+
* option under `CANON_NON_INTERACTIVE=1`, and the first option here was
|
|
79
|
+
* `Commit and open PR` whenever `gh` was installed, so an agent following the
|
|
80
|
+
* documented non-interactive path pushed a branch and opened a pull request on
|
|
81
|
+
* someone's repository without being asked.
|
|
82
|
+
*/
|
|
83
|
+
export async function runGitWorkflow(
|
|
84
|
+
target: string,
|
|
85
|
+
deps: WorkflowDeps,
|
|
86
|
+
): Promise<number> {
|
|
87
|
+
if (!isDirectory(join(target, '.git'))) return 0
|
|
88
|
+
|
|
89
|
+
const changes = await collectChanges(deps.git)
|
|
90
|
+
if (changes.length === 0) return 0
|
|
91
|
+
|
|
92
|
+
const domains = changes.map((change) => change.domain)
|
|
93
|
+
const message = commitMessage(domains)
|
|
94
|
+
const body = pullRequestBody(changes)
|
|
95
|
+
const branch = syncBranchName(deps.now)
|
|
96
|
+
|
|
97
|
+
intro('canon sync → git')
|
|
98
|
+
|
|
99
|
+
const [localTaken, remoteTaken] = await Promise.all([
|
|
100
|
+
deps.git.branchExists(branch),
|
|
101
|
+
deps.git.remoteBranchExists(branch),
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
if (localTaken || remoteTaken) {
|
|
105
|
+
logWarn(
|
|
106
|
+
`${branch} already exists. Wait a minute or delete it, then re-run.`,
|
|
107
|
+
)
|
|
108
|
+
outro()
|
|
109
|
+
return 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const currentBranch = await deps.git.currentBranch()
|
|
113
|
+
const onProtected =
|
|
114
|
+
currentBranch === '' || PROTECTED_BRANCHES.includes(currentBranch)
|
|
115
|
+
|
|
116
|
+
logStep('Preview')
|
|
117
|
+
logInfo(`Domains: ${domains.join(', ')}`)
|
|
118
|
+
logInfo(
|
|
119
|
+
onProtected
|
|
120
|
+
? `Branch: ${branch} (source: ${currentBranch || 'detached'})`
|
|
121
|
+
: `Branch: ${currentBranch} (commit only) or ${branch} (PR)`,
|
|
122
|
+
)
|
|
123
|
+
logInfo(`Commit: ${message}`)
|
|
124
|
+
|
|
125
|
+
logStep('PR body')
|
|
126
|
+
pipeOutput(body)
|
|
127
|
+
|
|
128
|
+
if (deps.nonInteractive) {
|
|
129
|
+
logWarn('Refusing the git workflow in non-interactive mode.')
|
|
130
|
+
logInfo(`Would commit ${domains.join(', ')} to ${branch}.`)
|
|
131
|
+
logInfo('Run interactively to commit, push, and open the pull request.')
|
|
132
|
+
outro()
|
|
133
|
+
return 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const opener = deps.pullRequests
|
|
137
|
+
if (opener === undefined) {
|
|
138
|
+
logWarn(
|
|
139
|
+
'gh CLI not found — PR option unavailable. Install from https://cli.github.com',
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const choose = deps.choose ?? promptChoice
|
|
144
|
+
const choice = await choose(opener !== undefined)
|
|
145
|
+
|
|
146
|
+
if (choice === 'cancel') {
|
|
147
|
+
logWarn('Skipped')
|
|
148
|
+
outro()
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const paths = [...new Set(changes.flatMap((change) => change.paths))]
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
logStep('Committing')
|
|
156
|
+
|
|
157
|
+
if (choice === 'pr' || onProtected) await deps.git.createBranch(branch)
|
|
158
|
+
await deps.git.stage(paths)
|
|
159
|
+
await deps.git.commit(message)
|
|
160
|
+
|
|
161
|
+
if (choice === 'commit' || opener === undefined) {
|
|
162
|
+
return succeed(
|
|
163
|
+
onProtected
|
|
164
|
+
? `Committed to ${branch}. Push and open a PR when ready.`
|
|
165
|
+
: `Committed to ${currentBranch}.`,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await deps.git.push(branch)
|
|
170
|
+
|
|
171
|
+
logStep('Opening PR')
|
|
172
|
+
const url = await opener.create(message, body)
|
|
173
|
+
|
|
174
|
+
return succeed(`Synced: ${url}`)
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logError(error instanceof Error ? error.message : String(error))
|
|
177
|
+
outro()
|
|
178
|
+
return 1
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function promptChoice(canOpenPullRequest: boolean): Promise<WorkflowChoice> {
|
|
183
|
+
return select({
|
|
184
|
+
message: 'Review changes, then?',
|
|
185
|
+
options: [
|
|
186
|
+
...(canOpenPullRequest
|
|
187
|
+
? [{ value: 'pr' as const, label: 'Commit and open PR' }]
|
|
188
|
+
: []),
|
|
189
|
+
{ value: 'commit' as const, label: 'Commit only' },
|
|
190
|
+
{ value: 'cancel' as const, label: 'Cancel' },
|
|
191
|
+
],
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function succeed(message: string): number {
|
|
196
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
197
|
+
outro()
|
|
198
|
+
process.stderr.write(`\n${GREEN}✓ ${message}${NC}\n`)
|
|
199
|
+
return 0
|
|
200
|
+
}
|
package/src/target.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { statSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { logError, outro } from '@/ui'
|
|
4
|
+
|
|
5
|
+
export function isDirectory(path: string): boolean {
|
|
6
|
+
try {
|
|
7
|
+
return statSync(path).isDirectory()
|
|
8
|
+
} catch {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stands in for `validate_target` plus `guard_root`. The bash `guard_root` read
|
|
15
|
+
* as a toolkit-root check, but its body was `cd "$target" && pwd`, so it also
|
|
16
|
+
* rejected a target that does not exist. Porting the name alone would let a
|
|
17
|
+
* typo'd path scaffold a whole new tree, since every apply step creates parents.
|
|
18
|
+
*
|
|
19
|
+
* Returns the resolved path, or the exit code the caller should hand back. The
|
|
20
|
+
* frame is already open by the time this runs, so a rejection closes it.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveTarget(
|
|
23
|
+
target: string,
|
|
24
|
+
protectedRoot: string,
|
|
25
|
+
): string | number {
|
|
26
|
+
const resolved = resolve(target)
|
|
27
|
+
|
|
28
|
+
if (!isDirectory(resolved)) {
|
|
29
|
+
logError(`Target directory not found: ${target}`)
|
|
30
|
+
outro()
|
|
31
|
+
return 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (resolved === protectedRoot) {
|
|
35
|
+
logError(
|
|
36
|
+
'Cannot run against toolkit root. Files here are the source of truth.',
|
|
37
|
+
)
|
|
38
|
+
outro()
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return resolved
|
|
43
|
+
}
|