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,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "FreeHarness cost-tracker price table. USD per million tokens (MTok) per model, columns: input, output, cache_read, cache_write, window (the model's standard token window; the tracker resolves 1,000,000 for a `[1m]` model-id suffix regardless of this field). PLACEHOLDERS: the numbers below are public list prices as of the file date and are NOT authoritative for your account — EDIT TO YOUR CONTRACT. The hook resolves a model by exact name, then by the longest key the model name starts with (claude-sonnet-4-5-20250929 -> claude-sonnet-4-5 -> claude-sonnet-4), then `default`. Re-running `freeharness init` never overwrites this file once it exists.",
|
|
3
|
+
"currency": "USD",
|
|
4
|
+
"unit": "per_million_tokens",
|
|
5
|
+
"prices": {
|
|
6
|
+
"claude-opus-4-1": { "input": 15, "output": 75, "cache_read": 1.5, "cache_write": 18.75, "window": 200000 },
|
|
7
|
+
"claude-opus-4": { "input": 15, "output": 75, "cache_read": 1.5, "cache_write": 18.75, "window": 200000 },
|
|
8
|
+
"claude-sonnet-4-5": { "input": 3, "output": 15, "cache_read": 0.3, "cache_write": 3.75, "window": 200000 },
|
|
9
|
+
"claude-sonnet-4": { "input": 3, "output": 15, "cache_read": 0.3, "cache_write": 3.75, "window": 200000 },
|
|
10
|
+
"claude-haiku-4-5": { "input": 1, "output": 5, "cache_read": 0.1, "cache_write": 1.25, "window": 200000 },
|
|
11
|
+
"claude-3-5-haiku": { "input": 0.8, "output": 4, "cache_read": 0.08, "cache_write": 1, "window": 200000 },
|
|
12
|
+
"default": { "input": 3, "output": 15, "cache_read": 0.3, "cache_write": 3.75, "window": 200000 }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// FreeHarness auto-memory tier-0 spike (Claude Code SessionEnd hook). OFF BY DEFAULT.
|
|
2
|
+
// When FREEHARNESS_AUTO_MEMORY=on, the session's last project telemetry record
|
|
3
|
+
// (<cwd>/.freeharness/telemetry.jsonl, last valid JSON line) is curated into
|
|
4
|
+
// ~/.claude/freeharness/MEMORY.md as ONE line per project (`- <project>: <change> <task>
|
|
5
|
+
// <tier>: main <N>k, subagents <N>k, observer <N>k|n/a`): a re-run with the same record
|
|
6
|
+
// changes nothing; a newer record replaces the project's line. Nothing else is written;
|
|
7
|
+
// the free-mem plugin stays the memory of record. Never blocks, never prints, exit 0 on every path.
|
|
8
|
+
// File override: FREEHARNESS_AUTO_MEMORY_FILE.
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
/** Header of the curated file. */
|
|
15
|
+
export const HEADER = "# FreeHarness auto-memory (tier-0 spike; one line per project from the telemetry log)";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* True when the spike is switched on.
|
|
19
|
+
* @param {NodeJS.ProcessEnv} env Environment.
|
|
20
|
+
* @returns {boolean} Enabled.
|
|
21
|
+
*/
|
|
22
|
+
export function enabled(env = process.env) {
|
|
23
|
+
return String(env.FREEHARNESS_AUTO_MEMORY || "").trim().toLowerCase() === "on";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The curated file path.
|
|
28
|
+
* @returns {string} Absolute path.
|
|
29
|
+
*/
|
|
30
|
+
export function memoryFile() {
|
|
31
|
+
return process.env.FREEHARNESS_AUTO_MEMORY_FILE || path.join(os.homedir(), ".claude", "freeharness", "MEMORY.md");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `<N>k`, non-finite treated as 0. */
|
|
35
|
+
function kTokens(n) {
|
|
36
|
+
const v = Number.isFinite(n) ? Number(n) : 0;
|
|
37
|
+
return `${Math.round(v / 1000)}k`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Renders a telemetry tag defensively: caps at 64 characters, keeps only `[\w.\-/ ,()]` (no
|
|
42
|
+
* control characters, no embedded newline that could split the one curated line per project).
|
|
43
|
+
* The producer already sanitizes at write time; this is a second pass against a tampered or
|
|
44
|
+
* pre-fix telemetry line.
|
|
45
|
+
* @param {unknown} s Raw field value.
|
|
46
|
+
* @returns {string} Sanitized value ("" for a non-string).
|
|
47
|
+
*/
|
|
48
|
+
function sanitizeTag(s) {
|
|
49
|
+
return typeof s === "string" ? s.trim().replace(/[^\w.\-/ ,()]/g, "").slice(0, 64).trim() : "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One telemetry record rendered as a curated line: `<change or untagged> <task or -> <tier or
|
|
54
|
+
* ->: main <N>k, subagents <N>k, observer <N>k|n/a`.
|
|
55
|
+
* @param {Record<string, any>} rec Parsed telemetry.jsonl record.
|
|
56
|
+
* @returns {string} Formatted line.
|
|
57
|
+
*/
|
|
58
|
+
function telemetryLine(rec) {
|
|
59
|
+
const change = sanitizeTag(rec.change) || "untagged";
|
|
60
|
+
const task = sanitizeTag(rec.task) || "-";
|
|
61
|
+
const tier = sanitizeTag(rec.tier) || "-";
|
|
62
|
+
const sinks = rec && typeof rec.sinks === "object" && rec.sinks ? rec.sinks : {};
|
|
63
|
+
const main = kTokens(sinks.main && sinks.main.total);
|
|
64
|
+
const subagents = kTokens(sinks.subagents && sinks.subagents.total);
|
|
65
|
+
const observerTokens = sinks.observer && Number.isFinite(sinks.observer.tokens) ? sinks.observer.tokens : null;
|
|
66
|
+
const observer = observerTokens === null ? "n/a" : kTokens(observerTokens);
|
|
67
|
+
return `${change} ${task} ${tier}: main ${main}, subagents ${subagents}, observer ${observer}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Last valid record of a project's telemetry file, rendered as a curated line.
|
|
72
|
+
* @param {string} project Project root.
|
|
73
|
+
* @returns {string | null} The line, or null when the file is absent or carries no valid record.
|
|
74
|
+
*/
|
|
75
|
+
export function lastTelemetryLine(project) {
|
|
76
|
+
let text;
|
|
77
|
+
try {
|
|
78
|
+
text = fs.readFileSync(path.join(project, ".freeharness", "telemetry.jsonl"), "utf8");
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
let last = null;
|
|
83
|
+
for (const raw of text.split("\n")) {
|
|
84
|
+
if (!raw.trim()) continue;
|
|
85
|
+
try {
|
|
86
|
+
const rec = JSON.parse(raw);
|
|
87
|
+
if (rec && typeof rec === "object") last = rec;
|
|
88
|
+
} catch {
|
|
89
|
+
// skip malformed lines
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return last ? telemetryLine(last) : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Upserts one project line into the curated text (idempotent).
|
|
97
|
+
* @param {string} text Current file text (may be empty).
|
|
98
|
+
* @param {string} project Project name (basename of cwd).
|
|
99
|
+
* @param {string} line Formatted telemetry line.
|
|
100
|
+
* @returns {{text:string, changed:boolean}} New text and whether it differs.
|
|
101
|
+
*/
|
|
102
|
+
export function upsert(text, project, line) {
|
|
103
|
+
const entry = `- ${project}: ${line}`;
|
|
104
|
+
// The file is FreeHarness-owned: header + one `- <project>: ...` line per project.
|
|
105
|
+
const entries = (text || "").split("\n").filter((l) => l.startsWith("- "));
|
|
106
|
+
const idx = entries.findIndex((l) => l.startsWith(`- ${project}: `));
|
|
107
|
+
if (idx >= 0) entries[idx] = entry;
|
|
108
|
+
else entries.push(entry);
|
|
109
|
+
const next = `${HEADER}\n\n${entries.join("\n")}\n`;
|
|
110
|
+
return { text: next, changed: next !== text };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Runs the hook for one payload.
|
|
115
|
+
* @param {Record<string, any>} payload Hook payload.
|
|
116
|
+
* @returns {{changed:boolean, file:string} | null} What happened, or null when nothing applied.
|
|
117
|
+
*/
|
|
118
|
+
export function respond(payload) {
|
|
119
|
+
const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
|
|
120
|
+
const line = lastTelemetryLine(cwd);
|
|
121
|
+
if (!line) return null;
|
|
122
|
+
const file = memoryFile();
|
|
123
|
+
let current = "";
|
|
124
|
+
try {
|
|
125
|
+
current = fs.readFileSync(file, "utf8");
|
|
126
|
+
} catch {
|
|
127
|
+
// first write
|
|
128
|
+
}
|
|
129
|
+
const r = upsert(current, path.basename(cwd), line);
|
|
130
|
+
if (r.changed) {
|
|
131
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
132
|
+
fs.writeFileSync(file, r.text, "utf8");
|
|
133
|
+
}
|
|
134
|
+
return { changed: r.changed, file };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Entry point: flag-gated, read stdin, curate, stay silent, exit 0. */
|
|
138
|
+
function main() {
|
|
139
|
+
if (!enabled()) return;
|
|
140
|
+
let payload = {};
|
|
141
|
+
try {
|
|
142
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
143
|
+
if (raw.trim()) payload = JSON.parse(raw);
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!payload || typeof payload !== "object") return;
|
|
148
|
+
try {
|
|
149
|
+
respond(payload);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
process.stderr.write(`freeharness-auto-memory: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) {
|
|
156
|
+
main();
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// FreeHarness config guard (Claude Code PreToolUse hook, matcher Edit|Write|MultiEdit|Bash).
|
|
2
|
+
// Denies writes to the files that invalidate the prompt cache or change the harness under
|
|
3
|
+
// the session (settings.json, settings.local.json, .mcp.json, CLAUDE.md, hook scripts and
|
|
4
|
+
// the plugin hooks.json) unless an allow marker exists and is fresh:
|
|
5
|
+
// <project>/.freeharness/allow-config-edit (or ~/.claude/freeharness/allow-config-edit)
|
|
6
|
+
// The marker is valid for --ttl minutes (30) after its mtime; a numeric (epoch ms) or ISO
|
|
7
|
+
// date body sets an explicit expiry instead. `touch .freeharness/allow-config-edit` in a
|
|
8
|
+
// terminal sets or refreshes it; `freeharness init`/`update` set and remove their own.
|
|
9
|
+
// Bash is denied only when the command writes to such a path (redirect, sed -i, rm, mv, cp,
|
|
10
|
+
// tee, truncate, touch); reads pass. The marker itself is protected from tool writes, so
|
|
11
|
+
// the user, not the model, opens the window. Never throws; exit 0 on every path.
|
|
12
|
+
// Disable: FREEHARNESS_HOOK_CONFIG_GUARD=off.
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
/** Marker file name (project: .freeharness/, user: ~/.claude/freeharness/). */
|
|
19
|
+
export const MARKER = "allow-config-edit";
|
|
20
|
+
/** Default marker lifetime in minutes. */
|
|
21
|
+
export const DEFAULT_TTL_MIN = 30;
|
|
22
|
+
|
|
23
|
+
/** Protected basenames. */
|
|
24
|
+
const NAMES = /^(settings\.json|settings\.local\.json|\.mcp\.json|CLAUDE\.md|hooks\.json)$/i;
|
|
25
|
+
/** Protected directories (hook scripts) and the marker. */
|
|
26
|
+
const DIRS = /\/(\.freeharness\/hooks|\.claude\/freeharness\/hooks|\.claude\/hooks|freeharness-plugin\/hooks)\/[^/]+$|\/\.freeharness\/allow-config-edit$|\/\.claude\/freeharness\/allow-config-edit$/;
|
|
27
|
+
/** Same set as a token pattern for Bash commands (relative spellings included). */
|
|
28
|
+
const TOKEN = /(?:^|[\s"'=:/])((?:[\w.~-]+\/)*(?:settings\.json|settings\.local\.json|\.mcp\.json|CLAUDE\.md|hooks\.json|allow-config-edit|(?:\.freeharness|\.claude\/freeharness|\.claude)\/hooks\/[\w.-]+|freeharness-[\w-]+\.mjs|freeharness-session-start))(?=$|[\s"';|&)])/g;
|
|
29
|
+
/** Write constructs a Bash command may apply to a protected token. */
|
|
30
|
+
const WRITERS = /(^|[\s;|&(])(sed\s+(-[a-zA-Z]*i|--in-place)|rm|mv|cp|tee|truncate|touch|install|ln|dd|shred|python3?\s+-c|perl\s+-[a-z]*i)\b|>{1,2}\s*["']?(?:[\w.~-]+\/)*(?:settings\.json|settings\.local\.json|\.mcp\.json|CLAUDE\.md|hooks\.json|allow-config-edit|freeharness-[\w-]+\.mjs|freeharness-session-start)/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parses --ttl (minutes).
|
|
34
|
+
* @param {string[]} argv Arguments after the script path.
|
|
35
|
+
* @returns {{ttlMin:number}} Options.
|
|
36
|
+
*/
|
|
37
|
+
export function parseOptions(argv) {
|
|
38
|
+
const opts = { ttlMin: DEFAULT_TTL_MIN };
|
|
39
|
+
for (let i = 0; i < argv.length; i++) {
|
|
40
|
+
if (argv[i] === "--ttl") {
|
|
41
|
+
const n = Number.parseInt(argv[++i], 10);
|
|
42
|
+
if (Number.isInteger(n) && n > 0) opts.ttlMin = n;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return opts;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether an absolute file path is protected.
|
|
50
|
+
* @param {string} file Path (any separator).
|
|
51
|
+
* @returns {boolean} True when protected.
|
|
52
|
+
*/
|
|
53
|
+
export function isProtectedPath(file) {
|
|
54
|
+
const p = String(file).replace(/\\/g, "/");
|
|
55
|
+
if (/\/node_modules\//.test(p)) return false;
|
|
56
|
+
const base = p.split("/").pop() || "";
|
|
57
|
+
return NAMES.test(base) || DIRS.test(p);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Protected paths a Bash command writes to (empty when it only reads them).
|
|
62
|
+
* @param {string} command The command.
|
|
63
|
+
* @returns {string[]} Matched tokens.
|
|
64
|
+
*/
|
|
65
|
+
export function bashTargets(command) {
|
|
66
|
+
const tokens = [...command.matchAll(TOKEN)].map((m) => m[1]).filter((t) => !/\/node_modules\//.test(t));
|
|
67
|
+
if (tokens.length === 0) return [];
|
|
68
|
+
return WRITERS.test(command) ? [...new Set(tokens)] : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether a marker file is present and unexpired.
|
|
73
|
+
* @param {string} file Marker path.
|
|
74
|
+
* @param {number} ttlMin Default lifetime from mtime, in minutes.
|
|
75
|
+
* @param {number} now Reference time (ms).
|
|
76
|
+
* @returns {"valid" | "expired" | "absent"} State.
|
|
77
|
+
*/
|
|
78
|
+
export function markerState(file, ttlMin, now = Date.now()) {
|
|
79
|
+
let stat;
|
|
80
|
+
try {
|
|
81
|
+
stat = fs.statSync(file);
|
|
82
|
+
} catch {
|
|
83
|
+
return "absent";
|
|
84
|
+
}
|
|
85
|
+
let expires = stat.mtimeMs + ttlMin * 60_000;
|
|
86
|
+
try {
|
|
87
|
+
const body = fs.readFileSync(file, "utf8").trim();
|
|
88
|
+
if (/^\d{10,}$/.test(body)) expires = Number(body);
|
|
89
|
+
else if (body && !Number.isNaN(Date.parse(body))) expires = Date.parse(body);
|
|
90
|
+
} catch {
|
|
91
|
+
// mtime rule
|
|
92
|
+
}
|
|
93
|
+
return now < expires ? "valid" : "expired";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Candidate marker paths for a payload: project first, then user.
|
|
98
|
+
* @param {Record<string, any>} payload Hook payload.
|
|
99
|
+
* @returns {string[]} Absolute paths.
|
|
100
|
+
*/
|
|
101
|
+
export function markerPaths(payload) {
|
|
102
|
+
const project = process.env.CLAUDE_PROJECT_DIR || (typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd());
|
|
103
|
+
return [path.join(project, ".freeharness", MARKER), path.join(os.homedir(), ".claude", "freeharness", MARKER)];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Builds the deny response for a PreToolUse payload, or null to allow.
|
|
108
|
+
* @param {Record<string, any>} payload Parsed hook payload.
|
|
109
|
+
* @param {{ttlMin:number}} opts Options.
|
|
110
|
+
* @returns {Record<string, unknown> | null} JSON to print, or null.
|
|
111
|
+
*/
|
|
112
|
+
export function respond(payload, opts) {
|
|
113
|
+
const tool = String(payload.tool_name || "");
|
|
114
|
+
const input = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {};
|
|
115
|
+
let targets = [];
|
|
116
|
+
if (/^(Edit|Write|MultiEdit|NotebookEdit)$/.test(tool)) {
|
|
117
|
+
const file = input.file_path || input.notebook_path || input.path || "";
|
|
118
|
+
if (file && isProtectedPath(String(file))) targets = [String(file)];
|
|
119
|
+
} else if (tool === "Bash") {
|
|
120
|
+
targets = bashTargets(typeof input.command === "string" ? input.command : "");
|
|
121
|
+
}
|
|
122
|
+
if (targets.length === 0) return null;
|
|
123
|
+
const markers = markerPaths(payload);
|
|
124
|
+
const states = markers.map((m) => markerState(m, opts.ttlMin));
|
|
125
|
+
if (states.includes("valid")) return null;
|
|
126
|
+
const expired = states.findIndex((s) => s === "expired");
|
|
127
|
+
const why = expired >= 0 ? `marker expired: ${markers[expired]}` : `no marker at ${markers[0]}`;
|
|
128
|
+
return {
|
|
129
|
+
hookSpecificOutput: {
|
|
130
|
+
hookEventName: "PreToolUse",
|
|
131
|
+
permissionDecision: "deny",
|
|
132
|
+
permissionDecisionReason:
|
|
133
|
+
`FreeHarness config guard: ${targets.join(", ")} is harness configuration (cache invalidator: settings, MCP, hooks, CLAUDE.md); ${why}. ` +
|
|
134
|
+
`Ask the user to open a ${opts.ttlMin}-minute window from a terminal: \`mkdir -p .freeharness && touch .freeharness/${MARKER}\` (project) or \`touch ~/.claude/freeharness/${MARKER}\` (user), then retry. ` +
|
|
135
|
+
"Do not create the marker yourself, and do not route the edit through another tool.",
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Entry point: read stdin, deny or stay silent, exit 0. */
|
|
141
|
+
function main() {
|
|
142
|
+
if ((process.env.FREEHARNESS_HOOK_CONFIG_GUARD || "").toLowerCase() === "off") return;
|
|
143
|
+
let payload = {};
|
|
144
|
+
try {
|
|
145
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
146
|
+
if (raw.trim()) payload = JSON.parse(raw);
|
|
147
|
+
} catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!payload || typeof payload !== "object") return;
|
|
151
|
+
try {
|
|
152
|
+
const out = respond(payload, parseOptions(process.argv.slice(2)));
|
|
153
|
+
if (out) process.stdout.write(JSON.stringify(out));
|
|
154
|
+
} catch (e) {
|
|
155
|
+
process.stderr.write(`freeharness-config-guard: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) {
|
|
160
|
+
main();
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// FreeHarness context monitor (Claude Code Stop hook).
|
|
2
|
+
// Reads the latest cost snapshot of the session (usage.jsonl, written by the cost tracker) and,
|
|
3
|
+
// when the context crosses 60% and then 85% of its token window, prints one advisory line as a
|
|
4
|
+
// `systemMessage` (shown to the user, never to the model: a Stop `additionalContext` would force
|
|
5
|
+
// another turn). Re-arm and window fallback: see `crossed` and `respond` below.
|
|
6
|
+
// Never blocks; exit 0 on every path. Disable: FREEHARNESS_HOOK_CONTEXT_MONITOR=off.
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
/** Defaults; the fragment overrides them with --budget / --thresholds. */
|
|
13
|
+
export const DEFAULTS = Object.freeze({ budget: 200_000, thresholds: [60, 85] });
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses --budget and --thresholds (percentages, ascending).
|
|
17
|
+
* @param {string[]} argv Arguments after the script path.
|
|
18
|
+
* @returns {{budget:number, thresholds:number[]}} Options.
|
|
19
|
+
*/
|
|
20
|
+
export function parseOptions(argv) {
|
|
21
|
+
const opts = { budget: DEFAULTS.budget, thresholds: [...DEFAULTS.thresholds] };
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
if (argv[i] === "--budget") {
|
|
24
|
+
const n = Number.parseInt(argv[++i], 10);
|
|
25
|
+
if (Number.isInteger(n) && n > 0) opts.budget = n;
|
|
26
|
+
} else if (argv[i] === "--thresholds") {
|
|
27
|
+
const list = String(argv[++i] ?? "")
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((s) => Number.parseInt(s.trim(), 10))
|
|
30
|
+
.filter((n) => Number.isInteger(n) && n > 0 && n <= 100);
|
|
31
|
+
if (list.length > 0) opts.thresholds = list.sort((a, b) => a - b);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return opts;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the state file of a session.
|
|
39
|
+
* @param {string} sessionId Session id.
|
|
40
|
+
* @returns {string} Absolute path.
|
|
41
|
+
*/
|
|
42
|
+
function stateFile(sessionId) {
|
|
43
|
+
const dir = process.env.FREEHARNESS_STATE_DIR || path.join(os.homedir(), ".claude", "freeharness", "state");
|
|
44
|
+
return path.join(dir, `${sessionId.replace(/[^\w.-]/g, "_") || "unknown"}.context.json`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Latest context size and token window recorded for the session in the cost log.
|
|
49
|
+
* @param {string} sessionId Session id.
|
|
50
|
+
* @returns {{tokens: number, window: number} | null} Tokens and window (default 200,000 when the
|
|
51
|
+
* snapshot lacks `context_window`), or null when no snapshot carries `context_tokens`.
|
|
52
|
+
*/
|
|
53
|
+
export function contextFromSnapshot(sessionId) {
|
|
54
|
+
const file = process.env.FREEHARNESS_COST_LOG || path.join(os.homedir(), ".claude", "freeharness", "cost", "usage.jsonl");
|
|
55
|
+
let text;
|
|
56
|
+
try {
|
|
57
|
+
text = fs.readFileSync(file, "utf8");
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
let best = null;
|
|
62
|
+
for (const raw of text.split("\n")) {
|
|
63
|
+
if (!raw.includes(sessionId)) continue;
|
|
64
|
+
try {
|
|
65
|
+
const rec = JSON.parse(raw);
|
|
66
|
+
if (rec && rec.session_id === sessionId && rec.usage && Number.isFinite(rec.context_tokens)) {
|
|
67
|
+
const window = Number.isInteger(rec.context_window) && rec.context_window > 0 ? rec.context_window : 200_000;
|
|
68
|
+
best = { tokens: rec.context_tokens, window };
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// skip malformed lines
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return best;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Context size from the transcript: the last assistant message's input + cache tokens.
|
|
79
|
+
* @param {string} transcript Transcript path.
|
|
80
|
+
* @returns {number | null} Tokens, or null when unreadable.
|
|
81
|
+
*/
|
|
82
|
+
export function contextFromTranscript(transcript) {
|
|
83
|
+
let text;
|
|
84
|
+
try {
|
|
85
|
+
text = fs.readFileSync(transcript, "utf8");
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
let last = null;
|
|
90
|
+
for (const raw of text.split("\n")) {
|
|
91
|
+
if (!raw.includes('"usage"')) continue;
|
|
92
|
+
try {
|
|
93
|
+
const rec = JSON.parse(raw);
|
|
94
|
+
const u = rec && rec.message && rec.message.usage;
|
|
95
|
+
if (u && typeof u === "object") last = (Number(u.input_tokens) || 0) + (Number(u.cache_read_input_tokens) || 0) + (Number(u.cache_creation_input_tokens) || 0);
|
|
96
|
+
} catch {
|
|
97
|
+
// skip
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return last;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decides which threshold (if any) to announce against the given window, records it, and
|
|
105
|
+
* re-arms (clears the announced set, bumps the epoch) once the context drops back below the
|
|
106
|
+
* lowest threshold, so a later climb announces both thresholds again.
|
|
107
|
+
* @param {number} tokens Current context tokens.
|
|
108
|
+
* @param {number} window Token window the percentage is computed against.
|
|
109
|
+
* @param {{budget:number, thresholds:number[]}} opts Options (thresholds only; budget is the
|
|
110
|
+
* caller's fallback window, not used here).
|
|
111
|
+
* @param {string} file State file.
|
|
112
|
+
* @returns {number | null} The percentage to announce, or null.
|
|
113
|
+
*/
|
|
114
|
+
export function crossed(tokens, window, opts, file) {
|
|
115
|
+
let state = { announced: [], epoch: 0 };
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
118
|
+
if (parsed && Array.isArray(parsed.announced)) state = { announced: parsed.announced, epoch: Number.isInteger(parsed.epoch) ? parsed.epoch : 0 };
|
|
119
|
+
} catch {
|
|
120
|
+
// fresh state
|
|
121
|
+
}
|
|
122
|
+
const pct = (tokens / window) * 100;
|
|
123
|
+
const lowest = opts.thresholds[0];
|
|
124
|
+
let changed = false;
|
|
125
|
+
if (pct < lowest && state.announced.length > 0) {
|
|
126
|
+
state.announced = [];
|
|
127
|
+
state.epoch += 1;
|
|
128
|
+
changed = true;
|
|
129
|
+
}
|
|
130
|
+
const due = opts.thresholds.filter((t) => pct >= t && !state.announced.includes(t));
|
|
131
|
+
let top = null;
|
|
132
|
+
if (due.length > 0) {
|
|
133
|
+
top = due[due.length - 1];
|
|
134
|
+
state.announced = [...new Set([...state.announced, ...due])].sort((a, b) => a - b);
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
if (changed) {
|
|
138
|
+
state.updated = new Date().toISOString();
|
|
139
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
140
|
+
fs.writeFileSync(file, JSON.stringify(state), "utf8");
|
|
141
|
+
}
|
|
142
|
+
return top;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Builds the advisory for one Stop payload, or null when nothing is due. The window is the
|
|
147
|
+
* snapshot's own `context_window`; the transcript fallback carries no window of its own, so
|
|
148
|
+
* --budget stands in for it there.
|
|
149
|
+
* @param {Record<string, any>} payload Parsed hook payload.
|
|
150
|
+
* @param {{budget:number, thresholds:number[]}} opts Options.
|
|
151
|
+
* @returns {Record<string, unknown> | null} JSON to print, or null.
|
|
152
|
+
*/
|
|
153
|
+
export function respond(payload, opts) {
|
|
154
|
+
const sessionId = typeof payload.session_id === "string" && payload.session_id ? payload.session_id : "unknown";
|
|
155
|
+
const snap = contextFromSnapshot(sessionId);
|
|
156
|
+
let tokens = null;
|
|
157
|
+
let window = null;
|
|
158
|
+
if (snap) {
|
|
159
|
+
tokens = snap.tokens;
|
|
160
|
+
window = snap.window;
|
|
161
|
+
} else if (typeof payload.transcript_path === "string") {
|
|
162
|
+
tokens = contextFromTranscript(payload.transcript_path);
|
|
163
|
+
if (tokens !== null) window = opts.budget;
|
|
164
|
+
}
|
|
165
|
+
if (tokens === null) return null;
|
|
166
|
+
const threshold = crossed(tokens, window, opts, stateFile(sessionId));
|
|
167
|
+
if (threshold === null) return null;
|
|
168
|
+
const usedPct = Math.round((tokens / window) * 100);
|
|
169
|
+
const tokensK = Math.round(tokens / 1000);
|
|
170
|
+
const windowK = Math.round(window / 1000);
|
|
171
|
+
return {
|
|
172
|
+
systemMessage: `context ${tokensK}k of ${windowK}k (${usedPct}%): compact with the project instructions, or /clear before unrelated work`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Entry point: read stdin, respond or stay silent, exit 0. */
|
|
177
|
+
function main() {
|
|
178
|
+
if ((process.env.FREEHARNESS_HOOK_CONTEXT_MONITOR || "").toLowerCase() === "off") return;
|
|
179
|
+
let payload = {};
|
|
180
|
+
try {
|
|
181
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
182
|
+
if (raw.trim()) payload = JSON.parse(raw);
|
|
183
|
+
} catch {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!payload || typeof payload !== "object") return;
|
|
187
|
+
try {
|
|
188
|
+
const out = respond(payload, parseOptions(process.argv.slice(2)));
|
|
189
|
+
if (out) process.stdout.write(JSON.stringify(out));
|
|
190
|
+
} catch (e) {
|
|
191
|
+
process.stderr.write(`freeharness-context-monitor: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) {
|
|
196
|
+
main();
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|