@hunter-harness/workflow-harness 0.3.1 → 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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/reference.md +30 -1
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +1 -0
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-execute/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +146 -8
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_knowledge_candidates.py +538 -4
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +30 -1
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +1 -0
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-execute/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +146 -8
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_knowledge_candidates.py +538 -4
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/reference.md +30 -1
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +1 -0
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-execute/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +146 -8
- package/harness/bundles/general/codex/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/codex/scripts/harness_knowledge_candidates.py +538 -4
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/reference.md +30 -1
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +1 -0
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-execute/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +146 -8
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/cursor/scripts/harness_knowledge_candidates.py +538 -4
- package/harness/bundles/general/pi/.harness-build.json +6 -0
- package/harness/bundles/general/pi/CONTEXT.md +98 -0
- package/harness/bundles/general/pi/README.md +478 -0
- package/harness/bundles/general/pi/contracts/build-profile-v3.schema.json +302 -0
- package/harness/bundles/general/pi/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/general/pi/contracts/doctor-capability.schema.json +43 -0
- package/harness/bundles/general/pi/contracts/external-platform-observation.schema.json +62 -0
- package/harness/bundles/general/pi/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/pi/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/pi/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/pi/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/pi/contracts/service-session.schema.json +147 -0
- package/harness/bundles/general/pi/contracts/workflow-policy.json +574 -0
- package/harness/bundles/general/pi/harness-archive/SKILL.md +322 -0
- package/harness/bundles/general/pi/harness-archive/checklist.md +71 -0
- package/harness/bundles/general/pi/harness-archive/reference.md +150 -0
- package/harness/bundles/general/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
- package/harness/bundles/general/pi/harness-archive/templates/summary-data-template.json +218 -0
- package/harness/bundles/general/pi/harness-codebase-map/SKILL.md +116 -0
- package/harness/bundles/general/pi/harness-codebase-map/checklist.md +126 -0
- package/harness/bundles/general/pi/harness-codebase-map/reference.md +364 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/STACK.md +55 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/TESTING.md +64 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/map-summary.md +64 -0
- package/harness/bundles/general/pi/harness-execute/SKILL.md +170 -0
- package/harness/bundles/general/pi/harness-knowledge-ingest/SKILL.md +103 -0
- package/harness/bundles/general/pi/harness-knowledge-query/SKILL.md +97 -0
- package/harness/bundles/general/pi/harness-plan/SKILL.md +158 -0
- package/harness/bundles/general/pi/harness-plan/checklist.md +243 -0
- package/harness/bundles/general/pi/harness-plan/protocols.md +133 -0
- package/harness/bundles/general/pi/harness-plan/reference.md +468 -0
- package/harness/bundles/general/pi/harness-pull/SKILL.md +55 -0
- package/harness/bundles/general/pi/harness-push/SKILL.md +83 -0
- package/harness/bundles/general/pi/harness-review/SKILL.md +175 -0
- package/harness/bundles/general/pi/harness-review/checklist.md +124 -0
- package/harness/bundles/general/pi/harness-review/protocols.md +69 -0
- package/harness/bundles/general/pi/harness-review/reference.md +114 -0
- package/harness/bundles/general/pi/harness-run/SKILL.md +188 -0
- package/harness/bundles/general/pi/harness-run/checklist.md +279 -0
- package/harness/bundles/general/pi/harness-run/protocols.md +141 -0
- package/harness/bundles/general/pi/harness-run/reference.md +886 -0
- package/harness/bundles/general/pi/harness-submit/SKILL.md +182 -0
- package/harness/bundles/general/pi/harness-submit/checklist.md +452 -0
- package/harness/bundles/general/pi/harness-submit/reference.md +152 -0
- package/harness/bundles/general/pi/harness-sync/SKILL.md +72 -0
- package/harness/bundles/general/pi/harness-sync/reference.md +176 -0
- package/harness/bundles/general/pi/harness-test/SKILL.md +250 -0
- package/harness/bundles/general/pi/harness-test/checklist.md +359 -0
- package/harness/bundles/general/pi/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/pi/harness-test/pitfalls.md +307 -0
- package/harness/bundles/general/pi/harness-test/reference.md +880 -0
- package/harness/bundles/general/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/general/pi/protocols/archive-report-protocol.md +196 -0
- package/harness/bundles/general/pi/protocols/ci-layering-protocol.md +22 -0
- package/harness/bundles/general/pi/protocols/evidence-based-reporting-protocol.md +134 -0
- package/harness/bundles/general/pi/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/general/pi/protocols/external-platform-protocol.md +27 -0
- package/harness/bundles/general/pi/protocols/ledger-protocol.md +365 -0
- package/harness/bundles/general/pi/protocols/memory-closeout-protocol.md +21 -0
- package/harness/bundles/general/pi/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/pi/protocols/powershell-protocol.md +238 -0
- package/harness/bundles/general/pi/protocols/registry-governance-protocol.md +31 -0
- package/harness/bundles/general/pi/protocols/report-pipeline-protocol.md +172 -0
- package/harness/bundles/general/pi/protocols/sensitive-info-protocol.md +110 -0
- package/harness/bundles/general/pi/protocols/state-layout-protocol.md +209 -0
- package/harness/bundles/general/pi/protocols/submit-protocol.md +78 -0
- package/harness/bundles/general/pi/protocols/verification-graph-protocol.md +32 -0
- package/harness/bundles/general/pi/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/general/pi/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/pi/scripts/harness_archive.py +11345 -0
- package/harness/bundles/general/pi/scripts/harness_change.py +1626 -0
- package/harness/bundles/general/pi/scripts/harness_check_gate.py +113 -0
- package/harness/bundles/general/pi/scripts/harness_context.py +1774 -0
- package/harness/bundles/general/pi/scripts/harness_deploy.py +968 -0
- package/harness/bundles/general/pi/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/general/pi/scripts/harness_environment.py +1690 -0
- package/harness/bundles/general/pi/scripts/harness_events.py +2234 -0
- package/harness/bundles/general/pi/scripts/harness_events_sync.py +1425 -0
- package/harness/bundles/general/pi/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/pi/scripts/harness_fixback.py +1975 -0
- package/harness/bundles/general/pi/scripts/harness_gate.py +3816 -0
- package/harness/bundles/general/pi/scripts/harness_headless.py +37 -0
- package/harness/bundles/general/pi/scripts/harness_integration.py +2564 -0
- package/harness/bundles/general/pi/scripts/harness_knowledge_candidates.py +796 -0
- package/harness/bundles/general/pi/scripts/harness_ledger.py +3556 -0
- package/harness/bundles/general/pi/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/pi/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/pi/scripts/harness_paths.py +676 -0
- package/harness/bundles/general/pi/scripts/harness_phase.py +1726 -0
- package/harness/bundles/general/pi/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/pi/scripts/harness_plan_finalize.py +1016 -0
- package/harness/bundles/general/pi/scripts/harness_preflight.py +650 -0
- package/harness/bundles/general/pi/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/pi/scripts/harness_profile.py +1817 -0
- package/harness/bundles/general/pi/scripts/harness_report_model.py +442 -0
- package/harness/bundles/general/pi/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/pi/scripts/harness_review.py +631 -0
- package/harness/bundles/general/pi/scripts/harness_runtime.py +2243 -0
- package/harness/bundles/general/pi/scripts/harness_service.py +3355 -0
- package/harness/bundles/general/pi/scripts/harness_state.py +365 -0
- package/harness/bundles/general/pi/scripts/harness_sync.py +407 -0
- package/harness/bundles/general/pi/scripts/harness_test_cleanup.py +175 -0
- package/harness/bundles/general/pi/scripts/harness_test_guard.py +1899 -0
- package/harness/bundles/general/pi/scripts/harness_test_runner.py +1586 -0
- package/harness/bundles/general/pi/scripts/harness_verification.py +593 -0
- package/harness/bundles/general/pi/scripts/harness_workflow_policy.py +266 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/reference.md +30 -1
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +1 -0
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-execute/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +146 -8
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_knowledge_candidates.py +538 -4
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +30 -1
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +1 -0
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-execute/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +146 -8
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_knowledge_candidates.py +538 -4
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/reference.md +30 -1
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +1 -0
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-execute/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +146 -8
- package/harness/bundles/java/codex/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/codex/scripts/harness_knowledge_candidates.py +538 -4
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/reference.md +30 -1
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +1 -0
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-execute/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +146 -8
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/cursor/scripts/harness_knowledge_candidates.py +538 -4
- package/harness/bundles/java/pi/.harness-build.json +6 -0
- package/harness/bundles/java/pi/CONTEXT.md +98 -0
- package/harness/bundles/java/pi/README.md +478 -0
- package/harness/bundles/java/pi/contracts/build-profile-v3.schema.json +302 -0
- package/harness/bundles/java/pi/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/java/pi/contracts/doctor-capability.schema.json +43 -0
- package/harness/bundles/java/pi/contracts/external-platform-observation.schema.json +62 -0
- package/harness/bundles/java/pi/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/pi/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/pi/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/pi/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/pi/contracts/service-session.schema.json +147 -0
- package/harness/bundles/java/pi/contracts/workflow-policy.json +574 -0
- package/harness/bundles/java/pi/harness-apidoc/SKILL.md +97 -0
- package/harness/bundles/java/pi/harness-apidoc/checklist.md +142 -0
- package/harness/bundles/java/pi/harness-apidoc/reference.md +233 -0
- package/harness/bundles/java/pi/harness-archive/SKILL.md +322 -0
- package/harness/bundles/java/pi/harness-archive/checklist.md +71 -0
- package/harness/bundles/java/pi/harness-archive/reference.md +150 -0
- package/harness/bundles/java/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
- package/harness/bundles/java/pi/harness-archive/templates/summary-data-template.json +218 -0
- package/harness/bundles/java/pi/harness-codebase-map/SKILL.md +116 -0
- package/harness/bundles/java/pi/harness-codebase-map/checklist.md +126 -0
- package/harness/bundles/java/pi/harness-codebase-map/reference.md +364 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/STACK.md +55 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/TESTING.md +64 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/map-summary.md +64 -0
- package/harness/bundles/java/pi/harness-execute/SKILL.md +170 -0
- package/harness/bundles/java/pi/harness-knowledge-ingest/SKILL.md +103 -0
- package/harness/bundles/java/pi/harness-knowledge-query/SKILL.md +97 -0
- package/harness/bundles/java/pi/harness-package/SKILL.md +94 -0
- package/harness/bundles/java/pi/harness-package/checklist.md +322 -0
- package/harness/bundles/java/pi/harness-package/reference.md +210 -0
- package/harness/bundles/java/pi/harness-plan/SKILL.md +158 -0
- package/harness/bundles/java/pi/harness-plan/checklist.md +243 -0
- package/harness/bundles/java/pi/harness-plan/protocols.md +133 -0
- package/harness/bundles/java/pi/harness-plan/reference.md +468 -0
- package/harness/bundles/java/pi/harness-pull/SKILL.md +55 -0
- package/harness/bundles/java/pi/harness-push/SKILL.md +83 -0
- package/harness/bundles/java/pi/harness-review/SKILL.md +175 -0
- package/harness/bundles/java/pi/harness-review/checklist.md +124 -0
- package/harness/bundles/java/pi/harness-review/protocols.md +69 -0
- package/harness/bundles/java/pi/harness-review/reference.md +114 -0
- package/harness/bundles/java/pi/harness-run/SKILL.md +206 -0
- package/harness/bundles/java/pi/harness-run/checklist.md +252 -0
- package/harness/bundles/java/pi/harness-run/protocols.md +141 -0
- package/harness/bundles/java/pi/harness-run/reference.md +820 -0
- package/harness/bundles/java/pi/harness-submit/SKILL.md +189 -0
- package/harness/bundles/java/pi/harness-submit/checklist.md +452 -0
- package/harness/bundles/java/pi/harness-submit/reference.md +152 -0
- package/harness/bundles/java/pi/harness-sync/SKILL.md +72 -0
- package/harness/bundles/java/pi/harness-sync/reference.md +176 -0
- package/harness/bundles/java/pi/harness-test/SKILL.md +264 -0
- package/harness/bundles/java/pi/harness-test/checklist.md +320 -0
- package/harness/bundles/java/pi/harness-test/pitfalls-java.md +16 -0
- package/harness/bundles/java/pi/harness-test/pitfalls.md +307 -0
- package/harness/bundles/java/pi/harness-test/reference.md +792 -0
- package/harness/bundles/java/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/java/pi/protocols/archive-report-protocol.md +196 -0
- package/harness/bundles/java/pi/protocols/ci-layering-protocol.md +22 -0
- package/harness/bundles/java/pi/protocols/evidence-based-reporting-protocol.md +134 -0
- package/harness/bundles/java/pi/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/java/pi/protocols/external-platform-protocol.md +27 -0
- package/harness/bundles/java/pi/protocols/ledger-protocol.md +365 -0
- package/harness/bundles/java/pi/protocols/memory-closeout-protocol.md +21 -0
- package/harness/bundles/java/pi/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/pi/protocols/powershell-protocol.md +238 -0
- package/harness/bundles/java/pi/protocols/registry-governance-protocol.md +31 -0
- package/harness/bundles/java/pi/protocols/report-pipeline-protocol.md +172 -0
- package/harness/bundles/java/pi/protocols/sensitive-info-protocol.md +110 -0
- package/harness/bundles/java/pi/protocols/state-layout-protocol.md +209 -0
- package/harness/bundles/java/pi/protocols/submit-protocol.md +78 -0
- package/harness/bundles/java/pi/protocols/verification-graph-protocol.md +32 -0
- package/harness/bundles/java/pi/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/java/pi/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/pi/scripts/harness_archive.py +11345 -0
- package/harness/bundles/java/pi/scripts/harness_change.py +1626 -0
- package/harness/bundles/java/pi/scripts/harness_check_gate.py +113 -0
- package/harness/bundles/java/pi/scripts/harness_context.py +1774 -0
- package/harness/bundles/java/pi/scripts/harness_deploy.py +968 -0
- package/harness/bundles/java/pi/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/java/pi/scripts/harness_environment.py +1690 -0
- package/harness/bundles/java/pi/scripts/harness_events.py +2234 -0
- package/harness/bundles/java/pi/scripts/harness_events_sync.py +1425 -0
- package/harness/bundles/java/pi/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/pi/scripts/harness_fixback.py +1975 -0
- package/harness/bundles/java/pi/scripts/harness_gate.py +3816 -0
- package/harness/bundles/java/pi/scripts/harness_headless.py +37 -0
- package/harness/bundles/java/pi/scripts/harness_integration.py +2564 -0
- package/harness/bundles/java/pi/scripts/harness_knowledge_candidates.py +796 -0
- package/harness/bundles/java/pi/scripts/harness_ledger.py +3556 -0
- package/harness/bundles/java/pi/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/pi/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/pi/scripts/harness_paths.py +676 -0
- package/harness/bundles/java/pi/scripts/harness_phase.py +1726 -0
- package/harness/bundles/java/pi/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/pi/scripts/harness_plan_finalize.py +1016 -0
- package/harness/bundles/java/pi/scripts/harness_preflight.py +650 -0
- package/harness/bundles/java/pi/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/pi/scripts/harness_profile.py +1817 -0
- package/harness/bundles/java/pi/scripts/harness_report_model.py +442 -0
- package/harness/bundles/java/pi/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/pi/scripts/harness_review.py +631 -0
- package/harness/bundles/java/pi/scripts/harness_runtime.py +2243 -0
- package/harness/bundles/java/pi/scripts/harness_service.py +3355 -0
- package/harness/bundles/java/pi/scripts/harness_state.py +365 -0
- package/harness/bundles/java/pi/scripts/harness_sync.py +407 -0
- package/harness/bundles/java/pi/scripts/harness_test_cleanup.py +175 -0
- package/harness/bundles/java/pi/scripts/harness_test_guard.py +1899 -0
- package/harness/bundles/java/pi/scripts/harness_test_runner.py +1586 -0
- package/harness/bundles/java/pi/scripts/harness_verification.py +593 -0
- package/harness/bundles/java/pi/scripts/harness_workflow_policy.py +266 -0
- package/harness/manifests/general/claude-code.json +19 -19
- package/harness/manifests/general/codebuddy.json +19 -19
- package/harness/manifests/general/codex.json +19 -19
- package/harness/manifests/general/cursor.json +19 -19
- package/harness/manifests/general/pi.json +475 -0
- package/harness/manifests/java/claude-code.json +21 -21
- package/harness/manifests/java/codebuddy.json +21 -21
- package/harness/manifests/java/codex.json +21 -21
- package/harness/manifests/java/cursor.json +21 -21
- package/harness/manifests/java/pi.json +499 -0
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Harness deploy: build core + overlay into self-contained skills tree (D9/D12).
|
|
3
|
+
|
|
4
|
+
Python 3.10+ stdlib only. UTF-8 without BOM.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import datetime as dt
|
|
11
|
+
import filecmp
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
import uuid
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Iterable
|
|
23
|
+
|
|
24
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
25
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
26
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
27
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
INCLUDE_RE = re.compile(r"<!--\s*@include\s+shared/([^\s]+)\s*-->")
|
|
30
|
+
# retro §5.17: wiki links [[shared/xxx.md|alias]] reference shared fragments
|
|
31
|
+
# that are inlined by expand_includes. After expansion, replace the wiki link
|
|
32
|
+
# with the alias text (the shared content is already in the document).
|
|
33
|
+
WIKI_LINK_RE = re.compile(r"\[\[shared/([^\]|]+)\|([^\]]*)\]\]")
|
|
34
|
+
# §7.5: overlay anchors support both new section-id and legacy section:"title".
|
|
35
|
+
OVERRIDE_RE = re.compile(r'<!--\s*@override\s+(?:section-id:"([^"]+)"|section:"([^"]+)")\s*-->')
|
|
36
|
+
APPEND_RE = re.compile(r'<!--\s*@append-after\s+(?:section-id:"([^"]+)"|section:"([^"]+)")\s*-->')
|
|
37
|
+
SECTION_ID_RE = re.compile(r"<!--\s*@section-id\s+([\w.-]+)\s*-->")
|
|
38
|
+
HEADING_RE = re.compile(r"^(#{2,6})\s+(.+?)\s*$")
|
|
39
|
+
FRAGMENT_HINT_RE = re.compile(r"^>\s*片段:\[\[shared/[^\]]+\]\]\s*.*$")
|
|
40
|
+
|
|
41
|
+
BUILD_MARKER = ".harness-build.json"
|
|
42
|
+
_REPLACE_RETRY_DELAYS_SECONDS = (0.05, 0.1, 0.2, 0.4, 0.8)
|
|
43
|
+
# Install-side managed manifests record which files a prior install owned, so a
|
|
44
|
+
# later conservative install can delete removed managed files while preserving
|
|
45
|
+
# user-owned files (design §3.8 要点4). Skills manifest lives in the skills
|
|
46
|
+
# target dir; agents manifest lives in .claude/agents.
|
|
47
|
+
MANIFEST_NAME = ".harness-managed.json"
|
|
48
|
+
AGENTS_MANAGED_NAME = ".harness-managed-agents.json"
|
|
49
|
+
INSTALL_METADATA = frozenset({MANIFEST_NAME, AGENTS_MANAGED_NAME})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read_managed_set(path: Path) -> set[str]:
|
|
53
|
+
"""Return the ``managedFiles`` set from a prior install manifest, or empty."""
|
|
54
|
+
if not path.is_file():
|
|
55
|
+
return set()
|
|
56
|
+
try:
|
|
57
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
58
|
+
except (json.JSONDecodeError, ValueError):
|
|
59
|
+
return set()
|
|
60
|
+
files = data.get("managedFiles") if isinstance(data, dict) else None
|
|
61
|
+
return set(files) if isinstance(files, list) else set()
|
|
62
|
+
|
|
63
|
+
SKIP_DIR_NAMES = {
|
|
64
|
+
"__pycache__",
|
|
65
|
+
".pytest_cache",
|
|
66
|
+
"redesign",
|
|
67
|
+
"shared",
|
|
68
|
+
"overlays",
|
|
69
|
+
"adapters",
|
|
70
|
+
".git",
|
|
71
|
+
}
|
|
72
|
+
SKIP_TOP_NAMES = {"harness-merge", "harness-report"}
|
|
73
|
+
HARNESS_AGENTS = ("claude-code", "codex", "cursor", "codebuddy", "pi")
|
|
74
|
+
AGENTS_WITH_CUSTOM_AGENTS = frozenset({"claude-code", "codebuddy"})
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def now_iso() -> str:
|
|
78
|
+
return dt.datetime.now().astimezone().isoformat(timespec="seconds")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def replace_with_retry(source: Path, target: Path) -> None:
|
|
82
|
+
"""Bounded retry for transient directory locks during atomic swaps.
|
|
83
|
+
|
|
84
|
+
Windows indexers and antivirus scanners may briefly hold a newly generated
|
|
85
|
+
bundle directory. Retrying only ``PermissionError`` keeps real path and
|
|
86
|
+
contract failures fail-fast while avoiding an expensive full bundle rerun.
|
|
87
|
+
"""
|
|
88
|
+
for attempt in range(len(_REPLACE_RETRY_DELAYS_SECONDS) + 1):
|
|
89
|
+
try:
|
|
90
|
+
os.replace(source, target)
|
|
91
|
+
return
|
|
92
|
+
except PermissionError:
|
|
93
|
+
if attempt >= len(_REPLACE_RETRY_DELAYS_SECONDS):
|
|
94
|
+
raise
|
|
95
|
+
time.sleep(_REPLACE_RETRY_DELAYS_SECONDS[attempt])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def emit_json(payload: dict[str, Any], *, ok: bool = True) -> int:
|
|
99
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
100
|
+
return 0 if ok else 1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def normalize_section_title(title: str) -> str:
|
|
104
|
+
t = title.strip()
|
|
105
|
+
t = re.sub(r"\s*⚠️.*$", "", t)
|
|
106
|
+
t = re.sub(r"\s*([^)]*)\s*$", "", t)
|
|
107
|
+
return t.strip()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def sha256_file(path: Path) -> str:
|
|
111
|
+
h = hashlib.sha256()
|
|
112
|
+
with path.open("rb") as fh:
|
|
113
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
114
|
+
h.update(chunk)
|
|
115
|
+
return h.hexdigest()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def iter_copy_entries(skills_root: Path, agent: str = "claude-code") -> Iterable[Path]:
|
|
119
|
+
for entry in sorted(skills_root.iterdir()):
|
|
120
|
+
if entry.name in SKIP_TOP_NAMES:
|
|
121
|
+
continue
|
|
122
|
+
if entry.is_dir() and entry.name in SKIP_DIR_NAMES:
|
|
123
|
+
continue
|
|
124
|
+
if entry.is_dir() and entry.name == "agents" and agent not in AGENTS_WITH_CUSTOM_AGENTS:
|
|
125
|
+
continue
|
|
126
|
+
if entry.is_file() and entry.name.startswith("."):
|
|
127
|
+
continue
|
|
128
|
+
yield entry
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
RUNTIME_IGNORE = shutil.ignore_patterns(
|
|
132
|
+
"tests", "__pycache__", "*.pyc", ".pytest_cache", "_last_run.txt", "_same_proc_*"
|
|
133
|
+
)
|
|
134
|
+
RUNTIME_SKIP_FILES = {"_last_run.txt", "_same_proc_pid.txt", "_same_proc_tmp.txt"}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def copy_tree(skills_root: Path, out_dir: Path, agent: str = "claude-code") -> list[str]:
|
|
138
|
+
copied: list[str] = []
|
|
139
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
for entry in iter_copy_entries(skills_root, agent):
|
|
141
|
+
dest = out_dir / entry.name
|
|
142
|
+
if entry.is_dir():
|
|
143
|
+
if entry.name == "scripts":
|
|
144
|
+
# §7.4: runtime keeps only scripts/*.py; scripts/tests/ excluded.
|
|
145
|
+
# harness_acceptance.py is an acceptance meta-tool (not a runtime
|
|
146
|
+
# skill script) and is excluded from the deployed tree.
|
|
147
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
for py in sorted(entry.glob("*.py")):
|
|
149
|
+
if py.name == "harness_acceptance.py":
|
|
150
|
+
continue
|
|
151
|
+
shutil.copy2(py, dest / py.name)
|
|
152
|
+
copied.append(f"scripts/{py.name}")
|
|
153
|
+
else:
|
|
154
|
+
shutil.copytree(entry, dest, ignore=RUNTIME_IGNORE)
|
|
155
|
+
copied.append(entry.name + "/")
|
|
156
|
+
else:
|
|
157
|
+
if entry.name in RUNTIME_SKIP_FILES or entry.name.endswith(".pyc"):
|
|
158
|
+
continue
|
|
159
|
+
shutil.copy2(entry, dest)
|
|
160
|
+
copied.append(entry.name)
|
|
161
|
+
return copied
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def expand_includes(text: str, shared_dir: Path) -> str:
|
|
165
|
+
def repl(match: re.Match[str]) -> str:
|
|
166
|
+
rel = match.group(1)
|
|
167
|
+
inc_path = shared_dir / rel
|
|
168
|
+
if not inc_path.is_file():
|
|
169
|
+
raise FileNotFoundError(f"shared include not found: {rel}")
|
|
170
|
+
body = inc_path.read_text(encoding="utf-8").rstrip()
|
|
171
|
+
return body
|
|
172
|
+
|
|
173
|
+
expanded = INCLUDE_RE.sub(repl, text)
|
|
174
|
+
# §5.17: replace [[shared/xxx.md|alias]] wiki links with the alias text,
|
|
175
|
+
# since the shared content has been inlined by @include expansion above.
|
|
176
|
+
expanded = WIKI_LINK_RE.sub(lambda m: m.group(2), expanded)
|
|
177
|
+
lines = [ln for ln in expanded.splitlines() if not FRAGMENT_HINT_RE.match(ln)]
|
|
178
|
+
return "\n".join(lines) + ("\n" if text.endswith("\n") else "")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def find_section_span(lines: list[str], section: str) -> tuple[int, int, int]:
|
|
182
|
+
target = normalize_section_title(section)
|
|
183
|
+
start = end = level = -1
|
|
184
|
+
for i, line in enumerate(lines):
|
|
185
|
+
m = HEADING_RE.match(line)
|
|
186
|
+
if not m:
|
|
187
|
+
continue
|
|
188
|
+
title = normalize_section_title(m.group(2))
|
|
189
|
+
if title == target or title.startswith(target):
|
|
190
|
+
start = i
|
|
191
|
+
level = len(m.group(1))
|
|
192
|
+
break
|
|
193
|
+
if start < 0:
|
|
194
|
+
raise KeyError(section)
|
|
195
|
+
end = len(lines)
|
|
196
|
+
for j in range(start + 1, len(lines)):
|
|
197
|
+
m = HEADING_RE.match(lines[j])
|
|
198
|
+
if m and len(m.group(1)) <= level:
|
|
199
|
+
end = j
|
|
200
|
+
break
|
|
201
|
+
return start, end, level
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_section_ids(text: str) -> dict[str, tuple[int, int, int]]:
|
|
205
|
+
"""Parse ``<!-- @section-id name -->`` markers; map id -> (start, end, level)
|
|
206
|
+
of the heading that follows. Raise ValueError on duplicate ids (§7.5)."""
|
|
207
|
+
lines = text.splitlines()
|
|
208
|
+
ids: dict[str, tuple[int, int, int]] = {}
|
|
209
|
+
pending: str | None = None
|
|
210
|
+
for i, line in enumerate(lines):
|
|
211
|
+
m = SECTION_ID_RE.search(line)
|
|
212
|
+
if m:
|
|
213
|
+
sid = m.group(1)
|
|
214
|
+
if sid in ids:
|
|
215
|
+
raise ValueError(f"duplicate section-id: {sid}")
|
|
216
|
+
pending = sid
|
|
217
|
+
continue
|
|
218
|
+
hm = HEADING_RE.match(line)
|
|
219
|
+
if hm and pending is not None:
|
|
220
|
+
level = len(hm.group(1))
|
|
221
|
+
start = i
|
|
222
|
+
end = len(lines)
|
|
223
|
+
for j in range(i + 1, len(lines)):
|
|
224
|
+
m2 = HEADING_RE.match(lines[j])
|
|
225
|
+
if m2 and len(m2.group(1)) <= level:
|
|
226
|
+
end = j
|
|
227
|
+
break
|
|
228
|
+
ids[pending] = (start, end, level)
|
|
229
|
+
pending = None
|
|
230
|
+
return ids
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def apply_overlay_blocks(
|
|
234
|
+
core_text: str, overlay_text: str, section_ids: dict[str, tuple[int, int, int]] | None = None
|
|
235
|
+
) -> str:
|
|
236
|
+
lines = core_text.splitlines()
|
|
237
|
+
pos = 0
|
|
238
|
+
while pos < len(overlay_text):
|
|
239
|
+
override = OVERRIDE_RE.search(overlay_text, pos)
|
|
240
|
+
append = APPEND_RE.search(overlay_text, pos)
|
|
241
|
+
candidates = [(m.start(), "override", m) for m in [override] if m]
|
|
242
|
+
candidates += [(m.start(), "append", m) for m in [append] if m]
|
|
243
|
+
if not candidates:
|
|
244
|
+
break
|
|
245
|
+
candidates.sort(key=lambda x: x[0])
|
|
246
|
+
_, kind, match = candidates[0]
|
|
247
|
+
section_id = match.group(1) # new style, may be None
|
|
248
|
+
section_title = match.group(2) # legacy style, may be None
|
|
249
|
+
content_start = match.end()
|
|
250
|
+
next_marker = min(
|
|
251
|
+
(
|
|
252
|
+
m.start()
|
|
253
|
+
for m in (
|
|
254
|
+
OVERRIDE_RE.search(overlay_text, content_start),
|
|
255
|
+
APPEND_RE.search(overlay_text, content_start),
|
|
256
|
+
)
|
|
257
|
+
if m
|
|
258
|
+
),
|
|
259
|
+
default=len(overlay_text),
|
|
260
|
+
)
|
|
261
|
+
block = overlay_text[content_start:next_marker].strip("\n")
|
|
262
|
+
if section_id:
|
|
263
|
+
if not section_ids or section_id not in section_ids:
|
|
264
|
+
raise KeyError(f"section-id:{section_id}")
|
|
265
|
+
start, end, _ = section_ids[section_id]
|
|
266
|
+
else:
|
|
267
|
+
start, end, _ = find_section_span(lines, section_title)
|
|
268
|
+
if kind == "override":
|
|
269
|
+
replacement = block.splitlines()
|
|
270
|
+
lines = lines[:start] + replacement + lines[end:]
|
|
271
|
+
else:
|
|
272
|
+
insertion = block.splitlines()
|
|
273
|
+
lines = lines[:end] + insertion + lines[end:]
|
|
274
|
+
pos = next_marker
|
|
275
|
+
return "\n".join(lines) + ("\n" if core_text.endswith("\n") else "")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _is_relative_to(child: Path, parent: Path) -> bool:
|
|
279
|
+
try:
|
|
280
|
+
child.relative_to(parent)
|
|
281
|
+
return True
|
|
282
|
+
except ValueError:
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def validate_build_paths(skills_root: Path, out_dir: Path) -> tuple[Path, Path]:
|
|
287
|
+
"""Return resolved safe paths; raise ValueError for every forbidden relation.
|
|
288
|
+
|
|
289
|
+
§7.1: refuse out==source, out as ancestor of source, out inside source,
|
|
290
|
+
an existing non-empty out without the build marker, and obvious danger
|
|
291
|
+
dirs (user home).
|
|
292
|
+
"""
|
|
293
|
+
skills_root = skills_root.resolve()
|
|
294
|
+
out_dir = out_dir.resolve()
|
|
295
|
+
|
|
296
|
+
if out_dir == skills_root:
|
|
297
|
+
raise ValueError(f"out_dir must not equal skills_root: {out_dir}")
|
|
298
|
+
if _is_relative_to(skills_root, out_dir):
|
|
299
|
+
raise ValueError(f"out_dir must not be an ancestor of skills_root: {out_dir}")
|
|
300
|
+
if _is_relative_to(out_dir, skills_root):
|
|
301
|
+
raise ValueError(f"out_dir must not be inside skills_root: {out_dir}")
|
|
302
|
+
try:
|
|
303
|
+
home = Path.home().resolve()
|
|
304
|
+
except OSError:
|
|
305
|
+
home = None
|
|
306
|
+
if home is not None and out_dir == home:
|
|
307
|
+
raise ValueError(f"out_dir must not be the user home: {out_dir}")
|
|
308
|
+
|
|
309
|
+
if out_dir.exists() and out_dir.is_dir():
|
|
310
|
+
has_marker = (out_dir / BUILD_MARKER).is_file()
|
|
311
|
+
has_user_files = any(p.name != BUILD_MARKER for p in out_dir.iterdir())
|
|
312
|
+
if has_user_files and not has_marker:
|
|
313
|
+
raise ValueError(
|
|
314
|
+
f"out_dir exists with user files and no {BUILD_MARKER} marker: {out_dir}"
|
|
315
|
+
)
|
|
316
|
+
return skills_root, out_dir
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def core_content_hash(
|
|
320
|
+
skills_root: Path,
|
|
321
|
+
overlay_dir: Path | None,
|
|
322
|
+
agent_overlay_dir: Path | None = None,
|
|
323
|
+
) -> str:
|
|
324
|
+
"""SHA-256 over the actual core/shared/protocol/script/overlay files that
|
|
325
|
+
participate in the build (path-relative + content). Deterministic across
|
|
326
|
+
time and machine paths (§7.3)."""
|
|
327
|
+
h = hashlib.sha256()
|
|
328
|
+
files: list[Path] = []
|
|
329
|
+
# Hash exactly the runtime source universe copied by copy_tree, including
|
|
330
|
+
# references/templates/checklists rather than only SKILL.md files.
|
|
331
|
+
for entry in iter_copy_entries(skills_root):
|
|
332
|
+
if entry.is_file():
|
|
333
|
+
if entry.name not in RUNTIME_SKIP_FILES and not entry.name.endswith(".pyc"):
|
|
334
|
+
files.append(entry)
|
|
335
|
+
elif entry.name == "scripts":
|
|
336
|
+
files.extend(p for p in entry.glob("*.py") if p.name != "harness_acceptance.py")
|
|
337
|
+
else:
|
|
338
|
+
files.extend(
|
|
339
|
+
p for p in entry.rglob("*")
|
|
340
|
+
if p.is_file() and "__pycache__" not in p.parts and "tests" not in p.parts
|
|
341
|
+
and not p.name.endswith(".pyc") and p.name not in RUNTIME_SKIP_FILES
|
|
342
|
+
)
|
|
343
|
+
if overlay_dir and overlay_dir.is_dir():
|
|
344
|
+
files.extend(p for p in overlay_dir.rglob("*") if p.is_file())
|
|
345
|
+
if agent_overlay_dir and agent_overlay_dir.is_dir():
|
|
346
|
+
files.extend(p for p in agent_overlay_dir.rglob("*") if p.is_file())
|
|
347
|
+
skills_root_resolved = skills_root.resolve()
|
|
348
|
+
files_by_relative_path = {
|
|
349
|
+
f.resolve().relative_to(skills_root_resolved).as_posix(): f
|
|
350
|
+
for f in files
|
|
351
|
+
}
|
|
352
|
+
for rel in sorted(files_by_relative_path):
|
|
353
|
+
f = files_by_relative_path[rel]
|
|
354
|
+
h.update(rel.encode("utf-8"))
|
|
355
|
+
h.update(b"\0")
|
|
356
|
+
# Hash the actual build input with one algorithm regardless of whether the
|
|
357
|
+
# source happens to live inside a Git checkout. Relative POSIX-path sorting
|
|
358
|
+
# keeps the order identical on case-insensitive Windows and Linux.
|
|
359
|
+
h.update(sha256_file(f).encode("ascii"))
|
|
360
|
+
h.update(b"\0")
|
|
361
|
+
return h.hexdigest()[:16]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def synthesis_header(
|
|
365
|
+
skills_root: Path,
|
|
366
|
+
overlay_dir: Path | None,
|
|
367
|
+
agent: str = "claude-code",
|
|
368
|
+
agent_overlay_dir: Path | None = None,
|
|
369
|
+
) -> str:
|
|
370
|
+
"""Deterministic synth header: no absolute path, no timestamp (§7.3)."""
|
|
371
|
+
core_hash = core_content_hash(skills_root, overlay_dir, agent_overlay_dir)
|
|
372
|
+
overlay_name = overlay_dir.name if overlay_dir else "none"
|
|
373
|
+
return (
|
|
374
|
+
f"<!-- generated by harness_deploy.py; core={core_hash}; "
|
|
375
|
+
f"overlay={overlay_name}; agent={agent}; do not edit -->\n"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def inject_header(text: str, header: str) -> str:
|
|
380
|
+
if text.startswith("---"):
|
|
381
|
+
parts = text.split("---", 2)
|
|
382
|
+
if len(parts) >= 3:
|
|
383
|
+
body = parts[2].lstrip("\n")
|
|
384
|
+
return f"---{parts[1]}---\n{header}{body}"
|
|
385
|
+
return header + text
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def process_skill_md(
|
|
389
|
+
path: Path,
|
|
390
|
+
shared_dir: Path,
|
|
391
|
+
overlay_path: Path | None,
|
|
392
|
+
header: str,
|
|
393
|
+
agent_overlay_path: Path | None = None,
|
|
394
|
+
) -> None:
|
|
395
|
+
text = path.read_text(encoding="utf-8")
|
|
396
|
+
text = expand_includes(text, shared_dir)
|
|
397
|
+
section_ids = parse_section_ids(text) # §7.5: raises on duplicate section-id
|
|
398
|
+
if overlay_path and overlay_path.is_file():
|
|
399
|
+
overlay = overlay_path.read_text(encoding="utf-8")
|
|
400
|
+
text = apply_overlay_blocks(text, overlay, section_ids)
|
|
401
|
+
section_ids = parse_section_ids(text)
|
|
402
|
+
if agent_overlay_path and agent_overlay_path.is_file():
|
|
403
|
+
agent_overlay = agent_overlay_path.read_text(encoding="utf-8")
|
|
404
|
+
text = apply_overlay_blocks(text, agent_overlay, section_ids)
|
|
405
|
+
text = inject_header(text, header)
|
|
406
|
+
if INCLUDE_RE.search(text):
|
|
407
|
+
raise RuntimeError(f"unexpanded include placeholder remains in {path}")
|
|
408
|
+
path.write_text(text, encoding="utf-8", newline="\n")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def overlay_for_skill(overlay_dir: Path, skill_name: str) -> Path | None:
|
|
412
|
+
candidate = overlay_dir / f"{skill_name}.overlay.md"
|
|
413
|
+
return candidate if candidate.is_file() else None
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def copy_adapter_agents(adapter_agents_dir: Path, staging: Path) -> list[str]:
|
|
417
|
+
"""Copy adapters/<agent>/agents/*.md over staging/agents/."""
|
|
418
|
+
if not adapter_agents_dir.is_dir():
|
|
419
|
+
return []
|
|
420
|
+
dest = staging / "agents"
|
|
421
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
422
|
+
copied: list[str] = []
|
|
423
|
+
for agent_file in sorted(adapter_agents_dir.glob("*.md")):
|
|
424
|
+
shutil.copy2(agent_file, dest / agent_file.name)
|
|
425
|
+
copied.append(f"agents/{agent_file.name}")
|
|
426
|
+
return copied
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def copy_overlay_unique_skills(overlay_dir: Path, out_dir: Path) -> list[str]:
|
|
430
|
+
added: list[str] = []
|
|
431
|
+
for entry in sorted(overlay_dir.iterdir()):
|
|
432
|
+
if not entry.is_dir():
|
|
433
|
+
continue
|
|
434
|
+
if not entry.name.startswith("harness-"):
|
|
435
|
+
continue
|
|
436
|
+
if not (entry / "SKILL.md").is_file():
|
|
437
|
+
continue
|
|
438
|
+
if (out_dir / entry.name).exists():
|
|
439
|
+
continue
|
|
440
|
+
shutil.copytree(
|
|
441
|
+
entry,
|
|
442
|
+
out_dir / entry.name,
|
|
443
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
|
444
|
+
)
|
|
445
|
+
added.append(entry.name + "/")
|
|
446
|
+
return added
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def merge_overlay_skill_extras(overlay_dir: Path, out_dir: Path) -> list[str]:
|
|
450
|
+
"""Copy overlays/java/harness-<name>/* into existing out/harness-<name>/ (reference, etc.)."""
|
|
451
|
+
merged: list[str] = []
|
|
452
|
+
unique = {
|
|
453
|
+
p.name
|
|
454
|
+
for p in overlay_dir.iterdir()
|
|
455
|
+
if p.is_dir() and (p / "SKILL.md").is_file()
|
|
456
|
+
}
|
|
457
|
+
for entry in sorted(overlay_dir.iterdir()):
|
|
458
|
+
if not entry.is_dir() or not entry.name.startswith("harness-"):
|
|
459
|
+
continue
|
|
460
|
+
if entry.name in unique:
|
|
461
|
+
continue
|
|
462
|
+
dest_skill = out_dir / entry.name
|
|
463
|
+
if not dest_skill.is_dir():
|
|
464
|
+
continue
|
|
465
|
+
for src in entry.rglob("*"):
|
|
466
|
+
if src.is_dir() or "__pycache__" in src.parts:
|
|
467
|
+
continue
|
|
468
|
+
rel = src.relative_to(entry)
|
|
469
|
+
dest = dest_skill / rel
|
|
470
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
471
|
+
shutil.copy2(src, dest)
|
|
472
|
+
merged.append(f"{entry.name}/{rel.as_posix()}")
|
|
473
|
+
return merged
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def copy_overlay_extras(overlay_dir: Path, out_dir: Path) -> list[str]:
|
|
477
|
+
extras: list[str] = []
|
|
478
|
+
# §7.4: PROJECT-PROFILE-EXAMPLE.md stays a Vault doc (UDP real values);
|
|
479
|
+
# it must NOT be installed to runtime. Only pitfalls-java.md is runtime.
|
|
480
|
+
pitfalls = overlay_dir / "pitfalls-java.md"
|
|
481
|
+
if pitfalls.is_file():
|
|
482
|
+
test_pit = out_dir / "harness-test" / "pitfalls-java.md"
|
|
483
|
+
test_pit.parent.mkdir(parents=True, exist_ok=True)
|
|
484
|
+
shutil.copy2(pitfalls, test_pit)
|
|
485
|
+
extras.append("harness-test/pitfalls-java.md")
|
|
486
|
+
return extras
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def cmd_build(
|
|
490
|
+
skills_root: Path,
|
|
491
|
+
out_dir: Path,
|
|
492
|
+
overlay: str | None,
|
|
493
|
+
agent: str | None = None,
|
|
494
|
+
) -> dict[str, Any]:
|
|
495
|
+
skills_root, out_dir = validate_build_paths(skills_root, out_dir)
|
|
496
|
+
shared_dir = skills_root / "shared"
|
|
497
|
+
if not shared_dir.is_dir():
|
|
498
|
+
raise FileNotFoundError(f"shared/ missing under {skills_root}")
|
|
499
|
+
if agent is None:
|
|
500
|
+
print(
|
|
501
|
+
"WARNING: --agent missing, defaulting to claude-code (deprecated)",
|
|
502
|
+
file=sys.stderr,
|
|
503
|
+
)
|
|
504
|
+
agent = "claude-code"
|
|
505
|
+
if agent not in HARNESS_AGENTS:
|
|
506
|
+
raise ValueError(f"unknown agent: {agent}")
|
|
507
|
+
overlay_dir = skills_root / "overlays" / overlay if overlay else None
|
|
508
|
+
if overlay and (not overlay_dir or not overlay_dir.is_dir()):
|
|
509
|
+
raise FileNotFoundError(f"overlay not found: overlays/{overlay}")
|
|
510
|
+
agent_overlay_dir = skills_root / "adapters" / agent / "skill-overlays"
|
|
511
|
+
if not agent_overlay_dir.is_dir():
|
|
512
|
+
agent_overlay_dir = None
|
|
513
|
+
adapter_agents_dir = skills_root / "adapters" / agent / "agents"
|
|
514
|
+
|
|
515
|
+
# §7.2: build entirely in a staging dir; out_dir is untouched until the
|
|
516
|
+
# atomic swap. Never `shutil.rmtree(out_dir)` at build start.
|
|
517
|
+
staging = out_dir.parent / f".{out_dir.name}.staging-{uuid.uuid4().hex[:8]}"
|
|
518
|
+
if staging.exists():
|
|
519
|
+
shutil.rmtree(staging)
|
|
520
|
+
try:
|
|
521
|
+
copied = copy_tree(skills_root, staging, agent)
|
|
522
|
+
if overlay:
|
|
523
|
+
copied.extend(copy_overlay_unique_skills(overlay_dir, staging))
|
|
524
|
+
copied.extend(copy_overlay_extras(overlay_dir, staging))
|
|
525
|
+
copied.extend(merge_overlay_skill_extras(overlay_dir, staging))
|
|
526
|
+
if agent in AGENTS_WITH_CUSTOM_AGENTS:
|
|
527
|
+
copied.extend(copy_adapter_agents(adapter_agents_dir, staging))
|
|
528
|
+
|
|
529
|
+
header = synthesis_header(skills_root, overlay_dir, agent, agent_overlay_dir)
|
|
530
|
+
processed: list[str] = []
|
|
531
|
+
for skill_md in sorted(staging.glob("harness-*/SKILL.md")):
|
|
532
|
+
ov = overlay_for_skill(overlay_dir, skill_md.parent.name) if overlay_dir else None
|
|
533
|
+
agent_ov = (
|
|
534
|
+
overlay_for_skill(agent_overlay_dir, skill_md.parent.name)
|
|
535
|
+
if agent_overlay_dir
|
|
536
|
+
else None
|
|
537
|
+
)
|
|
538
|
+
try:
|
|
539
|
+
process_skill_md(skill_md, shared_dir, ov, header, agent_ov)
|
|
540
|
+
except KeyError as exc:
|
|
541
|
+
raise KeyError(
|
|
542
|
+
f"overlay anchor not found: {exc.args[0]} ({skill_md})"
|
|
543
|
+
) from exc
|
|
544
|
+
processed.append(str(skill_md.relative_to(staging)))
|
|
545
|
+
|
|
546
|
+
for skill_md in sorted(staging.glob("harness-*/SKILL.md")):
|
|
547
|
+
if INCLUDE_RE.search(skill_md.read_text(encoding="utf-8")):
|
|
548
|
+
raise RuntimeError(f"include placeholder remains: {skill_md}")
|
|
549
|
+
|
|
550
|
+
# deterministic build marker (no timestamp -> byte-identical builds)
|
|
551
|
+
core_hash = core_content_hash(skills_root, overlay_dir, agent_overlay_dir)
|
|
552
|
+
(staging / BUILD_MARKER).write_text(
|
|
553
|
+
json.dumps(
|
|
554
|
+
{
|
|
555
|
+
"schemaVersion": 1,
|
|
556
|
+
"overlay": overlay or "none",
|
|
557
|
+
"agent": agent,
|
|
558
|
+
"coreHash": core_hash,
|
|
559
|
+
},
|
|
560
|
+
ensure_ascii=False,
|
|
561
|
+
indent=2,
|
|
562
|
+
)
|
|
563
|
+
+ "\n",
|
|
564
|
+
encoding="utf-8",
|
|
565
|
+
newline="\n",
|
|
566
|
+
)
|
|
567
|
+
except Exception:
|
|
568
|
+
# build failed: clean staging, leave out_dir completely untouched
|
|
569
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
570
|
+
raise
|
|
571
|
+
|
|
572
|
+
# atomic swap: move old out aside (if a valid build), rename staging -> out
|
|
573
|
+
old_backup = None
|
|
574
|
+
if out_dir.exists():
|
|
575
|
+
if (out_dir / BUILD_MARKER).is_file():
|
|
576
|
+
old_backup = out_dir.parent / f".{out_dir.name}.old-{uuid.uuid4().hex[:8]}"
|
|
577
|
+
replace_with_retry(out_dir, old_backup)
|
|
578
|
+
else:
|
|
579
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
580
|
+
raise ValueError(f"refusing to overwrite unmarked out_dir: {out_dir}")
|
|
581
|
+
try:
|
|
582
|
+
replace_with_retry(staging, out_dir)
|
|
583
|
+
except OSError:
|
|
584
|
+
if old_backup is not None and old_backup.exists():
|
|
585
|
+
replace_with_retry(old_backup, out_dir)
|
|
586
|
+
raise
|
|
587
|
+
if old_backup is not None:
|
|
588
|
+
shutil.rmtree(old_backup, ignore_errors=True)
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
"ok": True,
|
|
592
|
+
"action": "build",
|
|
593
|
+
"skillsRoot": str(skills_root),
|
|
594
|
+
"outDir": str(out_dir),
|
|
595
|
+
"overlay": overlay,
|
|
596
|
+
"agent": agent,
|
|
597
|
+
"copied": copied,
|
|
598
|
+
"processedSkills": processed,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def collect_files(root: Path) -> dict[str, str]:
|
|
603
|
+
files: dict[str, str] = {}
|
|
604
|
+
for path in sorted(root.rglob("*")):
|
|
605
|
+
if path.is_file() and "__pycache__" not in path.parts:
|
|
606
|
+
rel = path.relative_to(root).as_posix()
|
|
607
|
+
files[rel] = sha256_file(path)
|
|
608
|
+
return files
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
# --- Per-file bundle manifest (retro 2026-07-20 §5.1/5.25) -----------------
|
|
612
|
+
#
|
|
613
|
+
# registry_version + bundle_hash alone cannot prove every installed file
|
|
614
|
+
# belongs to the declared bundle. The bundle therefore ships a per-file
|
|
615
|
+
# manifest (relpath/sha256/size/mode/adapterTransformationId); install
|
|
616
|
+
# verifies staging content against it BEFORE the atomic switch and never
|
|
617
|
+
# updates metadata on partial failure.
|
|
618
|
+
|
|
619
|
+
BUNDLE_MANIFEST_NAME = "bundle-manifest.json"
|
|
620
|
+
_MANIFEST_EXCLUDED = frozenset(
|
|
621
|
+
{BUILD_MARKER, MANIFEST_NAME, AGENTS_MANAGED_NAME, BUNDLE_MANIFEST_NAME}
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def build_manifest(root: Path, transformation_id: str = "raw") -> list[dict[str, Any]]:
|
|
626
|
+
"""Return per-file manifest entries for every bundle file under ``root``.
|
|
627
|
+
|
|
628
|
+
Install metadata (build marker, managed manifests, the manifest itself)
|
|
629
|
+
is excluded. Entries are sorted by relpath for deterministic output.
|
|
630
|
+
"""
|
|
631
|
+
root = root.resolve()
|
|
632
|
+
entries: list[dict[str, Any]] = []
|
|
633
|
+
for path in sorted(root.rglob("*")):
|
|
634
|
+
if not path.is_file() or "__pycache__" in path.parts:
|
|
635
|
+
continue
|
|
636
|
+
rel = path.relative_to(root).as_posix()
|
|
637
|
+
if rel in _MANIFEST_EXCLUDED:
|
|
638
|
+
continue
|
|
639
|
+
stat = path.stat()
|
|
640
|
+
entries.append(
|
|
641
|
+
{
|
|
642
|
+
"relpath": rel,
|
|
643
|
+
"sha256": sha256_file(path),
|
|
644
|
+
"size": stat.st_size,
|
|
645
|
+
"mode": stat.st_mode,
|
|
646
|
+
"adapterTransformationId": transformation_id,
|
|
647
|
+
}
|
|
648
|
+
)
|
|
649
|
+
return entries
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def compute_bundle_manifest_hash(entries: list[dict[str, Any]]) -> str:
|
|
653
|
+
"""Deterministic aggregate hash over manifest entries (order-independent).
|
|
654
|
+
|
|
655
|
+
Must match packages/contracts/src/bundle-manifest.ts computeBundleManifestHash.
|
|
656
|
+
"""
|
|
657
|
+
ordered = sorted(entries, key=lambda entry: entry["relpath"])
|
|
658
|
+
lines = [
|
|
659
|
+
f"{entry['relpath']}:{entry['sha256']}:{entry['size']}:{entry['mode']}:{entry['adapterTransformationId']}"
|
|
660
|
+
for entry in ordered
|
|
661
|
+
]
|
|
662
|
+
return hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def aggregate_installed_content_hash(files: dict[str, str]) -> str:
|
|
666
|
+
"""Deterministic aggregate hash over actually-installed relpath->sha256."""
|
|
667
|
+
lines = [f"{rel}:{files[rel]}" for rel in sorted(files)]
|
|
668
|
+
return hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _verify_tree_against_manifest(
|
|
672
|
+
root: Path, entries: list[dict[str, Any]]
|
|
673
|
+
) -> list[dict[str, str]]:
|
|
674
|
+
"""Return mismatch details for files under ``root`` vs manifest entries."""
|
|
675
|
+
mismatches: list[dict[str, str]] = []
|
|
676
|
+
actual = collect_files(root)
|
|
677
|
+
declared = {entry["relpath"]: entry["sha256"] for entry in entries}
|
|
678
|
+
for rel, expected in sorted(declared.items()):
|
|
679
|
+
if rel not in actual:
|
|
680
|
+
mismatches.append({"relpath": rel, "expected": expected, "actual": "<missing>"})
|
|
681
|
+
elif actual[rel] != expected:
|
|
682
|
+
mismatches.append(
|
|
683
|
+
{"relpath": rel, "expected": expected, "actual": actual[rel]}
|
|
684
|
+
)
|
|
685
|
+
return mismatches
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def cmd_verify_installed(
|
|
689
|
+
installed_dir: Path, entries: list[dict[str, Any]], bundle_version: str
|
|
690
|
+
) -> dict[str, Any]:
|
|
691
|
+
"""Verify installed content against the bundle manifest, per file."""
|
|
692
|
+
installed_dir = installed_dir.resolve()
|
|
693
|
+
mismatches = _verify_tree_against_manifest(installed_dir, entries)
|
|
694
|
+
actual = collect_files(installed_dir)
|
|
695
|
+
declared = {entry["relpath"] for entry in entries}
|
|
696
|
+
content = {
|
|
697
|
+
rel: digest
|
|
698
|
+
for rel, digest in actual.items()
|
|
699
|
+
if rel not in _MANIFEST_EXCLUDED and rel in declared
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
"ok": not mismatches,
|
|
703
|
+
"action": "verify-installed",
|
|
704
|
+
"bundleVersion": bundle_version,
|
|
705
|
+
"verificationStatus": "verified" if not mismatches else "degraded",
|
|
706
|
+
"installedContentHash": aggregate_installed_content_hash(content),
|
|
707
|
+
"verifiedAt": now_iso(),
|
|
708
|
+
"mismatchDetails": mismatches,
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def cmd_install(build_out: Path, project: Path, target: Path | None) -> dict[str, Any]:
|
|
713
|
+
build_out = build_out.resolve()
|
|
714
|
+
project = project.resolve()
|
|
715
|
+
dest = (target or project / ".claude" / "skills").resolve()
|
|
716
|
+
if not (build_out / BUILD_MARKER).is_file():
|
|
717
|
+
raise ValueError(f"install source is not a marked harness build: {build_out}")
|
|
718
|
+
|
|
719
|
+
# Managed set = real files in the build (exclude build marker). Recorded so a
|
|
720
|
+
# later conservative install can delete removed managed files and preserve
|
|
721
|
+
# user-owned files (design §3.8 要点4). Paths are relative to dest, which
|
|
722
|
+
# mirrors build_out (agents/ included because copy_tree keeps it there).
|
|
723
|
+
managed_new = {p for p in collect_files(build_out) if p != BUILD_MARKER}
|
|
724
|
+
managed_agents_new = {
|
|
725
|
+
p[len("agents/"):]
|
|
726
|
+
for p in managed_new
|
|
727
|
+
if p.startswith("agents/") and "/" not in p[len("agents/"):]
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
backup: str | None = None
|
|
731
|
+
staging = dest.parent / f".{dest.name}.staging-{uuid.uuid4().hex[:8]}"
|
|
732
|
+
shutil.copytree(build_out, staging)
|
|
733
|
+
# Per-file content proof (retro §5.1): if the build carries a
|
|
734
|
+
# bundle-manifest.json, staging must match it exactly before anything is
|
|
735
|
+
# switched. A mismatch aborts the install without touching dest or any
|
|
736
|
+
# metadata — never a partial install under a fresh version label.
|
|
737
|
+
manifest_path = staging / BUNDLE_MANIFEST_NAME
|
|
738
|
+
if manifest_path.is_file():
|
|
739
|
+
manifest_data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
740
|
+
manifest_entries = (
|
|
741
|
+
manifest_data.get("files", []) if isinstance(manifest_data, dict) else []
|
|
742
|
+
)
|
|
743
|
+
mismatches = _verify_tree_against_manifest(staging, manifest_entries)
|
|
744
|
+
if mismatches:
|
|
745
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
746
|
+
raise ValueError(
|
|
747
|
+
"BUNDLE_CONTENT_MISMATCH: staging diverges from bundle-manifest.json: "
|
|
748
|
+
+ ", ".join(m["relpath"] for m in mismatches)
|
|
749
|
+
)
|
|
750
|
+
if dest.exists() and any(dest.iterdir()):
|
|
751
|
+
managed_old = _read_managed_set(dest / MANIFEST_NAME)
|
|
752
|
+
# Preserve user-owned files: anything in dest that was NOT managed by a
|
|
753
|
+
# prior install is copied into staging (never overwriting managed files
|
|
754
|
+
# that staging already carries from build_out). Removed managed files
|
|
755
|
+
# (in managed_old but not managed_new) are simply not carried over.
|
|
756
|
+
for rel in collect_files(dest):
|
|
757
|
+
if rel in (BUILD_MARKER, MANIFEST_NAME) or rel in INSTALL_METADATA:
|
|
758
|
+
continue
|
|
759
|
+
if rel in managed_old:
|
|
760
|
+
continue
|
|
761
|
+
dst = staging / rel
|
|
762
|
+
if dst.exists():
|
|
763
|
+
continue
|
|
764
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
765
|
+
shutil.copy2(dest / rel, dst)
|
|
766
|
+
stamp = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
767
|
+
backup_path = dest.parent / f"skills-backup-{stamp}"
|
|
768
|
+
os.replace(dest, backup_path)
|
|
769
|
+
backup = str(backup_path)
|
|
770
|
+
try:
|
|
771
|
+
os.replace(staging, dest)
|
|
772
|
+
except OSError:
|
|
773
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
774
|
+
if backup:
|
|
775
|
+
os.replace(Path(backup), dest)
|
|
776
|
+
raise
|
|
777
|
+
|
|
778
|
+
# Record the managed set so the next conservative install can distinguish
|
|
779
|
+
# managed vs user-owned files.
|
|
780
|
+
(dest / MANIFEST_NAME).write_text(
|
|
781
|
+
json.dumps(
|
|
782
|
+
{"schemaVersion": 1, "managedFiles": sorted(managed_new)},
|
|
783
|
+
ensure_ascii=False,
|
|
784
|
+
indent=2,
|
|
785
|
+
)
|
|
786
|
+
+ "\n",
|
|
787
|
+
encoding="utf-8",
|
|
788
|
+
newline="\n",
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
agents_src = build_out / "agents"
|
|
792
|
+
agents_dest = project / ".claude" / "agents"
|
|
793
|
+
if agents_src.is_dir():
|
|
794
|
+
agents_dest.mkdir(parents=True, exist_ok=True)
|
|
795
|
+
agents_old = _read_managed_set(agents_dest / AGENTS_MANAGED_NAME)
|
|
796
|
+
# Delete managed agents removed since the prior install; user agents
|
|
797
|
+
# (never in a managed set) are left untouched.
|
|
798
|
+
for name in list(agents_old):
|
|
799
|
+
if name not in managed_agents_new:
|
|
800
|
+
(agents_dest / name).unlink(missing_ok=True)
|
|
801
|
+
for agent in agents_src.glob("*.md"):
|
|
802
|
+
shutil.copy2(agent, agents_dest / agent.name)
|
|
803
|
+
(agents_dest / AGENTS_MANAGED_NAME).write_text(
|
|
804
|
+
json.dumps(
|
|
805
|
+
{"schemaVersion": 1, "managedFiles": sorted(managed_agents_new)},
|
|
806
|
+
ensure_ascii=False,
|
|
807
|
+
indent=2,
|
|
808
|
+
)
|
|
809
|
+
+ "\n",
|
|
810
|
+
encoding="utf-8",
|
|
811
|
+
newline="\n",
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
"ok": True,
|
|
816
|
+
"action": "install",
|
|
817
|
+
"from": str(build_out),
|
|
818
|
+
"project": str(project),
|
|
819
|
+
"target": str(dest),
|
|
820
|
+
"backup": backup,
|
|
821
|
+
"installedAt": now_iso(),
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def cmd_validate_manifest(
|
|
826
|
+
bundle_dir: Path, manifest_entries: list[dict[str, Any]]
|
|
827
|
+
) -> dict[str, Any]:
|
|
828
|
+
"""Compare a bundle directory's actual file set against the manifest's
|
|
829
|
+
declared set. Both missing (declared but absent) and extra (present but
|
|
830
|
+
undeclared) fail validation (design §3.8 要点2)."""
|
|
831
|
+
bundle_dir = bundle_dir.resolve()
|
|
832
|
+
actual = collect_files(bundle_dir)
|
|
833
|
+
declared: dict[str, str] = {}
|
|
834
|
+
for entry in manifest_entries:
|
|
835
|
+
path = entry.get("path")
|
|
836
|
+
sha = entry.get("sha256")
|
|
837
|
+
if isinstance(path, str) and isinstance(sha, str):
|
|
838
|
+
declared[path] = sha
|
|
839
|
+
missing = sorted(p for p in declared if p not in actual)
|
|
840
|
+
extra = sorted(p for p in actual if p not in declared)
|
|
841
|
+
hash_mismatch = sorted(
|
|
842
|
+
p for p in declared if p in actual and actual[p] != declared[p]
|
|
843
|
+
)
|
|
844
|
+
return {
|
|
845
|
+
"ok": not (missing or extra or hash_mismatch),
|
|
846
|
+
"action": "validate-manifest",
|
|
847
|
+
"missing": missing,
|
|
848
|
+
"extra": extra,
|
|
849
|
+
"hashMismatch": hash_mismatch,
|
|
850
|
+
"comparedAt": now_iso(),
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
855
|
+
p = argparse.ArgumentParser(description="Harness skills deploy (build/install)")
|
|
856
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
857
|
+
|
|
858
|
+
b = sub.add_parser("build", help="Synthesize core + overlay into output dir")
|
|
859
|
+
b.add_argument("--skills-root", type=Path, required=True)
|
|
860
|
+
b.add_argument("--out", type=Path, required=True)
|
|
861
|
+
b.add_argument("--overlay")
|
|
862
|
+
b.add_argument("--agent", choices=HARNESS_AGENTS)
|
|
863
|
+
b.add_argument("--json", action="store_true")
|
|
864
|
+
|
|
865
|
+
i = sub.add_parser("install", help="Install build output to project .claude/skills")
|
|
866
|
+
i.add_argument("--from", dest="from_dir", type=Path, required=True)
|
|
867
|
+
i.add_argument("--project", type=Path, required=True)
|
|
868
|
+
i.add_argument("--target", type=Path)
|
|
869
|
+
i.add_argument("--json", action="store_true")
|
|
870
|
+
|
|
871
|
+
v = sub.add_parser(
|
|
872
|
+
"validate-manifest", help="Validate bundle dir vs manifest (missing/extra/hash)"
|
|
873
|
+
)
|
|
874
|
+
v.add_argument("--bundle", type=Path, required=True)
|
|
875
|
+
v.add_argument("--manifest", type=Path, required=True)
|
|
876
|
+
v.add_argument("--json", action="store_true")
|
|
877
|
+
|
|
878
|
+
g = sub.add_parser(
|
|
879
|
+
"generate-manifest",
|
|
880
|
+
help="Generate per-file bundle-manifest.json for a bundle dir",
|
|
881
|
+
)
|
|
882
|
+
g.add_argument("--bundle", type=Path, required=True)
|
|
883
|
+
g.add_argument("--bundle-version", required=True)
|
|
884
|
+
g.add_argument(
|
|
885
|
+
"--transformation",
|
|
886
|
+
choices=["raw", "adapted", "workflow-packaged"],
|
|
887
|
+
default="raw",
|
|
888
|
+
)
|
|
889
|
+
g.add_argument("--out", type=Path)
|
|
890
|
+
g.add_argument("--json", action="store_true")
|
|
891
|
+
|
|
892
|
+
vi = sub.add_parser(
|
|
893
|
+
"verify-installed",
|
|
894
|
+
help="Verify installed tree content against bundle-manifest.json",
|
|
895
|
+
)
|
|
896
|
+
vi.add_argument("--installed", type=Path, required=True)
|
|
897
|
+
vi.add_argument("--manifest", type=Path, required=True)
|
|
898
|
+
vi.add_argument("--json", action="store_true")
|
|
899
|
+
return p
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def main(argv: list[str] | None = None) -> int:
|
|
903
|
+
args = build_parser().parse_args(argv)
|
|
904
|
+
try:
|
|
905
|
+
if args.command == "build":
|
|
906
|
+
result = cmd_build(args.skills_root, args.out, args.overlay, args.agent)
|
|
907
|
+
return emit_json(result) if args.json else 0
|
|
908
|
+
if args.command == "install":
|
|
909
|
+
result = cmd_install(args.from_dir, args.project, args.target)
|
|
910
|
+
return emit_json(result) if args.json else 0
|
|
911
|
+
if args.command == "validate-manifest":
|
|
912
|
+
manifest_data = json.loads(args.manifest.read_text(encoding="utf-8"))
|
|
913
|
+
entries = (
|
|
914
|
+
manifest_data.get("files", [])
|
|
915
|
+
if isinstance(manifest_data, dict)
|
|
916
|
+
else []
|
|
917
|
+
)
|
|
918
|
+
result = cmd_validate_manifest(args.bundle, entries)
|
|
919
|
+
if args.json:
|
|
920
|
+
return emit_json(result)
|
|
921
|
+
return 0 if result["ok"] else 1
|
|
922
|
+
if args.command == "generate-manifest":
|
|
923
|
+
entries = build_manifest(args.bundle, transformation_id=args.transformation)
|
|
924
|
+
manifest = {
|
|
925
|
+
"schemaVersion": 1,
|
|
926
|
+
"bundleVersion": args.bundle_version,
|
|
927
|
+
"bundleManifestHash": compute_bundle_manifest_hash(entries),
|
|
928
|
+
"files": entries,
|
|
929
|
+
}
|
|
930
|
+
text = json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
|
|
931
|
+
if args.out is not None:
|
|
932
|
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
933
|
+
args.out.write_text(text, encoding="utf-8", newline="\n")
|
|
934
|
+
result = {
|
|
935
|
+
"ok": True,
|
|
936
|
+
"action": "generate-manifest",
|
|
937
|
+
"bundleVersion": args.bundle_version,
|
|
938
|
+
"bundleManifestHash": manifest["bundleManifestHash"],
|
|
939
|
+
"fileCount": len(entries),
|
|
940
|
+
"out": str(args.out) if args.out is not None else None,
|
|
941
|
+
}
|
|
942
|
+
if args.json:
|
|
943
|
+
return emit_json(result)
|
|
944
|
+
if args.out is None:
|
|
945
|
+
print(text, end="")
|
|
946
|
+
return 0
|
|
947
|
+
if args.command == "verify-installed":
|
|
948
|
+
manifest_data = json.loads(args.manifest.read_text(encoding="utf-8"))
|
|
949
|
+
if not isinstance(manifest_data, dict):
|
|
950
|
+
raise ValueError("manifest must be a JSON object")
|
|
951
|
+
result = cmd_verify_installed(
|
|
952
|
+
args.installed,
|
|
953
|
+
manifest_data.get("files", []),
|
|
954
|
+
str(manifest_data.get("bundleVersion", "")),
|
|
955
|
+
)
|
|
956
|
+
if args.json:
|
|
957
|
+
return emit_json(result)
|
|
958
|
+
return 0 if result["ok"] else 1
|
|
959
|
+
except (FileNotFoundError, KeyError, RuntimeError, ValueError) as exc:
|
|
960
|
+
if getattr(args, "json", False):
|
|
961
|
+
return emit_json({"ok": False, "error": str(exc)}, ok=False)
|
|
962
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
963
|
+
return 1
|
|
964
|
+
return 1
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
if __name__ == "__main__":
|
|
968
|
+
raise SystemExit(main())
|