contextdevkit 1.8.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/CHANGELOG.md +592 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/docs/AGENT-PACKAGE-FORMAT.md +140 -0
- package/docs/ARCHITECTURE.md +258 -0
- package/docs/CHANGELOG.md +559 -0
- package/docs/CUSTOMIZING.md +211 -0
- package/docs/LEVELS.md +151 -0
- package/docs/ROADMAP.md +385 -0
- package/docs/SQUAD-PIPELINE-FORMAT.md +258 -0
- package/docs/SQUADS/agent-forge.md +65 -0
- package/docs/SQUADS/design-team.md +161 -0
- package/docs/token-economy-plan.md +135 -0
- package/install.mjs +273 -0
- package/instrucoes.md +274 -0
- package/package.json +46 -0
- package/templates/CLAUDE.md.tpl +133 -0
- package/templates/claude/agents/_TEMPLATE.md +52 -0
- package/templates/claude/agents/accessibility.md +36 -0
- package/templates/claude/agents/agent-architect.md +37 -0
- package/templates/claude/agents/architect.md +39 -0
- package/templates/claude/agents/code-reviewer.md +43 -0
- package/templates/claude/agents/code-security.md +59 -0
- package/templates/claude/agents/context-keeper.md +40 -0
- package/templates/claude/agents/devops.md +40 -0
- package/templates/claude/agents/eval-designer.md +40 -0
- package/templates/claude/agents/forge-orchestrator.md +42 -0
- package/templates/claude/agents/governance-officer.md +45 -0
- package/templates/claude/agents/growth.md +92 -0
- package/templates/claude/agents/infra-security.md +53 -0
- package/templates/claude/agents/landing-architect.md +154 -0
- package/templates/claude/agents/model-router.md +34 -0
- package/templates/claude/agents/packager.md +38 -0
- package/templates/claude/agents/privacy-lgpd.md +64 -0
- package/templates/claude/agents/product-owner.md +51 -0
- package/templates/claude/agents/prompt-engineer.md +33 -0
- package/templates/claude/agents/qa-e2e.md +52 -0
- package/templates/claude/agents/qa-fuzzer.md +24 -0
- package/templates/claude/agents/qa-integration.md +21 -0
- package/templates/claude/agents/qa-orchestrator.md +40 -0
- package/templates/claude/agents/qa-perf.md +40 -0
- package/templates/claude/agents/qa-unit.md +39 -0
- package/templates/claude/agents/rag-designer.md +54 -0
- package/templates/claude/agents/retention.md +85 -0
- package/templates/claude/agents/security.md +48 -0
- package/templates/claude/agents/seo-specialist.md +106 -0
- package/templates/claude/agents/test-engineer.md +48 -0
- package/templates/claude/agents/tool-designer.md +32 -0
- package/templates/claude/agents/ui-designer.md +37 -0
- package/templates/claude/agents/ux-designer.md +38 -0
- package/templates/claude/commands/README.md +95 -0
- package/templates/claude/commands/advise.md +80 -0
- package/templates/claude/commands/audit/analyze-code-ia-practices.md +75 -0
- package/templates/claude/commands/audit/audit.md +35 -0
- package/templates/claude/commands/audit/contract-check.md +21 -0
- package/templates/claude/commands/audit/deep-analysis.md +48 -0
- package/templates/claude/commands/audit/deps-audit.md +49 -0
- package/templates/claude/commands/audit/security-setup.md +35 -0
- package/templates/claude/commands/audit/seo-audit.md +63 -0
- package/templates/claude/commands/audit/tech-debt-sweep.md +35 -0
- package/templates/claude/commands/bug-hunt.md +42 -0
- package/templates/claude/commands/claude-md.md +36 -0
- package/templates/claude/commands/close-version.md +25 -0
- package/templates/claude/commands/context-refresh.md +19 -0
- package/templates/claude/commands/context-stats.md +15 -0
- package/templates/claude/commands/dashboard.md +66 -0
- package/templates/claude/commands/distill-apply.md +19 -0
- package/templates/claude/commands/distill-sessions.md +26 -0
- package/templates/claude/commands/fleet.md +47 -0
- package/templates/claude/commands/forge/forge-audit.md +16 -0
- package/templates/claude/commands/forge/forge-budget.md +16 -0
- package/templates/claude/commands/forge/forge-deprecate.md +16 -0
- package/templates/claude/commands/forge/forge-doctor.md +17 -0
- package/templates/claude/commands/forge/forge-eval.md +16 -0
- package/templates/claude/commands/forge/forge-fallback-test.md +17 -0
- package/templates/claude/commands/forge/forge-killswitch.md +17 -0
- package/templates/claude/commands/forge/forge-list.md +17 -0
- package/templates/claude/commands/forge/forge-new.md +41 -0
- package/templates/claude/commands/forge/forge-policy.md +16 -0
- package/templates/claude/commands/forge/forge-redteam.md +17 -0
- package/templates/claude/commands/forge/forge-refresh-matrix.md +20 -0
- package/templates/claude/commands/forge/forge-route.md +17 -0
- package/templates/claude/commands/forge/forge-show.md +16 -0
- package/templates/claude/commands/landing-page.md +71 -0
- package/templates/claude/commands/log-session.md +59 -0
- package/templates/claude/commands/media-gen.md +93 -0
- package/templates/claude/commands/new-adr.md +30 -0
- package/templates/claude/commands/pipeline/dev-start.md +64 -0
- package/templates/claude/commands/pipeline/pipeline.md +36 -0
- package/templates/claude/commands/pipeline/resume.md +70 -0
- package/templates/claude/commands/pipeline/retro.md +34 -0
- package/templates/claude/commands/pipeline/runs.md +63 -0
- package/templates/claude/commands/pipeline/ship.md +54 -0
- package/templates/claude/commands/pipeline/workflow.md +85 -0
- package/templates/claude/commands/playbook.md +27 -0
- package/templates/claude/commands/predictions-review.md +28 -0
- package/templates/claude/commands/qa/qa-signoff.md +24 -0
- package/templates/claude/commands/qa/scaffold-tests.md +27 -0
- package/templates/claude/commands/qa/test-plan.md +26 -0
- package/templates/claude/commands/qa/visual-test.md +42 -0
- package/templates/claude/commands/roadmap.md +48 -0
- package/templates/claude/commands/setup/aidevtool-from0.md +104 -0
- package/templates/claude/commands/setup/context-config.md +25 -0
- package/templates/claude/commands/setup/context-doctor.md +21 -0
- package/templates/claude/commands/setup/context-level.md +17 -0
- package/templates/claude/commands/setup/setupcontextdevkit.md +121 -0
- package/templates/claude/commands/simulate-impact.md +32 -0
- package/templates/claude/commands/squad.md +44 -0
- package/templates/claude/commands/state.md +21 -0
- package/templates/claude/commands/token-report.md +29 -0
- package/templates/claude/commands/tune-agents.md +35 -0
- package/templates/claude/commands/vcs/claim.md +18 -0
- package/templates/claude/commands/vcs/git.md +83 -0
- package/templates/claude/commands/vcs/release.md +15 -0
- package/templates/claude/commands/vcs/worktree-new.md +18 -0
- package/templates/claude/commands/watch.md +47 -0
- package/templates/contextkit/.env.example +36 -0
- package/templates/contextkit/CLAUDE.child.md.tpl +38 -0
- package/templates/contextkit/README.md +74 -0
- package/templates/contextkit/behaviors-examples.md +183 -0
- package/templates/contextkit/behaviors.md +116 -0
- package/templates/contextkit/best-practices.md +323 -0
- package/templates/contextkit/config.json +66 -0
- package/templates/contextkit/detectors/README.md +45 -0
- package/templates/contextkit/detectors/example-detector.mjs.example +25 -0
- package/templates/contextkit/instrucoes.md +114 -0
- package/templates/contextkit/memory/GLOSSARY.md +13 -0
- package/templates/contextkit/memory/SESSIONS.md +9 -0
- package/templates/contextkit/memory/WORKSPACE.md +7 -0
- package/templates/contextkit/memory/business-rules/_TEMPLATE.md +33 -0
- package/templates/contextkit/memory/decisions/0000-record-architecture-decisions.md +34 -0
- package/templates/contextkit/memory/decisions/_TEMPLATE.md +25 -0
- package/templates/contextkit/memory/predictions/.gitkeep +0 -0
- package/templates/contextkit/memory/roadmap.md +28 -0
- package/templates/contextkit/memory/sessions/.gitkeep +0 -0
- package/templates/contextkit/memory/workflows/.gitkeep +0 -0
- package/templates/contextkit/pipeline/backlog/.gitkeep +0 -0
- package/templates/contextkit/pipeline/conclusion/.gitkeep +0 -0
- package/templates/contextkit/pipeline/devpipeline.md +9 -0
- package/templates/contextkit/pipeline/testing/.gitkeep +0 -0
- package/templates/contextkit/pipeline/working/.gitkeep +0 -0
- package/templates/contextkit/review-protocol.md +214 -0
- package/templates/contextkit/runtime/config/defaults.mjs +215 -0
- package/templates/contextkit/runtime/config/levels.mjs +42 -0
- package/templates/contextkit/runtime/config/load.mjs +105 -0
- package/templates/contextkit/runtime/config/paths.mjs +92 -0
- package/templates/contextkit/runtime/config/presets.mjs +47 -0
- package/templates/contextkit/runtime/config/schema.mjs +88 -0
- package/templates/contextkit/runtime/config/settings-compose.mjs +55 -0
- package/templates/contextkit/runtime/git-hooks/commit-msg.mjs +55 -0
- package/templates/contextkit/runtime/git-hooks/pre-commit.mjs +47 -0
- package/templates/contextkit/runtime/git-hooks/pre-push.mjs +102 -0
- package/templates/contextkit/runtime/hooks/boot-context-readers.mjs +111 -0
- package/templates/contextkit/runtime/hooks/boot-signals.mjs +135 -0
- package/templates/contextkit/runtime/hooks/check-registration.mjs +228 -0
- package/templates/contextkit/runtime/hooks/concurrency-guard.mjs +110 -0
- package/templates/contextkit/runtime/hooks/ledger.mjs +231 -0
- package/templates/contextkit/runtime/hooks/md-extract.mjs +65 -0
- package/templates/contextkit/runtime/hooks/path-classification.mjs +62 -0
- package/templates/contextkit/runtime/hooks/safe-io.mjs +84 -0
- package/templates/contextkit/runtime/hooks/session-digest-core.mjs +85 -0
- package/templates/contextkit/runtime/hooks/session-start.mjs +248 -0
- package/templates/contextkit/runtime/hooks/simulate-gate.mjs +108 -0
- package/templates/contextkit/runtime/hooks/track-edits.mjs +154 -0
- package/templates/contextkit/runtime/providers/media/_adapter.mjs +120 -0
- package/templates/contextkit/runtime/providers/media/nano-banana.mjs +110 -0
- package/templates/contextkit/runtime/providers/media/veo.mjs +162 -0
- package/templates/contextkit/runtime/providers/review/_adapter.mjs +71 -0
- package/templates/contextkit/runtime/providers/review/detect.mjs +115 -0
- package/templates/contextkit/runtime/providers/review/gh.mjs +103 -0
- package/templates/contextkit/runtime/state/state-io.mjs +172 -0
- package/templates/contextkit/runtime/statusline.mjs +51 -0
- package/templates/contextkit/squads/README.md +115 -0
- package/templates/contextkit/squads/_BRIEFING.md.tpl +27 -0
- package/templates/contextkit/squads/agent-forge/README.md +69 -0
- package/templates/contextkit/squads/agent-forge/ROADMAP.md +108 -0
- package/templates/contextkit/squads/agent-forge/best-practices.md +89 -0
- package/templates/contextkit/squads/agent-forge/cli/forge-admin.mjs +132 -0
- package/templates/contextkit/squads/agent-forge/cli/forge-eval-cli.mjs +163 -0
- package/templates/contextkit/squads/agent-forge/cli/forge-new.mjs +97 -0
- package/templates/contextkit/squads/agent-forge/cli/forge-ops.mjs +177 -0
- package/templates/contextkit/squads/agent-forge/lib/architect.mjs +112 -0
- package/templates/contextkit/squads/agent-forge/lib/eval-designer.mjs +133 -0
- package/templates/contextkit/squads/agent-forge/lib/eval-runner.mjs +167 -0
- package/templates/contextkit/squads/agent-forge/lib/governance-officer.mjs +178 -0
- package/templates/contextkit/squads/agent-forge/lib/package-ops.mjs +101 -0
- package/templates/contextkit/squads/agent-forge/lib/packager.mjs +219 -0
- package/templates/contextkit/squads/agent-forge/lib/prompt-gen.mjs +122 -0
- package/templates/contextkit/squads/agent-forge/lib/rag-designer.mjs +102 -0
- package/templates/contextkit/squads/agent-forge/lib/router.mjs +165 -0
- package/templates/contextkit/squads/agent-forge/lib/tool-gen.mjs +113 -0
- package/templates/contextkit/squads/agent-forge/lib/yaml.mjs +47 -0
- package/templates/contextkit/squads/agent-forge/pipeline.yaml +65 -0
- package/templates/contextkit/squads/agent-forge/router/capability-matrix.json +112 -0
- package/templates/contextkit/squads/agent-forge/router/decision-rules.json +120 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/.agentforgerc +12 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/CHANGELOG.md +13 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/LICENSE +5 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/README.md +39 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/go/README.md +10 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/go/agent.go +14 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/go/go.mod +3 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/node/README.md +11 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/node/index.js +53 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/node/package.json +9 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/python/README.md +10 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/python/agent.py +16 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/python/pyproject.toml +10 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/golden.jsonl +1 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/red-team.jsonl +3 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/rubric.yaml +14 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/run-eval.md +17 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/thresholds.yaml +18 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/examples/basic.node.md +17 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/examples/with-fallback.node.md +24 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/examples/with-rag.python.md +20 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/audit.schema.json +23 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/compliance.policy.yaml +43 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/cost.policy.yaml +36 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/fallback-chain.yaml +16 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/quality.policy.yaml +43 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/manifest.yaml +91 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.anthropic.md +19 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.canonical.md +25 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.deepseek.md +21 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.google.md +19 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.ollama.md +21 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.openai.md +20 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/config.yaml +17 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/index/.gitkeep +3 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/ingestion/chunker.config.yaml +6 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/ingestion/sources.yaml +8 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/retrieval/query-template.md +16 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/retrieval/rerank.config.yaml +6 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/anthropic.tools.json +11 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/deepseek.tools.json +14 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/google.tools.json +11 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/ollama.tools.json +14 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/openai.tools.json +14 -0
- package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/schemas.canonical.json +25 -0
- package/templates/contextkit/starters/tanstack/README.md +86 -0
- package/templates/contextkit/starters/tanstack/index.html +12 -0
- package/templates/contextkit/starters/tanstack/package.json +25 -0
- package/templates/contextkit/starters/tanstack/src/main.tsx +40 -0
- package/templates/contextkit/starters/tanstack/src/router.tsx +12 -0
- package/templates/contextkit/starters/tanstack/src/routes/__root.tsx +10 -0
- package/templates/contextkit/starters/tanstack/src/routes/index.tsx +17 -0
- package/templates/contextkit/starters/tanstack/tsconfig.json +19 -0
- package/templates/contextkit/starters/tanstack/vite.config.ts +10 -0
- package/templates/contextkit/tools/scripts/adr-digest-core.mjs +42 -0
- package/templates/contextkit/tools/scripts/adr-digest.mjs +78 -0
- package/templates/contextkit/tools/scripts/agent-tuning.mjs +74 -0
- package/templates/contextkit/tools/scripts/aiso-audit.mjs +174 -0
- package/templates/contextkit/tools/scripts/audit-shared.mjs +129 -0
- package/templates/contextkit/tools/scripts/claim.mjs +133 -0
- package/templates/contextkit/tools/scripts/claude-md.mjs +123 -0
- package/templates/contextkit/tools/scripts/clean-drive.mjs +78 -0
- package/templates/contextkit/tools/scripts/context-config.mjs +111 -0
- package/templates/contextkit/tools/scripts/context-level.mjs +98 -0
- package/templates/contextkit/tools/scripts/context-pack.mjs +120 -0
- package/templates/contextkit/tools/scripts/contract-scan.mjs +186 -0
- package/templates/contextkit/tools/scripts/dashboard-data.mjs +198 -0
- package/templates/contextkit/tools/scripts/dashboard-html.mjs +215 -0
- package/templates/contextkit/tools/scripts/dashboard-server.mjs +129 -0
- package/templates/contextkit/tools/scripts/dashboard.mjs +107 -0
- package/templates/contextkit/tools/scripts/deep-analysis.mjs +62 -0
- package/templates/contextkit/tools/scripts/deps-audit.mjs +201 -0
- package/templates/contextkit/tools/scripts/detect-stack.mjs +164 -0
- package/templates/contextkit/tools/scripts/distill-detect.mjs +90 -0
- package/templates/contextkit/tools/scripts/doctor.mjs +165 -0
- package/templates/contextkit/tools/scripts/fleet.mjs +170 -0
- package/templates/contextkit/tools/scripts/generate-context.mjs +142 -0
- package/templates/contextkit/tools/scripts/gh-alerts.mjs +117 -0
- package/templates/contextkit/tools/scripts/git.mjs +97 -0
- package/templates/contextkit/tools/scripts/home.mjs +106 -0
- package/templates/contextkit/tools/scripts/mark-simulation.mjs +78 -0
- package/templates/contextkit/tools/scripts/media-gen.mjs +154 -0
- package/templates/contextkit/tools/scripts/pipeline-board.mjs +74 -0
- package/templates/contextkit/tools/scripts/pipeline-prioritize.mjs +68 -0
- package/templates/contextkit/tools/scripts/pipeline-session.mjs +99 -0
- package/templates/contextkit/tools/scripts/pipeline-validate.mjs +136 -0
- package/templates/contextkit/tools/scripts/pipeline.mjs +302 -0
- package/templates/contextkit/tools/scripts/playbook.mjs +123 -0
- package/templates/contextkit/tools/scripts/predictions-review.mjs +113 -0
- package/templates/contextkit/tools/scripts/release.mjs +60 -0
- package/templates/contextkit/tools/scripts/resume.mjs +114 -0
- package/templates/contextkit/tools/scripts/roadmap.mjs +86 -0
- package/templates/contextkit/tools/scripts/runs.mjs +116 -0
- package/templates/contextkit/tools/scripts/seo-audit.mjs +150 -0
- package/templates/contextkit/tools/scripts/session-digest.mjs +89 -0
- package/templates/contextkit/tools/scripts/session-reindex.mjs +91 -0
- package/templates/contextkit/tools/scripts/setup-complete.mjs +69 -0
- package/templates/contextkit/tools/scripts/squad-meta.mjs +23 -0
- package/templates/contextkit/tools/scripts/squad-pipeline-condition.mjs +192 -0
- package/templates/contextkit/tools/scripts/squad-pipeline.mjs +301 -0
- package/templates/contextkit/tools/scripts/squad.mjs +80 -0
- package/templates/contextkit/tools/scripts/stats.mjs +138 -0
- package/templates/contextkit/tools/scripts/sync-check.mjs +235 -0
- package/templates/contextkit/tools/scripts/tech-debt-detectors.mjs +76 -0
- package/templates/contextkit/tools/scripts/tech-debt-scan.mjs +164 -0
- package/templates/contextkit/tools/scripts/token-report.mjs +153 -0
- package/templates/contextkit/tools/scripts/visual-test.mjs +132 -0
- package/templates/contextkit/tools/scripts/watch.mjs +106 -0
- package/templates/contextkit/tools/scripts/workflow.mjs +136 -0
- package/templates/contextkit/tools/scripts/workspace-sync.mjs +220 -0
- package/templates/contextkit/tools/scripts/worktree-new.mjs +50 -0
- package/templates/contextkit/workflows/L1-static-loading.md +59 -0
- package/templates/contextkit/workflows/L2-session-ledger.md +86 -0
- package/templates/contextkit/workflows/L3-multi-session.md +80 -0
- package/templates/contextkit/workflows/L4-squads.md +68 -0
- package/templates/contextkit/workflows/L5-proactive.md +88 -0
- package/templates/contextkit/workflows/README.md +47 -0
- package/templates/contextkit/workflows/playbooks/distillation-cycle.md +74 -0
- package/templates/contextkit/workflows/playbooks/landing-page.md +197 -0
- package/templates/contextkit/workflows/playbooks/security-batch.md +68 -0
- package/templates/contextkit/workflows/playbooks/seo-aiso.md +288 -0
- package/templates/contextkit/workflows/playbooks/simulate-impact.md +83 -0
- package/templates/contextkit/workflows/playbooks/tanstack.md +164 -0
- package/templates/contextkit/workflows/playbooks/tech-debt-sweep.md +77 -0
- package/templates/docs/CHANGELOG.md.tpl +11 -0
- package/templates/gitattributes +3 -0
- package/templates/github/ISSUE_TEMPLATE/bug_report.md +30 -0
- package/templates/github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/templates/github/PULL_REQUEST_TEMPLATE.md +27 -0
- package/templates/github/dependabot.yml +27 -0
- package/templates/github/workflows/quality.yml +36 -0
- package/templates/github/workflows/security.yml +54 -0
- package/tools/install/cli.mjs +62 -0
- package/tools/install/fs.mjs +56 -0
- package/tools/install/git.mjs +114 -0
- package/tools/install/project.mjs +51 -0
- package/tools/install/uninstall.mjs +54 -0
- package/tools/integration-test-compozy.mjs +88 -0
- package/tools/integration-test-guards.mjs +269 -0
- package/tools/integration-test-tooling-agent-forge.mjs +189 -0
- package/tools/integration-test-tooling-pipeline.mjs +164 -0
- package/tools/integration-test-tooling.mjs +172 -0
- package/tools/integration-test.mjs +228 -0
- package/tools/it-helpers.mjs +60 -0
- package/tools/selfcheck-agent-forge-ops.mjs +107 -0
- package/tools/selfcheck-agent-forge.mjs +304 -0
- package/tools/selfcheck-config.mjs +80 -0
- package/tools/selfcheck-runtime.mjs +135 -0
- package/tools/selfcheck-source.mjs +326 -0
- package/tools/selfcheck.mjs +268 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Regenerates `contextkit/memory/SESSIONS.md` as an INDEX of the per-session
|
|
4
|
+
* markdown files under `contextkit/memory/sessions/`.
|
|
5
|
+
*
|
|
6
|
+
* Why a derived index? With multiple parallel sessions, a monolithic file is
|
|
7
|
+
* a merge-conflict hotspot. Each session writes its OWN file; the aggregate
|
|
8
|
+
* is rebuilt from filesystem state.
|
|
9
|
+
*
|
|
10
|
+
* Entry naming: `<YYYY-MM-DD>-<NN>-<slug>.md`
|
|
11
|
+
* - NN: zero-padded monotonic session number.
|
|
12
|
+
* - slug: `[a-z0-9._-]` (kebab-case + version dots/underscores).
|
|
13
|
+
*
|
|
14
|
+
* Usage: node contextkit/tools/scripts/session-reindex.mjs
|
|
15
|
+
* (also wired into the pre-commit git hook)
|
|
16
|
+
*/
|
|
17
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { resolve } from 'node:path';
|
|
19
|
+
import { pathsFor } from '../../runtime/config/paths.mjs';
|
|
20
|
+
|
|
21
|
+
const ROOT = process.cwd();
|
|
22
|
+
const P = pathsFor(ROOT);
|
|
23
|
+
const SESSIONS_DIR = P.sessions;
|
|
24
|
+
const INDEX_PATH = P.sessionsIndex;
|
|
25
|
+
const ENTRY_PATTERN = /^(\d{4}-\d{2}-\d{2})-(\d{2,})-([a-z0-9._-]+)\.md$/;
|
|
26
|
+
|
|
27
|
+
async function extractTitle(filePath, fallbackSlug) {
|
|
28
|
+
try {
|
|
29
|
+
const content = await readFile(filePath, 'utf-8');
|
|
30
|
+
const heading = content.split('\n').find((l) => l.startsWith('# '));
|
|
31
|
+
return heading ? heading.slice(2).trim() : fallbackSlug;
|
|
32
|
+
} catch {
|
|
33
|
+
return fallbackSlug;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function listEntries() {
|
|
38
|
+
let files = [];
|
|
39
|
+
try {
|
|
40
|
+
files = await readdir(SESSIONS_DIR);
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const entries = [];
|
|
45
|
+
for (const filename of files) {
|
|
46
|
+
const match = ENTRY_PATTERN.exec(filename);
|
|
47
|
+
if (!match) continue;
|
|
48
|
+
const [, date, numberStr, slug] = match;
|
|
49
|
+
const title = await extractTitle(resolve(SESSIONS_DIR, filename), slug);
|
|
50
|
+
entries.push({ filename, date, number: Number.parseInt(numberStr, 10), slug, title });
|
|
51
|
+
}
|
|
52
|
+
entries.sort((a, b) => (b.number !== a.number ? b.number - a.number : b.date.localeCompare(a.date)));
|
|
53
|
+
return entries;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildIndex(entries) {
|
|
57
|
+
const out = [];
|
|
58
|
+
out.push('# Session History');
|
|
59
|
+
out.push('');
|
|
60
|
+
out.push('> ⚠️ **AUTO-GENERATED FILE — DO NOT EDIT BY HAND**.');
|
|
61
|
+
out.push('> Each session lives in its own file under `contextkit/memory/sessions/`.');
|
|
62
|
+
out.push('> Regenerated by `session-reindex` (also runs on pre-commit).');
|
|
63
|
+
out.push('');
|
|
64
|
+
out.push('Register a new session with `/log-session`.');
|
|
65
|
+
out.push('');
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
out.push('_(No sessions registered yet.)_');
|
|
68
|
+
out.push('');
|
|
69
|
+
return out.join('\n');
|
|
70
|
+
}
|
|
71
|
+
out.push(`Total registered sessions: **${entries.length}** (newest first).`);
|
|
72
|
+
out.push('');
|
|
73
|
+
out.push('| # | Date | Title | File |');
|
|
74
|
+
out.push('| --- | --- | --- | --- |');
|
|
75
|
+
for (const e of entries) {
|
|
76
|
+
out.push(`| ${String(e.number).padStart(2, '0')} | ${e.date} | ${e.title} | [${e.filename}](sessions/${e.filename}) |`);
|
|
77
|
+
}
|
|
78
|
+
out.push('');
|
|
79
|
+
return out.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const entries = await listEntries();
|
|
84
|
+
await writeFile(INDEX_PATH, buildIndex(entries), 'utf-8');
|
|
85
|
+
console.log(`✅ SESSIONS.md regenerated — ${entries.length} entries indexed.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
main().catch((err) => {
|
|
89
|
+
console.error('❌ Failed to reindex sessions:', err);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Marks ContextDevKit onboarding complete (stops the first-run trigger) and,
|
|
4
|
+
* optionally, applies stack-tuned config produced by `detect-stack.mjs`.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node contextkit/tools/scripts/setup-complete.mjs
|
|
8
|
+
* → just flips config.setup.completed = true
|
|
9
|
+
*
|
|
10
|
+
* node contextkit/tools/scripts/setup-complete.mjs --detect
|
|
11
|
+
* → runs detect-stack.mjs itself and merges its suggestions, then flips
|
|
12
|
+
* the flag. Preferred — avoids shell-redirect encoding pitfalls.
|
|
13
|
+
*
|
|
14
|
+
* node contextkit/tools/scripts/setup-complete.mjs --apply <report.json>
|
|
15
|
+
* → merges suggestions from a pre-generated report file, then flips.
|
|
16
|
+
*
|
|
17
|
+
* Keeping this in a script (not free-form JSON editing) guarantees the config
|
|
18
|
+
* stays valid — the file is the contract the hooks read.
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from 'node:child_process';
|
|
21
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { resolve } from 'node:path';
|
|
23
|
+
import { parseJsonSafe, readJsonSafe } from '../../runtime/hooks/safe-io.mjs';
|
|
24
|
+
import { pathsFor } from '../../runtime/config/paths.mjs';
|
|
25
|
+
|
|
26
|
+
const ROOT = process.cwd();
|
|
27
|
+
const CONFIG = pathsFor(ROOT).config;
|
|
28
|
+
|
|
29
|
+
const readJson = (path, fallback = null) => readJsonSafe(path, fallback);
|
|
30
|
+
|
|
31
|
+
function runDetector() {
|
|
32
|
+
try {
|
|
33
|
+
const out = execFileSync('node', ['contextkit/tools/scripts/detect-stack.mjs'], { cwd: ROOT, encoding: 'utf-8' });
|
|
34
|
+
return parseJsonSafe(out, null);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(`detect-stack failed: ${err?.message ?? err}`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function main() {
|
|
42
|
+
const cfg = existsSync(CONFIG) ? readJson(CONFIG, {}) : {};
|
|
43
|
+
|
|
44
|
+
const detect = process.argv.includes('--detect');
|
|
45
|
+
const applyIdx = process.argv.indexOf('--apply');
|
|
46
|
+
if (detect || applyIdx !== -1) {
|
|
47
|
+
const report = detect ? runDetector() : readJson(resolve(ROOT, process.argv[applyIdx + 1] ?? ''), null);
|
|
48
|
+
const suggested = report?.suggested;
|
|
49
|
+
if (suggested) {
|
|
50
|
+
cfg.ledger = cfg.ledger || {};
|
|
51
|
+
if (Array.isArray(suggested.ledger?.important)) cfg.ledger.important = suggested.ledger.important;
|
|
52
|
+
if (Array.isArray(suggested.ledger?.irrelevant)) cfg.ledger.irrelevant = suggested.ledger.irrelevant;
|
|
53
|
+
if (!Array.isArray(cfg.ledger.registration)) cfg.ledger.registration = ['contextkit/memory/SESSIONS.md', 'docs/CHANGELOG.md'];
|
|
54
|
+
cfg.l5 = cfg.l5 || {};
|
|
55
|
+
if (Array.isArray(suggested.highRiskPaths)) cfg.l5.highRiskPaths = suggested.highRiskPaths;
|
|
56
|
+
cfg.qa = cfg.qa || {};
|
|
57
|
+
if (Array.isArray(suggested.qaCriticalPaths)) cfg.qa.criticalPaths = suggested.qaCriticalPaths;
|
|
58
|
+
console.log(`Applied suggestions: ${cfg.ledger.important.length} important paths, ${cfg.l5.highRiskPaths.length} high-risk paths, ${(cfg.qa.criticalPaths || []).length} qa-critical paths.`);
|
|
59
|
+
} else {
|
|
60
|
+
console.log('No report supplied or report had no suggestions — only flipping setup flag.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
cfg.setup = { ...(cfg.setup || {}), completed: true, completedAt: new Date().toISOString() };
|
|
65
|
+
writeFileSync(CONFIG, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
|
|
66
|
+
console.log('✅ ContextDevKit setup marked complete. The first-run trigger will no longer fire.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared squad detection — used by squad.mjs (/squad) and agent-tuning.mjs
|
|
3
|
+
* (/tune-agents) so the rule lives in exactly one place:
|
|
4
|
+
* qa-* → qa-team; otherwise the `(<name> squad)` / `(<name>-team)` tag in the
|
|
5
|
+
* agent's description frontmatter; falls back to devteam.
|
|
6
|
+
* Zero-dependency, defensive (never throws).
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
/** @param {string} agentsDir absolute path to `.claude/agents` @param {string} agent bare name */
|
|
12
|
+
export function squadOf(agentsDir, agent) {
|
|
13
|
+
if (/^qa-/.test(agent)) return 'qa-team';
|
|
14
|
+
let frontmatter = null;
|
|
15
|
+
try {
|
|
16
|
+
frontmatter = readFileSync(resolve(agentsDir, `${agent}.md`), 'utf-8').match(/^---\n([\s\S]*?)\n---/);
|
|
17
|
+
} catch {
|
|
18
|
+
return 'devteam';
|
|
19
|
+
}
|
|
20
|
+
const desc = (frontmatter && /description:\s*(.*)/.exec(frontmatter[1])?.[1]) || '';
|
|
21
|
+
const tag = desc.match(/\(([a-z][a-z0-9-]*?)(?: squad)?\)\s*$/i);
|
|
22
|
+
return tag ? tag[1] : 'devteam';
|
|
23
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whitelisted condition parser + evaluator for the squad pipeline DSL
|
|
3
|
+
* ([ADR-0015](../../memory/decisions/0015-pipeline-dsl-working-stage-and-multi-session-work-claims.md) Part A).
|
|
4
|
+
*
|
|
5
|
+
* Single responsibility: turn a `condition:` string from `pipeline.yaml` into
|
|
6
|
+
* a tiny AST, then evaluate it against the pipeline context. No arbitrary
|
|
7
|
+
* expression evaluation, no function calls, no boolean chaining — by design.
|
|
8
|
+
*
|
|
9
|
+
* Grammar (full reference in docs/SQUAD-PIPELINE-FORMAT.md §condition):
|
|
10
|
+
* condition := dotted_id <op> literal
|
|
11
|
+
* | dotted_id ".length" <op> int
|
|
12
|
+
* dotted_id := identifier ( "." identifier )*
|
|
13
|
+
* op := "==" | "!=" | ">=" | "<=" | ">" | "<"
|
|
14
|
+
* literal := string | int | float | bool | null
|
|
15
|
+
*
|
|
16
|
+
* API:
|
|
17
|
+
* parseCondition(expr) → { ok: true, ast } | { ok: false, reason }
|
|
18
|
+
* evalCondition(ast, ctx) → boolean
|
|
19
|
+
*
|
|
20
|
+
* Pure, zero-dep. Stays under the 280-line budget by keeping the lexer and
|
|
21
|
+
* the comparator inline (one small file, two ~40-line halves).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const OP_RE = /^(==|!=|>=|<=|>|<)/;
|
|
25
|
+
const IDENT_RE = /^[a-zA-Z_][a-zA-Z0-9_]*/;
|
|
26
|
+
const INT_RE = /^-?\d+(?![.\d])/;
|
|
27
|
+
const FLOAT_RE = /^-?\d+\.\d+/;
|
|
28
|
+
const STRING_RE = /^"([^"]*)"|^'([^']*)'/;
|
|
29
|
+
const VALID_OPS = ['==', '!=', '>=', '<=', '>', '<'];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses a condition expression into an AST.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} expr — the raw `condition:` value from `pipeline.yaml`
|
|
35
|
+
* @returns {{ ok: true, ast: { path: string[], isLength: boolean, op: string, literal: unknown } }
|
|
36
|
+
* | { ok: false, reason: string }}
|
|
37
|
+
*/
|
|
38
|
+
export function parseCondition(expr) {
|
|
39
|
+
if (typeof expr !== 'string' || expr.trim() === '') {
|
|
40
|
+
return { ok: false, reason: 'empty condition' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let src = expr.trim();
|
|
44
|
+
|
|
45
|
+
// --- identifier path -----------------------------------------------------
|
|
46
|
+
const path = [];
|
|
47
|
+
const firstId = src.match(IDENT_RE);
|
|
48
|
+
if (!firstId) return { ok: false, reason: 'expected identifier' };
|
|
49
|
+
path.push(firstId[0]);
|
|
50
|
+
src = src.slice(firstId[0].length);
|
|
51
|
+
|
|
52
|
+
while (src.startsWith('.')) {
|
|
53
|
+
src = src.slice(1);
|
|
54
|
+
const nextId = src.match(IDENT_RE);
|
|
55
|
+
if (!nextId) return { ok: false, reason: 'expected identifier after "."' };
|
|
56
|
+
path.push(nextId[0]);
|
|
57
|
+
src = src.slice(nextId[0].length);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isLength = path[path.length - 1] === 'length' && path.length > 1;
|
|
61
|
+
|
|
62
|
+
// --- operator ------------------------------------------------------------
|
|
63
|
+
src = src.trimStart();
|
|
64
|
+
const opMatch = src.match(OP_RE);
|
|
65
|
+
if (!opMatch) {
|
|
66
|
+
return { ok: false, reason: `expected one of ${VALID_OPS.join(' / ')} after dotted_id` };
|
|
67
|
+
}
|
|
68
|
+
const op = opMatch[0];
|
|
69
|
+
src = src.slice(op.length).trimStart();
|
|
70
|
+
|
|
71
|
+
// --- literal -------------------------------------------------------------
|
|
72
|
+
const lit = parseLiteral(src);
|
|
73
|
+
if (!lit.ok) return { ok: false, reason: lit.reason };
|
|
74
|
+
|
|
75
|
+
// .length restricts the RHS to int
|
|
76
|
+
if (isLength && !Number.isInteger(lit.value)) {
|
|
77
|
+
return { ok: false, reason: '.length must be compared to an integer literal' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- nothing else is allowed --------------------------------------------
|
|
81
|
+
if (lit.rest.trim() !== '') {
|
|
82
|
+
return { ok: false, reason: `unexpected trailing input: "${lit.rest.trim()}"` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { ok: true, ast: { path, isLength, op, literal: lit.value } };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reads one literal off the front of `src`. Returns the value and the
|
|
90
|
+
* remainder so the caller can assert nothing trails.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} src
|
|
93
|
+
* @returns {{ ok: true, value: unknown, rest: string } | { ok: false, reason: string }}
|
|
94
|
+
*/
|
|
95
|
+
function parseLiteral(src) {
|
|
96
|
+
if (src.startsWith('true')) {
|
|
97
|
+
return { ok: true, value: true, rest: src.slice(4) };
|
|
98
|
+
}
|
|
99
|
+
if (src.startsWith('false')) {
|
|
100
|
+
return { ok: true, value: false, rest: src.slice(5) };
|
|
101
|
+
}
|
|
102
|
+
if (src.startsWith('null')) {
|
|
103
|
+
return { ok: true, value: null, rest: src.slice(4) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const str = src.match(STRING_RE);
|
|
107
|
+
if (str) return { ok: true, value: str[1] ?? str[2], rest: src.slice(str[0].length) };
|
|
108
|
+
|
|
109
|
+
const flt = src.match(FLOAT_RE);
|
|
110
|
+
if (flt) return { ok: true, value: Number(flt[0]), rest: src.slice(flt[0].length) };
|
|
111
|
+
|
|
112
|
+
const int = src.match(INT_RE);
|
|
113
|
+
if (int) return { ok: true, value: parseInt(int[0], 10), rest: src.slice(int[0].length) };
|
|
114
|
+
|
|
115
|
+
return { ok: false, reason: 'expected literal (string / int / float / bool / null)' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolves a dotted path against `ctx`. Returns `undefined` on any miss.
|
|
120
|
+
* `.length` is special-cased for arrays and strings.
|
|
121
|
+
*
|
|
122
|
+
* @param {string[]} path
|
|
123
|
+
* @param {Record<string, unknown>} ctx
|
|
124
|
+
* @returns {unknown}
|
|
125
|
+
*/
|
|
126
|
+
function resolve(path, ctx) {
|
|
127
|
+
let cur = ctx;
|
|
128
|
+
for (let i = 0; i < path.length; i += 1) {
|
|
129
|
+
const key = path[i];
|
|
130
|
+
if (cur == null || typeof cur !== 'object') return undefined;
|
|
131
|
+
if (key === 'length' && i === path.length - 1 && i > 0) {
|
|
132
|
+
if (typeof cur === 'string' || Array.isArray(cur)) return cur.length;
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
cur = cur[key];
|
|
136
|
+
}
|
|
137
|
+
if (typeof cur === 'function') return undefined;
|
|
138
|
+
return cur;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Evaluates a parsed condition against `ctx`. The grammar guarantees the
|
|
143
|
+
* AST is well-formed; the only runtime concerns are `undefined`-resolution
|
|
144
|
+
* and the no-coercion compare semantics.
|
|
145
|
+
*
|
|
146
|
+
* Rules:
|
|
147
|
+
* - `undefined <op> <literal>` → false (always).
|
|
148
|
+
* - `==` / `!=` use strict equality (no coercion).
|
|
149
|
+
* - `>` / `<` / `>=` / `<=` require both sides to be `typeof "number"`;
|
|
150
|
+
* otherwise → false.
|
|
151
|
+
*
|
|
152
|
+
* @param {{ path: string[], isLength: boolean, op: string, literal: unknown }} ast
|
|
153
|
+
* @param {Record<string, unknown>} ctx
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
export function evalCondition(ast, ctx) {
|
|
157
|
+
const left = resolve(ast.path, ctx ?? {});
|
|
158
|
+
if (left === undefined) return false;
|
|
159
|
+
const right = ast.literal;
|
|
160
|
+
switch (ast.op) {
|
|
161
|
+
case '==':
|
|
162
|
+
return left === right;
|
|
163
|
+
case '!=':
|
|
164
|
+
return left !== right;
|
|
165
|
+
case '>':
|
|
166
|
+
return typeof left === 'number' && typeof right === 'number' && left > right;
|
|
167
|
+
case '<':
|
|
168
|
+
return typeof left === 'number' && typeof right === 'number' && left < right;
|
|
169
|
+
case '>=':
|
|
170
|
+
return typeof left === 'number' && typeof right === 'number' && left >= right;
|
|
171
|
+
case '<=':
|
|
172
|
+
return typeof left === 'number' && typeof right === 'number' && left <= right;
|
|
173
|
+
default:
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* One-shot helper: parse + evaluate. Useful in tests and for the engine
|
|
180
|
+
* when it doesn't need to cache the AST.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} expr
|
|
183
|
+
* @param {Record<string, unknown>} ctx
|
|
184
|
+
* @returns {boolean}
|
|
185
|
+
* @throws {Error} if the expression refuses to parse (grammar violation —
|
|
186
|
+
* the engine catches this and exits 1).
|
|
187
|
+
*/
|
|
188
|
+
export function parseAndEval(expr, ctx) {
|
|
189
|
+
const parsed = parseCondition(expr);
|
|
190
|
+
if (!parsed.ok) throw new Error(`condition refused: ${parsed.reason}`);
|
|
191
|
+
return evalCondition(parsed.ast, ctx);
|
|
192
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* squad-pipeline — engine for the declarative squad pipeline DSL
|
|
4
|
+
* ([ADR-0015](../../memory/decisions/0015-pipeline-dsl-working-stage-and-multi-session-work-claims.md) Part A).
|
|
5
|
+
*
|
|
6
|
+
* Reads `<contextkit>/squads/<squad>/pipeline.yaml`, validates it against the
|
|
7
|
+
* whitelisted grammar (see `docs/SQUAD-PIPELINE-FORMAT.md`), and — with
|
|
8
|
+
* `--dry-run` — walks the graph printing the would-be execution order.
|
|
9
|
+
*
|
|
10
|
+
* Refusal modes (see the spec for the full table):
|
|
11
|
+
* • `yaml` not installed → exit 0 (informative; opt-in feature)
|
|
12
|
+
* • pipeline.yaml malformed → exit 1
|
|
13
|
+
* • vendor model name instead of tier → exit 1
|
|
14
|
+
* • condition grammar violation → exit 1
|
|
15
|
+
* • on_reject target missing → exit 1
|
|
16
|
+
* • on_reject without max_review_cycles → exit 1
|
|
17
|
+
* • agent has no briefing → exit 1
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node <path>/squad-pipeline.mjs <squad> [--dry-run]
|
|
21
|
+
*
|
|
22
|
+
* Cohesion note: kept as one file (~250 lines) because validation, dry-run
|
|
23
|
+
* printing, and the path-discovery glue all consume the same parsed
|
|
24
|
+
* pipeline + the same context object. Splitting along those seams would
|
|
25
|
+
* scatter one pipeline lifecycle across three files with shared state in
|
|
26
|
+
* each — the constitution rewards keeping a single coherent unit together.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
29
|
+
import { dirname, resolve } from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { loadYaml } from '../../squads/agent-forge/lib/yaml.mjs';
|
|
32
|
+
import { parseCondition, evalCondition } from './squad-pipeline-condition.mjs';
|
|
33
|
+
|
|
34
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
+
const SCRIPT_DIR = dirname(__filename);
|
|
36
|
+
const CONTEXTKIT_ROOT = resolve(SCRIPT_DIR, '..', '..'); // .../[templates/]contextkit
|
|
37
|
+
const REPO_ROOT = resolve(CONTEXTKIT_ROOT, '..'); // .../[repo or project]
|
|
38
|
+
|
|
39
|
+
const VALID_TIERS = new Set(['fast', 'powerful', 'reasoning']);
|
|
40
|
+
const VALID_EXECUTIONS = new Set(['inline', 'subagent']);
|
|
41
|
+
const REQUIRED_AGENT_FIELDS = ['id', 'agent', 'execution', 'model_tier'];
|
|
42
|
+
const REQUIRED_CHECKPOINT_FIELDS = ['id', 'type', 'outputFile'];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Discovers the squad's `pipeline.yaml`. Handles both layouts:
|
|
46
|
+
* • source-tree: <repo>/templates/contextkit/squads/<squad>/pipeline.yaml
|
|
47
|
+
* • installed: <project>/contextkit/squads/<squad>/pipeline.yaml
|
|
48
|
+
*
|
|
49
|
+
* @param {string} squad
|
|
50
|
+
* @returns {string | null} absolute path, or null when not found
|
|
51
|
+
*/
|
|
52
|
+
function findPipelineFile(squad) {
|
|
53
|
+
const candidate = resolve(CONTEXTKIT_ROOT, 'squads', squad, 'pipeline.yaml');
|
|
54
|
+
return existsSync(candidate) ? candidate : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Discovers the agents directory. Same dual-layout handling.
|
|
59
|
+
* @returns {string | null}
|
|
60
|
+
*/
|
|
61
|
+
function findAgentsDir() {
|
|
62
|
+
const installed = resolve(REPO_ROOT, '.claude/agents');
|
|
63
|
+
if (existsSync(installed)) return installed;
|
|
64
|
+
const source = resolve(REPO_ROOT, 'templates/claude/agents');
|
|
65
|
+
if (existsSync(source)) return source;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Lists agent briefing ids (basename without `.md`). Empty when the
|
|
71
|
+
* directory cannot be read — selfcheck handles the diagnostic.
|
|
72
|
+
*
|
|
73
|
+
* @param {string | null} dir
|
|
74
|
+
* @returns {Set<string>}
|
|
75
|
+
*/
|
|
76
|
+
function listAgentIds(dir) {
|
|
77
|
+
if (!dir) return new Set();
|
|
78
|
+
try {
|
|
79
|
+
return new Set(readdirSync(dir).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3)));
|
|
80
|
+
} catch {
|
|
81
|
+
return new Set();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validates one step's shape and grammar. Returns an array of error strings.
|
|
87
|
+
*
|
|
88
|
+
* @param {Record<string, unknown>} step
|
|
89
|
+
* @param {Set<string>} stepIds — defined ids in the whole pipeline
|
|
90
|
+
* @param {Set<string>} agentIds — agent briefings present on disk
|
|
91
|
+
* @returns {string[]}
|
|
92
|
+
*/
|
|
93
|
+
function validateStep(step, stepIds, agentIds) {
|
|
94
|
+
const errors = [];
|
|
95
|
+
const isCheckpoint = step.type === 'checkpoint';
|
|
96
|
+
const required = isCheckpoint ? REQUIRED_CHECKPOINT_FIELDS : REQUIRED_AGENT_FIELDS;
|
|
97
|
+
for (const key of required) {
|
|
98
|
+
if (step[key] == null) errors.push(`step ${step.id ?? '?'}: missing required field "${key}"`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!isCheckpoint) {
|
|
102
|
+
if (step.type && step.type !== 'checkpoint') {
|
|
103
|
+
errors.push(`step ${step.id}: unknown type "${step.type}"`);
|
|
104
|
+
}
|
|
105
|
+
if (step.execution && !VALID_EXECUTIONS.has(step.execution)) {
|
|
106
|
+
errors.push(`step ${step.id}: execution must be inline | subagent`);
|
|
107
|
+
}
|
|
108
|
+
if (step.model_tier && !VALID_TIERS.has(step.model_tier)) {
|
|
109
|
+
errors.push(`step ${step.id}: model_tier must be fast | powerful | reasoning`);
|
|
110
|
+
}
|
|
111
|
+
if (typeof step.model === 'string') {
|
|
112
|
+
errors.push(`step ${step.id}: vendor model names are forbidden; use model_tier`);
|
|
113
|
+
}
|
|
114
|
+
if (step.agent && agentIds.size > 0 && !agentIds.has(String(step.agent))) {
|
|
115
|
+
errors.push(`step ${step.id}: agent "${step.agent}" has no briefing under .claude/agents/`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof step.condition === 'string') {
|
|
120
|
+
const parsed = parseCondition(step.condition);
|
|
121
|
+
if (!parsed.ok) errors.push(`step ${step.id}: condition refused — ${parsed.reason}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (step.on_reject != null) {
|
|
125
|
+
if (!stepIds.has(String(step.on_reject))) {
|
|
126
|
+
errors.push(`step ${step.id}: on_reject target "${step.on_reject}" not found`);
|
|
127
|
+
}
|
|
128
|
+
if (!Number.isInteger(step.max_review_cycles) || step.max_review_cycles < 1) {
|
|
129
|
+
errors.push(`step ${step.id}: on_reject requires max_review_cycles (integer >= 1)`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return errors;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validates the whole pipeline object. Throws on the first failure with a
|
|
138
|
+
* collected error report so callers see every problem in one shot.
|
|
139
|
+
*
|
|
140
|
+
* @param {{ squad?: string, version?: string, steps?: unknown[] }} pipeline
|
|
141
|
+
* @param {Set<string>} agentIds
|
|
142
|
+
*/
|
|
143
|
+
export function validatePipeline(pipeline, agentIds) {
|
|
144
|
+
const errors = [];
|
|
145
|
+
if (!pipeline || typeof pipeline !== 'object') errors.push('pipeline: missing root object');
|
|
146
|
+
if (typeof pipeline.squad !== 'string') errors.push('pipeline.squad: missing or not a string');
|
|
147
|
+
if (typeof pipeline.version !== 'string') errors.push('pipeline.version: missing or not a string');
|
|
148
|
+
if (!Array.isArray(pipeline.steps) || pipeline.steps.length === 0) errors.push('pipeline.steps: must be a non-empty array');
|
|
149
|
+
|
|
150
|
+
if (errors.length > 0) throw new Error(errors.join('\n'));
|
|
151
|
+
|
|
152
|
+
const stepIds = new Set();
|
|
153
|
+
for (const step of pipeline.steps) {
|
|
154
|
+
if (!step || typeof step !== 'object' || typeof step.id !== 'string') {
|
|
155
|
+
errors.push('step: missing id');
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (stepIds.has(step.id)) errors.push(`step ${step.id}: duplicate id`);
|
|
159
|
+
stepIds.add(step.id);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const step of pipeline.steps) {
|
|
163
|
+
if (!step || typeof step.id !== 'string') continue;
|
|
164
|
+
errors.push(...validateStep(step, stepIds, agentIds));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (errors.length > 0) throw new Error(errors.join('\n'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Walks the pipeline once, emitting one display row per step. Honours
|
|
172
|
+
* `condition` by skipping when it resolves to false against `ctx`. Honours
|
|
173
|
+
* `max_review_cycles` only as a marker — dry-run does not actually loop;
|
|
174
|
+
* the cap belongs to the executor.
|
|
175
|
+
*
|
|
176
|
+
* Marker legend (from the spec):
|
|
177
|
+
* ✓ runs · ⊘ skipped by condition · ↺ has retry loop
|
|
178
|
+
*
|
|
179
|
+
* @param {{ steps: Record<string, unknown>[] }} pipeline
|
|
180
|
+
* @param {Record<string, unknown>} ctx
|
|
181
|
+
* @returns {Array<{ id: string, marker: '✓' | '⊘' | '↺', kind: string, agent: string, execution: string, tier: string, note: string }>}
|
|
182
|
+
*/
|
|
183
|
+
export function plan(pipeline, ctx) {
|
|
184
|
+
const rows = [];
|
|
185
|
+
for (const step of pipeline.steps) {
|
|
186
|
+
const isCheckpoint = step.type === 'checkpoint';
|
|
187
|
+
let marker = '✓';
|
|
188
|
+
let note = '';
|
|
189
|
+
if (typeof step.condition === 'string') {
|
|
190
|
+
const parsed = parseCondition(step.condition);
|
|
191
|
+
if (parsed.ok && !evalCondition(parsed.ast, ctx)) {
|
|
192
|
+
marker = '⊘';
|
|
193
|
+
note = `condition: ${step.condition} → false`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (step.on_reject) {
|
|
197
|
+
marker = marker === '⊘' ? '⊘' : '↺';
|
|
198
|
+
note = `on_reject → ${step.on_reject}, max_cycles: ${step.max_review_cycles}`;
|
|
199
|
+
}
|
|
200
|
+
rows.push({
|
|
201
|
+
id: String(step.id),
|
|
202
|
+
marker,
|
|
203
|
+
kind: isCheckpoint ? 'checkpoint' : 'agent',
|
|
204
|
+
agent: isCheckpoint ? '' : String(step.agent ?? ''),
|
|
205
|
+
execution: isCheckpoint ? '' : String(step.execution ?? ''),
|
|
206
|
+
tier: isCheckpoint ? '' : String(step.model_tier ?? ''),
|
|
207
|
+
note,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return rows;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Formats the dry-run plan as a single text block ready for stdout.
|
|
215
|
+
* Kept here (not in a sibling) because the marker semantics are intimate
|
|
216
|
+
* with `plan` above — separating them would require re-exporting the row
|
|
217
|
+
* shape across two files for no gain.
|
|
218
|
+
*/
|
|
219
|
+
function formatPlan(pipeline, rows) {
|
|
220
|
+
const lines = [`Pipeline: ${pipeline.squad} v${pipeline.version}`];
|
|
221
|
+
for (const r of rows) {
|
|
222
|
+
const fields = r.kind === 'checkpoint'
|
|
223
|
+
? [r.marker, r.id.padEnd(28), 'checkpoint']
|
|
224
|
+
: [r.marker, r.id.padEnd(28), r.kind.padEnd(10), r.agent.padEnd(22), r.execution.padEnd(8), r.tier];
|
|
225
|
+
let line = ` ${fields.join(' ')}`;
|
|
226
|
+
if (r.note) line += ` (${r.note})`;
|
|
227
|
+
lines.push(line);
|
|
228
|
+
}
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Loads and validates a pipeline, returning the parsed object on success.
|
|
234
|
+
* Exposed so the selfcheck and the integration test can call it without
|
|
235
|
+
* spawning a process.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} squad
|
|
238
|
+
* @returns {Promise<{ pipeline: object, file: string, agentsDir: string | null } | { yamlAbsent: true }>}
|
|
239
|
+
*/
|
|
240
|
+
export async function loadAndValidate(squad) {
|
|
241
|
+
const file = findPipelineFile(squad);
|
|
242
|
+
if (!file) throw new Error(`pipeline.yaml not found for squad "${squad}"`);
|
|
243
|
+
|
|
244
|
+
let pipeline;
|
|
245
|
+
try {
|
|
246
|
+
const text = readFileSync(file, 'utf-8');
|
|
247
|
+
pipeline = await (await loadYaml()).parse(text.replace(/^/, ''));
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (/needs the `yaml` package/.test(String(err?.message))) return { yamlAbsent: true };
|
|
250
|
+
throw new Error(`pipeline.yaml malformed: ${err?.message ?? err}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Normalise: top-level may be { pipeline: {...} } or the bare pipeline body.
|
|
254
|
+
const body = pipeline?.pipeline ?? pipeline;
|
|
255
|
+
const agentsDir = findAgentsDir();
|
|
256
|
+
validatePipeline(body, listAgentIds(agentsDir));
|
|
257
|
+
return { pipeline: body, file, agentsDir };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function main() {
|
|
261
|
+
const squad = process.argv[2];
|
|
262
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
263
|
+
|
|
264
|
+
if (!squad) {
|
|
265
|
+
console.error('Usage: squad-pipeline.mjs <squad> [--dry-run]');
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let result;
|
|
270
|
+
try {
|
|
271
|
+
result = await loadAndValidate(squad);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error(`❌ ${err?.message ?? err}`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (result.yamlAbsent) {
|
|
278
|
+
console.log(`ℹ️ pipelines are opt-in — install the optional 'yaml' dep to use them:`);
|
|
279
|
+
console.log(` npm i yaml`);
|
|
280
|
+
console.log(` (squad "${squad}" continues to work without the DSL.)`);
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (dryRun) {
|
|
285
|
+
console.log(formatPlan(result.pipeline, plan(result.pipeline, {})));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log(`✅ ${squad} pipeline validated (${result.pipeline.steps.length} steps).`);
|
|
290
|
+
console.log(` Run with --dry-run to print the would-be execution order.`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Only run when invoked as a CLI; library imports stay side-effect-free.
|
|
294
|
+
// `process.argv[1]` is undefined when the module is imported via `node -e`,
|
|
295
|
+
// so we guard explicitly before doing any string work.
|
|
296
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
297
|
+
main().catch((err) => {
|
|
298
|
+
console.error(`❌ ${err?.message ?? err}`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
});
|
|
301
|
+
}
|