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,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ContextDevKit integration test — GUARDS (git hooks + config robustness + installer).
|
|
4
|
+
*
|
|
5
|
+
* Covers the safety nets that REJECT bad input rather than produce features —
|
|
6
|
+
* the parts most dangerous to leave untested:
|
|
7
|
+
* - commit-msg.mjs Conventional-Commits validator (enforces rule 5)
|
|
8
|
+
* - pre-push.mjs conflict block / warn / allow + the audited bypass
|
|
9
|
+
* - load.mjs zero-dep loader: deep-merge + BOM + malformed-JSON fallback
|
|
10
|
+
* - uninstall.mjs --uninstall / --purge (destructive — keeps memory)
|
|
11
|
+
* - concurrency-guard external-edit branch; gh-alerts mappers; malformed-settings
|
|
12
|
+
*
|
|
13
|
+
* Shared harness: it-helpers.mjs. Run: node tools/integration-test-guards.mjs
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { KIT, run, git, readJson, reporter, installFixture } from './it-helpers.mjs';
|
|
19
|
+
|
|
20
|
+
const rep = reporter();
|
|
21
|
+
const { ok, bad } = rep;
|
|
22
|
+
console.log('\n🌀 ContextDevKit integration test — guards\n');
|
|
23
|
+
|
|
24
|
+
const importKit = (rel) => import('file://' + join(KIT, rel).replaceAll('\\', '/'));
|
|
25
|
+
const tmp = (tag) => mkdtempSync(join(tmpdir(), `contextkit-${tag}-`));
|
|
26
|
+
const seedConfig = (root, obj, { bom = false } = {}) => {
|
|
27
|
+
mkdirSync(join(root, 'contextkit'), { recursive: true });
|
|
28
|
+
writeFileSync(join(root, 'contextkit', 'config.json'), (bom ? '' : '') + (typeof obj === 'string' ? obj : JSON.stringify(obj)));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** 017 — the zero-dep config loader's defensive behaviours (deep-merge / BOM / malformed). */
|
|
32
|
+
async function testConfigLoader() {
|
|
33
|
+
const { loadConfigSync } = await importKit('templates/contextkit/runtime/config/load.mjs');
|
|
34
|
+
|
|
35
|
+
const partial = tmp('cfg-a');
|
|
36
|
+
seedConfig(partial, { level: 3, ledger: { important: ['only/'] } });
|
|
37
|
+
const merged = loadConfigSync(partial);
|
|
38
|
+
merged.level === 3 && merged.ledger.important.join() === 'only/' && merged.l5 && typeof merged.l5 === 'object'
|
|
39
|
+
? ok('loadConfigSync deep-merges a partial override over defaults') : bad(`deepMerge wrong: ${JSON.stringify(merged.ledger)}`);
|
|
40
|
+
|
|
41
|
+
const bom = tmp('cfg-b');
|
|
42
|
+
seedConfig(bom, { level: 4 }, { bom: true });
|
|
43
|
+
loadConfigSync(bom).level === 4 ? ok('loadConfigSync strips a UTF-8 BOM before parse') : bad('BOM not stripped');
|
|
44
|
+
|
|
45
|
+
const broken = tmp('cfg-c');
|
|
46
|
+
seedConfig(broken, '{ not: valid json,,');
|
|
47
|
+
const empty = tmp('cfg-d');
|
|
48
|
+
JSON.stringify(loadConfigSync(broken)) === JSON.stringify(loadConfigSync(empty))
|
|
49
|
+
? ok('loadConfigSync falls back to defaults on malformed JSON (never throws)') : bad('malformed JSON did not fall back to defaults');
|
|
50
|
+
|
|
51
|
+
for (const d of [partial, bom, broken, empty]) rmSync(d, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 018 — gh-alerts pure mappers (GitHub alert shape → finding). */
|
|
55
|
+
async function testGhAlertMappers() {
|
|
56
|
+
const gha = await importKit('templates/contextkit/tools/scripts/gh-alerts.mjs');
|
|
57
|
+
const dep = gha.mapDependabotAlert({ security_advisory: { severity: 'high', summary: 'XSS', ghsa_id: 'GHSA-x' }, dependency: { package: { name: 'lodash' }, manifest_path: 'package.json' } });
|
|
58
|
+
dep.kind === 'dependabot' && dep.severity === 4 && dep.path === 'package.json' && dep.message.includes('lodash') && dep.source === 'gh:dependabot:package.json'
|
|
59
|
+
? ok('mapDependabotAlert shapes a finding (severity + source)') : bad(`mapDependabotAlert wrong: ${JSON.stringify(dep)}`);
|
|
60
|
+
const cs = gha.mapCodeScanningAlert({ rule: { id: 'js/xss', security_severity_level: 'critical', description: 'desc' }, most_recent_instance: { location: { path: 'src/a.js' } } });
|
|
61
|
+
cs.kind === 'code-scanning' && cs.severity === 5 && cs.path === 'src/a.js'
|
|
62
|
+
? ok('mapCodeScanningAlert shapes a finding') : bad(`mapCodeScanningAlert wrong: ${JSON.stringify(cs)}`);
|
|
63
|
+
gha.mapDependabotAlert({}).severity === 2 && gha.mapCodeScanningAlert({}).severity === 2
|
|
64
|
+
? ok('gh-alerts mappers default unknown severity to a safe floor') : bad('gh-alerts severity floor missing');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 015 — commit-msg Conventional-Commits validator (exit 0 allow / 1 block). */
|
|
68
|
+
function testCommitMsg(proj) {
|
|
69
|
+
const hook = join(proj, 'contextkit', 'runtime', 'git-hooks', 'commit-msg.mjs');
|
|
70
|
+
const check = (msg) => {
|
|
71
|
+
const f = join(proj, '_msg.txt');
|
|
72
|
+
writeFileSync(f, msg + '\n');
|
|
73
|
+
return run([hook, f], { cwd: proj }).status;
|
|
74
|
+
};
|
|
75
|
+
const cases = [
|
|
76
|
+
['feat(api): add endpoint', 0, 'valid type(scope)'],
|
|
77
|
+
['fix: correct thing', 0, 'valid no-scope'],
|
|
78
|
+
['added a thing', 1, 'missing type → blocked'],
|
|
79
|
+
['fix: trailing period.', 1, 'trailing period → blocked'],
|
|
80
|
+
["Merge branch 'x'", 0, 'merge commit allowed'],
|
|
81
|
+
['wip: messy [skip-cc]', 0, '[skip-cc] bypass allowed'],
|
|
82
|
+
];
|
|
83
|
+
for (const [msg, want, label] of cases) {
|
|
84
|
+
check(msg) === want ? ok(`commit-msg ${label}`) : bad(`commit-msg "${label}" expected exit ${want}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 018 — concurrency-guard branch 2: the file changed on disk since we last wrote it. */
|
|
89
|
+
function testConcurrencyExternalEdit(proj) {
|
|
90
|
+
const hook = (name, payload) => run([join(proj, 'contextkit', 'runtime', 'hooks', name)], { cwd: proj, input: JSON.stringify(payload) });
|
|
91
|
+
const rel = 'src/ext.js';
|
|
92
|
+
const abs = join(proj, rel);
|
|
93
|
+
mkdirSync(join(proj, 'src'), { recursive: true });
|
|
94
|
+
writeFileSync(abs, 'v1\n');
|
|
95
|
+
hook('track-edits.mjs', { session_id: 'extsess', tool_name: 'Write', tool_input: { file_path: rel } });
|
|
96
|
+
// Force a strictly-newer mtime to simulate an external edit after we recorded ours.
|
|
97
|
+
const future = new Date(Date.now() + 10_000);
|
|
98
|
+
utimesSync(abs, future, future);
|
|
99
|
+
const out = hook('concurrency-guard.mjs', { session_id: 'extsess', tool_name: 'Write', tool_input: { file_path: rel } }).stdout || '';
|
|
100
|
+
out.includes('changed on disk') ? ok('concurrency-guard warns on an external on-disk edit') : bad(`concurrency-guard external-edit branch silent: ${out}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 018 — installer recreates a malformed .claude/settings.json instead of crashing. */
|
|
104
|
+
function testMalformedSettingsRecovery(proj) {
|
|
105
|
+
const settings = join(proj, '.claude', 'settings.json');
|
|
106
|
+
writeFileSync(settings, '{ this is : not json ,,');
|
|
107
|
+
const res = run([join(KIT, 'install.mjs'), '--target', proj, '--update']);
|
|
108
|
+
let parsed = null;
|
|
109
|
+
try {
|
|
110
|
+
parsed = readJson(settings);
|
|
111
|
+
} catch {
|
|
112
|
+
/* stays null */
|
|
113
|
+
}
|
|
114
|
+
res.status === 0 && parsed && parsed.hooks
|
|
115
|
+
? ok('installer recovers from a malformed settings.json (--update)') : bad(`malformed-settings recovery failed (status ${res.status})`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 016 — pre-push conflict gate: allow disjoint, warn on auto-merge, block real conflict, bypass. */
|
|
119
|
+
function testPrePush() {
|
|
120
|
+
const fx = installFixture(rep);
|
|
121
|
+
const { proj } = fx;
|
|
122
|
+
const remote = tmp('remote');
|
|
123
|
+
const clone = tmp('clone');
|
|
124
|
+
const hook = join(proj, 'contextkit', 'runtime', 'git-hooks', 'pre-push.mjs');
|
|
125
|
+
const pp = (env) => run([hook], { cwd: proj, env: { ...process.env, ...env } });
|
|
126
|
+
const commit = (cwd, m) => {
|
|
127
|
+
git(['add', '-A'], cwd);
|
|
128
|
+
git(['commit', '-m', m, '--no-verify'], cwd);
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
writeFileSync(join(proj, 'shared.txt'), 'a\nb\nc\nd\ne\n');
|
|
132
|
+
writeFileSync(join(proj, 'mergeable.txt'), '1\n2\n3\n4\n5\n');
|
|
133
|
+
writeFileSync(join(proj, 'other.txt'), 'x\n');
|
|
134
|
+
commit(proj, 'feat: base');
|
|
135
|
+
git(['init', '--bare', '-b', 'main', remote]);
|
|
136
|
+
git(['remote', 'add', 'origin', remote], proj);
|
|
137
|
+
git(['push', '-u', 'origin', 'main'], proj);
|
|
138
|
+
// Origin diverges (via a clone): change shared.txt line c + mergeable.txt line 1.
|
|
139
|
+
git(['clone', remote, clone]);
|
|
140
|
+
git(['config', 'user.email', 'it@example.com'], clone);
|
|
141
|
+
git(['config', 'user.name', 'IT'], clone);
|
|
142
|
+
writeFileSync(join(clone, 'shared.txt'), 'a\nb\nREMOTE_C\nd\ne\n');
|
|
143
|
+
writeFileSync(join(clone, 'mergeable.txt'), 'REMOTE_1\n2\n3\n4\n5\n');
|
|
144
|
+
commit(clone, 'feat: remote change');
|
|
145
|
+
git(['push', 'origin', 'main'], clone);
|
|
146
|
+
|
|
147
|
+
// allow: local touches only other.txt → no overlap with origin.
|
|
148
|
+
writeFileSync(join(proj, 'other.txt'), 'y\n');
|
|
149
|
+
commit(proj, 'feat: local disjoint');
|
|
150
|
+
const allow = pp();
|
|
151
|
+
allow.status === 0 && !/both changed|BLOCKED/.test(allow.stderr || '') ? ok('pre-push ALLOWS a disjoint push') : bad(`pre-push didn't allow disjoint (status ${allow.status})`);
|
|
152
|
+
|
|
153
|
+
// warn: local edits mergeable.txt line 5 (origin edited line 1) → auto-merge → exit 0 + notice.
|
|
154
|
+
writeFileSync(join(proj, 'mergeable.txt'), '1\n2\n3\n4\nLOCAL_5\n');
|
|
155
|
+
commit(proj, 'feat: local mergeable');
|
|
156
|
+
const warn = pp();
|
|
157
|
+
warn.status === 0 && /both changed/.test(warn.stderr || '') ? ok('pre-push WARNS on an auto-mergeable overlap') : bad(`pre-push warn case wrong (status ${warn.status})`);
|
|
158
|
+
|
|
159
|
+
// block: local edits shared.txt line c (origin made it REMOTE_C) → real conflict.
|
|
160
|
+
writeFileSync(join(proj, 'shared.txt'), 'a\nb\nLOCAL_C\nd\ne\n');
|
|
161
|
+
commit(proj, 'feat: local conflict');
|
|
162
|
+
pp().status === 1 ? ok('pre-push BLOCKS a real conflict') : bad('pre-push did not block a real conflict');
|
|
163
|
+
|
|
164
|
+
// bypass: same conflicting state, audited override.
|
|
165
|
+
pp({ CONTEXT_ALLOW_CONFLICT_PUSH: '1' }).status === 0 ? ok('pre-push bypass (CONTEXT_ALLOW_CONFLICT_PUSH) allows') : bad('pre-push bypass did not allow');
|
|
166
|
+
} catch (err) {
|
|
167
|
+
bad(`pre-push setup crashed: ${err?.message ?? err}`);
|
|
168
|
+
} finally {
|
|
169
|
+
fx.cleanup();
|
|
170
|
+
rmSync(remote, { recursive: true, force: true });
|
|
171
|
+
rmSync(clone, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 014 — uninstall removes wiring (keeps memory); purge removes engine (keeps memory + CLAUDE.md). */
|
|
176
|
+
function testUninstall() {
|
|
177
|
+
const fx1 = installFixture(rep);
|
|
178
|
+
try {
|
|
179
|
+
run([join(KIT, 'install.mjs'), '--target', fx1.proj, '--uninstall']);
|
|
180
|
+
const settings = readJson(join(fx1.proj, '.claude', 'settings.json'));
|
|
181
|
+
const wired = Object.values(settings.hooks || {}).some((groups) => (groups || []).some((g) => (g.hooks || []).some((h) => String(h.command).includes('contextkit/runtime/hooks'))));
|
|
182
|
+
!wired ? ok('uninstall strips ContextDevKit hook wiring from settings.json') : bad('uninstall left hook wiring behind');
|
|
183
|
+
!existsSync(join(fx1.proj, '.git', 'hooks', 'pre-push')) ? ok('uninstall removes the git hooks') : bad('uninstall left git hooks');
|
|
184
|
+
existsSync(join(fx1.proj, 'contextkit', 'runtime')) && existsSync(join(fx1.proj, 'CLAUDE.md')) ? ok('uninstall keeps the engine + CLAUDE.md (non-purge)') : bad('uninstall wrongly removed engine/CLAUDE.md');
|
|
185
|
+
} finally {
|
|
186
|
+
fx1.cleanup();
|
|
187
|
+
}
|
|
188
|
+
const fx2 = installFixture(rep);
|
|
189
|
+
try {
|
|
190
|
+
run([join(KIT, 'install.mjs'), '--target', fx2.proj, '--uninstall', '--purge']);
|
|
191
|
+
!existsSync(join(fx2.proj, 'contextkit', 'runtime')) && !existsSync(join(fx2.proj, '.claude', 'commands'))
|
|
192
|
+
? ok('purge removes the engine + commands') : bad('purge left engine/commands');
|
|
193
|
+
existsSync(join(fx2.proj, 'contextkit', 'memory')) && existsSync(join(fx2.proj, 'CLAUDE.md'))
|
|
194
|
+
? ok('purge KEEPS memory + CLAUDE.md (no data loss)') : bad('purge destroyed memory/CLAUDE.md');
|
|
195
|
+
} finally {
|
|
196
|
+
fx2.cleanup();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 038 — installer follows .git pointer in worktrees (`.git` is a file, not a dir).
|
|
201
|
+
* Simulates the worktree layout by writing a `.git` file with `gitdir: <path>`
|
|
202
|
+
* and asserting hooks land in the pointed-at directory, not at `<target>/.git/hooks/`. */
|
|
203
|
+
function testInstallerWorktreeGitPointer() {
|
|
204
|
+
const target = tmp('wtgit');
|
|
205
|
+
const gitdir = tmp('wtgit-gitdir');
|
|
206
|
+
mkdirSync(join(gitdir, 'hooks'), { recursive: true });
|
|
207
|
+
// Write the `.git` file pointer that git itself writes in a worktree.
|
|
208
|
+
writeFileSync(join(target, '.git'), `gitdir: ${gitdir.replaceAll('\\', '/')}\n`);
|
|
209
|
+
// The installer needs a package.json / minimal project to run cleanly.
|
|
210
|
+
writeFileSync(join(target, 'package.json'), '{"name":"wt-fixture"}');
|
|
211
|
+
try {
|
|
212
|
+
const r = run([join(KIT, 'install.mjs'), '--target', target, '--level', '3', '--name', 'Worktree', '--yes']);
|
|
213
|
+
r.status === 0 ? ok('installer succeeds against a worktree-shaped .git (bug 038 fixed)') : bad(`installer failed in worktree: ${r.stderr || r.stdout}`);
|
|
214
|
+
existsSync(join(gitdir, 'hooks', 'pre-commit'))
|
|
215
|
+
? ok('installer writes hooks into the resolved gitdir, not into `<target>/.git/hooks/`') : bad('hooks not found in resolved gitdir');
|
|
216
|
+
const installed = existsSync(join(gitdir, 'hooks', 'pre-commit')) ? readFileSync(join(gitdir, 'hooks', 'pre-commit'), 'utf-8') : '';
|
|
217
|
+
installed.includes('contextkit/runtime/git-hooks')
|
|
218
|
+
? ok('installed hook in worktree points at contextkit/runtime') : bad(`hook body wrong: ${installed}`);
|
|
219
|
+
} finally {
|
|
220
|
+
rmSync(target, { recursive: true, force: true });
|
|
221
|
+
rmSync(gitdir, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Compozy follow-through lifecycle tests (workflow/041, distill-detect/043,
|
|
226
|
+
// resume/046) live in the sibling `integration-test-compozy.mjs`.
|
|
227
|
+
|
|
228
|
+
/** 021 — installer backs up a pre-existing non-ours git hook instead of clobbering it. */
|
|
229
|
+
function testInstallerHookBackup() {
|
|
230
|
+
const proj = tmp('hookbk');
|
|
231
|
+
git(['init', '-b', 'main'], proj);
|
|
232
|
+
git(['config', 'user.email', 'it@example.com'], proj);
|
|
233
|
+
git(['config', 'user.name', 'IT'], proj);
|
|
234
|
+
const hooksDir = join(proj, '.git', 'hooks');
|
|
235
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
236
|
+
writeFileSync(join(hooksDir, 'pre-commit'), '#!/bin/sh\necho "my custom hook"\n');
|
|
237
|
+
try {
|
|
238
|
+
run([join(KIT, 'install.mjs'), '--target', proj, '--level', '3', '--name', 'HookBak', '--yes']);
|
|
239
|
+
const installed = readFileSync(join(hooksDir, 'pre-commit'), 'utf-8');
|
|
240
|
+
const backup = existsSync(join(hooksDir, 'pre-commit.bak')) ? readFileSync(join(hooksDir, 'pre-commit.bak'), 'utf-8') : '';
|
|
241
|
+
installed.includes('contextkit/runtime/git-hooks') && backup.includes('my custom hook')
|
|
242
|
+
? ok('installer backs up an existing non-ours git hook (.bak)') : bad(`installer hook backup failed: backup="${backup.trim()}"`);
|
|
243
|
+
} finally {
|
|
244
|
+
rmSync(proj, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function main() {
|
|
249
|
+
await testConfigLoader();
|
|
250
|
+
await testGhAlertMappers();
|
|
251
|
+
testInstallerHookBackup();
|
|
252
|
+
testInstallerWorktreeGitPointer();
|
|
253
|
+
const fx = installFixture(rep);
|
|
254
|
+
try {
|
|
255
|
+
testCommitMsg(fx.proj);
|
|
256
|
+
testConcurrencyExternalEdit(fx.proj);
|
|
257
|
+
testMalformedSettingsRecovery(fx.proj);
|
|
258
|
+
} finally {
|
|
259
|
+
fx.cleanup();
|
|
260
|
+
}
|
|
261
|
+
testPrePush();
|
|
262
|
+
testUninstall();
|
|
263
|
+
rep.finish('Integration (guards)');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
main().catch((err) => {
|
|
267
|
+
bad(`guards crashed: ${err?.stack || err}`);
|
|
268
|
+
rep.finish('Integration (guards)');
|
|
269
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ContextDevKit integration test — TOOLING / agent-forge.
|
|
4
|
+
*
|
|
5
|
+
* Sibling of `integration-test-tooling.mjs`. Extracted as a responsibility
|
|
6
|
+
* seam (the split the parent file's cohesion note anticipated when budget
|
|
7
|
+
* pressure returned — Fase 6 was the trigger). agent-forge is the longest
|
|
8
|
+
* single subsystem in the tooling matrix: install round-trip + APF
|
|
9
|
+
* structure assertions (yaml-available branch) + pure-JS fallback
|
|
10
|
+
* (no-yaml branch) + Fase 6 pipeline DSL — together they crossed the
|
|
11
|
+
* 308-line hard limit when Fase 6 landed.
|
|
12
|
+
*
|
|
13
|
+
* Each sibling installs its own fixture — the cost is one extra install;
|
|
14
|
+
* the benefit is a focused, under-budget file per subsystem. Mirrors the
|
|
15
|
+
* ADR-0016 H1 split that produced `integration-test-tooling-pipeline.mjs`.
|
|
16
|
+
*
|
|
17
|
+
* Run: node tools/integration-test-tooling-agent-forge.mjs (exit 0 = healthy)
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { reporter, installFixture } from './it-helpers.mjs';
|
|
22
|
+
|
|
23
|
+
const rep = reporter();
|
|
24
|
+
const { ok, bad } = rep;
|
|
25
|
+
console.log('\n🌀 ContextDevKit integration test — tooling / agent-forge\n');
|
|
26
|
+
const fx = installFixture(rep);
|
|
27
|
+
const { proj } = fx;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// agent-forge — installed at L>=4 + /forge-new round-trip: the architect/router/packager
|
|
31
|
+
// pipeline writes a complete APF (yaml dep available) or proves the pure-JS half of the
|
|
32
|
+
// pipeline (assembleManifest + router + generators) when yaml is absent (CI default).
|
|
33
|
+
existsSync(join(proj, 'contextkit', 'squads', 'agent-forge', 'lib', 'router.mjs'))
|
|
34
|
+
? ok('agent-forge squad installed at L>=4 (contextkit/squads/agent-forge)')
|
|
35
|
+
: bad('agent-forge squad missing from L5 install');
|
|
36
|
+
const forgeBase = join(proj, 'contextkit', 'squads', 'agent-forge').replaceAll('\\', '/');
|
|
37
|
+
const blueprint = {
|
|
38
|
+
agent_name: 'intake-classifier',
|
|
39
|
+
role_one_line: 'You classify intake forms by department.',
|
|
40
|
+
intent: { category: 'classification', complexity: 'low' },
|
|
41
|
+
privacy: { allow_cloud_providers: true, data_residency: 'br-or-eu' },
|
|
42
|
+
capabilities: { tools: false, structured_output: true },
|
|
43
|
+
runtime_adapters: ['node', 'python'],
|
|
44
|
+
};
|
|
45
|
+
let yamlAvail = false;
|
|
46
|
+
try { await import('yaml'); yamlAvail = true; } catch { /* optional dep — ADR-0013 */ }
|
|
47
|
+
if (yamlAvail) {
|
|
48
|
+
const { forgeNew } = await import('file://' + join(forgeBase, 'cli', 'forge-new.mjs').replaceAll('\\', '/'));
|
|
49
|
+
const result = await forgeNew(blueprint, join(proj, 'agent-packages'), { now: '2026-05-26T12:00:00Z' });
|
|
50
|
+
const apf = result.summary.targetDir;
|
|
51
|
+
const expected = ['manifest.yaml', 'README.md', 'prompts/system.canonical.md',
|
|
52
|
+
'prompts/system.anthropic.md', 'prompts/system.openai.md',
|
|
53
|
+
'prompts/system.google.md', 'prompts/system.deepseek.md', 'prompts/system.ollama.md',
|
|
54
|
+
'tools/schemas.canonical.json',
|
|
55
|
+
'tools/adapters/anthropic.tools.json', 'tools/adapters/openai.tools.json',
|
|
56
|
+
'tools/adapters/google.tools.json', 'tools/adapters/deepseek.tools.json', 'tools/adapters/ollama.tools.json',
|
|
57
|
+
'evals/golden.jsonl', 'governance/cost.policy.yaml', 'adapters/node/index.js'];
|
|
58
|
+
const missing = expected.filter((f) => !existsSync(join(apf, f)));
|
|
59
|
+
missing.length === 0 ? ok(`forge-new writes a complete APF (${expected.length} files)`) : bad(`APF missing: ${missing.join(', ')}`);
|
|
60
|
+
const manifest = readFileSync(join(apf, 'manifest.yaml'), 'utf-8');
|
|
61
|
+
manifest.includes(result.summary.provenance.blueprint_hash)
|
|
62
|
+
? ok('forge-new stamps provenance.blueprint_hash into manifest.yaml') : bad('blueprint_hash not stamped');
|
|
63
|
+
manifest.includes('provider: ' + result.decision.primary.split('/')[0])
|
|
64
|
+
? ok('forge-new stamps the routed primary provider into manifest.yaml') : bad('primary provider not in manifest');
|
|
65
|
+
readFileSync(join(apf, 'prompts/system.anthropic.md'), 'utf-8').includes('<role>')
|
|
66
|
+
? ok('forge-new emits the Anthropic XML system prompt') : bad('Anthropic XML prompt missing');
|
|
67
|
+
readFileSync(join(apf, 'prompts/system.deepseek.md'), 'utf-8').includes('Think step by step')
|
|
68
|
+
? ok('forge-new emits the DeepSeek prompt with explicit CoT cue (Fase 2)') : bad('DeepSeek CoT cue missing');
|
|
69
|
+
readFileSync(join(apf, 'prompts/system.google.md'), 'utf-8').includes('systemInstruction')
|
|
70
|
+
? ok('forge-new emits the Gemini prompt with safetySettings note (Fase 2)') : bad('Gemini systemInstruction note missing');
|
|
71
|
+
JSON.parse(readFileSync(join(apf, 'tools/adapters/openai.tools.json'), 'utf-8')).tools.every((t) => t.type === 'function')
|
|
72
|
+
? ok('forge-new emits OpenAI function-format tool adapters') : bad('OpenAI adapter malformed');
|
|
73
|
+
const googleAdapter = JSON.parse(readFileSync(join(apf, 'tools/adapters/google.tools.json'), 'utf-8'));
|
|
74
|
+
Array.isArray(googleAdapter.functionDeclarations) && googleAdapter.functionDeclarations.every((decl) => !('additionalProperties' in (decl.parameters || {})))
|
|
75
|
+
? ok('forge-new emits Gemini functionDeclarations (additionalProperties stripped) (Fase 2)') : bad('Gemini adapter malformed or kept stripped fields');
|
|
76
|
+
JSON.parse(readFileSync(join(apf, 'tools/adapters/deepseek.tools.json'), 'utf-8')).tools.every((t) => t.type === 'function')
|
|
77
|
+
? ok('forge-new emits DeepSeek tool adapter (OpenAI-compat) (Fase 2)') : bad('DeepSeek adapter malformed');
|
|
78
|
+
readFileSync(join(apf, 'adapters/node/index.js'), 'utf-8').length > 0
|
|
79
|
+
? ok('forge-new ships the Node runtime adapter (round-trip ready)') : bad('Node adapter missing');
|
|
80
|
+
const pyproject = readFileSync(join(apf, 'adapters/python/pyproject.toml'), 'utf-8');
|
|
81
|
+
pyproject.includes(blueprint.agent_name) && !pyproject.includes('{{AGENT_NAME}}')
|
|
82
|
+
? ok('forge-new ships the Python runtime adapter with name stamped (Fase 2)') : bad('Python adapter not stamped (pyproject.toml)');
|
|
83
|
+
readFileSync(join(apf, 'manifest.yaml'), 'utf-8').includes('python')
|
|
84
|
+
? ok('forge-new stamps runtime_adapters: [..., python] into manifest.yaml (Fase 2)') : bad('python missing from manifest runtime_adapters');
|
|
85
|
+
const costPolicy = readFileSync(join(apf, 'governance/cost.policy.yaml'), 'utf-8');
|
|
86
|
+
!costPolicy.includes('{{') && costPolicy.includes('budgets:')
|
|
87
|
+
? ok('forge-new writes a POPULATED cost.policy.yaml (no {{TOKEN}}, Fase 3)') : bad('cost.policy still carries placeholders or no budgets');
|
|
88
|
+
const qualityPolicy = readFileSync(join(apf, 'governance/quality.policy.yaml'), 'utf-8');
|
|
89
|
+
qualityPolicy.includes('eval_gates:') && qualityPolicy.includes('fallback_chain:')
|
|
90
|
+
? ok('forge-new writes a populated quality.policy.yaml (Fase 3)') : bad('quality.policy missing required sections');
|
|
91
|
+
const fallbackChain = readFileSync(join(apf, 'governance/fallback-chain.yaml'), 'utf-8');
|
|
92
|
+
fallbackChain.includes('primary:') && fallbackChain.includes('on_safety_block: do_not_fallback')
|
|
93
|
+
? ok('forge-new writes a fallback-chain.yaml built from the router decision (Fase 3)') : bad('fallback-chain missing primary or safety_block rule');
|
|
94
|
+
readFileSync(join(apf, 'evals/thresholds.yaml'), 'utf-8').includes('release_gate:')
|
|
95
|
+
? ok('forge-new writes a populated evals/thresholds.yaml (Fase 3)') : bad('evals/thresholds.yaml not populated');
|
|
96
|
+
/eval_passed_at:\s*null/.test(readFileSync(join(apf, 'manifest.yaml'), 'utf-8'))
|
|
97
|
+
? ok('forge-new leaves eval_passed_at null without runEval (Fase 3 gate)') : bad('eval_passed_at not null without runEval');
|
|
98
|
+
const gated = await forgeNew(blueprint, join(proj, 'agent-packages-gated'), {
|
|
99
|
+
now: '2026-05-26T12:00:00Z',
|
|
100
|
+
runEval: { provider: (input) => (input.text ? { redacted: '[ok]' } : { y: 'ok' }) },
|
|
101
|
+
});
|
|
102
|
+
gated.evalResult?.verdict === 'pass' && readFileSync(join(gated.summary.targetDir, 'manifest.yaml'), 'utf-8').includes('eval_passed_at: \'2026-05-26')
|
|
103
|
+
? ok('forge-new with runEval (passing mock) stamps eval_passed_at into manifest.yaml (Fase 3)') : bad(`runEval gate failed: verdict=${gated.evalResult?.verdict}`);
|
|
104
|
+
const ragBlueprint = { agent_name: 'contract-qa', role_one_line: 'You answer questions about contracts from the knowledge base.', intent: { category: 'rag-answer', complexity: 'high', domain: 'legal-pt-br' }, privacy: { allow_cloud_providers: true, data_residency: 'br-or-eu' }, capabilities: { tools: false, rag: true }, runtime_adapters: ['node', 'go'] };
|
|
105
|
+
const ragResult = await forgeNew(ragBlueprint, join(proj, 'agent-packages-rag'), { now: '2026-05-26T12:00:00Z' });
|
|
106
|
+
const ragApf = ragResult.summary.targetDir;
|
|
107
|
+
const ragConfig = readFileSync(join(ragApf, 'rag/config.yaml'), 'utf-8');
|
|
108
|
+
!ragConfig.includes('{{') && /backend:\s*qdrant/.test(ragConfig)
|
|
109
|
+
? ok('forge-new writes a populated rag/config.yaml (qdrant backend for cloud-OK, Fase 5)') : bad('rag/config.yaml not populated or wrong backend');
|
|
110
|
+
/multilingual-e5/.test(ragConfig)
|
|
111
|
+
? ok('forge-new picks multilingual-e5 for non-`-en` domain (Fase 5)') : bad('embedding model wrong for multilingual domain');
|
|
112
|
+
const goMod = readFileSync(join(ragApf, 'adapters/go/go.mod'), 'utf-8');
|
|
113
|
+
goMod.includes('contract-qa') && !goMod.includes('{{')
|
|
114
|
+
? ok('forge-new stamps Go adapter go.mod when runtime_adapters includes go (Fase 5)') : bad(`go.mod not stamped: ${goMod}`);
|
|
115
|
+
} else {
|
|
116
|
+
const { validateBlueprint, fillDefaults } = await import('file://' + join(forgeBase, 'lib', 'architect.mjs').replaceAll('\\', '/'));
|
|
117
|
+
const { routeAgent } = await import('file://' + join(forgeBase, 'lib', 'router.mjs').replaceAll('\\', '/'));
|
|
118
|
+
const { assembleManifest } = await import('file://' + join(forgeBase, 'lib', 'packager.mjs').replaceAll('\\', '/'));
|
|
119
|
+
const { generatePrompts } = await import('file://' + join(forgeBase, 'lib', 'prompt-gen.mjs').replaceAll('\\', '/'));
|
|
120
|
+
const { generateAdapters } = await import('file://' + join(forgeBase, 'lib', 'tool-gen.mjs').replaceAll('\\', '/'));
|
|
121
|
+
validateBlueprint(blueprint).ok ? ok('forge-new (no-yaml): blueprint validates') : bad('blueprint invalid');
|
|
122
|
+
!validateBlueprint({ ...blueprint, runtime_adapters: ['rust'] }).ok
|
|
123
|
+
? ok('forge-new (no-yaml): validateBlueprint rejects unknown runtime_adapters (Fase 2)') : bad('validateBlueprint accepted bogus runtime');
|
|
124
|
+
const filled = fillDefaults(blueprint);
|
|
125
|
+
Array.isArray(filled.runtime_adapters) && filled.runtime_adapters[0] === 'node'
|
|
126
|
+
? ok('forge-new (no-yaml): fillDefaults sets runtime_adapters default [node] (Fase 2)') : bad('runtime_adapters default missing');
|
|
127
|
+
const decision = await routeAgent(filled);
|
|
128
|
+
const manifest = assembleManifest(filled, decision, { now: '2026-05-26T12:00:00Z' });
|
|
129
|
+
manifest.metadata.name === blueprint.agent_name && manifest.spec.model_selection.primary.provider === decision.primary.split('/')[0]
|
|
130
|
+
? ok('forge-new (no-yaml): assembleManifest stamps name + routed primary') : bad('assembleManifest mismatch');
|
|
131
|
+
const pyManifest = assembleManifest({ ...filled, runtime_adapters: ['node', 'python'] }, decision, { now: '2026-05-26T12:00:00Z' });
|
|
132
|
+
pyManifest.spec.runtime_adapters.includes('python')
|
|
133
|
+
? ok('forge-new (no-yaml): blueprint.runtime_adapters flows into manifest.spec.runtime_adapters (Fase 2)') : bad('runtime_adapters did not flow through');
|
|
134
|
+
/^[a-f0-9]{64}$/.test(manifest.metadata.provenance.blueprint_hash)
|
|
135
|
+
? ok('forge-new (no-yaml): provenance.blueprint_hash is SHA-256') : bad('blueprint_hash malformed');
|
|
136
|
+
const prompts = generatePrompts('# Role\nYou classify.\n\n# Context\nClinic.\n\n# Rules\n- JSON.\n\n# Output\nJSON.\n');
|
|
137
|
+
prompts.anthropic.includes('<role>') && prompts.openai.includes('# Role')
|
|
138
|
+
? ok('forge-new (no-yaml): prompt-gen renders Anthropic XML + OpenAI Markdown') : bad('prompt-gen output wrong');
|
|
139
|
+
prompts.google?.includes('systemInstruction') && prompts.deepseek?.includes('Think step by step') && prompts.ollama?.includes('chat_template')
|
|
140
|
+
? ok('forge-new (no-yaml): prompt-gen renders Gemini + DeepSeek (CoT) + Ollama (chat_template) (Fase 2)') : bad('Fase 2 prompts missing or malformed');
|
|
141
|
+
const adapters = generateAdapters({ classify: { description: 'Classify text', input_schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] } } });
|
|
142
|
+
adapters.anthropic.tools[0].name === 'classify' && adapters.openai.tools[0].type === 'function'
|
|
143
|
+
? ok('forge-new (no-yaml): tool-gen renders Anthropic + OpenAI adapters') : bad('tool-gen output wrong');
|
|
144
|
+
const geminiDecl = adapters.google?.functionDeclarations?.[0];
|
|
145
|
+
geminiDecl?.name === 'classify' && !('additionalProperties' in (geminiDecl.parameters || {}))
|
|
146
|
+
? ok('forge-new (no-yaml): tool-gen Gemini strips JSON-Schema fields not in the subset (Fase 2)') : bad('Gemini down-conversion wrong');
|
|
147
|
+
adapters.deepseek?.tools?.[0]?.type === 'function' && adapters.ollama?.tools?.[0]?.type === 'function'
|
|
148
|
+
? ok('forge-new (no-yaml): tool-gen DeepSeek + Ollama mirror OpenAI shape (Fase 2)') : bad('DeepSeek/Ollama adapters wrong');
|
|
149
|
+
console.log(' ⓘ yaml dep not installed — full file-write round-trip skipped (install: npm i yaml).');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fase 6 — squad-pipeline DSL: pipeline.yaml ships, validates, dry-run is non-empty.
|
|
153
|
+
// (ADR-0015 Part A; full grammar in docs/SQUAD-PIPELINE-FORMAT.md.)
|
|
154
|
+
existsSync(join(proj, 'contextkit', 'squads', 'agent-forge', 'pipeline.yaml'))
|
|
155
|
+
? ok('agent-forge ships pipeline.yaml (Fase 6)')
|
|
156
|
+
: bad('agent-forge pipeline.yaml missing from install');
|
|
157
|
+
const pipelineEngineUrl = 'file://' + join(proj, 'contextkit', 'tools', 'scripts', 'squad-pipeline.mjs').replaceAll('\\', '/');
|
|
158
|
+
const { loadAndValidate, plan } = await import(pipelineEngineUrl);
|
|
159
|
+
const lv = await loadAndValidate('agent-forge').catch((err) => ({ error: err }));
|
|
160
|
+
if (yamlAvail) {
|
|
161
|
+
if (lv.error) {
|
|
162
|
+
bad(`Fase 6: loadAndValidate threw with yaml available: ${lv.error.message}`);
|
|
163
|
+
} else {
|
|
164
|
+
lv.pipeline?.squad === 'agent-forge' && lv.pipeline.steps.length >= 8
|
|
165
|
+
? ok(`Fase 6: agent-forge pipeline validates (${lv.pipeline.steps.length} steps)`)
|
|
166
|
+
: bad(`Fase 6: pipeline shape wrong: ${JSON.stringify({ squad: lv.pipeline?.squad, steps: lv.pipeline?.steps?.length })}`);
|
|
167
|
+
const rows = plan(lv.pipeline, { blueprint: { tools: ['x'] }, capabilities: { rag: false } });
|
|
168
|
+
rows.find((r) => r.id === 'generate-tools')?.marker === '↺' || rows.find((r) => r.id === 'generate-tools')?.marker === '✓'
|
|
169
|
+
? ok('Fase 6: dry-run runs generate-tools when blueprint.tools.length > 0')
|
|
170
|
+
: bad(`Fase 6: generate-tools marker wrong: ${rows.find((r) => r.id === 'generate-tools')?.marker}`);
|
|
171
|
+
rows.find((r) => r.id === 'generate-rag')?.marker === '⊘'
|
|
172
|
+
? ok('Fase 6: dry-run skips generate-rag when capabilities.rag == false (⊘)')
|
|
173
|
+
: bad(`Fase 6: generate-rag marker wrong under rag=false: ${rows.find((r) => r.id === 'generate-rag')?.marker}`);
|
|
174
|
+
rows.find((r) => r.id === 'eval-gate')?.marker === '↺'
|
|
175
|
+
? ok('Fase 6: dry-run marks eval-gate retry loop (↺, max_cycles: 3)')
|
|
176
|
+
: bad(`Fase 6: eval-gate marker wrong: ${rows.find((r) => r.id === 'eval-gate')?.marker}`);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
lv.yamlAbsent === true
|
|
180
|
+
? ok('Fase 6: squad-pipeline takes the yaml-absent informative path (opt-in, not hot-path)')
|
|
181
|
+
: bad(`Fase 6: expected { yamlAbsent: true } when yaml is missing, got ${JSON.stringify(lv)}`);
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
bad(`crashed: ${err?.stack || err}`);
|
|
185
|
+
} finally {
|
|
186
|
+
fx.cleanup();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
rep.finish('Integration (tooling — agent-forge)');
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ContextDevKit integration test — TOOLING / DevPipeline.
|
|
4
|
+
*
|
|
5
|
+
* Sibling of `integration-test-tooling.mjs`. Extracted as a responsibility
|
|
6
|
+
* seam (ADR-0016 H1 fix) because the DevPipeline test chain is internally
|
|
7
|
+
* coupled (add → ingest → idempotent → prioritize → wsjf → bugs share fixture
|
|
8
|
+
* state) and was the natural unit to split out when the tooling file crossed
|
|
9
|
+
* the line-budget RED zone. Each sibling installs its own fixture — the cost
|
|
10
|
+
* is one extra install; the benefit is a focused, under-budget file per
|
|
11
|
+
* responsibility.
|
|
12
|
+
*
|
|
13
|
+
* Run: node tools/integration-test-tooling-pipeline.mjs (exit 0 = healthy)
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { KIT, run, reporter, installFixture } from './it-helpers.mjs';
|
|
18
|
+
|
|
19
|
+
const rep = reporter();
|
|
20
|
+
const { ok, bad } = rep;
|
|
21
|
+
console.log('\n🌀 ContextDevKit integration test — tooling / DevPipeline\n');
|
|
22
|
+
const fx = installFixture(rep);
|
|
23
|
+
const { proj, script } = fx;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// DevPipeline: add → move → sync reflects in devpipeline.md.
|
|
27
|
+
script('pipeline.mjs', 'add', '--type', 'bug', '--priority', 'P1', '--title', 'login crash');
|
|
28
|
+
const board1 = readFileSync(join(proj, 'contextkit', 'pipeline', 'devpipeline.md'), 'utf-8');
|
|
29
|
+
board1.includes('login crash') && /Backlog \*\*1\*\*/.test(board1) ? ok('pipeline add → backlog on board') : bad('pipeline add not reflected');
|
|
30
|
+
script('pipeline.mjs', 'move', '001', 'testing');
|
|
31
|
+
const board2 = readFileSync(join(proj, 'contextkit', 'pipeline', 'devpipeline.md'), 'utf-8');
|
|
32
|
+
/Testing \*\*1\*\*/.test(board2) ? ok('pipeline move → testing on board') : bad('pipeline move not reflected');
|
|
33
|
+
|
|
34
|
+
// DevPipeline ingest: analysis findings flow into the backlog, auto-prioritized.
|
|
35
|
+
writeFileSync(join(proj, 'findings.json'), JSON.stringify({ findings: [
|
|
36
|
+
{ kind: 'line-budget', severity: 5, path: 'src/big.js', line: 400, message: 'too big' },
|
|
37
|
+
{ kind: 'todo-marker', severity: 1, path: 'src/x.js', line: 3, message: 'leftover TODO' },
|
|
38
|
+
] }));
|
|
39
|
+
script('pipeline.mjs', 'ingest', 'findings.json', '--type', 'chore');
|
|
40
|
+
const ingested = JSON.parse(script('pipeline.mjs', 'list', '--json').stdout || '[]')
|
|
41
|
+
.filter((t) => /^line-budget|^todo-marker/.test(t.source || ''));
|
|
42
|
+
ingested.length === 2 && ingested.some((t) => t.priority === 'P1') && ingested.some((t) => t.priority === 'P3')
|
|
43
|
+
? ok('pipeline ingest creates auto-prioritized tasks from findings') : bad(`ingest failed: ${JSON.stringify(ingested)}`);
|
|
44
|
+
/Ingested 0 finding/.test(script('pipeline.mjs', 'ingest', 'findings.json', '--type', 'chore').stdout || '')
|
|
45
|
+
? ok('pipeline ingest is idempotent (no duplicates)') : bad('ingest re-added duplicates');
|
|
46
|
+
const lb = ingested.find((t) => /^line-budget/.test(t.source));
|
|
47
|
+
script('pipeline.mjs', 'prioritize', lb.id, 'P0');
|
|
48
|
+
JSON.parse(script('pipeline.mjs', 'list', '--json').stdout || '[]').find((t) => t.id === lb.id)?.priority === 'P0'
|
|
49
|
+
? ok('pipeline prioritize overrides the auto priority (user-editable)') : bad('prioritize did not change priority');
|
|
50
|
+
|
|
51
|
+
// WSJF (SAFe) → priority + bug severity (S1-S4) → priority + SLA due date.
|
|
52
|
+
script('pipeline.mjs', 'add', '--type', 'feature', '--title', 'wsjf item', '--wsjf', '8,9,5,3');
|
|
53
|
+
script('pipeline.mjs', 'add', '--type', 'bug', '--title', 'sev bug', '--severity', 'S1');
|
|
54
|
+
const prio = JSON.parse(script('pipeline.mjs', 'list', '--json').stdout || '[]');
|
|
55
|
+
const wsjfT = prio.find((t) => t.title === 'wsjf item');
|
|
56
|
+
const sevT = prio.find((t) => t.title === 'sev bug');
|
|
57
|
+
wsjfT?.priority === 'P1' && Number(wsjfT.wsjf) > 0 && sevT?.priority === 'P0' && sevT?.sla
|
|
58
|
+
? ok('pipeline WSJF→priority, bug severity→priority, SLA due date') : bad(`WSJF/severity failed: ${JSON.stringify({ wsjfT, sevT })}`);
|
|
59
|
+
|
|
60
|
+
// Known-bugs map: bug tasks grouped + a map file generated.
|
|
61
|
+
script('pipeline.mjs', 'bugs');
|
|
62
|
+
existsSync(join(proj, 'contextkit', 'pipeline', 'known-bugs.md')) &&
|
|
63
|
+
readFileSync(join(proj, 'contextkit', 'pipeline', 'known-bugs.md'), 'utf-8').includes('sev bug')
|
|
64
|
+
? ok('known-bugs map generated + groups bug tasks') : bad('known-bugs map missing/empty');
|
|
65
|
+
|
|
66
|
+
// ─ Ticket 040: task metadata v2 (DAG dependencies + complexity + spike/docs) ─
|
|
67
|
+
script('pipeline.mjs', 'add', '--type', 'spike', '--title', 'spike-test', '--complexity', 'L', '--depends-on', '[001, 002]');
|
|
68
|
+
const meta = JSON.parse(script('pipeline.mjs', 'list', '--json').stdout || '[]').find((t) => t.title === 'spike-test');
|
|
69
|
+
meta?.type === 'spike' && meta?.complexity === 'L' && Array.isArray(meta?.dependencies) && meta.dependencies.length === 2
|
|
70
|
+
? ok('pipeline add accepts --type spike + --complexity + --depends-on (ticket 040)') : bad(`metadata v2 wrong: ${JSON.stringify(meta)}`);
|
|
71
|
+
const boardV2 = readFileSync(join(proj, 'contextkit', 'pipeline', 'devpipeline.md'), 'utf-8');
|
|
72
|
+
boardV2.includes('blocked by') ? ok('board renders "blocked by N" hint when dependencies are open (ticket 040)') : bad('blocked-by hint missing from board');
|
|
73
|
+
// validate command: clean graph passes.
|
|
74
|
+
const validClean = script('pipeline.mjs', 'validate');
|
|
75
|
+
validClean.status === 0 ? ok('pipeline validate exits 0 on acyclic graph') : bad(`validate failed clean: ${validClean.stdout}${validClean.stderr}`);
|
|
76
|
+
// Manually inject a cycle and prove validate refuses.
|
|
77
|
+
const spikeFile = join(proj, 'contextkit', 'pipeline', 'backlog', `${meta.id}-spike-test.md`);
|
|
78
|
+
writeFileSync(spikeFile, readFileSync(spikeFile, 'utf-8').replace(/^dependencies:.*$/m, `dependencies: [${meta.id}]`));
|
|
79
|
+
const validCycle = script('pipeline.mjs', 'validate');
|
|
80
|
+
validCycle.status !== 0 && /cycle/i.test(validCycle.stdout + validCycle.stderr)
|
|
81
|
+
? ok('pipeline validate refuses on dependency cycle (ticket 040)') : bad(`validate did not refuse cycle: status=${validCycle.status}`);
|
|
82
|
+
|
|
83
|
+
// ─ ADR-0015 §B: working/ stage + tasks[] in workspace record + stale eviction ─
|
|
84
|
+
existsSync(join(proj, 'contextkit', 'pipeline', 'working'))
|
|
85
|
+
? ok('working/ folder seeded post-install (ADR-0015 §B)')
|
|
86
|
+
: bad('working/ folder missing');
|
|
87
|
+
// Add a fresh task to start from a known state.
|
|
88
|
+
script('pipeline.mjs', 'add', '--type', 'chore', '--title', 'wip-test');
|
|
89
|
+
const wipTask = JSON.parse(script('pipeline.mjs', 'list', '--json').stdout || '[]').find((t) => t.title === 'wip-test');
|
|
90
|
+
// Bootstrap the session pointer so claim.mjs can identify "this session".
|
|
91
|
+
mkdirSync(join(proj, '.claude', '.sessions'), { recursive: true });
|
|
92
|
+
writeFileSync(join(proj, '.claude', '.sessions', '.last-touched'), JSON.stringify({ sessionId: 'it-039', at: Date.now() }));
|
|
93
|
+
// /pipeline start → moves to working/ + attaches to workspace.
|
|
94
|
+
script('pipeline.mjs', 'start', wipTask.id);
|
|
95
|
+
const afterStart = JSON.parse(script('pipeline.mjs', 'list', '--json').stdout || '[]').find((t) => t.id === wipTask.id);
|
|
96
|
+
afterStart?.stage === 'working' ? ok('pipeline start → task moves to working/') : bad(`task stage after start = ${afterStart?.stage}`);
|
|
97
|
+
const wsFile = join(proj, '.claude', '.workspace', 'it-039.json');
|
|
98
|
+
const ws = existsSync(wsFile) ? JSON.parse(readFileSync(wsFile, 'utf-8')) : {};
|
|
99
|
+
Array.isArray(ws.tasks) && ws.tasks.some((t) => t.id === wipTask.id)
|
|
100
|
+
? ok('claim.attachTask appends task to workspace tasks[]') : bad(`workspace tasks[] wrong: ${JSON.stringify(ws.tasks)}`);
|
|
101
|
+
const workingBoard = readFileSync(join(proj, 'contextkit', 'pipeline', 'devpipeline.md'), 'utf-8');
|
|
102
|
+
/Working \*\*\d+\*\*/.test(workingBoard) && /## 🔵 Working/.test(workingBoard)
|
|
103
|
+
? ok('pipeline-board renders Working count + section') : bad('working stage missing from board');
|
|
104
|
+
// /pipeline stop → moves BACK to backlog (not testing), detaches.
|
|
105
|
+
script('pipeline.mjs', 'stop', wipTask.id);
|
|
106
|
+
const afterStop = JSON.parse(script('pipeline.mjs', 'list', '--json').stdout || '[]').find((t) => t.id === wipTask.id);
|
|
107
|
+
afterStop?.stage === 'backlog' ? ok('pipeline stop → task moves BACK to backlog (not testing)') : bad(`task stage after stop = ${afterStop?.stage}`);
|
|
108
|
+
const wsAfter = JSON.parse(readFileSync(wsFile, 'utf-8'));
|
|
109
|
+
!wsAfter.tasks?.some((t) => t.id === wipTask.id) ? ok('claim.detachTask removes task from workspace tasks[]') : bad('task still attached after stop');
|
|
110
|
+
// Stale eviction: artificially age a task's heartbeat past the configured threshold.
|
|
111
|
+
script('pipeline.mjs', 'start', wipTask.id);
|
|
112
|
+
const wsStale = JSON.parse(readFileSync(wsFile, 'utf-8'));
|
|
113
|
+
wsStale.tasks[0].lastHeartbeat = Date.now() - (91 * 60 * 1000);
|
|
114
|
+
wsStale.lastHeartbeat = Date.now(); // session itself stays alive
|
|
115
|
+
writeFileSync(wsFile, JSON.stringify(wsStale, null, 2));
|
|
116
|
+
script('workspace-sync.mjs');
|
|
117
|
+
const afterEvict = JSON.parse(script('pipeline.mjs', 'list', '--json').stdout || '[]').find((t) => t.id === wipTask.id);
|
|
118
|
+
afterEvict?.stage === 'backlog' ? ok('workspace-sync auto-evicts stale task back to backlog/') : bad(`stale evict failed: stage=${afterEvict?.stage}`);
|
|
119
|
+
|
|
120
|
+
// ─ ADR-0015 §C: canonical state.json substrate (per-task + per-pipeline-run) ─
|
|
121
|
+
script('pipeline.mjs', 'add', '--type', 'chore', '--title', 'state-test');
|
|
122
|
+
const stTask = JSON.parse(script('pipeline.mjs', 'list', '--json').stdout || '[]').find((t) => t.title === 'state-test');
|
|
123
|
+
script('pipeline.mjs', 'start', stTask.id);
|
|
124
|
+
const stateFile = join(proj, 'contextkit', 'pipeline', stTask.id, 'state.json');
|
|
125
|
+
existsSync(stateFile) ? ok('start writes state.json (ADR-0015 §C)') : bad('state.json not written on start');
|
|
126
|
+
const state1 = existsSync(stateFile) ? JSON.parse(readFileSync(stateFile, 'utf-8')) : {};
|
|
127
|
+
state1.kind === 'task' && state1.status === 'working' && typeof state1.startedAt === 'number'
|
|
128
|
+
? ok('state.json shape correct (kind=task, status=working, timestamps)') : bad(`state shape wrong: ${JSON.stringify(state1)}`);
|
|
129
|
+
script('pipeline.mjs', 'stop', stTask.id);
|
|
130
|
+
const state2 = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
131
|
+
state2.status === 'backlog' && typeof state2.endedAt === 'number'
|
|
132
|
+
? ok('stop stamps endedAt + flips status to backlog') : bad(`stop did not update state: ${JSON.stringify(state2)}`);
|
|
133
|
+
script('pipeline.mjs', 'start', stTask.id);
|
|
134
|
+
script('pipeline.mjs', 'move', stTask.id, 'conclusion');
|
|
135
|
+
// The move's state.json mirror is fire-and-forget; poll briefly.
|
|
136
|
+
const deadline = Date.now() + 2000;
|
|
137
|
+
while (Date.now() < deadline && JSON.parse(readFileSync(stateFile, 'utf-8')).status !== 'done') { /* spin */ }
|
|
138
|
+
const state3 = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
139
|
+
state3.status === 'done' && typeof state3.endedAt === 'number'
|
|
140
|
+
? ok('move conclusion mirrors into state.json (status=done, endedAt set)') : bad(`conclusion state wrong: ${JSON.stringify(state3)}`);
|
|
141
|
+
|
|
142
|
+
// ─ ADR-0015 §C follow-up: /runs command reads state.json substrate ─
|
|
143
|
+
const runsOut = script('runs.mjs').stdout || '';
|
|
144
|
+
runsOut.includes('tasks') && runsOut.includes(stTask.id)
|
|
145
|
+
? ok('/runs lists tasks from state.json') : bad(`/runs output missing tasks: ${runsOut.slice(0, 200)}`);
|
|
146
|
+
const runsJson = JSON.parse(script('runs.mjs', '--json').stdout || '{}');
|
|
147
|
+
Array.isArray(runsJson.states) && runsJson.total >= 1
|
|
148
|
+
? ok('/runs --json returns machine-readable shape') : bad(`/runs --json shape wrong: ${JSON.stringify(runsJson).slice(0, 200)}`);
|
|
149
|
+
const runsKindTask = JSON.parse(script('runs.mjs', '--json', '--kind', 'task').stdout || '{}');
|
|
150
|
+
runsKindTask.states?.every((s) => s.kind === 'task')
|
|
151
|
+
? ok('/runs --kind task filters correctly') : bad('/runs --kind task did not filter');
|
|
152
|
+
// No-state refusal: run from a sibling dir that has no contextkit/pipeline/*/state.json.
|
|
153
|
+
const emptyDir = join(proj, 'apps', 'web');
|
|
154
|
+
mkdirSync(emptyDir, { recursive: true });
|
|
155
|
+
const noStateOut = run([join(KIT, 'templates/contextkit/tools/scripts/runs.mjs')], { cwd: emptyDir });
|
|
156
|
+
String(noStateOut?.stdout || '').includes('No runs yet')
|
|
157
|
+
? ok('/runs prints clean refusal when no state files exist') : bad(`/runs no-state output: ${noStateOut?.stdout || noStateOut?.stderr}`);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
bad(`crashed: ${err?.stack || err}`);
|
|
160
|
+
} finally {
|
|
161
|
+
fx.cleanup();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
rep.finish('Integration (tooling — DevPipeline)');
|