freeharness 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +108 -0
- package/README.md +134 -0
- package/canonical/HARVEST.yaml +858 -0
- package/canonical/PROVENANCE.yaml +740 -0
- package/canonical/agents/README.md +95 -0
- package/canonical/agents/agentic-architect.md +18 -0
- package/canonical/agents/database-reviewer.md +41 -0
- package/canonical/agents/explorer-ai-engineer-analyst.md +65 -0
- package/canonical/agents/pr-reviewer.md +96 -0
- package/canonical/agents/python-reviewer.md +55 -0
- package/canonical/agents/report-formatter.md +19 -0
- package/canonical/agents/security-reviewer.md +53 -0
- package/canonical/agents/test-writer.md +34 -0
- package/canonical/commands/fh-context.md +37 -0
- package/canonical/evals/promptfoo.redteam.yaml +108 -0
- package/canonical/hooks/cost-prices.json +14 -0
- package/canonical/hooks/freeharness-auto-memory.mjs +158 -0
- package/canonical/hooks/freeharness-config-guard.mjs +162 -0
- package/canonical/hooks/freeharness-context-monitor.mjs +198 -0
- package/canonical/hooks/freeharness-converge.mjs +227 -0
- package/canonical/hooks/freeharness-cost-tracker.mjs +1027 -0
- package/canonical/hooks/freeharness-memory-guard.mjs +57 -0
- package/canonical/hooks/freeharness-output-pruner.mjs +333 -0
- package/canonical/hooks/freeharness-precompact.mjs +323 -0
- package/canonical/hooks/freeharness-repeat-tool.mjs +208 -0
- package/canonical/hooks/freeharness-security-guidance.mjs +164 -0
- package/canonical/hooks/freeharness-session-start +583 -0
- package/canonical/hooks/hooks.auto-memory.claude-code.json +16 -0
- package/canonical/hooks/hooks.claude-code.json +17 -0
- package/canonical/hooks/hooks.config-guard.claude-code.json +16 -0
- package/canonical/hooks/hooks.context-monitor.claude-code.json +15 -0
- package/canonical/hooks/hooks.converge.claude-code.json +15 -0
- package/canonical/hooks/hooks.cost-tracker.claude-code.json +37 -0
- package/canonical/hooks/hooks.memory-guard.claude-code.json +16 -0
- package/canonical/hooks/hooks.output-pruner.claude-code.json +16 -0
- package/canonical/hooks/hooks.precompact.claude-code.json +15 -0
- package/canonical/hooks/hooks.repeat-tool.claude-code.json +25 -0
- package/canonical/hooks/hooks.security-guidance.claude-code.json +16 -0
- package/canonical/mcp/servers.json +20 -0
- package/canonical/openspec/config.base.yaml +89 -0
- package/canonical/openspec/schema.spec-driven.yaml +277 -0
- package/canonical/openspec/templates/data-contract.md +36 -0
- package/canonical/openspec/templates/design.md +84 -0
- package/canonical/openspec/templates/eval-plan.md +33 -0
- package/canonical/openspec/templates/follow-ups.md +2 -0
- package/canonical/openspec/templates/proposal.md +45 -0
- package/canonical/openspec/templates/spec.md +42 -0
- package/canonical/openspec/templates/tasks.md +38 -0
- package/canonical/profiles/local-llamacpp.LOCAL-MODELS.md +47 -0
- package/canonical/profiles/local-llamacpp.json +27 -0
- package/canonical/profiles/local-ollama.json +20 -0
- package/canonical/profiles/mcp.json +91 -0
- package/canonical/rules/CLAUDE.md +78 -0
- package/canonical/rules/project-CLAUDE.template.md +62 -0
- package/canonical/rules/routing.md +20 -0
- package/canonical/rules/safety.md +41 -0
- package/canonical/settings/settings.cache-hygiene.claude-code.json +5 -0
- package/canonical/skills/agent-evaluation/SKILL.md +102 -0
- package/canonical/skills/agent-evaluation/assets/evaluation_report_template.md +206 -0
- package/canonical/skills/agent-evaluation/evals/cases.yaml +106 -0
- package/canonical/skills/agent-evaluation/references/dataset-preparation.md +351 -0
- package/canonical/skills/agent-evaluation/references/scorers-constraints.md +165 -0
- package/canonical/skills/agent-evaluation/references/scorers.md +431 -0
- package/canonical/skills/agent-evaluation/references/setup-guide.md +252 -0
- package/canonical/skills/agent-evaluation/references/tracing-integration.md +164 -0
- package/canonical/skills/agent-evaluation/references/troubleshooting.md +514 -0
- package/canonical/skills/agent-evaluation/scripts/analyze_results.py +462 -0
- package/canonical/skills/agent-evaluation/scripts/create_dataset_template.py +382 -0
- package/canonical/skills/agent-evaluation/scripts/list_datasets.py +273 -0
- package/canonical/skills/agent-evaluation/scripts/run_evaluation_template.py +309 -0
- package/canonical/skills/agent-evaluation/scripts/setup_mlflow.py +353 -0
- package/canonical/skills/agent-evaluation/scripts/utils/__init__.py +27 -0
- package/canonical/skills/agent-evaluation/scripts/utils/env_validation.py +123 -0
- package/canonical/skills/agent-evaluation/scripts/utils/tracing_utils.py +108 -0
- package/canonical/skills/agent-evaluation/scripts/validate_agent_tracing.py +148 -0
- package/canonical/skills/agent-evaluation/scripts/validate_auth.py +240 -0
- package/canonical/skills/agent-evaluation/scripts/validate_environment.py +185 -0
- package/canonical/skills/agent-evaluation/scripts/validate_tracing_runtime.py +318 -0
- package/canonical/skills/agentic-architecture/SKILL.md +41 -0
- package/canonical/skills/agentic-architecture/evals/cases.yaml +94 -0
- package/canonical/skills/agentic-architecture/reference/context-engineering.md +75 -0
- package/canonical/skills/agentic-architecture/reference/patterns.md +76 -0
- package/canonical/skills/agentic-architecture/reference/reliability-observability.md +94 -0
- package/canonical/skills/agentic-architecture/reference/review-framework.md +59 -0
- package/canonical/skills/code-review/SKILL.md +52 -0
- package/canonical/skills/code-review/evals/cases.yaml +197 -0
- package/canonical/skills/git-workflow/SKILL.md +41 -0
- package/canonical/skills/git-workflow/evals/cases.yaml +92 -0
- package/canonical/skills/local-code/SKILL.md +24 -0
- package/canonical/skills/local-code/evals/cases.yaml +85 -0
- package/canonical/skills/local-code/scripts/query_llama.py +68 -0
- package/canonical/skills/report-formatting/SKILL.md +62 -0
- package/canonical/skills/report-formatting/evals/cases.yaml +83 -0
- package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +135 -0
- package/canonical/skills/report-formatting/reference/HELPERS.md +120 -0
- package/canonical/skills/report-formatting/reference/PPTX.md +136 -0
- package/canonical/skills/report-formatting/reference/RELEASE.md +99 -0
- package/canonical/skills/report-formatting/reference/TEMPLATES.md +48 -0
- package/canonical/skills/report-formatting/reference/authoring.md +66 -0
- package/canonical/skills/report-formatting/scripts/architecture-template.md +271 -0
- package/canonical/skills/report-formatting/scripts/assets/claro-logo.png +0 -0
- package/canonical/skills/report-formatting/scripts/assets/claro-wordmark.svg +1 -0
- package/canonical/skills/report-formatting/scripts/build-architecture.js +57 -0
- package/canonical/skills/report-formatting/scripts/build-md-claro.js +224 -0
- package/canonical/skills/report-formatting/scripts/build-release.js +51 -0
- package/canonical/skills/report-formatting/scripts/check-env.js +58 -0
- package/canonical/skills/report-formatting/scripts/docmodel.js +283 -0
- package/canonical/skills/report-formatting/scripts/example-build.js +119 -0
- package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +195 -0
- package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +114 -0
- package/canonical/skills/report-formatting/scripts/package.json +17 -0
- package/canonical/skills/report-formatting/scripts/pptx/example_build_StatusReport_pptx.py +223 -0
- package/canonical/skills/report-formatting/scripts/pptx/example_build_WeeklyStatus_pptx.py +245 -0
- package/canonical/skills/report-formatting/scripts/pptx/pptx_office.py +144 -0
- package/canonical/skills/report-formatting/scripts/pptx/requirements.txt +7 -0
- package/canonical/skills/report-formatting/scripts/pptx/status_report_blocks.py +193 -0
- package/canonical/skills/report-formatting/scripts/pptx/theme_claro_pptx.py +473 -0
- package/canonical/skills/report-formatting/scripts/release-template.md +80 -0
- package/canonical/skills/report-formatting/scripts/render-mermaid.js +105 -0
- package/canonical/skills/report-formatting/scripts/theme-claro.js +403 -0
- package/canonical/skills/subagent-driven-development/SKILL.md +60 -0
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +208 -0
- package/canonical/skills/subagent-driven-development/reference/brief.md +91 -0
- package/canonical/skills/subagent-driven-development/reference/handoff.md +118 -0
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +850 -0
- package/canonical/skills/systematic-debugging/SKILL.md +44 -0
- package/canonical/skills/systematic-debugging/evals/cases.yaml +90 -0
- package/canonical/skills/test-driven-development/SKILL.md +42 -0
- package/canonical/skills/test-driven-development/evals/cases.yaml +86 -0
- package/canonical/skills/test-driven-development/reference/writing-good-tests.md +52 -0
- package/canonical/skills/verification-before-completion/SKILL.md +53 -0
- package/canonical/skills/verification-before-completion/evals/cases.yaml +107 -0
- package/dist/adapters/content/language-lint.d.ts +46 -0
- package/dist/adapters/content/language-lint.js +191 -0
- package/dist/adapters/harness/claude-code.d.ts +57 -0
- package/dist/adapters/harness/claude-code.js +385 -0
- package/dist/adapters/harness/index.d.ts +3 -0
- package/dist/adapters/harness/index.js +17 -0
- package/dist/adapters/harness/opencode.d.ts +7 -0
- package/dist/adapters/harness/opencode.js +166 -0
- package/dist/adapters/harness/plugin.d.ts +22 -0
- package/dist/adapters/harness/plugin.js +257 -0
- package/dist/adapters/memory/doctor.d.ts +480 -0
- package/dist/adapters/memory/doctor.js +1217 -0
- package/dist/adapters/memory/vendored.d.ts +150 -0
- package/dist/adapters/memory/vendored.js +487 -0
- package/dist/adapters/package/node.d.ts +9 -0
- package/dist/adapters/package/node.js +20 -0
- package/dist/adapters/prompt/readline.d.ts +19 -0
- package/dist/adapters/prompt/readline.js +53 -0
- package/dist/adapters/sync/diff.d.ts +43 -0
- package/dist/adapters/sync/diff.js +380 -0
- package/dist/cli/cleanup.d.ts +86 -0
- package/dist/cli/cleanup.js +382 -0
- package/dist/cli/cost.d.ts +304 -0
- package/dist/cli/cost.js +501 -0
- package/dist/cli/doctor.d.ts +205 -0
- package/dist/cli/doctor.js +654 -0
- package/dist/cli/executor.d.ts +5 -0
- package/dist/cli/executor.js +48 -0
- package/dist/cli/plugin-validate.d.ts +37 -0
- package/dist/cli/plugin-validate.js +256 -0
- package/dist/cli/prefix.d.ts +68 -0
- package/dist/cli/prefix.js +218 -0
- package/dist/cli/profile.d.ts +170 -0
- package/dist/cli/profile.js +285 -0
- package/dist/cli/runner.d.ts +10 -0
- package/dist/cli/runner.js +27 -0
- package/dist/cli/uninstall.d.ts +65 -0
- package/dist/cli/uninstall.js +147 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +989 -0
- package/dist/infra/dogfood.d.ts +31 -0
- package/dist/infra/dogfood.js +51 -0
- package/dist/infra/fsutil.d.ts +40 -0
- package/dist/infra/fsutil.js +143 -0
- package/dist/infra/manifest.d.ts +76 -0
- package/dist/infra/manifest.js +92 -0
- package/dist/infra/paths.d.ts +19 -0
- package/dist/infra/paths.js +74 -0
- package/dist/infra/routing.d.ts +4 -0
- package/dist/infra/routing.js +7 -0
- package/node_modules/@freeharness/core/dist/context.d.ts +30 -0
- package/node_modules/@freeharness/core/dist/context.js +1 -0
- package/node_modules/@freeharness/core/dist/index.d.ts +5 -0
- package/node_modules/@freeharness/core/dist/index.js +6 -0
- package/node_modules/@freeharness/core/dist/planner.d.ts +13 -0
- package/node_modules/@freeharness/core/dist/planner.js +18 -0
- package/node_modules/@freeharness/core/dist/ports.d.ts +90 -0
- package/node_modules/@freeharness/core/dist/ports.js +1 -0
- package/node_modules/@freeharness/core/dist/registry.d.ts +41 -0
- package/node_modules/@freeharness/core/dist/registry.js +124 -0
- package/node_modules/@freeharness/core/dist/tiers.d.ts +30 -0
- package/node_modules/@freeharness/core/dist/tiers.js +99 -0
- package/node_modules/@freeharness/core/package.json +34 -0
- package/node_modules/@freeharness/sdd/PROVENANCE.yaml +175 -0
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.d.ts +2 -0
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +166 -0
- package/node_modules/@freeharness/sdd/dist/src/index.d.ts +14 -0
- package/node_modules/@freeharness/sdd/dist/src/index.js +11 -0
- package/node_modules/@freeharness/sdd/dist/src/init.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/src/init.js +190 -0
- package/node_modules/@freeharness/sdd/dist/src/root-resolver.d.ts +8 -0
- package/node_modules/@freeharness/sdd/dist/src/root-resolver.js +32 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +81 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +483 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/context.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/context.js +48 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +57 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +279 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +67 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +460 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +5 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/list.js +95 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +150 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/schemas.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/schemas.js +23 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +56 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +140 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +8 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/status.js +85 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +229 -0
- package/node_modules/@freeharness/sdd/dist/src/validate-change.d.ts +40 -0
- package/node_modules/@freeharness/sdd/dist/src/validate-change.js +114 -0
- package/node_modules/@freeharness/sdd/dist/src/validate.d.ts +24 -0
- package/node_modules/@freeharness/sdd/dist/src/validate.js +72 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.d.ts +130 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +981 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-metadata.d.ts +46 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-metadata.js +128 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +26 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/file-system.d.ts +50 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/file-system.js +394 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/global-config.d.ts +15 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/global-config.js +49 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/graph.d.ts +71 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/graph.js +164 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +11 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +21 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/instruction-loader.d.ts +102 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/instruction-loader.js +230 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +18 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +138 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/resolver.d.ts +82 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/resolver.js +333 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/schema.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/schema.js +110 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/state.d.ts +11 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/state.js +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.d.ts +33 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +149 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +49 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/claude.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/claude.js +31 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/index.d.ts +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/index.js +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/opencode.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/opencode.js +26 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/generator.d.ts +28 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/generator.js +41 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/index.d.ts +23 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/index.js +25 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/invocation.d.ts +72 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/invocation.js +76 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/registry.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/registry.js +53 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/types.d.ts +62 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/types.js +7 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/yaml.d.ts +35 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/yaml.js +47 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-references.d.ts +33 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-references.js +55 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/allowed-tools.d.ts +11 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/allowed-tools.js +11 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/skill-generation.d.ts +48 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/skill-generation.js +99 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/skill-templates.d.ts +19 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/skill-templates.js +18 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/types.d.ts +18 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/types.js +4 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.d.ts +19 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +207 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +409 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/bulk-archive-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/bulk-archive-change.js +673 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/continue-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/continue-change.js +230 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +550 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/feedback.d.ts +8 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/feedback.js +107 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/ff-change.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/ff-change.js +228 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/new-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/new-change.js +147 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/onboard.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/onboard.js +571 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +322 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/store-selection.d.ts +7 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/store-selection.js +7 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +518 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +176 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/verify-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/verify-change.js +344 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +54 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/delta-resolution.d.ts +35 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/delta-resolution.js +171 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +27 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +210 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/code-fence.d.ts +14 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/code-fence.js +48 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.d.ts +22 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +186 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +65 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +409 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +55 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +98 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.d.ts +8 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +77 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.d.ts +44 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +116 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/base.schema.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/base.schema.js +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/change.schema.d.ts +72 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/change.schema.js +30 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/index.d.ts +3 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/index.js +3 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/spec.schema.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/spec.schema.js +14 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +114 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +45 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +105 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-numbering.d.ts +15 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-numbering.js +56 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/types.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/types.js +1 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +94 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +756 -0
- package/node_modules/@freeharness/sdd/package.json +57 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/LICENSE.upstream +22 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +232 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/README.md +19 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +187 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +181 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +338 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +117 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +342 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +116 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +76 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +560 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +162 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +261 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +90 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +174 -0
- package/package.json +70 -0
- package/provenance/canonical.PROVENANCE.yaml +740 -0
- package/provenance/claude-mem.LICENSE.upstream +202 -0
- package/provenance/memory.PROVENANCE.yaml +399 -0
- package/provenance/sdd.PROVENANCE.yaml +175 -0
- package/vendor-licenses/caveman.LICENSE +27 -0
- package/vendor-licenses/deepseek-harness.LICENSE +21 -0
- package/vendor-licenses/ecc.LICENSE +21 -0
- package/vendor-licenses/pi.LICENSE +21 -0
- package/vendor-licenses/superpowers.LICENSE +21 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Flatten results into a deduplicated action list (same command+cwd runs once).
|
|
2
|
+
export function collectActions(results) {
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const r of results) {
|
|
6
|
+
for (const a of r.actions) {
|
|
7
|
+
const key = `${a.cwd ?? ""}::${a.command}`;
|
|
8
|
+
if (seen.has(key))
|
|
9
|
+
continue;
|
|
10
|
+
seen.add(key);
|
|
11
|
+
out.push(a);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
// Render the planned writes/notes per capability plus the consolidated actions.
|
|
17
|
+
export function printPlan(results, actions, dryRun) {
|
|
18
|
+
const tag = dryRun ? "[dry-run] " : "";
|
|
19
|
+
for (const r of results) {
|
|
20
|
+
if (r.written.length === 0 && r.notes.length === 0)
|
|
21
|
+
continue;
|
|
22
|
+
console.log(`\n${tag}${r.target} :: ${r.capabilityId}`);
|
|
23
|
+
for (const f of r.written)
|
|
24
|
+
console.log(` write: ${f}`);
|
|
25
|
+
for (const n of r.notes)
|
|
26
|
+
console.log(` note: ${n}`);
|
|
27
|
+
}
|
|
28
|
+
if (actions.length > 0) {
|
|
29
|
+
console.log(`\n${tag}actions:`);
|
|
30
|
+
for (const a of actions) {
|
|
31
|
+
const cwd = a.cwd ? ` [cwd: ${a.cwd}]` : "";
|
|
32
|
+
console.log(` - ${a.description}${a.optional ? " (optional)" : ""}${cwd}\n $ ${a.command}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Run the actions in order. Optional actions warn on failure; required ones abort.
|
|
37
|
+
export async function runActions(actions, pkg) {
|
|
38
|
+
for (const a of actions) {
|
|
39
|
+
console.log(`\n$ ${a.command}${a.cwd ? ` (in ${a.cwd})` : ""}`);
|
|
40
|
+
const r = await pkg.exec(a);
|
|
41
|
+
if (!r.ok) {
|
|
42
|
+
if (a.optional)
|
|
43
|
+
console.warn(`WARN optional action failed (exit ${r.code}): ${a.description}`);
|
|
44
|
+
else
|
|
45
|
+
throw new Error(`Action failed (exit ${r.code}): ${a.description}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Result of validating one plugin directory. */
|
|
2
|
+
export interface PluginValidation {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
warnings: string[];
|
|
6
|
+
/** Counts for the summary line. */
|
|
7
|
+
counts: {
|
|
8
|
+
agents: number;
|
|
9
|
+
skills: number;
|
|
10
|
+
hookCommands: number;
|
|
11
|
+
mcpServers: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Hook events Claude Code dispatches, per the lifecycle table of
|
|
16
|
+
* https://code.claude.com/docs/en/hooks (a typo here is a silent no-op, so it is an error).
|
|
17
|
+
* `scripts/simulate-session.mjs` checks every emitted event against the same list.
|
|
18
|
+
*/
|
|
19
|
+
export declare const HOOK_EVENTS: Set<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Validates a plugin directory (and, when present, the marketplace manifest in its parent).
|
|
22
|
+
* @param dir The plugin directory (holds `.claude-plugin/plugin.json`).
|
|
23
|
+
* @param opts `strict`: warnings count as errors; `marketplace`: explicit marketplace.json path (default: `<dir>/../.claude-plugin/marketplace.json` when it exists).
|
|
24
|
+
* @returns Errors, warnings and counts.
|
|
25
|
+
*/
|
|
26
|
+
export declare function validatePluginDir(dir: string, opts?: {
|
|
27
|
+
strict?: boolean;
|
|
28
|
+
marketplace?: string;
|
|
29
|
+
}): PluginValidation;
|
|
30
|
+
/**
|
|
31
|
+
* Renders a validation as printable lines.
|
|
32
|
+
* @param dir Directory validated.
|
|
33
|
+
* @param v Result.
|
|
34
|
+
* @param strict Whether warnings fail.
|
|
35
|
+
* @returns Lines.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatValidation(dir: string, v: PluginValidation, strict: boolean): string[];
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Local schema check of an emitted Claude Code plugin directory (v4 P5.1): the manifest's
|
|
2
|
+
// required fields, the hooks.json shape, every ${CLAUDE_PLUGIN_ROOT} path resolving to a file,
|
|
3
|
+
// .mcp.json, agent and skill front matter, and the sibling marketplace manifest. Mirrors what
|
|
4
|
+
// `claude plugin validate --strict` rejects so the distribution test and a CI runner without
|
|
5
|
+
// the `claude` CLI catch the same classes of defect. Pure functions over the file system;
|
|
6
|
+
// index.ts prints and sets the exit code.
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
/**
|
|
10
|
+
* Hook events Claude Code dispatches, per the lifecycle table of
|
|
11
|
+
* https://code.claude.com/docs/en/hooks (a typo here is a silent no-op, so it is an error).
|
|
12
|
+
* `scripts/simulate-session.mjs` checks every emitted event against the same list.
|
|
13
|
+
*/
|
|
14
|
+
export const HOOK_EVENTS = new Set([
|
|
15
|
+
"SessionStart", "Setup", "UserPromptSubmit", "UserPromptExpansion", "PreToolUse", "PermissionRequest", "PermissionDenied", "PostToolUse", "PostToolUseFailure", "PostToolBatch",
|
|
16
|
+
"Notification", "MessageDisplay", "SubagentStart", "SubagentStop", "TaskCreated", "TaskCompleted", "Stop", "StopFailure", "TeammateIdle", "InstructionsLoaded", "ConfigChange",
|
|
17
|
+
"CwdChanged", "DirectoryAdded", "FileChanged", "WorktreeCreate", "WorktreeRemove", "PreCompact", "PostCompact", "PreModelSwitch", "PostModelSwitch", "Elicitation",
|
|
18
|
+
"ElicitationResult", "SessionEnd",
|
|
19
|
+
]);
|
|
20
|
+
/** Plugin names are kebab-case (marketplace and `claude plugin install <name>@<market>`). */
|
|
21
|
+
const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
22
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/;
|
|
23
|
+
/**
|
|
24
|
+
* Reads and parses a JSON file; a parse failure becomes one error.
|
|
25
|
+
* @param file Path.
|
|
26
|
+
* @param errors Sink.
|
|
27
|
+
* @returns Parsed object or undefined.
|
|
28
|
+
*/
|
|
29
|
+
function readJson(file, errors) {
|
|
30
|
+
try {
|
|
31
|
+
const v = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
32
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
33
|
+
errors.push(`${file}: not a JSON object`);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return v;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
errors.push(`${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Front matter of a markdown file (`name`, `description`), or undefined when absent.
|
|
45
|
+
* @param file Path.
|
|
46
|
+
* @returns Key -> first-line value.
|
|
47
|
+
*/
|
|
48
|
+
function frontMatter(file) {
|
|
49
|
+
const text = fs.readFileSync(file, "utf8");
|
|
50
|
+
const m = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
51
|
+
if (!m)
|
|
52
|
+
return undefined;
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const line of m[1].split("\n")) {
|
|
55
|
+
const kv = /^([\w-]+):\s*(.*)$/.exec(line);
|
|
56
|
+
if (kv)
|
|
57
|
+
out[kv[1]] = kv[2].trim();
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validates a plugin directory (and, when present, the marketplace manifest in its parent).
|
|
63
|
+
* @param dir The plugin directory (holds `.claude-plugin/plugin.json`).
|
|
64
|
+
* @param opts `strict`: warnings count as errors; `marketplace`: explicit marketplace.json path (default: `<dir>/../.claude-plugin/marketplace.json` when it exists).
|
|
65
|
+
* @returns Errors, warnings and counts.
|
|
66
|
+
*/
|
|
67
|
+
export function validatePluginDir(dir, opts = {}) {
|
|
68
|
+
const errors = [];
|
|
69
|
+
const warnings = [];
|
|
70
|
+
const counts = { agents: 0, skills: 0, hookCommands: 0, mcpServers: 0 };
|
|
71
|
+
const root = path.resolve(dir);
|
|
72
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
73
|
+
return { ok: false, errors: [`${root}: not a directory`], warnings, counts };
|
|
74
|
+
}
|
|
75
|
+
// Manifest: name, version, description required; author/keywords typed when present.
|
|
76
|
+
const manifestFile = path.join(root, ".claude-plugin", "plugin.json");
|
|
77
|
+
if (!fs.existsSync(manifestFile))
|
|
78
|
+
errors.push(`${manifestFile}: missing (required: .claude-plugin/plugin.json)`);
|
|
79
|
+
const manifest = fs.existsSync(manifestFile) ? readJson(manifestFile, errors) : undefined;
|
|
80
|
+
if (manifest) {
|
|
81
|
+
for (const key of ["name", "version", "description"]) {
|
|
82
|
+
if (typeof manifest[key] !== "string" || !manifest[key].trim())
|
|
83
|
+
errors.push(`plugin.json: required field '${key}' missing or not a non-empty string`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof manifest.name === "string" && !KEBAB.test(manifest.name))
|
|
86
|
+
errors.push(`plugin.json: name '${manifest.name}' is not kebab-case`);
|
|
87
|
+
if (typeof manifest.version === "string" && !SEMVER.test(manifest.version))
|
|
88
|
+
errors.push(`plugin.json: version '${manifest.version}' is not semver`);
|
|
89
|
+
if (manifest.author !== undefined && (typeof manifest.author !== "object" || manifest.author === null || typeof manifest.author.name !== "string"))
|
|
90
|
+
errors.push("plugin.json: author must be an object with a name");
|
|
91
|
+
if (manifest.keywords !== undefined && (!Array.isArray(manifest.keywords) || manifest.keywords.some((k) => typeof k !== "string")))
|
|
92
|
+
errors.push("plugin.json: keywords must be an array of strings");
|
|
93
|
+
for (const key of ["homepage", "repository", "license"])
|
|
94
|
+
if (manifest[key] !== undefined && typeof manifest[key] !== "string")
|
|
95
|
+
errors.push(`plugin.json: ${key} must be a string`);
|
|
96
|
+
}
|
|
97
|
+
// Agents: one .md per agent with name + description front matter.
|
|
98
|
+
const agentsDir = path.join(root, "agents");
|
|
99
|
+
if (fs.existsSync(agentsDir)) {
|
|
100
|
+
for (const f of fs.readdirSync(agentsDir).filter((n) => n.endsWith(".md")).sort()) {
|
|
101
|
+
counts.agents++;
|
|
102
|
+
const fm = frontMatter(path.join(agentsDir, f));
|
|
103
|
+
if (!fm)
|
|
104
|
+
errors.push(`agents/${f}: front matter missing`);
|
|
105
|
+
else {
|
|
106
|
+
if (!fm.name)
|
|
107
|
+
errors.push(`agents/${f}: front matter 'name' missing`);
|
|
108
|
+
if (!fm.description)
|
|
109
|
+
errors.push(`agents/${f}: front matter 'description' missing`);
|
|
110
|
+
if (fm.model && !/^(opus|sonnet|haiku|inherit|[\w.-]+)$/.test(fm.model))
|
|
111
|
+
errors.push(`agents/${f}: model '${fm.model}' invalid`);
|
|
112
|
+
// Key presence, not truthiness: `memory:` with no value declares the key with an unusable value.
|
|
113
|
+
if ("memory" in fm && !["user", "project", "local"].includes(fm.memory))
|
|
114
|
+
errors.push(`agents/${f}: memory '${fm.memory}' invalid`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Skills: <name>/SKILL.md with name + description.
|
|
119
|
+
const skillsDir = path.join(root, "skills");
|
|
120
|
+
if (fs.existsSync(skillsDir)) {
|
|
121
|
+
for (const e of fs.readdirSync(skillsDir, { withFileTypes: true }).filter((x) => x.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
122
|
+
const skill = path.join(skillsDir, e.name, "SKILL.md");
|
|
123
|
+
if (!fs.existsSync(skill)) {
|
|
124
|
+
warnings.push(`skills/${e.name}: no SKILL.md (directory ignored by Claude Code)`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
counts.skills++;
|
|
128
|
+
const fm = frontMatter(skill);
|
|
129
|
+
if (!fm)
|
|
130
|
+
errors.push(`skills/${e.name}/SKILL.md: front matter missing`);
|
|
131
|
+
else {
|
|
132
|
+
if (!fm.name)
|
|
133
|
+
errors.push(`skills/${e.name}/SKILL.md: front matter 'name' missing`);
|
|
134
|
+
else if (fm.name !== e.name)
|
|
135
|
+
warnings.push(`skills/${e.name}/SKILL.md: front matter name '${fm.name}' differs from the directory name`);
|
|
136
|
+
if (!fm.description)
|
|
137
|
+
errors.push(`skills/${e.name}/SKILL.md: front matter 'description' missing`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Hooks: {description?, hooks: {Event: [{matcher?, hooks: [{type: "command", command}]}]}}.
|
|
142
|
+
const hooksFile = path.join(root, "hooks", "hooks.json");
|
|
143
|
+
if (fs.existsSync(hooksFile)) {
|
|
144
|
+
const doc = readJson(hooksFile, errors);
|
|
145
|
+
const events = doc?.hooks;
|
|
146
|
+
if (doc && (!events || typeof events !== "object" || Array.isArray(events)))
|
|
147
|
+
errors.push("hooks/hooks.json: 'hooks' must be an object keyed by event");
|
|
148
|
+
else if (events) {
|
|
149
|
+
for (const [event, entries] of Object.entries(events)) {
|
|
150
|
+
if (!HOOK_EVENTS.has(event))
|
|
151
|
+
errors.push(`hooks/hooks.json: unknown event '${event}'`);
|
|
152
|
+
if (!Array.isArray(entries)) {
|
|
153
|
+
errors.push(`hooks/hooks.json: ${event} must be an array of entries`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
entries.forEach((entry, i) => {
|
|
157
|
+
const e = entry;
|
|
158
|
+
if (e.matcher !== undefined && typeof e.matcher !== "string")
|
|
159
|
+
errors.push(`hooks/hooks.json: ${event}[${i}].matcher must be a string`);
|
|
160
|
+
if (!Array.isArray(e.hooks) || e.hooks.length === 0) {
|
|
161
|
+
errors.push(`hooks/hooks.json: ${event}[${i}].hooks must be a non-empty array`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
e.hooks.forEach((h, j) => {
|
|
165
|
+
const hook = h;
|
|
166
|
+
counts.hookCommands++;
|
|
167
|
+
if (!["command", "prompt", "agent", "http", "mcp_tool"].includes(String(hook.type)))
|
|
168
|
+
errors.push(`hooks/hooks.json: ${event}[${i}].hooks[${j}].type must be command|http|mcp_tool|prompt|agent`);
|
|
169
|
+
if (hook.type === "command" && (typeof hook.command !== "string" || !hook.command.trim()))
|
|
170
|
+
errors.push(`hooks/hooks.json: ${event}[${i}].hooks[${j}].command missing`);
|
|
171
|
+
if (hook.timeout !== undefined && typeof hook.timeout !== "number")
|
|
172
|
+
errors.push(`hooks/hooks.json: ${event}[${i}].hooks[${j}].timeout must be a number`);
|
|
173
|
+
if (typeof hook.command === "string") {
|
|
174
|
+
for (const m of hook.command.matchAll(/\$\{CLAUDE_PLUGIN_ROOT\}\/([^\s"']+)/g)) {
|
|
175
|
+
const target = path.join(root, m[1]);
|
|
176
|
+
if (!fs.existsSync(target))
|
|
177
|
+
errors.push(`hooks/hooks.json: ${event}[${i}].hooks[${j}] references \${CLAUDE_PLUGIN_ROOT}/${m[1]} which does not exist`);
|
|
178
|
+
}
|
|
179
|
+
if (!hook.command.includes("${CLAUDE_PLUGIN_ROOT}") && /(^|\s)\/\S+/.test(hook.command))
|
|
180
|
+
warnings.push(`hooks/hooks.json: ${event}[${i}].hooks[${j}] uses an absolute path instead of \${CLAUDE_PLUGIN_ROOT}`);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// .mcp.json: {mcpServers: {name: {command|url}}}.
|
|
188
|
+
const mcpFile = path.join(root, ".mcp.json");
|
|
189
|
+
if (fs.existsSync(mcpFile)) {
|
|
190
|
+
const doc = readJson(mcpFile, errors);
|
|
191
|
+
const servers = doc?.mcpServers;
|
|
192
|
+
if (doc && (!servers || typeof servers !== "object" || Array.isArray(servers)))
|
|
193
|
+
errors.push(".mcp.json: 'mcpServers' must be an object");
|
|
194
|
+
else if (servers) {
|
|
195
|
+
for (const [name, s] of Object.entries(servers)) {
|
|
196
|
+
counts.mcpServers++;
|
|
197
|
+
if (typeof s.command !== "string" && typeof s.url !== "string")
|
|
198
|
+
errors.push(`.mcp.json: server '${name}' needs a command or a url`);
|
|
199
|
+
if (s.args !== undefined && !Array.isArray(s.args))
|
|
200
|
+
errors.push(`.mcp.json: server '${name}' args must be an array`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Marketplace (sibling .claude-plugin/marketplace.json): name, owner, plugins[] with a
|
|
205
|
+
// source that resolves to a plugin dir.
|
|
206
|
+
const marketFile = opts.marketplace ?? path.join(path.dirname(root), ".claude-plugin", "marketplace.json");
|
|
207
|
+
if (fs.existsSync(marketFile)) {
|
|
208
|
+
const m = readJson(marketFile, errors);
|
|
209
|
+
if (m) {
|
|
210
|
+
if (typeof m.name !== "string" || !KEBAB.test(m.name))
|
|
211
|
+
errors.push("marketplace.json: 'name' missing or not kebab-case");
|
|
212
|
+
if (!m.owner || typeof m.owner !== "object" || typeof m.owner.name !== "string")
|
|
213
|
+
errors.push("marketplace.json: 'owner.name' missing");
|
|
214
|
+
if (!Array.isArray(m.plugins) || m.plugins.length === 0)
|
|
215
|
+
errors.push("marketplace.json: 'plugins' must be a non-empty array");
|
|
216
|
+
else {
|
|
217
|
+
m.plugins.forEach((p, i) => {
|
|
218
|
+
if (typeof p.name !== "string" || !KEBAB.test(p.name))
|
|
219
|
+
errors.push(`marketplace.json: plugins[${i}].name missing or not kebab-case`);
|
|
220
|
+
if (typeof p.source === "string") {
|
|
221
|
+
if (p.source.startsWith("./") || p.source.startsWith("../")) {
|
|
222
|
+
const target = path.resolve(path.dirname(marketFile), "..", p.source);
|
|
223
|
+
if (!fs.existsSync(path.join(target, ".claude-plugin", "plugin.json")))
|
|
224
|
+
errors.push(`marketplace.json: plugins[${i}].source '${p.source}' has no .claude-plugin/plugin.json`);
|
|
225
|
+
else if (manifest && p.name === manifest.name && p.version !== undefined && p.version !== manifest.version)
|
|
226
|
+
warnings.push(`marketplace.json: plugins[${i}].version ${String(p.version)} differs from plugin.json ${String(manifest.version)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (!p.source || typeof p.source !== "object")
|
|
230
|
+
errors.push(`marketplace.json: plugins[${i}].source must be a relative path or a source object`);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (!opts.marketplace) {
|
|
236
|
+
warnings.push(`no marketplace manifest at ${marketFile} (install with --plugin-dir, or add one for \`claude plugin marketplace add\`)`);
|
|
237
|
+
}
|
|
238
|
+
const ok = errors.length === 0 && (!opts.strict || warnings.length === 0);
|
|
239
|
+
return { ok, errors, warnings, counts };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Renders a validation as printable lines.
|
|
243
|
+
* @param dir Directory validated.
|
|
244
|
+
* @param v Result.
|
|
245
|
+
* @param strict Whether warnings fail.
|
|
246
|
+
* @returns Lines.
|
|
247
|
+
*/
|
|
248
|
+
export function formatValidation(dir, v, strict) {
|
|
249
|
+
const lines = [`plugin validate${strict ? " --strict" : ""}: ${dir}`];
|
|
250
|
+
for (const e of v.errors)
|
|
251
|
+
lines.push(` ERROR ${e}`);
|
|
252
|
+
for (const w of v.warnings)
|
|
253
|
+
lines.push(` WARN ${w}`);
|
|
254
|
+
lines.push(` ${v.ok ? "OK " : "FAIL"} ${v.counts.agents} agents, ${v.counts.skills} skills, ${v.counts.hookCommands} hook commands, ${v.counts.mcpServers} MCP servers; ${v.errors.length} error(s), ${v.warnings.length} warning(s)${strict && v.warnings.length > 0 ? " (strict: warnings fail)" : ""}`);
|
|
255
|
+
return lines;
|
|
256
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Default prefix budget (`FREEHARNESS_PREFIX_BUDGET` or `--budget` override). */
|
|
2
|
+
export declare const DEFAULT_PREFIX_BUDGET = 12000;
|
|
3
|
+
/** Line limit for a CLAUDE.md (global or project). */
|
|
4
|
+
export declare const CLAUDE_MD_LINE_LIMIT = 200;
|
|
5
|
+
/** One prefix contributor. */
|
|
6
|
+
export interface PrefixItem {
|
|
7
|
+
/** Absolute file (or a `mcp:` pseudo path). */
|
|
8
|
+
source: string;
|
|
9
|
+
/** rules | agent description | skill description | mcp servers | session-start. */
|
|
10
|
+
kind: string;
|
|
11
|
+
words: number;
|
|
12
|
+
tokens: number;
|
|
13
|
+
}
|
|
14
|
+
/** The prefix estimate. */
|
|
15
|
+
export interface PrefixEstimate {
|
|
16
|
+
items: PrefixItem[];
|
|
17
|
+
tokens: number;
|
|
18
|
+
budget: number;
|
|
19
|
+
ok: boolean;
|
|
20
|
+
/** Session-start routing injection: apart from `items`, `tokens` and `ok`. */
|
|
21
|
+
injection: PrefixItem;
|
|
22
|
+
}
|
|
23
|
+
/** ~1.3 tokens per word of English prose: the rule of thumb used everywhere here. */
|
|
24
|
+
export declare function estimateTokens(text: string): {
|
|
25
|
+
words: number;
|
|
26
|
+
tokens: number;
|
|
27
|
+
};
|
|
28
|
+
/** A front matter `description:` value (single-line or folded until the next key), CRLF-tolerant. */
|
|
29
|
+
export declare function frontMatterDescription(text: string): string;
|
|
30
|
+
/** True when SKILL.md sets `disable-model-invocation: true` (excluded from the prefix). */
|
|
31
|
+
export declare function isModelDisabled(text: string): boolean;
|
|
32
|
+
/** Where Claude Code stores an MCP server: the three `claude mcp add --scope` values. */
|
|
33
|
+
export type McpScope = "user" | "local" | "project";
|
|
34
|
+
/** One registered MCP server with the scope it lives in. */
|
|
35
|
+
export interface McpServerEntry {
|
|
36
|
+
name: string;
|
|
37
|
+
scope: McpScope;
|
|
38
|
+
/** The file that declares it (`~/.claude.json` or `<project>/.mcp.json`). */
|
|
39
|
+
file: string;
|
|
40
|
+
/** Local: the keyed project path; project: the project root. */
|
|
41
|
+
projectPath?: string;
|
|
42
|
+
}
|
|
43
|
+
/** `~/.claude.json`, or `CLAUDE_CONFIG_DIR`'s copy when that override is set and present. */
|
|
44
|
+
export declare function claudeJsonPath(home: string, env?: NodeJS.ProcessEnv): string;
|
|
45
|
+
/** MCP servers per scope: user (`.claude.json` top level), local (its `projects[<path>]`), project (`<projectRoot>/.mcp.json`). */
|
|
46
|
+
export declare function mcpServerEntries(home: string, projectRoot: string): McpServerEntry[];
|
|
47
|
+
/** The `claude mcp remove` invocation; local/project scope must run from the entry's project dir. */
|
|
48
|
+
export declare function mcpRemoveCommand(e: McpServerEntry): string;
|
|
49
|
+
/** Deduplicated MCP server names across every scope (the estimate's tool-count proxy). */
|
|
50
|
+
export declare function mcpServerNames(home: string, projectRoot: string): string[];
|
|
51
|
+
/** Hook commands registered in a settings.json (0 when absent or malformed). */
|
|
52
|
+
export declare function hookCount(file: string): number;
|
|
53
|
+
/** Estimates the prefix: rules, agent/skill descriptions under `claudeDir`, MCP server names. */
|
|
54
|
+
export declare function estimatePrefix(claudeDir: string, rulesFiles: string[], mcp: string[], budget?: number): PrefixEstimate;
|
|
55
|
+
/** Settings keys whose project-scope override differs from user scope (cache invalidators). */
|
|
56
|
+
export interface Invalidator {
|
|
57
|
+
key: string;
|
|
58
|
+
project: unknown;
|
|
59
|
+
user: unknown;
|
|
60
|
+
}
|
|
61
|
+
/** Reads a settings.json (empty object when absent or malformed). */
|
|
62
|
+
export declare function readSettings(file: string): Record<string, unknown>;
|
|
63
|
+
/** Project-scope `model`/`effortLevel`/`permissions.deny` overrides that invalidate the prompt cache. */
|
|
64
|
+
export declare function cacheInvalidators(userSettings: Record<string, unknown>, projectSettings: Record<string, unknown>): Invalidator[];
|
|
65
|
+
/** Missing prompt-cache TTL keys (empty when both are present). */
|
|
66
|
+
export declare function missingTtlKeys(settings: Record<string, unknown>): string[];
|
|
67
|
+
/** Line count of a file (0 when absent). */
|
|
68
|
+
export declare function lineCount(file: string): number;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Prefix estimate: tokens the harness puts in front of every request, the session-start
|
|
2
|
+
// injection counted apart, and the cache-hygiene facts `doctor --context-budget` and
|
|
3
|
+
// `cost prefix` report. Pure functions over the file system; index.ts prints.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { frontmatterBody } from "@freeharness/core";
|
|
7
|
+
import { ROUTING_FILE } from "../infra/routing.js";
|
|
8
|
+
/** Default prefix budget (`FREEHARNESS_PREFIX_BUDGET` or `--budget` override). */
|
|
9
|
+
export const DEFAULT_PREFIX_BUDGET = 12_000;
|
|
10
|
+
/** Line limit for a CLAUDE.md (global or project). */
|
|
11
|
+
export const CLAUDE_MD_LINE_LIMIT = 200;
|
|
12
|
+
/** ~1.3 tokens per word of English prose: the rule of thumb used everywhere here. */
|
|
13
|
+
export function estimateTokens(text) {
|
|
14
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
15
|
+
return { words, tokens: Math.round(words * 1.3) };
|
|
16
|
+
}
|
|
17
|
+
/** A front matter `description:` value (single-line or folded until the next key), CRLF-tolerant. */
|
|
18
|
+
export function frontMatterDescription(text) {
|
|
19
|
+
const body = frontmatterBody(text);
|
|
20
|
+
if (body === undefined)
|
|
21
|
+
return "";
|
|
22
|
+
const lines = body.split(/\r?\n/);
|
|
23
|
+
const out = [];
|
|
24
|
+
let inDesc = false;
|
|
25
|
+
for (const l of lines) {
|
|
26
|
+
const m = /^description:\s*(.*)$/.exec(l);
|
|
27
|
+
if (m) {
|
|
28
|
+
inDesc = true;
|
|
29
|
+
out.push(m[1].replace(/^[>|]-?\s*$/, ""));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (inDesc) {
|
|
33
|
+
if (/^\S/.test(l))
|
|
34
|
+
break;
|
|
35
|
+
out.push(l.trim());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out.join(" ").trim();
|
|
39
|
+
}
|
|
40
|
+
/** True when SKILL.md sets `disable-model-invocation: true` (excluded from the prefix). */
|
|
41
|
+
export function isModelDisabled(text) {
|
|
42
|
+
const body = frontmatterBody(text);
|
|
43
|
+
if (body === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
return body.split(/\r?\n/).some((l) => /^disable-model-invocation:\s*true\s*$/.test(l));
|
|
46
|
+
}
|
|
47
|
+
/** Word content of a session-start script's `POINTER_LINES` array; `found` false on a miss (e.g. a rename) names it instead of a silent 0. */
|
|
48
|
+
function pointerLineText(text) {
|
|
49
|
+
const arr = /POINTER_LINES\s*=\s*\[([\s\S]*?)\]/.exec(text);
|
|
50
|
+
if (!arr)
|
|
51
|
+
return { text: "", found: false };
|
|
52
|
+
const strings = [];
|
|
53
|
+
const re = /(['"`])((?:\\.|(?!\1)[^\\])*)\1/g;
|
|
54
|
+
let m;
|
|
55
|
+
while ((m = re.exec(arr[1])))
|
|
56
|
+
strings.push(m[2]);
|
|
57
|
+
return { text: strings.join(" "), found: true };
|
|
58
|
+
}
|
|
59
|
+
/** `.md` files of a directory, or each subdirectory's SKILL.md when `skills`. */
|
|
60
|
+
function listMarkdown(dir, skills) {
|
|
61
|
+
if (!fs.existsSync(dir))
|
|
62
|
+
return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
65
|
+
if (skills) {
|
|
66
|
+
const f = path.join(dir, e.name, "SKILL.md");
|
|
67
|
+
if (e.isDirectory() && fs.existsSync(f))
|
|
68
|
+
out.push(f);
|
|
69
|
+
}
|
|
70
|
+
else if (e.isFile() && e.name.endsWith(".md") && e.name !== "README.md")
|
|
71
|
+
out.push(path.join(dir, e.name));
|
|
72
|
+
}
|
|
73
|
+
return out.sort();
|
|
74
|
+
}
|
|
75
|
+
/** `~/.claude.json`, or `CLAUDE_CONFIG_DIR`'s copy when that override is set and present. */
|
|
76
|
+
export function claudeJsonPath(home, env = process.env) {
|
|
77
|
+
const configDir = env.CLAUDE_CONFIG_DIR;
|
|
78
|
+
if (configDir) {
|
|
79
|
+
const inConfig = path.join(configDir, ".claude.json");
|
|
80
|
+
if (fs.existsSync(inConfig))
|
|
81
|
+
return inConfig;
|
|
82
|
+
}
|
|
83
|
+
return path.join(home, ".claude.json");
|
|
84
|
+
}
|
|
85
|
+
/** MCP servers per scope: user (`.claude.json` top level), local (its `projects[<path>]`), project (`<projectRoot>/.mcp.json`). */
|
|
86
|
+
export function mcpServerEntries(home, projectRoot) {
|
|
87
|
+
const out = [];
|
|
88
|
+
const claudeJson = claudeJsonPath(home);
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(fs.readFileSync(claudeJson, "utf8"));
|
|
91
|
+
for (const n of Object.keys(parsed.mcpServers ?? {}))
|
|
92
|
+
out.push({ name: n, scope: "user", file: claudeJson });
|
|
93
|
+
for (const [p, cfg] of Object.entries(parsed.projects ?? {}))
|
|
94
|
+
for (const n of Object.keys(cfg?.mcpServers ?? {}))
|
|
95
|
+
out.push({ name: n, scope: "local", file: claudeJson, projectPath: p });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// absent or malformed
|
|
99
|
+
}
|
|
100
|
+
const projectFile = path.join(projectRoot, ".mcp.json");
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(fs.readFileSync(projectFile, "utf8"));
|
|
103
|
+
for (const n of Object.keys(parsed.mcpServers ?? {}))
|
|
104
|
+
out.push({ name: n, scope: "project", file: projectFile, projectPath: projectRoot });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// absent or malformed
|
|
108
|
+
}
|
|
109
|
+
const rank = { user: 0, local: 1, project: 2 };
|
|
110
|
+
return out.sort((a, b) => a.name.localeCompare(b.name) || rank[a.scope] - rank[b.scope] || (a.projectPath ?? "").localeCompare(b.projectPath ?? ""));
|
|
111
|
+
}
|
|
112
|
+
/** The `claude mcp remove` invocation; local/project scope must run from the entry's project dir. */
|
|
113
|
+
export function mcpRemoveCommand(e) {
|
|
114
|
+
const remove = `claude mcp remove ${e.name} -s ${e.scope}`;
|
|
115
|
+
return e.scope === "user" || !e.projectPath ? remove : `cd "${e.projectPath}" && ${remove}`;
|
|
116
|
+
}
|
|
117
|
+
/** Deduplicated MCP server names across every scope (the estimate's tool-count proxy). */
|
|
118
|
+
export function mcpServerNames(home, projectRoot) {
|
|
119
|
+
return [...new Set(mcpServerEntries(home, projectRoot).map((e) => e.name))].sort();
|
|
120
|
+
}
|
|
121
|
+
/** Hook commands registered in a settings.json (0 when absent or malformed). */
|
|
122
|
+
export function hookCount(file) {
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
125
|
+
let n = 0;
|
|
126
|
+
for (const entries of Object.values(parsed.hooks ?? {}))
|
|
127
|
+
for (const e of entries ?? [])
|
|
128
|
+
n += (e.hooks ?? []).length;
|
|
129
|
+
return n;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Estimates the prefix: rules, agent/skill descriptions under `claudeDir`, MCP server names. */
|
|
136
|
+
export function estimatePrefix(claudeDir, rulesFiles, mcp, budget = DEFAULT_PREFIX_BUDGET) {
|
|
137
|
+
const items = [];
|
|
138
|
+
const read = (f) => {
|
|
139
|
+
try {
|
|
140
|
+
return fs.readFileSync(f, "utf8");
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
for (const f of rulesFiles) {
|
|
147
|
+
if (!fs.existsSync(f))
|
|
148
|
+
continue;
|
|
149
|
+
items.push({ source: f, kind: "rules", ...estimateTokens(read(f)) });
|
|
150
|
+
}
|
|
151
|
+
for (const f of listMarkdown(path.join(claudeDir, "agents"), false))
|
|
152
|
+
items.push({ source: f, kind: "agent description", ...estimateTokens(frontMatterDescription(read(f))) });
|
|
153
|
+
for (const f of listMarkdown(path.join(claudeDir, "skills"), true)) {
|
|
154
|
+
const text = read(f);
|
|
155
|
+
if (isModelDisabled(text))
|
|
156
|
+
continue;
|
|
157
|
+
items.push({ source: f, kind: "skill description", ...estimateTokens(frontMatterDescription(text)) });
|
|
158
|
+
}
|
|
159
|
+
if (mcp.length > 0)
|
|
160
|
+
items.push({ source: `mcp:${mcp.join(",")}`, kind: "mcp servers", ...estimateTokens(mcp.join(" ")) });
|
|
161
|
+
const tokens = items.reduce((n, i) => n + i.tokens, 0);
|
|
162
|
+
items.sort((a, b) => b.tokens - a.tokens || a.source.localeCompare(b.source));
|
|
163
|
+
// Injected once into the session-start turn, never every request: excluded from `tokens`/`ok`.
|
|
164
|
+
// A missing file contributes 0 words, named in `source`.
|
|
165
|
+
const routingFile = path.join(claudeDir, "freeharness", "hooks", ROUTING_FILE);
|
|
166
|
+
const sessionStartFile = path.join(claudeDir, "freeharness", "hooks", "freeharness-session-start");
|
|
167
|
+
const routingExists = fs.existsSync(routingFile);
|
|
168
|
+
const sessionStartExists = fs.existsSync(sessionStartFile);
|
|
169
|
+
const pointer = sessionStartExists ? pointerLineText(read(sessionStartFile)) : { text: "", found: true };
|
|
170
|
+
const injectionText = `${routingExists ? read(routingFile) : ""} ${pointer.text}`;
|
|
171
|
+
const routingLabel = routingExists ? routingFile : `${routingFile} (missing)`;
|
|
172
|
+
const pointerLabel = sessionStartExists
|
|
173
|
+
? `${sessionStartFile}${pointer.found ? "" : " (pointer lines not found)"}`
|
|
174
|
+
: `${sessionStartFile} (missing)`;
|
|
175
|
+
const injection = {
|
|
176
|
+
source: `${routingLabel} + ${pointerLabel}`,
|
|
177
|
+
kind: "session-start",
|
|
178
|
+
...estimateTokens(injectionText),
|
|
179
|
+
};
|
|
180
|
+
return { items, tokens, budget, ok: tokens <= budget, injection };
|
|
181
|
+
}
|
|
182
|
+
/** Reads a settings.json (empty object when absent or malformed). */
|
|
183
|
+
export function readSettings(file) {
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Project-scope `model`/`effortLevel`/`permissions.deny` overrides that invalidate the prompt cache. */
|
|
192
|
+
export function cacheInvalidators(userSettings, projectSettings) {
|
|
193
|
+
const out = [];
|
|
194
|
+
const get = (s, key) => (key === "permissions.deny" ? s.permissions?.deny : s[key]);
|
|
195
|
+
for (const key of ["model", "effortLevel", "permissions.deny"]) {
|
|
196
|
+
const p = get(projectSettings, key);
|
|
197
|
+
if (p === undefined)
|
|
198
|
+
continue;
|
|
199
|
+
const u = get(userSettings, key);
|
|
200
|
+
if (JSON.stringify(p) !== JSON.stringify(u))
|
|
201
|
+
out.push({ key, project: p, user: u });
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
/** Missing prompt-cache TTL keys (empty when both are present). */
|
|
206
|
+
export function missingTtlKeys(settings) {
|
|
207
|
+
return ["promptCacheTtl", "subagentPromptCacheTtl"].filter((k) => settings[k] === undefined);
|
|
208
|
+
}
|
|
209
|
+
/** Line count of a file (0 when absent). */
|
|
210
|
+
export function lineCount(file) {
|
|
211
|
+
try {
|
|
212
|
+
const text = fs.readFileSync(file, "utf8");
|
|
213
|
+
return text === "" ? 0 : text.replace(/\n$/, "").split("\n").length;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
}
|