@hunter-harness/workflow-harness 0.2.5 → 0.2.7
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/contracts/workflow-policy.json +169 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +7 -2
- package/harness/bundles/general/claude-code/harness-archive/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +109 -7
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +6 -2
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +8 -3
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +8 -3
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +13 -7
- package/harness/bundles/general/claude-code/harness-run/checklist.md +2 -2
- package/harness/bundles/general/claude-code/harness-run/reference.md +6 -3
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +11 -6
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +12 -3
- package/harness/bundles/general/claude-code/harness-test/reference.md +1 -1
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/claude-code/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +231 -48
- package/harness/bundles/general/claude-code/scripts/harness_change.py +854 -0
- package/harness/bundles/general/claude-code/scripts/harness_events.py +134 -40
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +1038 -0
- package/harness/bundles/general/claude-code/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +2 -2
- package/harness/bundles/general/claude-code/scripts/harness_service.py +34 -1
- package/harness/bundles/general/claude-code/scripts/harness_state.py +148 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +169 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +7 -2
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +109 -7
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +6 -2
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +8 -3
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +8 -3
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +13 -7
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +2 -2
- package/harness/bundles/general/codebuddy/harness-run/reference.md +6 -3
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +11 -6
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +12 -3
- package/harness/bundles/general/codebuddy/harness-test/reference.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/codebuddy/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +231 -48
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +854 -0
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +134 -40
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +1038 -0
- package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +2 -2
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +34 -1
- package/harness/bundles/general/codebuddy/scripts/harness_state.py +148 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +169 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +7 -2
- package/harness/bundles/general/codex/harness-archive/reference.md +1 -1
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +109 -7
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +6 -2
- package/harness/bundles/general/codex/harness-plan/SKILL.md +8 -3
- package/harness/bundles/general/codex/harness-review/SKILL.md +7 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +13 -7
- package/harness/bundles/general/codex/harness-run/checklist.md +2 -2
- package/harness/bundles/general/codex/harness-run/reference.md +6 -3
- package/harness/bundles/general/codex/harness-submit/SKILL.md +11 -6
- package/harness/bundles/general/codex/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/codex/harness-test/SKILL.md +12 -3
- package/harness/bundles/general/codex/harness-test/reference.md +1 -1
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/codex/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +231 -48
- package/harness/bundles/general/codex/scripts/harness_change.py +854 -0
- package/harness/bundles/general/codex/scripts/harness_events.py +134 -40
- package/harness/bundles/general/codex/scripts/harness_gate.py +1038 -0
- package/harness/bundles/general/codex/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/codex/scripts/harness_profile.py +2 -2
- package/harness/bundles/general/codex/scripts/harness_service.py +34 -1
- package/harness/bundles/general/codex/scripts/harness_state.py +148 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +169 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +7 -2
- package/harness/bundles/general/cursor/harness-archive/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +109 -7
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +6 -2
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +8 -3
- package/harness/bundles/general/cursor/harness-review/SKILL.md +7 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +13 -7
- package/harness/bundles/general/cursor/harness-run/checklist.md +2 -2
- package/harness/bundles/general/cursor/harness-run/reference.md +6 -3
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +11 -6
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/cursor/harness-test/SKILL.md +12 -3
- package/harness/bundles/general/cursor/harness-test/reference.md +1 -1
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/cursor/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +231 -48
- package/harness/bundles/general/cursor/scripts/harness_change.py +854 -0
- package/harness/bundles/general/cursor/scripts/harness_events.py +134 -40
- package/harness/bundles/general/cursor/scripts/harness_gate.py +1038 -0
- package/harness/bundles/general/cursor/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/cursor/scripts/harness_profile.py +2 -2
- package/harness/bundles/general/cursor/scripts/harness_service.py +34 -1
- package/harness/bundles/general/cursor/scripts/harness_state.py +148 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +169 -0
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +11 -2
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +7 -2
- package/harness/bundles/java/claude-code/harness-archive/reference.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +109 -7
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +6 -2
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +10 -4
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +8 -3
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +8 -3
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +13 -7
- package/harness/bundles/java/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +11 -6
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +12 -3
- package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/claude-code/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +231 -48
- package/harness/bundles/java/claude-code/scripts/harness_change.py +854 -0
- package/harness/bundles/java/claude-code/scripts/harness_events.py +134 -40
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +1038 -0
- package/harness/bundles/java/claude-code/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +2 -2
- package/harness/bundles/java/claude-code/scripts/harness_service.py +34 -1
- package/harness/bundles/java/claude-code/scripts/harness_state.py +148 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +169 -0
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +11 -2
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +7 -2
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +109 -7
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +6 -2
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +10 -4
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +8 -3
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +8 -3
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +13 -7
- package/harness/bundles/java/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +11 -6
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +12 -3
- package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/codebuddy/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +231 -48
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +854 -0
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +134 -40
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +1038 -0
- package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +2 -2
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +34 -1
- package/harness/bundles/java/codebuddy/scripts/harness_state.py +148 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +169 -0
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +11 -2
- package/harness/bundles/java/codex/harness-archive/SKILL.md +7 -2
- package/harness/bundles/java/codex/harness-archive/reference.md +1 -1
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +109 -7
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +6 -2
- package/harness/bundles/java/codex/harness-package/SKILL.md +10 -4
- package/harness/bundles/java/codex/harness-plan/SKILL.md +8 -3
- package/harness/bundles/java/codex/harness-review/SKILL.md +7 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +13 -7
- package/harness/bundles/java/codex/harness-run/reference.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +11 -6
- package/harness/bundles/java/codex/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/codex/harness-test/SKILL.md +12 -3
- package/harness/bundles/java/codex/harness-test/reference.md +1 -1
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/codex/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +231 -48
- package/harness/bundles/java/codex/scripts/harness_change.py +854 -0
- package/harness/bundles/java/codex/scripts/harness_events.py +134 -40
- package/harness/bundles/java/codex/scripts/harness_gate.py +1038 -0
- package/harness/bundles/java/codex/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/codex/scripts/harness_profile.py +2 -2
- package/harness/bundles/java/codex/scripts/harness_service.py +34 -1
- package/harness/bundles/java/codex/scripts/harness_state.py +148 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +169 -0
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +11 -2
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +7 -2
- package/harness/bundles/java/cursor/harness-archive/reference.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +109 -7
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +6 -2
- package/harness/bundles/java/cursor/harness-package/SKILL.md +10 -4
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +8 -3
- package/harness/bundles/java/cursor/harness-review/SKILL.md +7 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +13 -7
- package/harness/bundles/java/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +11 -6
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/cursor/harness-test/SKILL.md +12 -3
- package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/cursor/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +231 -48
- package/harness/bundles/java/cursor/scripts/harness_change.py +854 -0
- package/harness/bundles/java/cursor/scripts/harness_events.py +134 -40
- package/harness/bundles/java/cursor/scripts/harness_gate.py +1038 -0
- package/harness/bundles/java/cursor/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/cursor/scripts/harness_profile.py +2 -2
- package/harness/bundles/java/cursor/scripts/harness_service.py +34 -1
- package/harness/bundles/java/cursor/scripts/harness_state.py +148 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +160 -0
- package/harness/manifests/general/claude-code.json +48 -28
- package/harness/manifests/general/codebuddy.json +48 -28
- package/harness/manifests/general/codex.json +48 -28
- package/harness/manifests/general/cursor.json +48 -28
- package/harness/manifests/java/claude-code.json +49 -29
- package/harness/manifests/java/codebuddy.json +49 -29
- package/harness/manifests/java/codex.json +49 -29
- package/harness/manifests/java/cursor.json +49 -29
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -313,7 +313,23 @@ def _parse_scalar(val: str) -> Any:
|
|
|
313
313
|
return val
|
|
314
314
|
|
|
315
315
|
|
|
316
|
-
def
|
|
316
|
+
def _resolve_agents_root(skills_root: Path, agents_root: Path | None) -> tuple[Path | None, str | None]:
|
|
317
|
+
"""Resolve agent definitions without confusing a skills root with its sibling."""
|
|
318
|
+
if agents_root is not None:
|
|
319
|
+
return agents_root.resolve(), None
|
|
320
|
+
if skills_root.name == "skills" and skills_root.parent.name in (".claude", ".codebuddy"):
|
|
321
|
+
return (skills_root.parent / "agents").resolve(), None
|
|
322
|
+
if skills_root.name == "skills" and skills_root.parent.name in (".agents", ".cursor"):
|
|
323
|
+
return None, "CUSTOM_AGENTS_UNSUPPORTED"
|
|
324
|
+
return (skills_root / "agents").resolve(), None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def cmd_check_agents(
|
|
328
|
+
skills_root: Path,
|
|
329
|
+
agent: str,
|
|
330
|
+
*,
|
|
331
|
+
agents_root: Path | None = None,
|
|
332
|
+
) -> dict[str, Any]:
|
|
317
333
|
skills_root = skills_root.resolve()
|
|
318
334
|
agent_name = agent.strip()
|
|
319
335
|
if not agent_name:
|
|
@@ -325,9 +341,23 @@ def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
|
|
|
325
341
|
"reason": "agent name is empty",
|
|
326
342
|
}
|
|
327
343
|
|
|
344
|
+
resolved_agents_root, capability_error = _resolve_agents_root(skills_root, agents_root)
|
|
345
|
+
if capability_error is not None:
|
|
346
|
+
return {
|
|
347
|
+
"ok": True,
|
|
348
|
+
"action": "check-agents",
|
|
349
|
+
"agent": agent_name,
|
|
350
|
+
"usable": False,
|
|
351
|
+
"definitionValid": False,
|
|
352
|
+
"runtimeSupported": False,
|
|
353
|
+
"reasonCode": capability_error,
|
|
354
|
+
"reason": "current adapter runtime does not support custom agent definitions; use main-session fallback",
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
assert resolved_agents_root is not None
|
|
328
358
|
# Accept bare name or with .md
|
|
329
359
|
filename = agent_name if agent_name.endswith(".md") else f"{agent_name}.md"
|
|
330
|
-
agent_path =
|
|
360
|
+
agent_path = resolved_agents_root / filename
|
|
331
361
|
|
|
332
362
|
if not agent_path.is_file():
|
|
333
363
|
return {
|
|
@@ -335,7 +365,11 @@ def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
|
|
|
335
365
|
"action": "check-agents",
|
|
336
366
|
"agent": agent_name,
|
|
337
367
|
"path": str(agent_path),
|
|
368
|
+
"agentsRoot": str(resolved_agents_root),
|
|
338
369
|
"usable": False,
|
|
370
|
+
"definitionValid": False,
|
|
371
|
+
"runtimeSupported": True,
|
|
372
|
+
"reasonCode": "AGENT_DEFINITION_NOT_FOUND",
|
|
339
373
|
"reason": f"agent file not found: {agent_path}",
|
|
340
374
|
}
|
|
341
375
|
|
|
@@ -390,7 +424,11 @@ def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
|
|
|
390
424
|
"action": "check-agents",
|
|
391
425
|
"agent": agent_name,
|
|
392
426
|
"path": str(agent_path),
|
|
427
|
+
"agentsRoot": str(resolved_agents_root),
|
|
393
428
|
"usable": True,
|
|
429
|
+
"definitionValid": True,
|
|
430
|
+
"runtimeSupported": True,
|
|
431
|
+
"reasonCode": "READY",
|
|
394
432
|
"reason": "agent file exists; frontmatter parsed; tools declared",
|
|
395
433
|
"name": name_field,
|
|
396
434
|
"tools": tools,
|
|
@@ -431,6 +469,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
431
469
|
|
|
432
470
|
p_agents = sub.add_parser("check-agents", help="Validate agent definition usability")
|
|
433
471
|
p_agents.add_argument("--skills-root", required=True, type=Path)
|
|
472
|
+
p_agents.add_argument("--agents-root", type=Path)
|
|
434
473
|
p_agents.add_argument("--agent", required=True)
|
|
435
474
|
p_agents.add_argument("--json", action="store_true")
|
|
436
475
|
|
|
@@ -459,7 +498,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
459
498
|
)
|
|
460
499
|
return emit_json(result, ok=bool(result.get("ok", False)))
|
|
461
500
|
if args.command == "check-agents":
|
|
462
|
-
result = cmd_check_agents(
|
|
501
|
+
result = cmd_check_agents(
|
|
502
|
+
args.skills_root,
|
|
503
|
+
args.agent,
|
|
504
|
+
agents_root=args.agents_root,
|
|
505
|
+
)
|
|
463
506
|
return emit_json(result, ok=True)
|
|
464
507
|
|
|
465
508
|
parser.error(f"unknown command: {args.command}")
|
|
@@ -223,8 +223,8 @@ def _java_commands(reactor_modules: list[str], pom_hash: str) -> dict[str, Any]:
|
|
|
223
223
|
"basis": dict(basis),
|
|
224
224
|
},
|
|
225
225
|
"package": {
|
|
226
|
-
"command": "mvn -f pom.xml package '-Dmaven.test.skip=true'",
|
|
227
|
-
"argvTemplate": ["mvn", "-f", "pom.xml", "package", "-Dmaven.test.skip=true"],
|
|
226
|
+
"command": "mvn -f pom.xml clean package '-Dmaven.test.skip=true'",
|
|
227
|
+
"argvTemplate": ["mvn", "-f", "pom.xml", "clean", "package", "-Dmaven.test.skip=true"],
|
|
228
228
|
"scope": "module",
|
|
229
229
|
"inputs": _cmd_inputs(reactor_modules, pom=True, main=True, test=True),
|
|
230
230
|
"coverage": "package",
|
|
@@ -194,6 +194,8 @@ def resolve_service_start(
|
|
|
194
194
|
change_name: str | None = None,
|
|
195
195
|
worktree_root: Path | None = None,
|
|
196
196
|
overlay_path: str | None = None,
|
|
197
|
+
leased_port: int | None = None,
|
|
198
|
+
lease_owner: str | None = None,
|
|
197
199
|
) -> dict[str, Any]:
|
|
198
200
|
"""从模板 serviceStart + runtime context 生成 resolved serviceStart。
|
|
199
201
|
|
|
@@ -220,6 +222,16 @@ def resolve_service_start(
|
|
|
220
222
|
# runtime profile 注入:持久 profile 留空时用 change_name;非空则保留
|
|
221
223
|
if not str(resolved.get("profile") or "").strip():
|
|
222
224
|
resolved["profile"] = change_name or "local-dev"
|
|
225
|
+
if leased_port is not None:
|
|
226
|
+
if not lease_owner:
|
|
227
|
+
raise ValueError("lease owner is required when leased port is provided")
|
|
228
|
+
resolved["leasedPort"] = leased_port
|
|
229
|
+
resolved["leaseOwner"] = lease_owner
|
|
230
|
+
resolved["port"] = leased_port
|
|
231
|
+
for field in ("command", "healthUrl", "healthFile", "overlayPath"):
|
|
232
|
+
value = resolved.get(field)
|
|
233
|
+
if isinstance(value, str):
|
|
234
|
+
resolved[field] = value.replace("{leasedPort}", str(leased_port))
|
|
223
235
|
# worktree_root 预留给未来相对 overlay 路径解析(spec §3.6 state snapshot);
|
|
224
236
|
# 不写入返回值,避免污染 session。
|
|
225
237
|
_ = worktree_root
|
|
@@ -914,7 +926,7 @@ def build_session(
|
|
|
914
926
|
) -> dict[str, Any]:
|
|
915
927
|
profile_name = service_start.get("profile") or "local-dev"
|
|
916
928
|
overlay = service_start.get("overlayPath") or ""
|
|
917
|
-
|
|
929
|
+
session = {
|
|
918
930
|
"pid": pid,
|
|
919
931
|
"startedBy": "AI",
|
|
920
932
|
"moduleInputsHash": module_inputs_hash,
|
|
@@ -925,6 +937,10 @@ def build_session(
|
|
|
925
937
|
"startedAt": started_at or now_iso(),
|
|
926
938
|
"command": command,
|
|
927
939
|
}
|
|
940
|
+
if isinstance(service_start.get("leasedPort"), int):
|
|
941
|
+
session["leasedPort"] = service_start["leasedPort"]
|
|
942
|
+
session["leaseOwner"] = service_start.get("leaseOwner")
|
|
943
|
+
return session
|
|
928
944
|
|
|
929
945
|
|
|
930
946
|
def compute_module_hash(
|
|
@@ -1015,12 +1031,16 @@ def cmd_ensure(args: argparse.Namespace) -> int:
|
|
|
1015
1031
|
# 含 worktree/change 陈旧持久值时拒绝(修复输入端陈旧 profile)。
|
|
1016
1032
|
change_name = getattr(args, "change_name", None) or change_dir.name
|
|
1017
1033
|
overlay = getattr(args, "overlay", None)
|
|
1034
|
+
leased_port = getattr(args, "leased_port", None)
|
|
1035
|
+
lease_owner = str(getattr(args, "lease_owner", None) or "").strip() or None
|
|
1018
1036
|
try:
|
|
1019
1037
|
service_start = resolve_service_start(
|
|
1020
1038
|
profile,
|
|
1021
1039
|
change_name=change_name,
|
|
1022
1040
|
worktree_root=project,
|
|
1023
1041
|
overlay_path=overlay,
|
|
1042
|
+
leased_port=leased_port,
|
|
1043
|
+
lease_owner=lease_owner,
|
|
1024
1044
|
)
|
|
1025
1045
|
except ValueError as exc:
|
|
1026
1046
|
return emit_error(str(exc), as_json=as_json)
|
|
@@ -1084,6 +1104,8 @@ def cmd_ensure(args: argparse.Namespace) -> int:
|
|
|
1084
1104
|
and session.get("startCommandHash") == current_cmd_hash
|
|
1085
1105
|
and session.get("profile") == current_profile
|
|
1086
1106
|
and session.get("overlayPath") == current_overlay
|
|
1107
|
+
and session.get("leasedPort") == service_start.get("leasedPort")
|
|
1108
|
+
and session.get("leaseOwner") == service_start.get("leaseOwner")
|
|
1087
1109
|
)
|
|
1088
1110
|
if fingerprint_match:
|
|
1089
1111
|
payload = {
|
|
@@ -1389,6 +1411,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1389
1411
|
default=None,
|
|
1390
1412
|
help="runtime overlay path injected into resolved serviceStart",
|
|
1391
1413
|
)
|
|
1414
|
+
p_ensure.add_argument(
|
|
1415
|
+
"--leased-port",
|
|
1416
|
+
type=int,
|
|
1417
|
+
default=None,
|
|
1418
|
+
help="port allocated by harness_change.py lease-port",
|
|
1419
|
+
)
|
|
1420
|
+
p_ensure.add_argument(
|
|
1421
|
+
"--lease-owner",
|
|
1422
|
+
default=None,
|
|
1423
|
+
help="run id that owns --leased-port",
|
|
1424
|
+
)
|
|
1392
1425
|
p_ensure.add_argument("--json", action="store_true")
|
|
1393
1426
|
p_ensure.set_defaults(func=cmd_ensure)
|
|
1394
1427
|
|
|
@@ -12,9 +12,11 @@ Python 3.10+ stdlib only. UTF-8 without BOM. Windows path friendly.
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import datetime as dt
|
|
15
|
+
import argparse
|
|
15
16
|
import hashlib
|
|
16
17
|
import json
|
|
17
18
|
import os
|
|
19
|
+
import subprocess
|
|
18
20
|
import sys
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
from typing import Any
|
|
@@ -177,3 +179,149 @@ def refresh_segments(
|
|
|
177
179
|
refreshed["segments"] = refreshed_segments
|
|
178
180
|
refreshed["generatedAt"] = now_iso()
|
|
179
181
|
return refreshed
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _git(project: Path, *args: str) -> str:
|
|
185
|
+
proc = subprocess.run(
|
|
186
|
+
["git", "-C", str(project), *args],
|
|
187
|
+
capture_output=True,
|
|
188
|
+
text=True,
|
|
189
|
+
encoding="utf-8",
|
|
190
|
+
errors="replace",
|
|
191
|
+
check=False,
|
|
192
|
+
)
|
|
193
|
+
return proc.stdout.strip() if proc.returncode == 0 else ""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _existing_files(paths: list[Path]) -> list[str]:
|
|
197
|
+
return sorted({str(path.resolve()) for path in paths if path.is_file()})
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def discover_segment_files(
|
|
201
|
+
project: Path, change_dir: Path, *, base: str = "", head: str = "HEAD"
|
|
202
|
+
) -> dict[str, list[str]]:
|
|
203
|
+
"""Discover the compact inputs shared by plan/run/test/review/submit."""
|
|
204
|
+
project = project.resolve()
|
|
205
|
+
rules = [project / "AGENTS.md", project / "CLAUDE.md"]
|
|
206
|
+
for pattern in (
|
|
207
|
+
".claude/rules/**/*",
|
|
208
|
+
".cursor/rules/**/*",
|
|
209
|
+
".codebuddy/rules/**/*",
|
|
210
|
+
".codebuddy/.rules/**/*",
|
|
211
|
+
):
|
|
212
|
+
rules.extend(project.glob(pattern))
|
|
213
|
+
|
|
214
|
+
profile = [
|
|
215
|
+
project / ".harness/config/build-profile.json",
|
|
216
|
+
project / ".harness/context-index.json",
|
|
217
|
+
project / "project.yaml",
|
|
218
|
+
project / "harness.json",
|
|
219
|
+
]
|
|
220
|
+
codebase_map = list((project / ".harness/codebase/map").glob("**/*"))
|
|
221
|
+
knowledge = [
|
|
222
|
+
project / ".harness/knowledge/index.json",
|
|
223
|
+
project / ".harness/knowledge/status.json",
|
|
224
|
+
project / ".harness/knowledge/config.json",
|
|
225
|
+
]
|
|
226
|
+
change_inputs: list[Path] = []
|
|
227
|
+
for folder in ("spec", "plans", "meta"):
|
|
228
|
+
change_inputs.extend((change_dir / folder).glob("**/*"))
|
|
229
|
+
change_inputs = [path for path in change_inputs if path != snapshot_path(change_dir)]
|
|
230
|
+
|
|
231
|
+
changed_names: list[str] = []
|
|
232
|
+
if base:
|
|
233
|
+
raw = _git(project, "diff", "--name-only", "--diff-filter=ACMRT", f"{base}..{head}")
|
|
234
|
+
changed_names = [line for line in raw.splitlines() if line.strip()]
|
|
235
|
+
code = [project / name for name in changed_names]
|
|
236
|
+
return {
|
|
237
|
+
"profile": _existing_files(profile),
|
|
238
|
+
"rules": _existing_files(rules),
|
|
239
|
+
"map": _existing_files(codebase_map),
|
|
240
|
+
"knowledge": _existing_files(knowledge),
|
|
241
|
+
"change": _existing_files(change_inputs),
|
|
242
|
+
"code": _existing_files(code),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def capture_current_state(
|
|
247
|
+
*,
|
|
248
|
+
project: Path,
|
|
249
|
+
change_dir: Path,
|
|
250
|
+
change_name: str,
|
|
251
|
+
worktree_root: Path,
|
|
252
|
+
base: str = "",
|
|
253
|
+
head: str = "HEAD",
|
|
254
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
255
|
+
resolved_head = _git(project, "rev-parse", head) or head
|
|
256
|
+
resolved_base = _git(project, "rev-parse", base) if base else ""
|
|
257
|
+
files = discover_segment_files(project, change_dir, base=resolved_base, head=resolved_head)
|
|
258
|
+
previous = load_snapshot(change_dir)
|
|
259
|
+
fresh = capture_snapshot(
|
|
260
|
+
change_dir,
|
|
261
|
+
change_name=change_name,
|
|
262
|
+
project=project,
|
|
263
|
+
worktree_root=worktree_root,
|
|
264
|
+
base=resolved_base,
|
|
265
|
+
head=resolved_head,
|
|
266
|
+
segment_files=files,
|
|
267
|
+
)
|
|
268
|
+
changed: list[str] = []
|
|
269
|
+
if previous:
|
|
270
|
+
old_segments = previous.get("segments") or {}
|
|
271
|
+
for name, segment in fresh["segments"].items():
|
|
272
|
+
old = old_segments.get(name)
|
|
273
|
+
if isinstance(old, dict) and old.get("fingerprint") == segment.get("fingerprint"):
|
|
274
|
+
segment["capturedAt"] = old.get("capturedAt", segment["capturedAt"])
|
|
275
|
+
else:
|
|
276
|
+
changed.append(name)
|
|
277
|
+
else:
|
|
278
|
+
changed = list(fresh["segments"])
|
|
279
|
+
write_snapshot(change_dir, fresh)
|
|
280
|
+
return fresh, changed
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
284
|
+
parser = argparse.ArgumentParser(description="Capture reusable Harness state")
|
|
285
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
286
|
+
capture = sub.add_parser("capture", help="capture or refresh changed segments")
|
|
287
|
+
capture.add_argument("--project", default=".")
|
|
288
|
+
capture.add_argument("--change-dir", required=True)
|
|
289
|
+
capture.add_argument("--change-name")
|
|
290
|
+
capture.add_argument("--worktree-root")
|
|
291
|
+
capture.add_argument("--base", default="")
|
|
292
|
+
capture.add_argument("--head", default="HEAD")
|
|
293
|
+
capture.add_argument("--json", action="store_true")
|
|
294
|
+
return parser
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main(argv: list[str] | None = None) -> int:
|
|
298
|
+
args = build_parser().parse_args(argv)
|
|
299
|
+
project = Path(args.project).resolve()
|
|
300
|
+
change_dir = Path(args.change_dir).resolve()
|
|
301
|
+
name = args.change_name or change_dir.name
|
|
302
|
+
worktree = Path(args.worktree_root).resolve() if args.worktree_root else project
|
|
303
|
+
snapshot, changed = capture_current_state(
|
|
304
|
+
project=project,
|
|
305
|
+
change_dir=change_dir,
|
|
306
|
+
change_name=name,
|
|
307
|
+
worktree_root=worktree,
|
|
308
|
+
base=args.base,
|
|
309
|
+
head=args.head,
|
|
310
|
+
)
|
|
311
|
+
result = {
|
|
312
|
+
"ok": True,
|
|
313
|
+
"path": str(snapshot_path(change_dir)),
|
|
314
|
+
"changedSegments": changed,
|
|
315
|
+
"reusedSegments": sorted(set(snapshot["segments"]) - set(changed)),
|
|
316
|
+
"git": snapshot["git"],
|
|
317
|
+
}
|
|
318
|
+
if args.json:
|
|
319
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
320
|
+
else:
|
|
321
|
+
print(f"state snapshot: {result['path']}")
|
|
322
|
+
print(f"changed: {', '.join(changed) if changed else 'none'}")
|
|
323
|
+
return 0
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
raise SystemExit(main())
|
|
@@ -24,6 +24,7 @@ from typing import Any
|
|
|
24
24
|
SCHEMA_VERSION = 1
|
|
25
25
|
MODE = "force-track-touched"
|
|
26
26
|
MANIFEST_REL = Path("evidence") / "test-tracking.json"
|
|
27
|
+
SNAPSHOT_REL = Path("evidence") / "test-guard-snapshot.json"
|
|
27
28
|
PROFILE_REL = Path(".harness") / "config" / "build-profile.json"
|
|
28
29
|
REASONS = ("tdd-created", "stale-test-repair", "test-updated")
|
|
29
30
|
|
|
@@ -125,7 +126,7 @@ def _exclusive_lock(path: Path, *, wait_seconds: float) -> Any:
|
|
|
125
126
|
while descriptor is None:
|
|
126
127
|
try:
|
|
127
128
|
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
128
|
-
except FileExistsError as exc:
|
|
129
|
+
except (FileExistsError, PermissionError) as exc:
|
|
129
130
|
if time.monotonic() >= deadline:
|
|
130
131
|
raise LockUnavailable(str(path)) from exc
|
|
131
132
|
time.sleep(0.01)
|
|
@@ -137,22 +138,41 @@ def _exclusive_lock(path: Path, *, wait_seconds: float) -> Any:
|
|
|
137
138
|
path.unlink(missing_ok=True)
|
|
138
139
|
|
|
139
140
|
|
|
141
|
+
def _state_project_root(project: Path) -> Path:
|
|
142
|
+
result = _git(project, "rev-parse", "--git-common-dir")
|
|
143
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
144
|
+
return project
|
|
145
|
+
common = Path(result.stdout.strip())
|
|
146
|
+
if not common.is_absolute():
|
|
147
|
+
common = project / common
|
|
148
|
+
resolved = common.resolve()
|
|
149
|
+
return resolved.parent if resolved.name == ".git" else project
|
|
150
|
+
|
|
151
|
+
|
|
140
152
|
def _change_dir(project: Path, change_dir: Path | str) -> Path | None:
|
|
153
|
+
state_project = _state_project_root(project)
|
|
141
154
|
candidate = Path(change_dir)
|
|
142
155
|
if not candidate.is_absolute():
|
|
143
|
-
candidate =
|
|
156
|
+
candidate = (
|
|
157
|
+
state_project / candidate
|
|
158
|
+
if candidate.parts[:2] == (".harness", "changes")
|
|
159
|
+
else project / candidate
|
|
160
|
+
)
|
|
144
161
|
resolved = candidate.resolve()
|
|
145
|
-
|
|
162
|
+
allowed_roots = {
|
|
163
|
+
(project / ".harness" / "changes").resolve(),
|
|
164
|
+
(state_project / ".harness" / "changes").resolve(),
|
|
165
|
+
}
|
|
166
|
+
return resolved if any(_inside(resolved, root) for root in allowed_roots) else None
|
|
146
167
|
|
|
147
168
|
|
|
148
|
-
def _manifest_path(
|
|
169
|
+
def _manifest_path(change_root: Path) -> Path | None:
|
|
149
170
|
evidence = change_root / MANIFEST_REL.parent
|
|
150
171
|
manifest = change_root / MANIFEST_REL
|
|
151
172
|
expected_evidence = evidence.absolute()
|
|
152
173
|
resolved_evidence = evidence.resolve()
|
|
153
174
|
if (
|
|
154
|
-
not _inside(change_root.resolve()
|
|
155
|
-
or not _inside(resolved_evidence, project)
|
|
175
|
+
not _inside(resolved_evidence, change_root.resolve())
|
|
156
176
|
or os.path.normcase(str(resolved_evidence))
|
|
157
177
|
!= os.path.normcase(str(expected_evidence))
|
|
158
178
|
):
|
|
@@ -160,9 +180,12 @@ def _manifest_path(project: Path, change_root: Path) -> Path | None:
|
|
|
160
180
|
return manifest
|
|
161
181
|
|
|
162
182
|
|
|
163
|
-
def _manifest_target_inside(
|
|
183
|
+
def _manifest_target_inside(change_root: Path, manifest: Path) -> bool:
|
|
164
184
|
resolved = manifest.resolve()
|
|
165
|
-
return
|
|
185
|
+
return (
|
|
186
|
+
_inside(resolved, change_root.resolve())
|
|
187
|
+
and _inside(resolved, manifest.parent.absolute())
|
|
188
|
+
)
|
|
166
189
|
|
|
167
190
|
|
|
168
191
|
def _profile_config(project: Path) -> tuple[list[str], list[str]] | None:
|
|
@@ -325,13 +348,13 @@ def record(
|
|
|
325
348
|
assert path is not None and rel is not None
|
|
326
349
|
validated.append((path, rel))
|
|
327
350
|
|
|
328
|
-
manifest_path = _manifest_path(
|
|
351
|
+
manifest_path = _manifest_path(change_root)
|
|
329
352
|
if manifest_path is None:
|
|
330
353
|
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
331
354
|
lock_path = manifest_path.with_name(manifest_path.name + ".lock")
|
|
332
355
|
try:
|
|
333
356
|
with _exclusive_lock(lock_path, wait_seconds=5.0):
|
|
334
|
-
if not _manifest_target_inside(
|
|
357
|
+
if not _manifest_target_inside(change_root, manifest_path):
|
|
335
358
|
return _result(
|
|
336
359
|
False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", []
|
|
337
360
|
)
|
|
@@ -378,10 +401,10 @@ def record(
|
|
|
378
401
|
|
|
379
402
|
|
|
380
403
|
def _stage_locked(
|
|
381
|
-
project_root: Path, manifest_path: Path, index_path: Path
|
|
404
|
+
project_root: Path, change_root: Path, manifest_path: Path, index_path: Path
|
|
382
405
|
) -> dict[str, Any]:
|
|
383
406
|
action = "stage"
|
|
384
|
-
if not _manifest_target_inside(
|
|
407
|
+
if not _manifest_target_inside(change_root, manifest_path):
|
|
385
408
|
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
386
409
|
try:
|
|
387
410
|
manifest = _read_json(manifest_path)
|
|
@@ -444,13 +467,164 @@ def _stage_locked(
|
|
|
444
467
|
temp_index.unlink(missing_ok=True)
|
|
445
468
|
|
|
446
469
|
|
|
470
|
+
def _glob_files_for_pattern(base: Path, pattern: str) -> list[Path]:
|
|
471
|
+
normalized = pattern.replace("\\", "/")
|
|
472
|
+
if normalized.endswith("/**"):
|
|
473
|
+
prefix = normalized[:-3].rstrip("/")
|
|
474
|
+
root = base / prefix
|
|
475
|
+
if not root.is_dir():
|
|
476
|
+
return []
|
|
477
|
+
return [path for path in root.rglob("*") if path.is_file()]
|
|
478
|
+
return [path for path in base.glob(normalized) if path.is_file()]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _enumerate_allowed_test_files(project_root: Path) -> dict[str, str]:
|
|
482
|
+
"""Map repo-relative test path -> sha256 for all allowed existing files."""
|
|
483
|
+
found: dict[str, str] = {}
|
|
484
|
+
config = _profile_config(project_root)
|
|
485
|
+
if config is None:
|
|
486
|
+
patterns = ["test/**", "tests/**", "src/test/**"]
|
|
487
|
+
else:
|
|
488
|
+
patterns, _excluded = config
|
|
489
|
+
base = project_root.resolve()
|
|
490
|
+
seen: set[str] = set()
|
|
491
|
+
for pattern in patterns:
|
|
492
|
+
for match in _glob_files_for_pattern(base, pattern):
|
|
493
|
+
resolved = match.resolve()
|
|
494
|
+
if not _inside(resolved, base):
|
|
495
|
+
continue
|
|
496
|
+
rel = resolved.relative_to(base).as_posix()
|
|
497
|
+
if rel in seen or not _allowed_test_path(project_root, rel):
|
|
498
|
+
continue
|
|
499
|
+
seen.add(rel)
|
|
500
|
+
found[rel] = _sha256(resolved)
|
|
501
|
+
return found
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def begin(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
505
|
+
action = "begin"
|
|
506
|
+
project_root = Path(project).resolve()
|
|
507
|
+
change_root = _change_dir(project_root, change_dir)
|
|
508
|
+
if change_root is None:
|
|
509
|
+
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
510
|
+
manifest_path = _manifest_path(change_root)
|
|
511
|
+
if manifest_path is None:
|
|
512
|
+
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
513
|
+
snapshot_path = change_root / SNAPSHOT_REL
|
|
514
|
+
if not _manifest_target_inside(change_root, snapshot_path):
|
|
515
|
+
return _result(False, action, "SNAPSHOT_PATH_OUTSIDE_PROJECT", [])
|
|
516
|
+
|
|
517
|
+
files = _enumerate_allowed_test_files(project_root)
|
|
518
|
+
snapshot = {
|
|
519
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
520
|
+
"mode": MODE,
|
|
521
|
+
"projectRoot": str(project_root),
|
|
522
|
+
"capturedAt": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
523
|
+
"files": [
|
|
524
|
+
{"path": rel, "sha256": digest, "ignored": _is_ignored(project_root, rel)}
|
|
525
|
+
for rel, digest in sorted(files.items())
|
|
526
|
+
],
|
|
527
|
+
}
|
|
528
|
+
_write_json(snapshot_path, snapshot)
|
|
529
|
+
return _result(
|
|
530
|
+
True,
|
|
531
|
+
action,
|
|
532
|
+
"SNAPSHOT_CAPTURED",
|
|
533
|
+
list(files),
|
|
534
|
+
snapshotPath=str(snapshot_path),
|
|
535
|
+
fileCount=len(files),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def close(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
540
|
+
action = "close"
|
|
541
|
+
project_root = Path(project).resolve()
|
|
542
|
+
change_root = _change_dir(project_root, change_dir)
|
|
543
|
+
if change_root is None:
|
|
544
|
+
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
545
|
+
snapshot_path = change_root / SNAPSHOT_REL
|
|
546
|
+
if not snapshot_path.is_file():
|
|
547
|
+
return _result(False, action, "SNAPSHOT_MISSING", [])
|
|
548
|
+
try:
|
|
549
|
+
snapshot = _read_json(snapshot_path)
|
|
550
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
551
|
+
return _result(False, action, "SNAPSHOT_INVALID", [], error=str(exc))
|
|
552
|
+
if (
|
|
553
|
+
not isinstance(snapshot, dict)
|
|
554
|
+
or snapshot.get("schemaVersion") != SCHEMA_VERSION
|
|
555
|
+
or snapshot.get("projectRoot") != str(project_root)
|
|
556
|
+
):
|
|
557
|
+
return _result(False, action, "SNAPSHOT_INVALID", [])
|
|
558
|
+
|
|
559
|
+
before_entries = snapshot.get("files")
|
|
560
|
+
if not isinstance(before_entries, list):
|
|
561
|
+
return _result(False, action, "SNAPSHOT_INVALID", [])
|
|
562
|
+
before: dict[str, dict[str, Any]] = {}
|
|
563
|
+
for item in before_entries:
|
|
564
|
+
if not isinstance(item, dict):
|
|
565
|
+
return _result(False, action, "SNAPSHOT_INVALID", [])
|
|
566
|
+
rel = item.get("path")
|
|
567
|
+
digest = item.get("sha256")
|
|
568
|
+
if not isinstance(rel, str) or not rel or not isinstance(digest, str):
|
|
569
|
+
return _result(False, action, "SNAPSHOT_INVALID", [])
|
|
570
|
+
path, normalized, error = _validate_file(project_root, rel)
|
|
571
|
+
if error:
|
|
572
|
+
return _result(False, action, error, [rel])
|
|
573
|
+
if normalized != rel or path is None:
|
|
574
|
+
return _result(False, action, "PATH_ESCAPE", [rel])
|
|
575
|
+
before[rel] = item
|
|
576
|
+
|
|
577
|
+
current = _enumerate_allowed_test_files(project_root)
|
|
578
|
+
for rel in current:
|
|
579
|
+
path, normalized, error = _validate_file(project_root, rel)
|
|
580
|
+
if error:
|
|
581
|
+
return _result(False, action, error, [rel])
|
|
582
|
+
if normalized != rel or path is None:
|
|
583
|
+
return _result(False, action, "PATH_ESCAPE", [rel])
|
|
584
|
+
|
|
585
|
+
touched: list[tuple[str, str]] = []
|
|
586
|
+
for rel, digest in current.items():
|
|
587
|
+
if rel not in before:
|
|
588
|
+
touched.append((rel, "tdd-created"))
|
|
589
|
+
continue
|
|
590
|
+
if before[rel]["sha256"] != digest:
|
|
591
|
+
touched.append((rel, "test-updated"))
|
|
592
|
+
|
|
593
|
+
recorded: list[str] = []
|
|
594
|
+
for rel, reason in touched:
|
|
595
|
+
result = record(project_root, change_root, [str(project_root / rel)], reason)
|
|
596
|
+
if not result.get("ok"):
|
|
597
|
+
code = result.get("code", "RECORD_FAILED")
|
|
598
|
+
if code == "TEST_PATH_NOT_ALLOWED":
|
|
599
|
+
return _result(False, action, "UNCLASSIFIABLE_TEST", [rel])
|
|
600
|
+
return result
|
|
601
|
+
recorded.append(rel)
|
|
602
|
+
|
|
603
|
+
return _result(
|
|
604
|
+
True,
|
|
605
|
+
action,
|
|
606
|
+
"CLOSED",
|
|
607
|
+
recorded,
|
|
608
|
+
recordedCount=len(recorded),
|
|
609
|
+
unchangedPreexisting=len(before) - sum(1 for rel, _ in touched if rel in before),
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def mark(
|
|
614
|
+
project: Path | str,
|
|
615
|
+
change_dir: Path | str,
|
|
616
|
+
files: list[str],
|
|
617
|
+
) -> dict[str, Any]:
|
|
618
|
+
return record(project, change_dir, files, "stale-test-repair")
|
|
619
|
+
|
|
620
|
+
|
|
447
621
|
def stage(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
448
622
|
action = "stage"
|
|
449
623
|
project_root = Path(project).resolve()
|
|
450
624
|
change_root = _change_dir(project_root, change_dir)
|
|
451
625
|
if change_root is None:
|
|
452
626
|
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
453
|
-
manifest_path = _manifest_path(
|
|
627
|
+
manifest_path = _manifest_path(change_root)
|
|
454
628
|
if manifest_path is None:
|
|
455
629
|
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
456
630
|
|
|
@@ -468,7 +642,9 @@ def stage(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
|
468
642
|
manifest_lock = manifest_path.with_name(manifest_path.name + ".lock")
|
|
469
643
|
try:
|
|
470
644
|
with _exclusive_lock(manifest_lock, wait_seconds=0.0):
|
|
471
|
-
return _stage_locked(
|
|
645
|
+
return _stage_locked(
|
|
646
|
+
project_root, change_root, manifest_path, index_path
|
|
647
|
+
)
|
|
472
648
|
except LockUnavailable:
|
|
473
649
|
return _result(False, action, "MANIFEST_LOCKED", [])
|
|
474
650
|
except LockUnavailable:
|
|
@@ -488,10 +664,30 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
488
664
|
stage_parser.add_argument("--project", required=True)
|
|
489
665
|
stage_parser.add_argument("--change-dir", required=True)
|
|
490
666
|
stage_parser.add_argument("--json", action="store_true")
|
|
667
|
+
begin_parser = sub.add_parser("begin")
|
|
668
|
+
begin_parser.add_argument("--project", required=True)
|
|
669
|
+
begin_parser.add_argument("--change-dir", required=True)
|
|
670
|
+
begin_parser.add_argument("--json", action="store_true")
|
|
671
|
+
close_parser = sub.add_parser("close")
|
|
672
|
+
close_parser.add_argument("--project", required=True)
|
|
673
|
+
close_parser.add_argument("--change-dir", required=True)
|
|
674
|
+
close_parser.add_argument("--json", action="store_true")
|
|
675
|
+
mark_parser = sub.add_parser("mark")
|
|
676
|
+
mark_parser.add_argument("--project", required=True)
|
|
677
|
+
mark_parser.add_argument("--change-dir", required=True)
|
|
678
|
+
mark_parser.add_argument("--files", required=True)
|
|
679
|
+
mark_parser.add_argument("--json", action="store_true")
|
|
491
680
|
args = parser.parse_args(argv)
|
|
492
681
|
if args.action == "record":
|
|
493
682
|
files = [item.strip() for item in args.files.split(",") if item.strip()]
|
|
494
683
|
result = record(args.project, args.change_dir, files, args.reason)
|
|
684
|
+
elif args.action == "begin":
|
|
685
|
+
result = begin(args.project, args.change_dir)
|
|
686
|
+
elif args.action == "close":
|
|
687
|
+
result = close(args.project, args.change_dir)
|
|
688
|
+
elif args.action == "mark":
|
|
689
|
+
files = [item.strip() for item in args.files.split(",") if item.strip()]
|
|
690
|
+
result = mark(args.project, args.change_dir, files)
|
|
495
691
|
else:
|
|
496
692
|
result = stage(args.project, args.change_dir)
|
|
497
693
|
print(json.dumps(result, ensure_ascii=False, indent=2 if args.json else None))
|