@mc-and-his-agents/loom-installer 0.1.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/README.md +47 -0
- package/dist/src/claude.js +157 -0
- package/dist/src/cli.js +35 -0
- package/dist/src/codex.js +191 -0
- package/dist/src/index.js +175 -0
- package/dist/src/payload.js +42 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils.js +84 -0
- package/dist/test/installer.test.js +167 -0
- package/package.json +46 -0
- package/payload/manifest.json +4294 -0
- package/payload/plugin/loom/.codex-plugin/plugin.json +37 -0
- package/payload/plugin/loom/skills/README.md +128 -0
- package/payload/plugin/loom/skills/distribution-and-adapter-contract.md +359 -0
- package/payload/plugin/loom/skills/install-layout.json +91 -0
- package/payload/plugin/loom/skills/loom-adopt/SKILL.md +80 -0
- package/payload/plugin/loom/skills/loom-adopt/agents/openai.yaml +4 -0
- package/payload/plugin/loom/skills/loom-adopt/contract.json +45 -0
- package/payload/plugin/loom/skills/loom-adopt/references/input-signals.md +17 -0
- package/payload/plugin/loom/skills/loom-adopt/references/output-contract.md +17 -0
- package/payload/plugin/loom/skills/loom-adopt/scripts/loom-adopt.py +14 -0
- package/payload/plugin/loom/skills/loom-handoff/SKILL.md +23 -0
- package/payload/plugin/loom/skills/loom-handoff/agents/openai.yaml +4 -0
- package/payload/plugin/loom/skills/loom-handoff/contract.json +53 -0
- package/payload/plugin/loom/skills/loom-handoff/references/input-signals.md +7 -0
- package/payload/plugin/loom/skills/loom-handoff/references/output-contract.md +42 -0
- package/payload/plugin/loom/skills/loom-handoff/scripts/loom-handoff.py +14 -0
- package/payload/plugin/loom/skills/loom-init/SKILL.md +246 -0
- package/payload/plugin/loom/skills/loom-init/agents/openai.yaml +4 -0
- package/payload/plugin/loom/skills/loom-init/contract.json +76 -0
- package/payload/plugin/loom/skills/loom-init/references/input-signals.md +16 -0
- package/payload/plugin/loom/skills/loom-init/references/intake-signals.md +155 -0
- package/payload/plugin/loom/skills/loom-init/references/output-contract.md +208 -0
- package/payload/plugin/loom/skills/loom-init/scripts/loom-init.py +14 -0
- package/payload/plugin/loom/skills/loom-merge-ready/SKILL.md +23 -0
- package/payload/plugin/loom/skills/loom-merge-ready/agents/openai.yaml +4 -0
- package/payload/plugin/loom/skills/loom-merge-ready/contract.json +49 -0
- package/payload/plugin/loom/skills/loom-merge-ready/references/input-signals.md +7 -0
- package/payload/plugin/loom/skills/loom-merge-ready/references/output-contract.md +34 -0
- package/payload/plugin/loom/skills/loom-merge-ready/scripts/loom-merge-ready.py +14 -0
- package/payload/plugin/loom/skills/loom-pre-review/SKILL.md +69 -0
- package/payload/plugin/loom/skills/loom-pre-review/agents/openai.yaml +4 -0
- package/payload/plugin/loom/skills/loom-pre-review/contract.json +41 -0
- package/payload/plugin/loom/skills/loom-pre-review/references/input-signals.md +14 -0
- package/payload/plugin/loom/skills/loom-pre-review/references/output-contract.md +20 -0
- package/payload/plugin/loom/skills/loom-pre-review/scripts/loom-pre-review.py +14 -0
- package/payload/plugin/loom/skills/loom-resume/SKILL.md +72 -0
- package/payload/plugin/loom/skills/loom-resume/agents/openai.yaml +4 -0
- package/payload/plugin/loom/skills/loom-resume/contract.json +47 -0
- package/payload/plugin/loom/skills/loom-resume/references/input-signals.md +15 -0
- package/payload/plugin/loom/skills/loom-resume/references/output-contract.md +58 -0
- package/payload/plugin/loom/skills/loom-resume/scripts/loom-resume.py +14 -0
- package/payload/plugin/loom/skills/loom-retire/SKILL.md +26 -0
- package/payload/plugin/loom/skills/loom-retire/agents/openai.yaml +4 -0
- package/payload/plugin/loom/skills/loom-retire/contract.json +41 -0
- package/payload/plugin/loom/skills/loom-retire/references/input-signals.md +7 -0
- package/payload/plugin/loom/skills/loom-retire/references/output-contract.md +13 -0
- package/payload/plugin/loom/skills/loom-retire/scripts/loom-retire.py +14 -0
- package/payload/plugin/loom/skills/loom-review/SKILL.md +95 -0
- package/payload/plugin/loom/skills/loom-review/agents/openai.yaml +4 -0
- package/payload/plugin/loom/skills/loom-review/contract.json +49 -0
- package/payload/plugin/loom/skills/loom-review/references/input-signals.md +15 -0
- package/payload/plugin/loom/skills/loom-review/references/output-contract.md +57 -0
- package/payload/plugin/loom/skills/loom-review/scripts/loom-review.py +14 -0
- package/payload/plugin/loom/skills/registry.json +64 -0
- package/payload/plugin/loom/skills/route-matrix.md +63 -0
- package/payload/plugin/loom/skills/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/payload/plugin/loom/skills/shared/assets/review/loom-review-result-schema.json +93 -0
- package/payload/plugin/loom/skills/shared/assets/templates/scaffold/plan.md +51 -0
- package/payload/plugin/loom/skills/shared/assets/templates/scaffold/spec.md +48 -0
- package/payload/plugin/loom/skills/shared/references/adoption/deep-existing-repo-default.md +64 -0
- package/payload/plugin/loom/skills/shared/references/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/plugin/loom/skills/shared/references/adoption/routing-and-checkpoints.md +88 -0
- package/payload/plugin/loom/skills/shared/references/governance/host-object-taxonomy.md +148 -0
- package/payload/plugin/loom/skills/shared/references/governance/issue-model.md +127 -0
- package/payload/plugin/loom/skills/shared/references/governance/maturity-and-closing.md +69 -0
- package/payload/plugin/loom/skills/shared/references/governance/principles.md +180 -0
- package/payload/plugin/loom/skills/shared/references/governance/review-model.md +109 -0
- package/payload/plugin/loom/skills/shared/references/governance/state-machine.md +163 -0
- package/payload/plugin/loom/skills/shared/references/governance/truth-and-sync-boundary.md +161 -0
- package/payload/plugin/loom/skills/shared/references/harness/automation-frontload.md +139 -0
- package/payload/plugin/loom/skills/shared/references/harness/closeout-gate.md +97 -0
- package/payload/plugin/loom/skills/shared/references/harness/execution-chain.md +56 -0
- package/payload/plugin/loom/skills/shared/references/harness/execution-context.md +71 -0
- package/payload/plugin/loom/skills/shared/references/harness/fact-chain-contract.md +160 -0
- package/payload/plugin/loom/skills/shared/references/harness/host-action-contract.md +128 -0
- package/payload/plugin/loom/skills/shared/references/harness/host-issue-binding.md +90 -0
- package/payload/plugin/loom/skills/shared/references/harness/host-lifecycle-boundary.md +56 -0
- package/payload/plugin/loom/skills/shared/references/harness/merge-checkpoint.md +95 -0
- package/payload/plugin/loom/skills/shared/references/harness/reconciliation-audit.md +64 -0
- package/payload/plugin/loom/skills/shared/references/harness/recovery-model.md +101 -0
- package/payload/plugin/loom/skills/shared/references/harness/review-execution.md +99 -0
- package/payload/plugin/loom/skills/shared/references/harness/runtime-state.md +103 -0
- package/payload/plugin/loom/skills/shared/references/harness/status-surface.md +121 -0
- package/payload/plugin/loom/skills/shared/references/harness/work-item-contract.md +104 -0
- package/payload/plugin/loom/skills/shared/references/harness/workspace-and-purity.md +73 -0
- package/payload/plugin/loom/skills/shared/references/harness/workspace-model.md +56 -0
- package/payload/plugin/loom/skills/shared/references/templates/pull-request.md +44 -0
- package/payload/plugin/loom/skills/shared/references/templates/review-record.md +35 -0
- package/payload/plugin/loom/skills/shared/references/templates/spec-suite.md +57 -0
- package/payload/plugin/loom/skills/shared/scripts/fact_chain_support.py +509 -0
- package/payload/plugin/loom/skills/shared/scripts/governance_surface.py +869 -0
- package/payload/plugin/loom/skills/shared/scripts/loom_check.py +4724 -0
- package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +5390 -0
- package/payload/plugin/loom/skills/shared/scripts/loom_init.py +1966 -0
- package/payload/plugin/loom/skills/shared/scripts/runtime_paths.py +116 -0
- package/payload/plugin/loom/skills/shared/scripts/runtime_state.py +405 -0
- package/payload/plugin/loom/skills/upgrade-contract.json +29 -0
- package/payload/skills/loom-adopt/.loom-runtime/install-layout.json +77 -0
- package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/SKILL.md +80 -0
- package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/agents/openai.yaml +4 -0
- package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/contract.json +45 -0
- package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/references/input-signals.md +17 -0
- package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/references/output-contract.md +17 -0
- package/payload/skills/loom-adopt/.loom-runtime/loom-adopt/scripts/loom-adopt.py +14 -0
- package/payload/skills/loom-adopt/.loom-runtime/loom-init/references/input-signals.md +16 -0
- package/payload/skills/loom-adopt/.loom-runtime/loom-init/references/intake-signals.md +155 -0
- package/payload/skills/loom-adopt/.loom-runtime/loom-init/references/output-contract.md +208 -0
- package/payload/skills/loom-adopt/.loom-runtime/registry.json +15 -0
- package/payload/skills/loom-adopt/.loom-runtime/route-matrix.md +63 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/issue-model.md +127 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/principles.md +180 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/review-model.md +109 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/state-machine.md +163 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/execution-context.md +71 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/review-execution.md +99 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/status-surface.md +121 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/templates/pull-request.md +44 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/templates/review-record.md +35 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/governance_surface.py +879 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +4683 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py +1966 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/runtime_state.py +405 -0
- package/payload/skills/loom-adopt/.loom-runtime/upgrade-contract.json +29 -0
- package/payload/skills/loom-adopt/SKILL.md +80 -0
- package/payload/skills/loom-adopt/agents/openai.yaml +4 -0
- package/payload/skills/loom-adopt/contract.json +45 -0
- package/payload/skills/loom-adopt/references/input-signals.md +17 -0
- package/payload/skills/loom-adopt/references/loom-init/input-signals.md +16 -0
- package/payload/skills/loom-adopt/references/loom-init/intake-signals.md +155 -0
- package/payload/skills/loom-adopt/references/loom-init/output-contract.md +208 -0
- package/payload/skills/loom-adopt/references/output-contract.md +17 -0
- package/payload/skills/loom-adopt/references/route-matrix.md +63 -0
- package/payload/skills/loom-adopt/references/shared/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-adopt/references/shared/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-adopt/references/shared/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-adopt/references/shared/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-adopt/references/shared/governance/issue-model.md +127 -0
- package/payload/skills/loom-adopt/references/shared/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-adopt/references/shared/governance/principles.md +180 -0
- package/payload/skills/loom-adopt/references/shared/governance/review-model.md +109 -0
- package/payload/skills/loom-adopt/references/shared/governance/state-machine.md +163 -0
- package/payload/skills/loom-adopt/references/shared/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-adopt/references/shared/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-adopt/references/shared/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-adopt/references/shared/harness/execution-chain.md +56 -0
- package/payload/skills/loom-adopt/references/shared/harness/execution-context.md +71 -0
- package/payload/skills/loom-adopt/references/shared/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-adopt/references/shared/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-adopt/references/shared/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-adopt/references/shared/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-adopt/references/shared/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-adopt/references/shared/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-adopt/references/shared/harness/recovery-model.md +101 -0
- package/payload/skills/loom-adopt/references/shared/harness/review-execution.md +99 -0
- package/payload/skills/loom-adopt/references/shared/harness/runtime-state.md +103 -0
- package/payload/skills/loom-adopt/references/shared/harness/status-surface.md +121 -0
- package/payload/skills/loom-adopt/references/shared/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-adopt/references/shared/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-adopt/references/shared/harness/workspace-model.md +56 -0
- package/payload/skills/loom-adopt/references/shared/templates/pull-request.md +44 -0
- package/payload/skills/loom-adopt/references/shared/templates/review-record.md +35 -0
- package/payload/skills/loom-adopt/references/shared/templates/spec-suite.md +57 -0
- package/payload/skills/loom-adopt/scripts/loom-adopt.py +16 -0
- package/payload/skills/loom-handoff/.loom-runtime/install-layout.json +77 -0
- package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/SKILL.md +23 -0
- package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/agents/openai.yaml +4 -0
- package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/contract.json +53 -0
- package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/references/input-signals.md +7 -0
- package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/references/output-contract.md +42 -0
- package/payload/skills/loom-handoff/.loom-runtime/loom-handoff/scripts/loom-handoff.py +14 -0
- package/payload/skills/loom-handoff/.loom-runtime/loom-init/references/input-signals.md +16 -0
- package/payload/skills/loom-handoff/.loom-runtime/loom-init/references/intake-signals.md +155 -0
- package/payload/skills/loom-handoff/.loom-runtime/loom-init/references/output-contract.md +208 -0
- package/payload/skills/loom-handoff/.loom-runtime/registry.json +15 -0
- package/payload/skills/loom-handoff/.loom-runtime/route-matrix.md +63 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/issue-model.md +127 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/principles.md +180 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/review-model.md +109 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/state-machine.md +163 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/execution-context.md +71 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/review-execution.md +99 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/status-surface.md +121 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/templates/pull-request.md +44 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/templates/review-record.md +35 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/governance_surface.py +879 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +4683 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py +1966 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/runtime_state.py +405 -0
- package/payload/skills/loom-handoff/.loom-runtime/upgrade-contract.json +29 -0
- package/payload/skills/loom-handoff/SKILL.md +23 -0
- package/payload/skills/loom-handoff/agents/openai.yaml +4 -0
- package/payload/skills/loom-handoff/contract.json +53 -0
- package/payload/skills/loom-handoff/references/input-signals.md +7 -0
- package/payload/skills/loom-handoff/references/loom-init/input-signals.md +16 -0
- package/payload/skills/loom-handoff/references/loom-init/intake-signals.md +155 -0
- package/payload/skills/loom-handoff/references/loom-init/output-contract.md +208 -0
- package/payload/skills/loom-handoff/references/output-contract.md +42 -0
- package/payload/skills/loom-handoff/references/route-matrix.md +63 -0
- package/payload/skills/loom-handoff/references/shared/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-handoff/references/shared/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-handoff/references/shared/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-handoff/references/shared/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-handoff/references/shared/governance/issue-model.md +127 -0
- package/payload/skills/loom-handoff/references/shared/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-handoff/references/shared/governance/principles.md +180 -0
- package/payload/skills/loom-handoff/references/shared/governance/review-model.md +109 -0
- package/payload/skills/loom-handoff/references/shared/governance/state-machine.md +163 -0
- package/payload/skills/loom-handoff/references/shared/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-handoff/references/shared/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-handoff/references/shared/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-handoff/references/shared/harness/execution-chain.md +56 -0
- package/payload/skills/loom-handoff/references/shared/harness/execution-context.md +71 -0
- package/payload/skills/loom-handoff/references/shared/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-handoff/references/shared/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-handoff/references/shared/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-handoff/references/shared/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-handoff/references/shared/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-handoff/references/shared/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-handoff/references/shared/harness/recovery-model.md +101 -0
- package/payload/skills/loom-handoff/references/shared/harness/review-execution.md +99 -0
- package/payload/skills/loom-handoff/references/shared/harness/runtime-state.md +103 -0
- package/payload/skills/loom-handoff/references/shared/harness/status-surface.md +121 -0
- package/payload/skills/loom-handoff/references/shared/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-handoff/references/shared/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-handoff/references/shared/harness/workspace-model.md +56 -0
- package/payload/skills/loom-handoff/references/shared/templates/pull-request.md +44 -0
- package/payload/skills/loom-handoff/references/shared/templates/review-record.md +35 -0
- package/payload/skills/loom-handoff/references/shared/templates/spec-suite.md +57 -0
- package/payload/skills/loom-handoff/scripts/loom-handoff.py +16 -0
- package/payload/skills/loom-init/.loom-runtime/install-layout.json +77 -0
- package/payload/skills/loom-init/.loom-runtime/loom-init/SKILL.md +246 -0
- package/payload/skills/loom-init/.loom-runtime/loom-init/agents/openai.yaml +4 -0
- package/payload/skills/loom-init/.loom-runtime/loom-init/contract.json +76 -0
- package/payload/skills/loom-init/.loom-runtime/loom-init/references/input-signals.md +16 -0
- package/payload/skills/loom-init/.loom-runtime/loom-init/references/intake-signals.md +155 -0
- package/payload/skills/loom-init/.loom-runtime/loom-init/references/output-contract.md +208 -0
- package/payload/skills/loom-init/.loom-runtime/loom-init/scripts/loom-init.py +14 -0
- package/payload/skills/loom-init/.loom-runtime/registry.json +15 -0
- package/payload/skills/loom-init/.loom-runtime/route-matrix.md +63 -0
- package/payload/skills/loom-init/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/payload/skills/loom-init/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
- package/payload/skills/loom-init/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
- package/payload/skills/loom-init/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/governance/issue-model.md +127 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/governance/principles.md +180 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/governance/review-model.md +109 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/governance/state-machine.md +163 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/execution-context.md +71 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/review-execution.md +99 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/status-surface.md +121 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/templates/pull-request.md +44 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/templates/review-record.md +35 -0
- package/payload/skills/loom-init/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/governance_surface.py +879 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +4683 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_init.py +1966 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/runtime_state.py +405 -0
- package/payload/skills/loom-init/.loom-runtime/upgrade-contract.json +29 -0
- package/payload/skills/loom-init/SKILL.md +246 -0
- package/payload/skills/loom-init/agents/openai.yaml +4 -0
- package/payload/skills/loom-init/contract.json +76 -0
- package/payload/skills/loom-init/references/input-signals.md +16 -0
- package/payload/skills/loom-init/references/intake-signals.md +155 -0
- package/payload/skills/loom-init/references/output-contract.md +208 -0
- package/payload/skills/loom-init/references/route-matrix.md +63 -0
- package/payload/skills/loom-init/references/shared/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-init/references/shared/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-init/references/shared/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-init/references/shared/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-init/references/shared/governance/issue-model.md +127 -0
- package/payload/skills/loom-init/references/shared/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-init/references/shared/governance/principles.md +180 -0
- package/payload/skills/loom-init/references/shared/governance/review-model.md +109 -0
- package/payload/skills/loom-init/references/shared/governance/state-machine.md +163 -0
- package/payload/skills/loom-init/references/shared/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-init/references/shared/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-init/references/shared/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-init/references/shared/harness/execution-chain.md +56 -0
- package/payload/skills/loom-init/references/shared/harness/execution-context.md +71 -0
- package/payload/skills/loom-init/references/shared/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-init/references/shared/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-init/references/shared/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-init/references/shared/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-init/references/shared/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-init/references/shared/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-init/references/shared/harness/recovery-model.md +101 -0
- package/payload/skills/loom-init/references/shared/harness/review-execution.md +99 -0
- package/payload/skills/loom-init/references/shared/harness/runtime-state.md +103 -0
- package/payload/skills/loom-init/references/shared/harness/status-surface.md +121 -0
- package/payload/skills/loom-init/references/shared/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-init/references/shared/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-init/references/shared/harness/workspace-model.md +56 -0
- package/payload/skills/loom-init/references/shared/templates/pull-request.md +44 -0
- package/payload/skills/loom-init/references/shared/templates/review-record.md +35 -0
- package/payload/skills/loom-init/references/shared/templates/spec-suite.md +57 -0
- package/payload/skills/loom-init/scripts/loom-init.py +16 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/install-layout.json +77 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/loom-init/references/input-signals.md +16 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/loom-init/references/intake-signals.md +155 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/loom-init/references/output-contract.md +208 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/SKILL.md +23 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/agents/openai.yaml +4 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/contract.json +49 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/references/input-signals.md +7 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/references/output-contract.md +34 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/loom-merge-ready/scripts/loom-merge-ready.py +14 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/registry.json +15 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/route-matrix.md +63 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/issue-model.md +127 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/principles.md +180 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/review-model.md +109 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/state-machine.md +163 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/execution-context.md +71 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/review-execution.md +99 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/status-surface.md +121 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/templates/pull-request.md +44 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/templates/review-record.md +35 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/governance_surface.py +879 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +4683 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py +1966 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/runtime_state.py +405 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/upgrade-contract.json +29 -0
- package/payload/skills/loom-merge-ready/SKILL.md +23 -0
- package/payload/skills/loom-merge-ready/agents/openai.yaml +4 -0
- package/payload/skills/loom-merge-ready/contract.json +49 -0
- package/payload/skills/loom-merge-ready/references/input-signals.md +7 -0
- package/payload/skills/loom-merge-ready/references/loom-init/input-signals.md +16 -0
- package/payload/skills/loom-merge-ready/references/loom-init/intake-signals.md +155 -0
- package/payload/skills/loom-merge-ready/references/loom-init/output-contract.md +208 -0
- package/payload/skills/loom-merge-ready/references/output-contract.md +34 -0
- package/payload/skills/loom-merge-ready/references/route-matrix.md +63 -0
- package/payload/skills/loom-merge-ready/references/shared/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-merge-ready/references/shared/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-merge-ready/references/shared/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-merge-ready/references/shared/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-merge-ready/references/shared/governance/issue-model.md +127 -0
- package/payload/skills/loom-merge-ready/references/shared/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-merge-ready/references/shared/governance/principles.md +180 -0
- package/payload/skills/loom-merge-ready/references/shared/governance/review-model.md +109 -0
- package/payload/skills/loom-merge-ready/references/shared/governance/state-machine.md +163 -0
- package/payload/skills/loom-merge-ready/references/shared/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/execution-chain.md +56 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/execution-context.md +71 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/recovery-model.md +101 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/review-execution.md +99 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/runtime-state.md +103 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/status-surface.md +121 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-merge-ready/references/shared/harness/workspace-model.md +56 -0
- package/payload/skills/loom-merge-ready/references/shared/templates/pull-request.md +44 -0
- package/payload/skills/loom-merge-ready/references/shared/templates/review-record.md +35 -0
- package/payload/skills/loom-merge-ready/references/shared/templates/spec-suite.md +57 -0
- package/payload/skills/loom-merge-ready/scripts/loom-merge-ready.py +16 -0
- package/payload/skills/loom-pre-review/.loom-runtime/install-layout.json +77 -0
- package/payload/skills/loom-pre-review/.loom-runtime/loom-init/references/input-signals.md +16 -0
- package/payload/skills/loom-pre-review/.loom-runtime/loom-init/references/intake-signals.md +155 -0
- package/payload/skills/loom-pre-review/.loom-runtime/loom-init/references/output-contract.md +208 -0
- package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/SKILL.md +69 -0
- package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/agents/openai.yaml +4 -0
- package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/contract.json +41 -0
- package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/references/input-signals.md +14 -0
- package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/references/output-contract.md +20 -0
- package/payload/skills/loom-pre-review/.loom-runtime/loom-pre-review/scripts/loom-pre-review.py +14 -0
- package/payload/skills/loom-pre-review/.loom-runtime/registry.json +15 -0
- package/payload/skills/loom-pre-review/.loom-runtime/route-matrix.md +63 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/issue-model.md +127 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/principles.md +180 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/review-model.md +109 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/state-machine.md +163 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/execution-context.md +71 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/review-execution.md +99 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/status-surface.md +121 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/templates/pull-request.md +44 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/templates/review-record.md +35 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/governance_surface.py +879 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +4683 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py +1966 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/runtime_state.py +405 -0
- package/payload/skills/loom-pre-review/.loom-runtime/upgrade-contract.json +29 -0
- package/payload/skills/loom-pre-review/SKILL.md +69 -0
- package/payload/skills/loom-pre-review/agents/openai.yaml +4 -0
- package/payload/skills/loom-pre-review/contract.json +41 -0
- package/payload/skills/loom-pre-review/references/input-signals.md +14 -0
- package/payload/skills/loom-pre-review/references/loom-init/input-signals.md +16 -0
- package/payload/skills/loom-pre-review/references/loom-init/intake-signals.md +155 -0
- package/payload/skills/loom-pre-review/references/loom-init/output-contract.md +208 -0
- package/payload/skills/loom-pre-review/references/output-contract.md +20 -0
- package/payload/skills/loom-pre-review/references/route-matrix.md +63 -0
- package/payload/skills/loom-pre-review/references/shared/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-pre-review/references/shared/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-pre-review/references/shared/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-pre-review/references/shared/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-pre-review/references/shared/governance/issue-model.md +127 -0
- package/payload/skills/loom-pre-review/references/shared/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-pre-review/references/shared/governance/principles.md +180 -0
- package/payload/skills/loom-pre-review/references/shared/governance/review-model.md +109 -0
- package/payload/skills/loom-pre-review/references/shared/governance/state-machine.md +163 -0
- package/payload/skills/loom-pre-review/references/shared/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-pre-review/references/shared/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-pre-review/references/shared/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-pre-review/references/shared/harness/execution-chain.md +56 -0
- package/payload/skills/loom-pre-review/references/shared/harness/execution-context.md +71 -0
- package/payload/skills/loom-pre-review/references/shared/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-pre-review/references/shared/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-pre-review/references/shared/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-pre-review/references/shared/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-pre-review/references/shared/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-pre-review/references/shared/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-pre-review/references/shared/harness/recovery-model.md +101 -0
- package/payload/skills/loom-pre-review/references/shared/harness/review-execution.md +99 -0
- package/payload/skills/loom-pre-review/references/shared/harness/runtime-state.md +103 -0
- package/payload/skills/loom-pre-review/references/shared/harness/status-surface.md +121 -0
- package/payload/skills/loom-pre-review/references/shared/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-pre-review/references/shared/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-pre-review/references/shared/harness/workspace-model.md +56 -0
- package/payload/skills/loom-pre-review/references/shared/templates/pull-request.md +44 -0
- package/payload/skills/loom-pre-review/references/shared/templates/review-record.md +35 -0
- package/payload/skills/loom-pre-review/references/shared/templates/spec-suite.md +57 -0
- package/payload/skills/loom-pre-review/scripts/loom-pre-review.py +16 -0
- package/payload/skills/loom-resume/.loom-runtime/install-layout.json +77 -0
- package/payload/skills/loom-resume/.loom-runtime/loom-init/references/input-signals.md +16 -0
- package/payload/skills/loom-resume/.loom-runtime/loom-init/references/intake-signals.md +155 -0
- package/payload/skills/loom-resume/.loom-runtime/loom-init/references/output-contract.md +208 -0
- package/payload/skills/loom-resume/.loom-runtime/loom-resume/SKILL.md +72 -0
- package/payload/skills/loom-resume/.loom-runtime/loom-resume/agents/openai.yaml +4 -0
- package/payload/skills/loom-resume/.loom-runtime/loom-resume/contract.json +47 -0
- package/payload/skills/loom-resume/.loom-runtime/loom-resume/references/input-signals.md +15 -0
- package/payload/skills/loom-resume/.loom-runtime/loom-resume/references/output-contract.md +58 -0
- package/payload/skills/loom-resume/.loom-runtime/loom-resume/scripts/loom-resume.py +14 -0
- package/payload/skills/loom-resume/.loom-runtime/registry.json +15 -0
- package/payload/skills/loom-resume/.loom-runtime/route-matrix.md +63 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/issue-model.md +127 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/principles.md +180 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/review-model.md +109 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/state-machine.md +163 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/execution-context.md +71 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/review-execution.md +99 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/status-surface.md +121 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/templates/pull-request.md +44 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/templates/review-record.md +35 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/governance_surface.py +879 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +4683 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py +1966 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/runtime_state.py +405 -0
- package/payload/skills/loom-resume/.loom-runtime/upgrade-contract.json +29 -0
- package/payload/skills/loom-resume/SKILL.md +72 -0
- package/payload/skills/loom-resume/agents/openai.yaml +4 -0
- package/payload/skills/loom-resume/contract.json +47 -0
- package/payload/skills/loom-resume/references/input-signals.md +15 -0
- package/payload/skills/loom-resume/references/loom-init/input-signals.md +16 -0
- package/payload/skills/loom-resume/references/loom-init/intake-signals.md +155 -0
- package/payload/skills/loom-resume/references/loom-init/output-contract.md +208 -0
- package/payload/skills/loom-resume/references/output-contract.md +58 -0
- package/payload/skills/loom-resume/references/route-matrix.md +63 -0
- package/payload/skills/loom-resume/references/shared/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-resume/references/shared/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-resume/references/shared/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-resume/references/shared/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-resume/references/shared/governance/issue-model.md +127 -0
- package/payload/skills/loom-resume/references/shared/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-resume/references/shared/governance/principles.md +180 -0
- package/payload/skills/loom-resume/references/shared/governance/review-model.md +109 -0
- package/payload/skills/loom-resume/references/shared/governance/state-machine.md +163 -0
- package/payload/skills/loom-resume/references/shared/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-resume/references/shared/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-resume/references/shared/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-resume/references/shared/harness/execution-chain.md +56 -0
- package/payload/skills/loom-resume/references/shared/harness/execution-context.md +71 -0
- package/payload/skills/loom-resume/references/shared/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-resume/references/shared/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-resume/references/shared/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-resume/references/shared/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-resume/references/shared/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-resume/references/shared/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-resume/references/shared/harness/recovery-model.md +101 -0
- package/payload/skills/loom-resume/references/shared/harness/review-execution.md +99 -0
- package/payload/skills/loom-resume/references/shared/harness/runtime-state.md +103 -0
- package/payload/skills/loom-resume/references/shared/harness/status-surface.md +121 -0
- package/payload/skills/loom-resume/references/shared/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-resume/references/shared/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-resume/references/shared/harness/workspace-model.md +56 -0
- package/payload/skills/loom-resume/references/shared/templates/pull-request.md +44 -0
- package/payload/skills/loom-resume/references/shared/templates/review-record.md +35 -0
- package/payload/skills/loom-resume/references/shared/templates/spec-suite.md +57 -0
- package/payload/skills/loom-resume/scripts/loom-resume.py +16 -0
- package/payload/skills/loom-retire/.loom-runtime/install-layout.json +77 -0
- package/payload/skills/loom-retire/.loom-runtime/loom-init/references/input-signals.md +16 -0
- package/payload/skills/loom-retire/.loom-runtime/loom-init/references/intake-signals.md +155 -0
- package/payload/skills/loom-retire/.loom-runtime/loom-init/references/output-contract.md +208 -0
- package/payload/skills/loom-retire/.loom-runtime/loom-retire/SKILL.md +26 -0
- package/payload/skills/loom-retire/.loom-runtime/loom-retire/agents/openai.yaml +4 -0
- package/payload/skills/loom-retire/.loom-runtime/loom-retire/contract.json +41 -0
- package/payload/skills/loom-retire/.loom-runtime/loom-retire/references/input-signals.md +7 -0
- package/payload/skills/loom-retire/.loom-runtime/loom-retire/references/output-contract.md +13 -0
- package/payload/skills/loom-retire/.loom-runtime/loom-retire/scripts/loom-retire.py +14 -0
- package/payload/skills/loom-retire/.loom-runtime/registry.json +15 -0
- package/payload/skills/loom-retire/.loom-runtime/route-matrix.md +63 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/issue-model.md +127 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/principles.md +180 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/review-model.md +109 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/state-machine.md +163 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/execution-context.md +71 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/review-execution.md +99 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/status-surface.md +121 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/templates/pull-request.md +44 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/templates/review-record.md +35 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/governance_surface.py +879 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +4683 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py +1966 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/runtime_state.py +405 -0
- package/payload/skills/loom-retire/.loom-runtime/upgrade-contract.json +29 -0
- package/payload/skills/loom-retire/SKILL.md +26 -0
- package/payload/skills/loom-retire/agents/openai.yaml +4 -0
- package/payload/skills/loom-retire/contract.json +41 -0
- package/payload/skills/loom-retire/references/input-signals.md +7 -0
- package/payload/skills/loom-retire/references/loom-init/input-signals.md +16 -0
- package/payload/skills/loom-retire/references/loom-init/intake-signals.md +155 -0
- package/payload/skills/loom-retire/references/loom-init/output-contract.md +208 -0
- package/payload/skills/loom-retire/references/output-contract.md +13 -0
- package/payload/skills/loom-retire/references/route-matrix.md +63 -0
- package/payload/skills/loom-retire/references/shared/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-retire/references/shared/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-retire/references/shared/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-retire/references/shared/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-retire/references/shared/governance/issue-model.md +127 -0
- package/payload/skills/loom-retire/references/shared/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-retire/references/shared/governance/principles.md +180 -0
- package/payload/skills/loom-retire/references/shared/governance/review-model.md +109 -0
- package/payload/skills/loom-retire/references/shared/governance/state-machine.md +163 -0
- package/payload/skills/loom-retire/references/shared/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-retire/references/shared/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-retire/references/shared/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-retire/references/shared/harness/execution-chain.md +56 -0
- package/payload/skills/loom-retire/references/shared/harness/execution-context.md +71 -0
- package/payload/skills/loom-retire/references/shared/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-retire/references/shared/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-retire/references/shared/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-retire/references/shared/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-retire/references/shared/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-retire/references/shared/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-retire/references/shared/harness/recovery-model.md +101 -0
- package/payload/skills/loom-retire/references/shared/harness/review-execution.md +99 -0
- package/payload/skills/loom-retire/references/shared/harness/runtime-state.md +103 -0
- package/payload/skills/loom-retire/references/shared/harness/status-surface.md +121 -0
- package/payload/skills/loom-retire/references/shared/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-retire/references/shared/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-retire/references/shared/harness/workspace-model.md +56 -0
- package/payload/skills/loom-retire/references/shared/templates/pull-request.md +44 -0
- package/payload/skills/loom-retire/references/shared/templates/review-record.md +35 -0
- package/payload/skills/loom-retire/references/shared/templates/spec-suite.md +57 -0
- package/payload/skills/loom-retire/scripts/loom-retire.py +16 -0
- package/payload/skills/loom-review/.loom-runtime/install-layout.json +77 -0
- package/payload/skills/loom-review/.loom-runtime/loom-init/references/input-signals.md +16 -0
- package/payload/skills/loom-review/.loom-runtime/loom-init/references/intake-signals.md +155 -0
- package/payload/skills/loom-review/.loom-runtime/loom-init/references/output-contract.md +208 -0
- package/payload/skills/loom-review/.loom-runtime/loom-review/SKILL.md +95 -0
- package/payload/skills/loom-review/.loom-runtime/loom-review/agents/openai.yaml +4 -0
- package/payload/skills/loom-review/.loom-runtime/loom-review/contract.json +49 -0
- package/payload/skills/loom-review/.loom-runtime/loom-review/references/input-signals.md +15 -0
- package/payload/skills/loom-review/.loom-runtime/loom-review/references/output-contract.md +57 -0
- package/payload/skills/loom-review/.loom-runtime/loom-review/scripts/loom-review.py +14 -0
- package/payload/skills/loom-review/.loom-runtime/registry.json +15 -0
- package/payload/skills/loom-review/.loom-runtime/route-matrix.md +63 -0
- package/payload/skills/loom-review/.loom-runtime/shared/assets/github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/payload/skills/loom-review/.loom-runtime/shared/assets/review/loom-review-result-schema.json +93 -0
- package/payload/skills/loom-review/.loom-runtime/shared/assets/templates/scaffold/plan.md +51 -0
- package/payload/skills/loom-review/.loom-runtime/shared/assets/templates/scaffold/spec.md +48 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/governance/issue-model.md +127 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/governance/principles.md +180 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/governance/review-model.md +109 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/governance/state-machine.md +163 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/execution-chain.md +56 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/execution-context.md +71 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/recovery-model.md +101 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/review-execution.md +99 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/runtime-state.md +103 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/status-surface.md +121 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/harness/workspace-model.md +56 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/templates/pull-request.md +44 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/templates/review-record.md +35 -0
- package/payload/skills/loom-review/.loom-runtime/shared/references/templates/spec-suite.md +57 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/fact_chain_support.py +509 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/governance_surface.py +879 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +4683 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +5390 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_init.py +1966 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/runtime_paths.py +116 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/runtime_state.py +405 -0
- package/payload/skills/loom-review/.loom-runtime/upgrade-contract.json +29 -0
- package/payload/skills/loom-review/SKILL.md +95 -0
- package/payload/skills/loom-review/agents/openai.yaml +4 -0
- package/payload/skills/loom-review/contract.json +49 -0
- package/payload/skills/loom-review/references/input-signals.md +15 -0
- package/payload/skills/loom-review/references/loom-init/input-signals.md +16 -0
- package/payload/skills/loom-review/references/loom-init/intake-signals.md +155 -0
- package/payload/skills/loom-review/references/loom-init/output-contract.md +208 -0
- package/payload/skills/loom-review/references/output-contract.md +57 -0
- package/payload/skills/loom-review/references/route-matrix.md +63 -0
- package/payload/skills/loom-review/references/shared/adoption/deep-existing-repo-default.md +64 -0
- package/payload/skills/loom-review/references/shared/adoption/lightweight-retrofit-default.md +71 -0
- package/payload/skills/loom-review/references/shared/adoption/routing-and-checkpoints.md +88 -0
- package/payload/skills/loom-review/references/shared/governance/host-object-taxonomy.md +148 -0
- package/payload/skills/loom-review/references/shared/governance/issue-model.md +127 -0
- package/payload/skills/loom-review/references/shared/governance/maturity-and-closing.md +69 -0
- package/payload/skills/loom-review/references/shared/governance/principles.md +180 -0
- package/payload/skills/loom-review/references/shared/governance/review-model.md +109 -0
- package/payload/skills/loom-review/references/shared/governance/state-machine.md +163 -0
- package/payload/skills/loom-review/references/shared/governance/truth-and-sync-boundary.md +161 -0
- package/payload/skills/loom-review/references/shared/harness/automation-frontload.md +139 -0
- package/payload/skills/loom-review/references/shared/harness/closeout-gate.md +97 -0
- package/payload/skills/loom-review/references/shared/harness/execution-chain.md +56 -0
- package/payload/skills/loom-review/references/shared/harness/execution-context.md +71 -0
- package/payload/skills/loom-review/references/shared/harness/fact-chain-contract.md +160 -0
- package/payload/skills/loom-review/references/shared/harness/host-action-contract.md +128 -0
- package/payload/skills/loom-review/references/shared/harness/host-issue-binding.md +90 -0
- package/payload/skills/loom-review/references/shared/harness/host-lifecycle-boundary.md +56 -0
- package/payload/skills/loom-review/references/shared/harness/merge-checkpoint.md +95 -0
- package/payload/skills/loom-review/references/shared/harness/reconciliation-audit.md +64 -0
- package/payload/skills/loom-review/references/shared/harness/recovery-model.md +101 -0
- package/payload/skills/loom-review/references/shared/harness/review-execution.md +99 -0
- package/payload/skills/loom-review/references/shared/harness/runtime-state.md +103 -0
- package/payload/skills/loom-review/references/shared/harness/status-surface.md +121 -0
- package/payload/skills/loom-review/references/shared/harness/work-item-contract.md +104 -0
- package/payload/skills/loom-review/references/shared/harness/workspace-and-purity.md +73 -0
- package/payload/skills/loom-review/references/shared/harness/workspace-model.md +56 -0
- package/payload/skills/loom-review/references/shared/templates/pull-request.md +44 -0
- package/payload/skills/loom-review/references/shared/templates/review-record.md +35 -0
- package/payload/skills/loom-review/references/shared/templates/spec-suite.md +57 -0
- package/payload/skills/loom-review/scripts/loom-review.py +16 -0
|
@@ -0,0 +1,4724 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Minimal Loom repository mechanical self-check."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import unicodedata
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from collections import Counter, defaultdict
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from fact_chain_support import inspect_fact_chain
|
|
19
|
+
from governance_surface import build_governance_surface
|
|
20
|
+
from loom_flow import repo_specific_requirements_payload
|
|
21
|
+
from runtime_paths import repo_local_root
|
|
22
|
+
|
|
23
|
+
TOP_LEVEL_DIRS = (
|
|
24
|
+
"adoption",
|
|
25
|
+
"governance",
|
|
26
|
+
"harness",
|
|
27
|
+
"skills",
|
|
28
|
+
"templates",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
TOP_LEVEL_FILES = (
|
|
32
|
+
"AGENTS.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"Makefile",
|
|
35
|
+
"README.md",
|
|
36
|
+
"VISION.md",
|
|
37
|
+
"governance-design.md",
|
|
38
|
+
"harness-design.md",
|
|
39
|
+
"system-design.md",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
AREA_READMES = (
|
|
43
|
+
"adoption/README.md",
|
|
44
|
+
"governance/README.md",
|
|
45
|
+
"harness/README.md",
|
|
46
|
+
"skills/README.md",
|
|
47
|
+
"templates/README.md",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
CORE_DOCS = (
|
|
51
|
+
".github/PULL_REQUEST_TEMPLATE.md",
|
|
52
|
+
".github/workflows/loom-check.yml",
|
|
53
|
+
".github/workflows/node-installer-pr.yml",
|
|
54
|
+
".github/workflows/node-installer-release.yml",
|
|
55
|
+
"governance/principles.md",
|
|
56
|
+
"governance/review-model.md",
|
|
57
|
+
"governance/maturity-and-closing.md",
|
|
58
|
+
"governance/state-machine.md",
|
|
59
|
+
"governance/truth-and-sync-boundary.md",
|
|
60
|
+
"governance/host-object-taxonomy.md",
|
|
61
|
+
"harness/work-item-contract.md",
|
|
62
|
+
"harness/fact-chain-contract.md",
|
|
63
|
+
"harness/execution-context.md",
|
|
64
|
+
"harness/execution-chain.md",
|
|
65
|
+
"harness/checkpoint-model.md",
|
|
66
|
+
"harness/workspace-model.md",
|
|
67
|
+
"harness/workspace-lifecycle.md",
|
|
68
|
+
"harness/host-action-contract.md",
|
|
69
|
+
"harness/host-lifecycle-boundary.md",
|
|
70
|
+
"harness/reconciliation-audit.md",
|
|
71
|
+
"harness/recovery-model.md",
|
|
72
|
+
"harness/review-execution.md",
|
|
73
|
+
"harness/status-surface.md",
|
|
74
|
+
"harness/automation-frontload.md",
|
|
75
|
+
"harness/merge-checkpoint.md",
|
|
76
|
+
"harness/closeout-gate.md",
|
|
77
|
+
"harness/workspace-and-purity.md",
|
|
78
|
+
"templates/spec-suite.md",
|
|
79
|
+
"templates/pull-request.md",
|
|
80
|
+
"adoption/extraction-ledger.md",
|
|
81
|
+
"adoption/landing-map.md",
|
|
82
|
+
"adoption/rationale.md",
|
|
83
|
+
"adoption/routing-and-checkpoints.md",
|
|
84
|
+
"adoption/lightweight-retrofit-default.md",
|
|
85
|
+
"adoption/repo-companion-contract.md",
|
|
86
|
+
"adoption/repo-interop-contract.md",
|
|
87
|
+
"skills/distribution-and-adapter-contract.md",
|
|
88
|
+
"skills/registry.json",
|
|
89
|
+
"skills/install-layout.json",
|
|
90
|
+
"skills/upgrade-contract.json",
|
|
91
|
+
"skills/route-matrix.md",
|
|
92
|
+
"skills/loom-init/SKILL.md",
|
|
93
|
+
"skills/loom-init/contract.json",
|
|
94
|
+
"skills/loom-init/references/input-signals.md",
|
|
95
|
+
"skills/loom-init/references/intake-signals.md",
|
|
96
|
+
"skills/loom-init/references/output-contract.md",
|
|
97
|
+
"skills/loom-review/SKILL.md",
|
|
98
|
+
"skills/loom-review/contract.json",
|
|
99
|
+
"skills/loom-review/references/input-signals.md",
|
|
100
|
+
"skills/loom-review/references/output-contract.md",
|
|
101
|
+
"templates/review-record.md",
|
|
102
|
+
"templates/scaffold/spec.md",
|
|
103
|
+
"templates/scaffold/plan.md",
|
|
104
|
+
"packages/loom-installer/README.md",
|
|
105
|
+
"packages/loom-installer/package.json",
|
|
106
|
+
"packages/loom-installer/package-lock.json",
|
|
107
|
+
"packages/loom-installer/tsconfig.json",
|
|
108
|
+
"packages/loom-installer/scripts/build-payload.mjs",
|
|
109
|
+
"packages/loom-installer/scripts/check-doc-sync.mjs",
|
|
110
|
+
"packages/loom-installer/scripts/check-payload-drift.mjs",
|
|
111
|
+
"packages/loom-installer/scripts/check-version-bump.mjs",
|
|
112
|
+
"packages/loom-installer/src/cli.ts",
|
|
113
|
+
"packages/loom-installer/src/index.ts",
|
|
114
|
+
"packages/loom-installer/test/installer.test.ts",
|
|
115
|
+
"tools/loom_init.py",
|
|
116
|
+
"tools/loom_flow.py",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
AUTOMATION_FRONTLOAD_TEMPLATES = (
|
|
120
|
+
"templates/spec-suite.md",
|
|
121
|
+
"templates/pull-request.md",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
AUTOMATION_FRONTLOAD_SKILLS = (
|
|
125
|
+
"skills/README.md",
|
|
126
|
+
"skills/distribution-and-adapter-contract.md",
|
|
127
|
+
"skills/install-layout.json",
|
|
128
|
+
"skills/route-matrix.md",
|
|
129
|
+
"skills/loom-init/SKILL.md",
|
|
130
|
+
"skills/loom-init/references/input-signals.md",
|
|
131
|
+
"skills/loom-init/references/intake-signals.md",
|
|
132
|
+
"skills/loom-init/references/output-contract.md",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
AUTOMATION_FRONTLOAD_EXECUTION_SUPPORT = (
|
|
136
|
+
"harness/work-item-contract.md",
|
|
137
|
+
"harness/execution-context.md",
|
|
138
|
+
"harness/execution-chain.md",
|
|
139
|
+
"harness/checkpoint-model.md",
|
|
140
|
+
"harness/workspace-model.md",
|
|
141
|
+
"harness/workspace-lifecycle.md",
|
|
142
|
+
"harness/recovery-model.md",
|
|
143
|
+
"harness/status-surface.md",
|
|
144
|
+
"harness/automation-frontload.md",
|
|
145
|
+
"harness/merge-checkpoint.md",
|
|
146
|
+
"harness/workspace-and-purity.md",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
DEMO_ASSETS = (
|
|
150
|
+
"examples/new-project/.gitkeep",
|
|
151
|
+
"examples/new-project/AGENTS.md",
|
|
152
|
+
"examples/new-project/.github/PULL_REQUEST_TEMPLATE.md",
|
|
153
|
+
"examples/new-project/.loom/bootstrap/init-result.json",
|
|
154
|
+
"examples/new-project/.loom/bootstrap/manifest.json",
|
|
155
|
+
"examples/new-project/.loom/work-items/INIT-0001.md",
|
|
156
|
+
"examples/new-project/.loom/progress/INIT-0001.md",
|
|
157
|
+
"examples/new-project/.loom/reviews/INIT-0001.json",
|
|
158
|
+
"examples/new-project/.loom/status/current.md",
|
|
159
|
+
"examples/new-project/.loom/bin/loom_init.py",
|
|
160
|
+
"examples/new-project/.loom/bin/fact_chain_support.py",
|
|
161
|
+
"examples/new-project/.loom/bin/runtime_paths.py",
|
|
162
|
+
"examples/new-project/.loom/bin/runtime_state.py",
|
|
163
|
+
"examples/new-project/.loom/bin/loom_flow.py",
|
|
164
|
+
"examples/new-project/.loom/bin/loom_check.py",
|
|
165
|
+
"examples/new-project/.loom/specs/INIT-0001/spec.md",
|
|
166
|
+
"examples/new-project/.loom/specs/INIT-0001/plan.md",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
LINK_RE = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)")
|
|
170
|
+
HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)(?:\s+#+\s*)?$")
|
|
171
|
+
CODE_FENCE_RE = re.compile(r"^(```|~~~)")
|
|
172
|
+
EXTERNAL_SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*:")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(frozen=True)
|
|
176
|
+
class Failure:
|
|
177
|
+
category: str
|
|
178
|
+
detail: str
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
GOVERNANCE_SURFACE_ROUTE_SKILLS = {
|
|
182
|
+
"loom-adopt",
|
|
183
|
+
"loom-resume",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
GOVERNANCE_SURFACE_CONTRACT_SKILLS = {
|
|
187
|
+
"loom-adopt",
|
|
188
|
+
"loom-resume",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
REVIEW_FINDING_SEVERITIES = {"warn", "block"}
|
|
192
|
+
REVIEW_FINDING_DISPOSITION_STATUSES = {"accepted", "rejected", "deferred"}
|
|
193
|
+
REPO_INTERFACE_AVAILABILITY = {"absent", "companion_docs_only", "incomplete", "present"}
|
|
194
|
+
REPO_INTERFACE_ENFORCEMENT = {"blocking", "advisory"}
|
|
195
|
+
REPO_INTEROP_AVAILABILITY = {"absent", "incomplete", "present"}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def repo_root_from_argv(argv: list[str]) -> Path:
|
|
199
|
+
if len(argv) > 2:
|
|
200
|
+
raise SystemExit("usage: loom_check.py [repo-root]")
|
|
201
|
+
if len(argv) == 2:
|
|
202
|
+
return Path(argv[1]).expanduser().resolve()
|
|
203
|
+
hinted_root = repo_local_root(__file__)
|
|
204
|
+
if hinted_root is not None:
|
|
205
|
+
return hinted_root
|
|
206
|
+
current = Path.cwd().resolve()
|
|
207
|
+
if (current / "skills").exists() and (current / "README.md").exists():
|
|
208
|
+
return current
|
|
209
|
+
return Path(__file__).resolve().parent.parent
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def check_required_paths(root: Path, category: str, paths: tuple[str, ...]) -> list[Failure]:
|
|
213
|
+
failures: list[Failure] = []
|
|
214
|
+
for relative_path in paths:
|
|
215
|
+
if not (root / relative_path).exists():
|
|
216
|
+
failures.append(Failure(category, f"missing `{relative_path}`"))
|
|
217
|
+
return failures
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def iter_markdown_files(root: Path) -> list[Path]:
|
|
221
|
+
return sorted(path for path in root.rglob("*.md") if path.is_file())
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def split_link_target(raw_target: str) -> tuple[str, str]:
|
|
225
|
+
target = raw_target.strip()
|
|
226
|
+
if not target:
|
|
227
|
+
return "", ""
|
|
228
|
+
if target.startswith("<") and target.endswith(">"):
|
|
229
|
+
target = target[1:-1].strip()
|
|
230
|
+
if " " in target:
|
|
231
|
+
target = target.split(" ", 1)[0]
|
|
232
|
+
if "#" in target:
|
|
233
|
+
path_part, fragment = target.split("#", 1)
|
|
234
|
+
return path_part, fragment
|
|
235
|
+
return target, ""
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def markdown_links(path: Path) -> list[tuple[int, str]]:
|
|
239
|
+
results: list[tuple[int, str]] = []
|
|
240
|
+
in_code_fence = False
|
|
241
|
+
for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
242
|
+
if CODE_FENCE_RE.match(line.strip()):
|
|
243
|
+
in_code_fence = not in_code_fence
|
|
244
|
+
continue
|
|
245
|
+
if in_code_fence:
|
|
246
|
+
continue
|
|
247
|
+
for match in LINK_RE.finditer(line):
|
|
248
|
+
results.append((line_no, match.group(1)))
|
|
249
|
+
return results
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def strip_inline_markdown(text: str) -> str:
|
|
253
|
+
text = re.sub(r"`([^`]*)`", r"\1", text)
|
|
254
|
+
text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", text)
|
|
255
|
+
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
|
256
|
+
text = re.sub(r"[*_~]", "", text)
|
|
257
|
+
return text
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def github_anchor_map(path: Path, cache: dict[Path, set[str]]) -> set[str]:
|
|
261
|
+
cached = cache.get(path)
|
|
262
|
+
if cached is not None:
|
|
263
|
+
return cached
|
|
264
|
+
|
|
265
|
+
anchors: set[str] = set()
|
|
266
|
+
duplicates: Counter[str] = Counter()
|
|
267
|
+
in_code_fence = False
|
|
268
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
269
|
+
stripped = line.strip()
|
|
270
|
+
if CODE_FENCE_RE.match(stripped):
|
|
271
|
+
in_code_fence = not in_code_fence
|
|
272
|
+
continue
|
|
273
|
+
if in_code_fence:
|
|
274
|
+
continue
|
|
275
|
+
match = HEADING_RE.match(line)
|
|
276
|
+
if not match:
|
|
277
|
+
continue
|
|
278
|
+
base = github_slug(strip_inline_markdown(match.group(2)))
|
|
279
|
+
if not base:
|
|
280
|
+
continue
|
|
281
|
+
duplicates[base] += 1
|
|
282
|
+
anchor = base if duplicates[base] == 1 else f"{base}-{duplicates[base] - 1}"
|
|
283
|
+
anchors.add(anchor)
|
|
284
|
+
|
|
285
|
+
cache[path] = anchors
|
|
286
|
+
return anchors
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def github_slug(text: str) -> str:
|
|
290
|
+
text = unicodedata.normalize("NFKD", text).lower().strip()
|
|
291
|
+
slug_chars: list[str] = []
|
|
292
|
+
last_was_dash = False
|
|
293
|
+
for char in text:
|
|
294
|
+
if char.isspace() or char == "-":
|
|
295
|
+
if slug_chars and not last_was_dash:
|
|
296
|
+
slug_chars.append("-")
|
|
297
|
+
last_was_dash = True
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
category = unicodedata.category(char)
|
|
301
|
+
if category[0] in {"L", "N"} or category == "Mn":
|
|
302
|
+
slug_chars.append(char)
|
|
303
|
+
last_was_dash = False
|
|
304
|
+
|
|
305
|
+
return "".join(slug_chars).strip("-")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def resolve_link_target(root: Path, source_path: Path, raw_target: str) -> tuple[Path | None, str]:
|
|
309
|
+
target, fragment = split_link_target(raw_target)
|
|
310
|
+
if not target:
|
|
311
|
+
return source_path, fragment
|
|
312
|
+
if EXTERNAL_SCHEME_RE.match(target) or target.startswith("//"):
|
|
313
|
+
return None, ""
|
|
314
|
+
if target.startswith("/"):
|
|
315
|
+
return None, ""
|
|
316
|
+
|
|
317
|
+
resolved = (source_path.parent / target).resolve()
|
|
318
|
+
if resolved.exists():
|
|
319
|
+
return resolved, fragment
|
|
320
|
+
if resolved.is_dir():
|
|
321
|
+
readme = resolved / "README.md"
|
|
322
|
+
if readme.exists():
|
|
323
|
+
return readme, fragment
|
|
324
|
+
try:
|
|
325
|
+
resolved.relative_to(root)
|
|
326
|
+
except ValueError:
|
|
327
|
+
return resolved, fragment
|
|
328
|
+
return resolved, fragment
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def check_markdown_links(root: Path) -> list[Failure]:
|
|
332
|
+
failures: list[Failure] = []
|
|
333
|
+
anchor_cache: dict[Path, set[str]] = {}
|
|
334
|
+
for markdown_path in iter_markdown_files(root):
|
|
335
|
+
for line_no, raw_target in markdown_links(markdown_path):
|
|
336
|
+
resolved, fragment = resolve_link_target(root, markdown_path, raw_target)
|
|
337
|
+
if resolved is None:
|
|
338
|
+
continue
|
|
339
|
+
if not resolved.exists():
|
|
340
|
+
detail = (
|
|
341
|
+
f"`{markdown_path.relative_to(root)}:{line_no}` -> `{raw_target}` "
|
|
342
|
+
f"(missing `{resolved.relative_to(root) if resolved.is_absolute() and is_within(resolved, root) else resolved}`)"
|
|
343
|
+
)
|
|
344
|
+
failures.append(Failure("markdown-links", detail))
|
|
345
|
+
continue
|
|
346
|
+
if fragment and resolved.suffix.lower() == ".md":
|
|
347
|
+
anchors = github_anchor_map(resolved, anchor_cache)
|
|
348
|
+
if fragment not in anchors:
|
|
349
|
+
detail = (
|
|
350
|
+
f"`{markdown_path.relative_to(root)}:{line_no}` -> `{raw_target}` "
|
|
351
|
+
f"(missing anchor `#{fragment}` in `{resolved.relative_to(root)}`)"
|
|
352
|
+
)
|
|
353
|
+
failures.append(Failure("markdown-links", detail))
|
|
354
|
+
return failures
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def load_json_file(path: Path) -> object:
|
|
358
|
+
with path.open(encoding="utf-8") as handle:
|
|
359
|
+
return json.load(handle)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def run_command(
|
|
363
|
+
root: Path,
|
|
364
|
+
args: list[str],
|
|
365
|
+
cwd: Path | None = None,
|
|
366
|
+
env: dict[str, str] | None = None,
|
|
367
|
+
timeout_seconds: float | None = None,
|
|
368
|
+
) -> subprocess.CompletedProcess[str]:
|
|
369
|
+
command_env = os.environ.copy()
|
|
370
|
+
for key in ("LOOM_SOURCE_REPO_ROOT", "LOOM_INSTALLED_SKILLS_ROOT", "LOOM_RUNTIME_SCENE"):
|
|
371
|
+
command_env.pop(key, None)
|
|
372
|
+
if env:
|
|
373
|
+
command_env.update(env)
|
|
374
|
+
return subprocess.run(
|
|
375
|
+
args,
|
|
376
|
+
cwd=cwd or root,
|
|
377
|
+
check=False,
|
|
378
|
+
capture_output=True,
|
|
379
|
+
text=True,
|
|
380
|
+
env=command_env,
|
|
381
|
+
timeout=timeout_seconds,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def load_command_json(
|
|
386
|
+
root: Path,
|
|
387
|
+
args: list[str],
|
|
388
|
+
*,
|
|
389
|
+
cwd: Path | None = None,
|
|
390
|
+
env: dict[str, str] | None = None,
|
|
391
|
+
timeout_seconds: float | None = None,
|
|
392
|
+
) -> tuple[dict[str, object] | None, str | None]:
|
|
393
|
+
try:
|
|
394
|
+
result = run_command(root, args, cwd=cwd, env=env, timeout_seconds=timeout_seconds)
|
|
395
|
+
except subprocess.TimeoutExpired:
|
|
396
|
+
return None, f"command timed out after {int(timeout_seconds or 0)}s"
|
|
397
|
+
if not result.stdout.strip():
|
|
398
|
+
detail = "command produced no JSON output"
|
|
399
|
+
if result.stderr.strip():
|
|
400
|
+
detail += f": {result.stderr.strip()}"
|
|
401
|
+
return None, detail
|
|
402
|
+
try:
|
|
403
|
+
payload = json.loads(result.stdout)
|
|
404
|
+
except json.JSONDecodeError as exc:
|
|
405
|
+
return None, f"invalid JSON output: {exc.msg}"
|
|
406
|
+
if not isinstance(payload, dict):
|
|
407
|
+
return None, "command output must be a JSON object"
|
|
408
|
+
return payload, None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def load_command_json_with_retry(
|
|
412
|
+
root: Path,
|
|
413
|
+
args: list[str],
|
|
414
|
+
*,
|
|
415
|
+
cwd: Path | None = None,
|
|
416
|
+
env: dict[str, str] | None = None,
|
|
417
|
+
timeout_seconds: float | None = None,
|
|
418
|
+
retries: int = 2,
|
|
419
|
+
) -> tuple[dict[str, object] | None, str | None]:
|
|
420
|
+
transient_needles = (
|
|
421
|
+
"EOF",
|
|
422
|
+
"unknown owner type",
|
|
423
|
+
"command timed out",
|
|
424
|
+
"connection reset",
|
|
425
|
+
"TLS handshake timeout",
|
|
426
|
+
)
|
|
427
|
+
last_payload: dict[str, object] | None = None
|
|
428
|
+
last_error: str | None = None
|
|
429
|
+
for _ in range(retries):
|
|
430
|
+
payload, error = load_command_json(
|
|
431
|
+
root,
|
|
432
|
+
args,
|
|
433
|
+
cwd=cwd,
|
|
434
|
+
env=env,
|
|
435
|
+
timeout_seconds=timeout_seconds,
|
|
436
|
+
)
|
|
437
|
+
if error is None:
|
|
438
|
+
return payload, None
|
|
439
|
+
last_payload = payload
|
|
440
|
+
last_error = error
|
|
441
|
+
if not any(needle in error for needle in transient_needles):
|
|
442
|
+
return payload, error
|
|
443
|
+
return last_payload, last_error
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def payload_has_github_rate_limit(payload: object) -> bool:
|
|
447
|
+
if not isinstance(payload, dict):
|
|
448
|
+
return False
|
|
449
|
+
missing_inputs = payload.get("missing_inputs")
|
|
450
|
+
if not isinstance(missing_inputs, list):
|
|
451
|
+
return False
|
|
452
|
+
return any(isinstance(item, str) and "API rate limit exceeded" in item for item in missing_inputs)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def prepend_path_env(bin_dir: Path, extra: dict[str, str] | None = None) -> dict[str, str]:
|
|
456
|
+
env = dict(extra or {})
|
|
457
|
+
current_path = os.environ.get("PATH", "")
|
|
458
|
+
env["PATH"] = str(bin_dir) if not current_path else f"{bin_dir}:{current_path}"
|
|
459
|
+
return env
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def write_fake_codex(
|
|
463
|
+
path: Path,
|
|
464
|
+
*,
|
|
465
|
+
mode: str,
|
|
466
|
+
tracked_edit_target: str | None = None,
|
|
467
|
+
) -> None:
|
|
468
|
+
if mode == "success":
|
|
469
|
+
body = """#!/usr/bin/env python3
|
|
470
|
+
import json
|
|
471
|
+
import pathlib
|
|
472
|
+
import sys
|
|
473
|
+
|
|
474
|
+
args = sys.argv[1:]
|
|
475
|
+
output_path = pathlib.Path(args[args.index("-o") + 1])
|
|
476
|
+
payload = {
|
|
477
|
+
"decision": "allow",
|
|
478
|
+
"summary": "Default Codex reviewer found the item ready for merge checkpoint consumption.",
|
|
479
|
+
"findings": [
|
|
480
|
+
{
|
|
481
|
+
"id": "warn-1",
|
|
482
|
+
"summary": "Keep the follow-up validation note visible in the review record.",
|
|
483
|
+
"severity": "warn",
|
|
484
|
+
"rebuttal": None,
|
|
485
|
+
"disposition": {
|
|
486
|
+
"status": "accepted",
|
|
487
|
+
"summary": "The reviewer accepts the current validation coverage."
|
|
488
|
+
},
|
|
489
|
+
"details": "This finding is advisory and should not block merge-ready."
|
|
490
|
+
}
|
|
491
|
+
]
|
|
492
|
+
}
|
|
493
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
494
|
+
output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\\n", encoding="utf-8")
|
|
495
|
+
sys.exit(0)
|
|
496
|
+
"""
|
|
497
|
+
elif mode == "schema_drift":
|
|
498
|
+
body = """#!/usr/bin/env python3
|
|
499
|
+
import json
|
|
500
|
+
import pathlib
|
|
501
|
+
import sys
|
|
502
|
+
|
|
503
|
+
args = sys.argv[1:]
|
|
504
|
+
output_path = pathlib.Path(args[args.index("-o") + 1])
|
|
505
|
+
payload = {
|
|
506
|
+
"decision": "allow",
|
|
507
|
+
"findings": []
|
|
508
|
+
}
|
|
509
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
510
|
+
output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\\n", encoding="utf-8")
|
|
511
|
+
sys.exit(0)
|
|
512
|
+
"""
|
|
513
|
+
elif mode == "tracked_edit":
|
|
514
|
+
target = tracked_edit_target or ""
|
|
515
|
+
body = f"""#!/usr/bin/env python3
|
|
516
|
+
import json
|
|
517
|
+
import pathlib
|
|
518
|
+
import sys
|
|
519
|
+
|
|
520
|
+
args = sys.argv[1:]
|
|
521
|
+
cwd = pathlib.Path(args[args.index("-C") + 1])
|
|
522
|
+
output_path = pathlib.Path(args[args.index("-o") + 1])
|
|
523
|
+
target = cwd / {target!r}
|
|
524
|
+
target.write_text(target.read_text(encoding="utf-8") + "\\ntracked edit from fake codex\\n", encoding="utf-8")
|
|
525
|
+
payload = {{
|
|
526
|
+
"decision": "block",
|
|
527
|
+
"summary": "Tracked repository content was modified during review.",
|
|
528
|
+
"findings": [
|
|
529
|
+
{{
|
|
530
|
+
"id": "block-1",
|
|
531
|
+
"summary": "Tracked repo content changed during review execution.",
|
|
532
|
+
"severity": "block",
|
|
533
|
+
"rebuttal": None,
|
|
534
|
+
"disposition": {{
|
|
535
|
+
"status": "rejected",
|
|
536
|
+
"summary": "The run must fail closed."
|
|
537
|
+
}}
|
|
538
|
+
}}
|
|
539
|
+
]
|
|
540
|
+
}}
|
|
541
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
542
|
+
output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\\n", encoding="utf-8")
|
|
543
|
+
sys.exit(0)
|
|
544
|
+
"""
|
|
545
|
+
else:
|
|
546
|
+
raise ValueError(f"unknown fake codex mode: {mode}")
|
|
547
|
+
path.write_text(body, encoding="utf-8")
|
|
548
|
+
path.chmod(0o755)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def require_governance_surface(
|
|
552
|
+
failures: list[Failure],
|
|
553
|
+
*,
|
|
554
|
+
category: str,
|
|
555
|
+
context: str,
|
|
556
|
+
payload: dict[str, object],
|
|
557
|
+
) -> None:
|
|
558
|
+
governance_surface = payload.get("governance_surface")
|
|
559
|
+
if not isinstance(governance_surface, dict):
|
|
560
|
+
failures.append(Failure(category, f"{context} must include `governance_surface` as an object"))
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
required_keys = (
|
|
564
|
+
"repository_mode",
|
|
565
|
+
"loom_state",
|
|
566
|
+
"carrier_summary",
|
|
567
|
+
"execution_entry",
|
|
568
|
+
"validation_entry",
|
|
569
|
+
"review_merge_surface",
|
|
570
|
+
"github_control_plane",
|
|
571
|
+
"repo_interface",
|
|
572
|
+
"repo_interop",
|
|
573
|
+
"summary",
|
|
574
|
+
"missing_inputs",
|
|
575
|
+
)
|
|
576
|
+
for key in required_keys:
|
|
577
|
+
if key not in governance_surface:
|
|
578
|
+
failures.append(Failure(category, f"{context} governance_surface must include `{key}`"))
|
|
579
|
+
|
|
580
|
+
for key in ("repository_mode", "loom_state", "execution_entry", "validation_entry", "summary"):
|
|
581
|
+
if key in governance_surface and (not isinstance(governance_surface.get(key), str) or not governance_surface.get(key)):
|
|
582
|
+
failures.append(Failure(category, f"{context} governance_surface `{key}` must be a non-empty string"))
|
|
583
|
+
if governance_surface.get("repository_mode") not in {"new", "small-existing", "complex-existing"}:
|
|
584
|
+
failures.append(Failure(category, f"{context} governance_surface `repository_mode` must stay within the stable contract"))
|
|
585
|
+
if governance_surface.get("loom_state") not in {"active", "partial", "absent"}:
|
|
586
|
+
failures.append(Failure(category, f"{context} governance_surface `loom_state` must stay within the stable contract"))
|
|
587
|
+
|
|
588
|
+
missing_inputs = governance_surface.get("missing_inputs")
|
|
589
|
+
if missing_inputs is not None and not isinstance(missing_inputs, list):
|
|
590
|
+
failures.append(Failure(category, f"{context} governance_surface `missing_inputs` must be a list"))
|
|
591
|
+
|
|
592
|
+
carrier_summary = governance_surface.get("carrier_summary")
|
|
593
|
+
if not isinstance(carrier_summary, dict):
|
|
594
|
+
failures.append(Failure(category, f"{context} governance_surface must include `carrier_summary`"))
|
|
595
|
+
else:
|
|
596
|
+
required_carriers = ("work_item", "recovery", "review", "status_surface", "spec_path", "plan_path")
|
|
597
|
+
if set(carrier_summary.keys()) != set(required_carriers):
|
|
598
|
+
failures.append(Failure(category, f"{context} governance_surface carrier keys must stay within the stable contract"))
|
|
599
|
+
for carrier in required_carriers:
|
|
600
|
+
entry = carrier_summary.get(carrier)
|
|
601
|
+
if not isinstance(entry, dict):
|
|
602
|
+
failures.append(Failure(category, f"{context} governance_surface carrier `{carrier}` must be an object"))
|
|
603
|
+
continue
|
|
604
|
+
if entry.get("status") not in {"present", "missing", "planned"}:
|
|
605
|
+
failures.append(
|
|
606
|
+
Failure(category, f"{context} governance_surface carrier `{carrier}` status must stay within the stable contract")
|
|
607
|
+
)
|
|
608
|
+
for field in ("locator", "source"):
|
|
609
|
+
value = entry.get(field)
|
|
610
|
+
if not isinstance(value, str) or not value:
|
|
611
|
+
failures.append(
|
|
612
|
+
Failure(category, f"{context} governance_surface carrier `{carrier}` must include non-empty `{field}`")
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
review_merge_surface = governance_surface.get("review_merge_surface")
|
|
616
|
+
if review_merge_surface is not None and not isinstance(review_merge_surface, dict):
|
|
617
|
+
failures.append(Failure(category, f"{context} governance_surface `review_merge_surface` must be an object"))
|
|
618
|
+
elif isinstance(review_merge_surface, dict):
|
|
619
|
+
for key in ("pr_template", "validation_surface", "merge_surface"):
|
|
620
|
+
value = review_merge_surface.get(key)
|
|
621
|
+
if not isinstance(value, str) or not value:
|
|
622
|
+
failures.append(Failure(category, f"{context} governance_surface `review_merge_surface.{key}` must be a non-empty string"))
|
|
623
|
+
|
|
624
|
+
github_control_plane = governance_surface.get("github_control_plane")
|
|
625
|
+
if github_control_plane is not None and not isinstance(github_control_plane, dict):
|
|
626
|
+
failures.append(Failure(category, f"{context} governance_surface `github_control_plane` must be an object"))
|
|
627
|
+
elif isinstance(github_control_plane, dict):
|
|
628
|
+
for key in ("repository", "default_branch", "branch_protection", "required_checks", "pr_reviews"):
|
|
629
|
+
if key not in github_control_plane:
|
|
630
|
+
failures.append(Failure(category, f"{context} governance_surface `github_control_plane.{key}` must exist"))
|
|
631
|
+
for key in ("repository", "default_branch"):
|
|
632
|
+
value = github_control_plane.get(key)
|
|
633
|
+
if not isinstance(value, str) or not value:
|
|
634
|
+
failures.append(Failure(category, f"{context} governance_surface `github_control_plane.{key}` must be a non-empty string"))
|
|
635
|
+
if github_control_plane.get("branch_protection") not in {"enabled", "disabled", "unknown"}:
|
|
636
|
+
failures.append(Failure(category, f"{context} governance_surface `github_control_plane.branch_protection` must stay within the stable contract"))
|
|
637
|
+
if github_control_plane.get("pr_reviews") not in {"required", "not_required", "unknown"}:
|
|
638
|
+
failures.append(Failure(category, f"{context} governance_surface `github_control_plane.pr_reviews` must stay within the stable contract"))
|
|
639
|
+
required_checks = github_control_plane.get("required_checks")
|
|
640
|
+
if not (
|
|
641
|
+
required_checks == "unknown"
|
|
642
|
+
or (isinstance(required_checks, list) and all(isinstance(item, str) and item for item in required_checks))
|
|
643
|
+
):
|
|
644
|
+
failures.append(Failure(category, f"{context} governance_surface `github_control_plane.required_checks` must be `unknown` or a string list"))
|
|
645
|
+
|
|
646
|
+
require_repo_interface_payload(
|
|
647
|
+
failures,
|
|
648
|
+
category=category,
|
|
649
|
+
context=f"{context} governance_surface.repo_interface",
|
|
650
|
+
payload=governance_surface.get("repo_interface"),
|
|
651
|
+
)
|
|
652
|
+
require_repo_interop_payload(
|
|
653
|
+
failures,
|
|
654
|
+
category=category,
|
|
655
|
+
context=f"{context} governance_surface.repo_interop",
|
|
656
|
+
payload=governance_surface.get("repo_interop"),
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def require_locator_entry(
|
|
661
|
+
failures: list[Failure],
|
|
662
|
+
*,
|
|
663
|
+
category: str,
|
|
664
|
+
context: str,
|
|
665
|
+
payload: object,
|
|
666
|
+
allowed_statuses: set[str],
|
|
667
|
+
) -> None:
|
|
668
|
+
if not isinstance(payload, dict):
|
|
669
|
+
failures.append(Failure(category, f"{context} must be an object"))
|
|
670
|
+
return
|
|
671
|
+
if payload.get("status") not in allowed_statuses:
|
|
672
|
+
failures.append(Failure(category, f"{context} status must stay within the stable contract"))
|
|
673
|
+
for field in ("locator", "source"):
|
|
674
|
+
value = payload.get(field)
|
|
675
|
+
if not isinstance(value, str) or not value:
|
|
676
|
+
failures.append(Failure(category, f"{context} must include non-empty `{field}`"))
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def require_repo_interface_payload(
|
|
680
|
+
failures: list[Failure],
|
|
681
|
+
*,
|
|
682
|
+
category: str,
|
|
683
|
+
context: str,
|
|
684
|
+
payload: object,
|
|
685
|
+
) -> None:
|
|
686
|
+
if not isinstance(payload, dict):
|
|
687
|
+
failures.append(Failure(category, f"{context} must be an object"))
|
|
688
|
+
return
|
|
689
|
+
if payload.get("availability") not in REPO_INTERFACE_AVAILABILITY:
|
|
690
|
+
failures.append(Failure(category, f"{context} availability must stay within the stable contract"))
|
|
691
|
+
require_locator_entry(
|
|
692
|
+
failures,
|
|
693
|
+
category=category,
|
|
694
|
+
context=f"{context}.manifest",
|
|
695
|
+
payload=payload.get("manifest"),
|
|
696
|
+
allowed_statuses={"present", "missing"},
|
|
697
|
+
)
|
|
698
|
+
for key in ("companion_entry", "repo_specific_requirements", "specialized_gates"):
|
|
699
|
+
require_locator_entry(
|
|
700
|
+
failures,
|
|
701
|
+
category=category,
|
|
702
|
+
context=f"{context}.{key}",
|
|
703
|
+
payload=payload.get(key),
|
|
704
|
+
allowed_statuses={"present", "missing"},
|
|
705
|
+
)
|
|
706
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
707
|
+
failures.append(Failure(category, f"{context} must include non-empty `summary`"))
|
|
708
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
709
|
+
failures.append(Failure(category, f"{context} must include `missing_inputs` as a list"))
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def require_repo_interop_payload(
|
|
713
|
+
failures: list[Failure],
|
|
714
|
+
*,
|
|
715
|
+
category: str,
|
|
716
|
+
context: str,
|
|
717
|
+
payload: object,
|
|
718
|
+
) -> None:
|
|
719
|
+
if not isinstance(payload, dict):
|
|
720
|
+
failures.append(Failure(category, f"{context} must be an object"))
|
|
721
|
+
return
|
|
722
|
+
if payload.get("availability") not in REPO_INTEROP_AVAILABILITY:
|
|
723
|
+
failures.append(Failure(category, f"{context} availability must stay within the stable contract"))
|
|
724
|
+
require_locator_entry(
|
|
725
|
+
failures,
|
|
726
|
+
category=category,
|
|
727
|
+
context=f"{context}.contract",
|
|
728
|
+
payload=payload.get("contract"),
|
|
729
|
+
allowed_statuses={"present", "missing"},
|
|
730
|
+
)
|
|
731
|
+
for key in ("host_adapters", "repo_native_carriers", "shadow_surfaces"):
|
|
732
|
+
require_locator_entry(
|
|
733
|
+
failures,
|
|
734
|
+
category=category,
|
|
735
|
+
context=f"{context}.{key}",
|
|
736
|
+
payload=payload.get(key),
|
|
737
|
+
allowed_statuses={"present", "missing"},
|
|
738
|
+
)
|
|
739
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
740
|
+
failures.append(Failure(category, f"{context} must include non-empty `summary`"))
|
|
741
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
742
|
+
failures.append(Failure(category, f"{context} must include `missing_inputs` as a list"))
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def require_repo_specific_requirements_payload(
|
|
746
|
+
failures: list[Failure],
|
|
747
|
+
*,
|
|
748
|
+
category: str,
|
|
749
|
+
context: str,
|
|
750
|
+
payload: object,
|
|
751
|
+
expected_surface: str,
|
|
752
|
+
) -> None:
|
|
753
|
+
if not isinstance(payload, dict):
|
|
754
|
+
failures.append(Failure(category, f"{context} must be an object"))
|
|
755
|
+
return
|
|
756
|
+
if payload.get("surface") != expected_surface:
|
|
757
|
+
failures.append(Failure(category, f"{context} must report `surface: {expected_surface}`"))
|
|
758
|
+
if payload.get("result") not in {"pass", "block"}:
|
|
759
|
+
failures.append(Failure(category, f"{context} result must be `pass` or `block`"))
|
|
760
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
761
|
+
failures.append(Failure(category, f"{context} must include non-empty `summary`"))
|
|
762
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
763
|
+
failures.append(Failure(category, f"{context} must include `missing_inputs`"))
|
|
764
|
+
if payload.get("fallback_to") not in {None, "build", "merge"}:
|
|
765
|
+
failures.append(Failure(category, f"{context} fallback must stay within the stable contract"))
|
|
766
|
+
for key in ("declared_requirements", "blocking_requirements", "advisory_requirements"):
|
|
767
|
+
entries = payload.get(key)
|
|
768
|
+
if not isinstance(entries, list):
|
|
769
|
+
failures.append(Failure(category, f"{context} must include `{key}` as a list"))
|
|
770
|
+
continue
|
|
771
|
+
for index, entry in enumerate(entries):
|
|
772
|
+
if not isinstance(entry, dict):
|
|
773
|
+
failures.append(Failure(category, f"{context} {key}[{index}] must be an object"))
|
|
774
|
+
continue
|
|
775
|
+
for field in ("id", "summary", "locator", "enforcement"):
|
|
776
|
+
value = entry.get(field)
|
|
777
|
+
if not isinstance(value, str) or not value:
|
|
778
|
+
failures.append(Failure(category, f"{context} {key}[{index}] missing `{field}`"))
|
|
779
|
+
if entry.get("enforcement") not in REPO_INTERFACE_ENFORCEMENT:
|
|
780
|
+
failures.append(Failure(category, f"{context} {key}[{index}] enforcement must stay within the stable contract"))
|
|
781
|
+
declared = payload.get("declared_requirements")
|
|
782
|
+
blocking = payload.get("blocking_requirements")
|
|
783
|
+
advisory = payload.get("advisory_requirements")
|
|
784
|
+
if isinstance(declared, list) and isinstance(blocking, list) and isinstance(advisory, list):
|
|
785
|
+
if len(declared) != len(blocking) + len(advisory):
|
|
786
|
+
failures.append(Failure(category, f"{context} declared requirements must split cleanly into blocking and advisory"))
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def require_shadow_parity_payload(
|
|
790
|
+
failures: list[Failure],
|
|
791
|
+
*,
|
|
792
|
+
category: str,
|
|
793
|
+
context: str,
|
|
794
|
+
payload: object,
|
|
795
|
+
expected_reports: int,
|
|
796
|
+
) -> None:
|
|
797
|
+
if not isinstance(payload, dict):
|
|
798
|
+
failures.append(Failure(category, f"{context} must be an object"))
|
|
799
|
+
return
|
|
800
|
+
if payload.get("command") != "shadow-parity":
|
|
801
|
+
failures.append(Failure(category, f"{context} must report `command: shadow-parity`"))
|
|
802
|
+
if payload.get("result") not in {"pass", "warn"}:
|
|
803
|
+
failures.append(Failure(category, f"{context} result must be `pass` or `warn`"))
|
|
804
|
+
if payload.get("fallback_to") is not None:
|
|
805
|
+
failures.append(Failure(category, f"{context} fallback_to must remain `null`"))
|
|
806
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
807
|
+
failures.append(Failure(category, f"{context} must include non-empty `summary`"))
|
|
808
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
809
|
+
failures.append(Failure(category, f"{context} must include `missing_inputs`"))
|
|
810
|
+
require_runtime_state_payload(
|
|
811
|
+
failures,
|
|
812
|
+
category=category,
|
|
813
|
+
context=context,
|
|
814
|
+
payload=payload.get("runtime_state"),
|
|
815
|
+
expected_scene="repo-local-demo",
|
|
816
|
+
expected_carrier="repo-local-wrapper",
|
|
817
|
+
allowed_results={"pass"},
|
|
818
|
+
)
|
|
819
|
+
governance_surface = {"governance_surface": payload.get("governance_surface")}
|
|
820
|
+
if isinstance(payload.get("governance_surface"), dict):
|
|
821
|
+
require_governance_surface(
|
|
822
|
+
failures,
|
|
823
|
+
category=category,
|
|
824
|
+
context=context,
|
|
825
|
+
payload=governance_surface,
|
|
826
|
+
)
|
|
827
|
+
reports = payload.get("reports")
|
|
828
|
+
if not isinstance(reports, list):
|
|
829
|
+
failures.append(Failure(category, f"{context} must include `reports` as a list"))
|
|
830
|
+
return
|
|
831
|
+
if len(reports) != expected_reports:
|
|
832
|
+
failures.append(Failure(category, f"{context} must include {expected_reports} parity reports"))
|
|
833
|
+
for index, report in enumerate(reports):
|
|
834
|
+
if not isinstance(report, dict):
|
|
835
|
+
failures.append(Failure(category, f"{context} reports[{index}] must be an object"))
|
|
836
|
+
continue
|
|
837
|
+
if report.get("surface") not in {"admission", "review", "merge_ready", "closeout"}:
|
|
838
|
+
failures.append(Failure(category, f"{context} reports[{index}] must declare a known surface"))
|
|
839
|
+
if report.get("result") not in {"match", "mismatch", "unreadable"}:
|
|
840
|
+
failures.append(Failure(category, f"{context} reports[{index}] result must stay within the stable contract"))
|
|
841
|
+
if not isinstance(report.get("summary"), str) or not report.get("summary"):
|
|
842
|
+
failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `summary`"))
|
|
843
|
+
if not isinstance(report.get("missing_inputs"), list):
|
|
844
|
+
failures.append(Failure(category, f"{context} reports[{index}] must include `missing_inputs`"))
|
|
845
|
+
for key in ("host_adapters", "repo_native_carriers"):
|
|
846
|
+
if not isinstance(report.get(key), list):
|
|
847
|
+
failures.append(Failure(category, f"{context} reports[{index}] must include `{key}` as a list"))
|
|
848
|
+
for surface_key in ("loom_surface", "repo_surface"):
|
|
849
|
+
surface_payload = report.get(surface_key)
|
|
850
|
+
if not isinstance(surface_payload, dict):
|
|
851
|
+
failures.append(Failure(category, f"{context} reports[{index}] must include `{surface_key}`"))
|
|
852
|
+
continue
|
|
853
|
+
if surface_payload.get("status") not in {"readable", "missing"}:
|
|
854
|
+
failures.append(Failure(category, f"{context} reports[{index}] `{surface_key}.status` must stay within the stable contract"))
|
|
855
|
+
locator = surface_payload.get("locator")
|
|
856
|
+
if not isinstance(locator, str) or not locator:
|
|
857
|
+
failures.append(Failure(category, f"{context} reports[{index}] `{surface_key}.locator` must be non-empty"))
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def require_host_lifecycle_payload(
|
|
861
|
+
failures: list[Failure],
|
|
862
|
+
*,
|
|
863
|
+
category: str,
|
|
864
|
+
context: str,
|
|
865
|
+
payload: dict[str, object],
|
|
866
|
+
) -> None:
|
|
867
|
+
if payload.get("result") not in {"pass", "block"}:
|
|
868
|
+
failures.append(Failure(category, f"{context} must return `pass` or `block`"))
|
|
869
|
+
if payload.get("fallback_to") not in {None, "admission"}:
|
|
870
|
+
failures.append(Failure(category, f"{context} fallback must be `null` or `admission`"))
|
|
871
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
872
|
+
failures.append(Failure(category, f"{context} must include a non-empty `summary`"))
|
|
873
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
874
|
+
failures.append(Failure(category, f"{context} must include `missing_inputs`"))
|
|
875
|
+
|
|
876
|
+
objects = payload.get("objects")
|
|
877
|
+
if not isinstance(objects, dict):
|
|
878
|
+
failures.append(Failure(category, f"{context} must include `objects`"))
|
|
879
|
+
return
|
|
880
|
+
|
|
881
|
+
workspace = objects.get("workspace")
|
|
882
|
+
branch = objects.get("branch")
|
|
883
|
+
pr = objects.get("pr")
|
|
884
|
+
worktree = objects.get("worktree")
|
|
885
|
+
for key, value in (("workspace", workspace), ("branch", branch), ("pr", pr), ("worktree", worktree)):
|
|
886
|
+
if not isinstance(value, dict):
|
|
887
|
+
failures.append(Failure(category, f"{context} must include `{key}`"))
|
|
888
|
+
if not isinstance(workspace, dict) or not isinstance(branch, dict) or not isinstance(pr, dict) or not isinstance(worktree, dict):
|
|
889
|
+
return
|
|
890
|
+
|
|
891
|
+
if workspace.get("ownership") != "loom":
|
|
892
|
+
failures.append(Failure(category, f"{context} workspace ownership must stay `loom`"))
|
|
893
|
+
for field in ("entry", "path", "lifecycle_entry"):
|
|
894
|
+
value = workspace.get(field)
|
|
895
|
+
if not isinstance(value, str) or not value:
|
|
896
|
+
failures.append(Failure(category, f"{context} workspace must include non-empty `{field}`"))
|
|
897
|
+
|
|
898
|
+
if branch.get("ownership") != "host":
|
|
899
|
+
failures.append(Failure(category, f"{context} branch ownership must stay `host`"))
|
|
900
|
+
if branch.get("purity_status") not in {"report_only", "host_managed_without_local_branch"}:
|
|
901
|
+
failures.append(Failure(category, f"{context} branch purity_status must stay within the stable contract"))
|
|
902
|
+
if not isinstance(branch.get("next_action"), str) or not branch.get("next_action"):
|
|
903
|
+
failures.append(Failure(category, f"{context} branch must include non-empty `next_action`"))
|
|
904
|
+
|
|
905
|
+
if pr.get("ownership") != "host":
|
|
906
|
+
failures.append(Failure(category, f"{context} PR ownership must stay `host`"))
|
|
907
|
+
if pr.get("purity_status") != "report_only":
|
|
908
|
+
failures.append(Failure(category, f"{context} PR purity_status must stay `report_only`"))
|
|
909
|
+
if not isinstance(pr.get("next_action"), str) or not pr.get("next_action"):
|
|
910
|
+
failures.append(Failure(category, f"{context} PR must include non-empty `next_action`"))
|
|
911
|
+
|
|
912
|
+
if worktree.get("ownership") != "host":
|
|
913
|
+
failures.append(Failure(category, f"{context} worktree ownership must stay `host`"))
|
|
914
|
+
if worktree.get("status") != "host_managed":
|
|
915
|
+
failures.append(Failure(category, f"{context} worktree status must stay `host_managed`"))
|
|
916
|
+
for field in ("cwd_within_repo", "next_action"):
|
|
917
|
+
value = worktree.get(field)
|
|
918
|
+
if not isinstance(value, str) or not value:
|
|
919
|
+
failures.append(Failure(category, f"{context} worktree must include non-empty `{field}`"))
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def require_reconciliation_payload(
|
|
923
|
+
failures: list[Failure],
|
|
924
|
+
*,
|
|
925
|
+
category: str,
|
|
926
|
+
context: str,
|
|
927
|
+
payload: object,
|
|
928
|
+
) -> None:
|
|
929
|
+
if not isinstance(payload, dict):
|
|
930
|
+
failures.append(Failure(category, f"{context} must include `reconciliation` as an object"))
|
|
931
|
+
return
|
|
932
|
+
if payload.get("command") != "reconciliation":
|
|
933
|
+
failures.append(Failure(category, f"{context} must report `command: reconciliation`"))
|
|
934
|
+
if payload.get("operation") != "audit":
|
|
935
|
+
failures.append(Failure(category, f"{context} must report `operation: audit`"))
|
|
936
|
+
if payload.get("result") not in {"pass", "warn", "fix-needed", "block"}:
|
|
937
|
+
failures.append(Failure(category, f"{context} returned an unknown reconciliation result"))
|
|
938
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
939
|
+
failures.append(Failure(category, f"{context} must include a non-empty reconciliation `summary`"))
|
|
940
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
941
|
+
failures.append(Failure(category, f"{context} must include reconciliation `missing_inputs`"))
|
|
942
|
+
if payload.get("fallback_to") not in {None, "manual-reconciliation"}:
|
|
943
|
+
failures.append(Failure(category, f"{context} reconciliation fallback must be `null` or `manual-reconciliation`"))
|
|
944
|
+
findings = payload.get("findings")
|
|
945
|
+
if not isinstance(findings, list):
|
|
946
|
+
failures.append(Failure(category, f"{context} must include reconciliation `findings` as a list"))
|
|
947
|
+
return
|
|
948
|
+
for finding in findings:
|
|
949
|
+
if not isinstance(finding, dict):
|
|
950
|
+
failures.append(Failure(category, f"{context} reconciliation findings must be JSON objects"))
|
|
951
|
+
continue
|
|
952
|
+
if finding.get("kind") not in {"absorbed_but_open", "parent_drift", "project_drift"}:
|
|
953
|
+
failures.append(Failure(category, f"{context} reconciliation finding kind must stay within the stable contract"))
|
|
954
|
+
if finding.get("severity") not in {"warn", "fix-needed", "block"}:
|
|
955
|
+
failures.append(Failure(category, f"{context} reconciliation finding severity must stay within the stable contract"))
|
|
956
|
+
if not isinstance(finding.get("subject"), str) or not finding.get("subject"):
|
|
957
|
+
failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `subject`"))
|
|
958
|
+
if not isinstance(finding.get("evidence"), dict):
|
|
959
|
+
failures.append(Failure(category, f"{context} reconciliation findings must include `evidence`"))
|
|
960
|
+
if not isinstance(finding.get("recommended_action"), str) or not finding.get("recommended_action"):
|
|
961
|
+
failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `recommended_action`"))
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def require_closeout_reconciliation_contract(
|
|
965
|
+
failures: list[Failure],
|
|
966
|
+
*,
|
|
967
|
+
category: str,
|
|
968
|
+
context: str,
|
|
969
|
+
payload: dict[str, object],
|
|
970
|
+
) -> None:
|
|
971
|
+
reconciliation = payload.get("reconciliation")
|
|
972
|
+
if reconciliation is None:
|
|
973
|
+
return
|
|
974
|
+
require_reconciliation_payload(
|
|
975
|
+
failures,
|
|
976
|
+
category=category,
|
|
977
|
+
context=f"{context} reconciliation",
|
|
978
|
+
payload=reconciliation,
|
|
979
|
+
)
|
|
980
|
+
if not isinstance(reconciliation, dict):
|
|
981
|
+
return
|
|
982
|
+
reconciliation_result = reconciliation.get("result")
|
|
983
|
+
closeout_result = payload.get("result")
|
|
984
|
+
fallback_to = payload.get("fallback_to")
|
|
985
|
+
if reconciliation_result == "fix-needed":
|
|
986
|
+
if closeout_result != "block":
|
|
987
|
+
failures.append(Failure(category, f"{context} must block when reconciliation returns `fix-needed`"))
|
|
988
|
+
if fallback_to != "reconciliation-sync":
|
|
989
|
+
failures.append(Failure(category, f"{context} must point `fix-needed` reconciliation drift to `reconciliation-sync`"))
|
|
990
|
+
if reconciliation_result == "block":
|
|
991
|
+
if closeout_result != "block":
|
|
992
|
+
failures.append(Failure(category, f"{context} must block when reconciliation returns `block`"))
|
|
993
|
+
if fallback_to != "manual-reconciliation":
|
|
994
|
+
failures.append(Failure(category, f"{context} must point blocked reconciliation drift to `manual-reconciliation`"))
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def require_review_record_contract(
|
|
998
|
+
failures: list[Failure],
|
|
999
|
+
*,
|
|
1000
|
+
category: str,
|
|
1001
|
+
context: str,
|
|
1002
|
+
payload: object,
|
|
1003
|
+
) -> None:
|
|
1004
|
+
if not isinstance(payload, dict):
|
|
1005
|
+
failures.append(Failure(category, f"{context} must include a review record object"))
|
|
1006
|
+
return
|
|
1007
|
+
findings = payload.get("findings")
|
|
1008
|
+
if not isinstance(findings, list):
|
|
1009
|
+
failures.append(Failure(category, f"{context} must include review `findings` as a list"))
|
|
1010
|
+
return
|
|
1011
|
+
for list_field in ("blocking_issues", "follow_ups"):
|
|
1012
|
+
if not isinstance(payload.get(list_field), list):
|
|
1013
|
+
failures.append(Failure(category, f"{context} must include review `{list_field}` as a list"))
|
|
1014
|
+
consumed_inputs = payload.get("consumed_inputs")
|
|
1015
|
+
if consumed_inputs is not None:
|
|
1016
|
+
if not isinstance(consumed_inputs, dict):
|
|
1017
|
+
failures.append(Failure(category, f"{context} review `consumed_inputs` must be an object when present"))
|
|
1018
|
+
else:
|
|
1019
|
+
for key in ("engine_adapter", "engine_evidence", "normalized_findings"):
|
|
1020
|
+
value = consumed_inputs.get(key)
|
|
1021
|
+
if value is not None and (not isinstance(value, str) or not value):
|
|
1022
|
+
failures.append(Failure(category, f"{context} review consumed input `{key}` must be null or a non-empty string"))
|
|
1023
|
+
for finding in findings:
|
|
1024
|
+
if not isinstance(finding, dict):
|
|
1025
|
+
failures.append(Failure(category, f"{context} review findings must be JSON objects"))
|
|
1026
|
+
continue
|
|
1027
|
+
if not isinstance(finding.get("id"), str) or not finding.get("id"):
|
|
1028
|
+
failures.append(Failure(category, f"{context} review findings must include non-empty `id`"))
|
|
1029
|
+
if not isinstance(finding.get("summary"), str) or not finding.get("summary"):
|
|
1030
|
+
failures.append(Failure(category, f"{context} review findings must include non-empty `summary`"))
|
|
1031
|
+
if finding.get("severity") not in REVIEW_FINDING_SEVERITIES:
|
|
1032
|
+
failures.append(Failure(category, f"{context} review finding severity must stay within the stable contract"))
|
|
1033
|
+
rebuttal = finding.get("rebuttal")
|
|
1034
|
+
if rebuttal is not None and (not isinstance(rebuttal, str) or not rebuttal):
|
|
1035
|
+
failures.append(
|
|
1036
|
+
Failure(category, f"{context} review finding `rebuttal` must be `null` or a non-empty string")
|
|
1037
|
+
)
|
|
1038
|
+
disposition = finding.get("disposition")
|
|
1039
|
+
if disposition is not None:
|
|
1040
|
+
if not isinstance(disposition, dict):
|
|
1041
|
+
failures.append(Failure(category, f"{context} review finding disposition must be `null` or an object"))
|
|
1042
|
+
continue
|
|
1043
|
+
if disposition.get("status") not in REVIEW_FINDING_DISPOSITION_STATUSES:
|
|
1044
|
+
failures.append(Failure(category, f"{context} review finding disposition status must stay within the stable contract"))
|
|
1045
|
+
if not isinstance(disposition.get("summary"), str) or not disposition.get("summary"):
|
|
1046
|
+
failures.append(Failure(category, f"{context} review finding disposition must include non-empty `summary`"))
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def require_review_run_payload(
|
|
1050
|
+
failures: list[Failure],
|
|
1051
|
+
*,
|
|
1052
|
+
category: str,
|
|
1053
|
+
context: str,
|
|
1054
|
+
payload: object,
|
|
1055
|
+
expected_result: set[str],
|
|
1056
|
+
) -> None:
|
|
1057
|
+
if not isinstance(payload, dict):
|
|
1058
|
+
failures.append(Failure(category, f"{context} must return a JSON object"))
|
|
1059
|
+
return
|
|
1060
|
+
if payload.get("command") != "review":
|
|
1061
|
+
failures.append(Failure(category, f"{context} must report `command: review`"))
|
|
1062
|
+
if payload.get("operation") != "run":
|
|
1063
|
+
failures.append(Failure(category, f"{context} must report `operation: run`"))
|
|
1064
|
+
if payload.get("result") not in expected_result:
|
|
1065
|
+
failures.append(Failure(category, f"{context} returned an unexpected result"))
|
|
1066
|
+
for key in ("item", "state_check", "runtime_evidence", "build_checkpoint", "review", "current_checkpoint", "engine", "manual_review"):
|
|
1067
|
+
if not isinstance(payload.get(key), dict):
|
|
1068
|
+
failures.append(Failure(category, f"{context} must include `{key}`"))
|
|
1069
|
+
require_runtime_state_payload(
|
|
1070
|
+
failures,
|
|
1071
|
+
category=category,
|
|
1072
|
+
context=context,
|
|
1073
|
+
payload=payload.get("runtime_state"),
|
|
1074
|
+
allowed_results={"pass", "block"},
|
|
1075
|
+
)
|
|
1076
|
+
engine = payload.get("engine")
|
|
1077
|
+
if not isinstance(engine, dict):
|
|
1078
|
+
return
|
|
1079
|
+
if engine.get("engine") != "codex":
|
|
1080
|
+
failures.append(Failure(category, f"{context} engine must stay `codex` for the default path"))
|
|
1081
|
+
if engine.get("adapter") != "loom/default-codex":
|
|
1082
|
+
failures.append(Failure(category, f"{context} adapter must stay `loom/default-codex`"))
|
|
1083
|
+
if engine.get("result") not in {"pass", "block", "not_run"}:
|
|
1084
|
+
failures.append(Failure(category, f"{context} engine result must stay within the stable contract"))
|
|
1085
|
+
if engine.get("failure_reason") not in {None, "engine_unavailable", "schema_drift", "runtime_conflict", "repo_diff_detected"}:
|
|
1086
|
+
failures.append(Failure(category, f"{context} engine failure reason must stay within the stable contract"))
|
|
1087
|
+
evidence = engine.get("evidence")
|
|
1088
|
+
if engine.get("result") == "not_run":
|
|
1089
|
+
if evidence is not None:
|
|
1090
|
+
failures.append(Failure(category, f"{context} engine evidence must be null when the engine is not run"))
|
|
1091
|
+
else:
|
|
1092
|
+
if not isinstance(evidence, dict):
|
|
1093
|
+
failures.append(Failure(category, f"{context} engine must include `evidence` when it runs"))
|
|
1094
|
+
else:
|
|
1095
|
+
for key in ("runtime_root", "prompt", "raw_result", "normalized_findings", "metadata"):
|
|
1096
|
+
value = evidence.get(key)
|
|
1097
|
+
if not isinstance(value, str) or not value:
|
|
1098
|
+
failures.append(Failure(category, f"{context} engine evidence must include non-empty `{key}`"))
|
|
1099
|
+
manual_review = payload.get("manual_review")
|
|
1100
|
+
if isinstance(manual_review, dict):
|
|
1101
|
+
if not isinstance(manual_review.get("summary"), str) or not manual_review.get("summary"):
|
|
1102
|
+
failures.append(Failure(category, f"{context} manual_review must include non-empty `summary`"))
|
|
1103
|
+
if not isinstance(manual_review.get("review_record_path"), str) or not manual_review.get("review_record_path"):
|
|
1104
|
+
failures.append(Failure(category, f"{context} manual_review must include `review_record_path`"))
|
|
1105
|
+
if manual_review.get("recommended_kind") not in {"general_review", "code_review", "spec_review"}:
|
|
1106
|
+
failures.append(Failure(category, f"{context} manual_review recommended kind must stay within the stable contract"))
|
|
1107
|
+
if not isinstance(manual_review.get("command"), list):
|
|
1108
|
+
failures.append(Failure(category, f"{context} manual_review must include `command` as a list"))
|
|
1109
|
+
review_record_input = payload.get("review_record_input")
|
|
1110
|
+
if payload.get("result") == "pass":
|
|
1111
|
+
if not isinstance(review_record_input, dict):
|
|
1112
|
+
failures.append(Failure(category, f"{context} must include `review_record_input` when engine review passes"))
|
|
1113
|
+
else:
|
|
1114
|
+
for key in ("decision", "summary", "reviewer", "kind", "findings_file", "engine_adapter", "engine_evidence", "normalized_findings"):
|
|
1115
|
+
value = review_record_input.get(key)
|
|
1116
|
+
if not isinstance(value, str) or not value:
|
|
1117
|
+
failures.append(Failure(category, f"{context} review_record_input must include non-empty `{key}`"))
|
|
1118
|
+
if review_record_input.get("decision") not in {"allow", "block", "fallback"}:
|
|
1119
|
+
failures.append(Failure(category, f"{context} review_record_input decision must stay within the stable contract"))
|
|
1120
|
+
if review_record_input.get("reviewer") != "loom/default-codex":
|
|
1121
|
+
failures.append(Failure(category, f"{context} review_record_input reviewer must stay `loom/default-codex`"))
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def require_runtime_state_payload(
|
|
1125
|
+
failures: list[Failure],
|
|
1126
|
+
*,
|
|
1127
|
+
category: str,
|
|
1128
|
+
context: str,
|
|
1129
|
+
payload: object,
|
|
1130
|
+
expected_scene: str | None = None,
|
|
1131
|
+
expected_carrier: str | None = None,
|
|
1132
|
+
allowed_results: set[str] | None = None,
|
|
1133
|
+
) -> None:
|
|
1134
|
+
if not isinstance(payload, dict):
|
|
1135
|
+
failures.append(Failure(category, f"{context} must include `runtime_state` as an object"))
|
|
1136
|
+
return
|
|
1137
|
+
if payload.get("result") not in (allowed_results or {"pass", "block"}):
|
|
1138
|
+
failures.append(Failure(category, f"{context} runtime_state.result must stay within the stable contract"))
|
|
1139
|
+
if expected_scene is not None and payload.get("scene") != expected_scene:
|
|
1140
|
+
failures.append(Failure(category, f"{context} runtime_state.scene must be `{expected_scene}`"))
|
|
1141
|
+
if expected_carrier is not None and payload.get("carrier") != expected_carrier:
|
|
1142
|
+
failures.append(Failure(category, f"{context} runtime_state.carrier must be `{expected_carrier}`"))
|
|
1143
|
+
if payload.get("entry_family") not in {"loom-init", "loom-flow"}:
|
|
1144
|
+
failures.append(Failure(category, f"{context} runtime_state.entry_family must stay within the stable contract"))
|
|
1145
|
+
if not isinstance(payload.get("runtime_root"), str) or not payload.get("runtime_root"):
|
|
1146
|
+
failures.append(Failure(category, f"{context} runtime_state must include non-empty `runtime_root`"))
|
|
1147
|
+
checks = payload.get("checks")
|
|
1148
|
+
if not isinstance(checks, dict):
|
|
1149
|
+
failures.append(Failure(category, f"{context} runtime_state must include `checks`"))
|
|
1150
|
+
return
|
|
1151
|
+
for key in ("scene_marker", "carrier_layout", "registry_contract", "shared_runtime", "referenced_resources"):
|
|
1152
|
+
check = checks.get(key)
|
|
1153
|
+
if not isinstance(check, dict):
|
|
1154
|
+
failures.append(Failure(category, f"{context} runtime_state must include check `{key}`"))
|
|
1155
|
+
continue
|
|
1156
|
+
if check.get("status") not in {"pass", "block", "not_applicable"}:
|
|
1157
|
+
failures.append(Failure(category, f"{context} runtime_state check `{key}` returned an unknown status"))
|
|
1158
|
+
if not isinstance(check.get("summary"), str) or not check.get("summary"):
|
|
1159
|
+
failures.append(Failure(category, f"{context} runtime_state check `{key}` must include non-empty `summary`"))
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def require_route_payload(
|
|
1163
|
+
failures: list[Failure],
|
|
1164
|
+
*,
|
|
1165
|
+
category: str,
|
|
1166
|
+
context: str,
|
|
1167
|
+
payload: object,
|
|
1168
|
+
expected_skill: str,
|
|
1169
|
+
expected_mode: str,
|
|
1170
|
+
expected_runtime_scene: str | None = None,
|
|
1171
|
+
expected_runtime_carrier: str | None = None,
|
|
1172
|
+
allowed_results: set[str] | None = None,
|
|
1173
|
+
) -> None:
|
|
1174
|
+
if not isinstance(payload, dict):
|
|
1175
|
+
failures.append(Failure(category, f"{context} must return a JSON object"))
|
|
1176
|
+
return
|
|
1177
|
+
if payload.get("command") != "route":
|
|
1178
|
+
failures.append(Failure(category, f"{context} must report `command: route`"))
|
|
1179
|
+
if payload.get("result") not in (allowed_results or {"pass"}):
|
|
1180
|
+
failures.append(Failure(category, f"{context} result must stay within the stable contract"))
|
|
1181
|
+
if payload.get("selected_skill") != expected_skill:
|
|
1182
|
+
failures.append(Failure(category, f"{context} must select `{expected_skill}`"))
|
|
1183
|
+
if payload.get("mode") != expected_mode:
|
|
1184
|
+
failures.append(Failure(category, f"{context} must report `mode: {expected_mode}`"))
|
|
1185
|
+
if not isinstance(payload.get("matched_signals"), list):
|
|
1186
|
+
failures.append(Failure(category, f"{context} must include `matched_signals`"))
|
|
1187
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
1188
|
+
failures.append(Failure(category, f"{context} must include `missing_inputs`"))
|
|
1189
|
+
if payload.get("fallback_to") not in {"loom-init", "refresh-install", "rebootstrap-runtime", "manual-runtime-reconciliation", None}:
|
|
1190
|
+
failures.append(Failure(category, f"{context} fallback must stay within the stable contract"))
|
|
1191
|
+
if expected_runtime_scene is not None or expected_runtime_carrier is not None:
|
|
1192
|
+
require_runtime_state_payload(
|
|
1193
|
+
failures,
|
|
1194
|
+
category=category,
|
|
1195
|
+
context=context,
|
|
1196
|
+
payload=payload.get("runtime_state"),
|
|
1197
|
+
expected_scene=expected_runtime_scene,
|
|
1198
|
+
expected_carrier=expected_runtime_carrier,
|
|
1199
|
+
allowed_results={"pass", "block"},
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def check_skill_manifests(root: Path) -> list[Failure]:
|
|
1204
|
+
failures: list[Failure] = []
|
|
1205
|
+
expected_entries = {
|
|
1206
|
+
"loom-init": "bootstrap/root",
|
|
1207
|
+
"loom-adopt": "scenario/adopt",
|
|
1208
|
+
"loom-resume": "scenario/resume",
|
|
1209
|
+
"loom-pre-review": "scenario/pre-review",
|
|
1210
|
+
"loom-review": "scenario/review",
|
|
1211
|
+
"loom-handoff": "scenario/handoff",
|
|
1212
|
+
"loom-retire": "scenario/retire",
|
|
1213
|
+
"loom-merge-ready": "scenario/merge-ready",
|
|
1214
|
+
}
|
|
1215
|
+
registry_path = root / "skills/registry.json"
|
|
1216
|
+
upgrade_contract_path = root / "skills/upgrade-contract.json"
|
|
1217
|
+
|
|
1218
|
+
for candidate in (registry_path, upgrade_contract_path):
|
|
1219
|
+
if not candidate.exists():
|
|
1220
|
+
return failures
|
|
1221
|
+
|
|
1222
|
+
try:
|
|
1223
|
+
registry = load_json_file(registry_path)
|
|
1224
|
+
except json.JSONDecodeError as exc:
|
|
1225
|
+
return [Failure("skill-manifests", f"`skills/registry.json` is invalid JSON: {exc.msg}")]
|
|
1226
|
+
|
|
1227
|
+
try:
|
|
1228
|
+
upgrade_contract = load_json_file(upgrade_contract_path)
|
|
1229
|
+
except json.JSONDecodeError as exc:
|
|
1230
|
+
return [Failure("skill-manifests", f"`skills/upgrade-contract.json` is invalid JSON: {exc.msg}")]
|
|
1231
|
+
|
|
1232
|
+
if not isinstance(registry, dict):
|
|
1233
|
+
failures.append(Failure("skill-manifests", "`skills/registry.json` must be a JSON object"))
|
|
1234
|
+
return failures
|
|
1235
|
+
if not isinstance(upgrade_contract, dict):
|
|
1236
|
+
failures.append(Failure("skill-manifests", "`skills/upgrade-contract.json` must be a JSON object"))
|
|
1237
|
+
return failures
|
|
1238
|
+
|
|
1239
|
+
registry_version = registry.get("registry_version")
|
|
1240
|
+
root_entry = registry.get("root_entry")
|
|
1241
|
+
entries = registry.get("entries")
|
|
1242
|
+
upgrade_reference = registry.get("upgrade_contract")
|
|
1243
|
+
install_layout_reference = registry.get("install_layout")
|
|
1244
|
+
layout_manifest: dict[str, object] | None = None
|
|
1245
|
+
if registry_version != upgrade_contract.get("registry_version"):
|
|
1246
|
+
failures.append(Failure("skill-manifests", "`skills/upgrade-contract.json` registry version must match `skills/registry.json`"))
|
|
1247
|
+
if install_layout_reference != "install-layout.json":
|
|
1248
|
+
failures.append(Failure("skill-manifests", "`skills/registry.json` must point `install_layout` to `install-layout.json`"))
|
|
1249
|
+
else:
|
|
1250
|
+
install_layout_path = registry_path.parent / install_layout_reference
|
|
1251
|
+
if not install_layout_path.exists():
|
|
1252
|
+
failures.append(Failure("skill-manifests", "`skills/install-layout.json` must exist"))
|
|
1253
|
+
else:
|
|
1254
|
+
try:
|
|
1255
|
+
candidate_layout = load_json_file(install_layout_path)
|
|
1256
|
+
except json.JSONDecodeError as exc:
|
|
1257
|
+
failures.append(Failure("skill-manifests", f"`skills/install-layout.json` is invalid JSON: {exc.msg}"))
|
|
1258
|
+
else:
|
|
1259
|
+
layout_manifest = candidate_layout
|
|
1260
|
+
required_paths = candidate_layout.get("required_paths")
|
|
1261
|
+
if not isinstance(required_paths, list) or not required_paths:
|
|
1262
|
+
failures.append(Failure("skill-manifests", "`skills/install-layout.json` must declare a non-empty `required_paths`"))
|
|
1263
|
+
else:
|
|
1264
|
+
for relative in required_paths:
|
|
1265
|
+
if not isinstance(relative, str) or not relative:
|
|
1266
|
+
failures.append(Failure("skill-manifests", "`skills/install-layout.json` required paths must be non-empty strings"))
|
|
1267
|
+
continue
|
|
1268
|
+
if not (registry_path.parent / relative).exists():
|
|
1269
|
+
failures.append(Failure("skill-manifests", f"`skills/install-layout.json` points to missing path `{relative}`"))
|
|
1270
|
+
runtime_state = candidate_layout.get("runtime_state")
|
|
1271
|
+
if not isinstance(runtime_state, dict):
|
|
1272
|
+
failures.append(Failure("skill-manifests", "`skills/install-layout.json` must declare `runtime_state`"))
|
|
1273
|
+
else:
|
|
1274
|
+
recognized_states = runtime_state.get("recognized_states")
|
|
1275
|
+
if recognized_states != ["installed-runtime", "repo-local-demo", "upgrade-rehearsal"]:
|
|
1276
|
+
failures.append(
|
|
1277
|
+
Failure(
|
|
1278
|
+
"skill-manifests",
|
|
1279
|
+
"`skills/install-layout.json` runtime_state recognized_states must stay in the stable order",
|
|
1280
|
+
)
|
|
1281
|
+
)
|
|
1282
|
+
if not isinstance(root_entry, str) or not root_entry:
|
|
1283
|
+
failures.append(Failure("skill-manifests", "`skills/registry.json` must declare a non-empty `root_entry`"))
|
|
1284
|
+
return failures
|
|
1285
|
+
if not isinstance(entries, list) or not entries:
|
|
1286
|
+
failures.append(Failure("skill-manifests", "`skills/registry.json` must declare at least one entry"))
|
|
1287
|
+
return failures
|
|
1288
|
+
if root_entry != "loom-init":
|
|
1289
|
+
failures.append(Failure("skill-manifests", "`skills/registry.json` root entry must remain `loom-init`"))
|
|
1290
|
+
|
|
1291
|
+
root_registry_entry: dict[str, object] | None = None
|
|
1292
|
+
seen_ids: set[str] = set()
|
|
1293
|
+
for entry in entries:
|
|
1294
|
+
if not isinstance(entry, dict):
|
|
1295
|
+
failures.append(Failure("skill-manifests", "every registry entry must be an object"))
|
|
1296
|
+
continue
|
|
1297
|
+
entry_id = entry.get("id")
|
|
1298
|
+
if not isinstance(entry_id, str) or not entry_id:
|
|
1299
|
+
failures.append(Failure("skill-manifests", "every registry entry must declare a non-empty `id`"))
|
|
1300
|
+
continue
|
|
1301
|
+
if entry_id in seen_ids:
|
|
1302
|
+
failures.append(Failure("skill-manifests", f"registry declares duplicate entry `{entry_id}`"))
|
|
1303
|
+
continue
|
|
1304
|
+
seen_ids.add(entry_id)
|
|
1305
|
+
if entry_id == root_entry:
|
|
1306
|
+
root_registry_entry = entry
|
|
1307
|
+
expected_role = expected_entries.get(entry_id)
|
|
1308
|
+
if expected_role is None:
|
|
1309
|
+
failures.append(Failure("skill-manifests", f"registry declares unexpected entry `{entry_id}`"))
|
|
1310
|
+
elif entry.get("role") != expected_role:
|
|
1311
|
+
failures.append(Failure("skill-manifests", f"registry entry `{entry_id}` must declare role `{expected_role}`"))
|
|
1312
|
+
|
|
1313
|
+
for field in ("role", "contract_version", "manifest", "executable"):
|
|
1314
|
+
value = entry.get(field)
|
|
1315
|
+
if not isinstance(value, str) or not value:
|
|
1316
|
+
failures.append(Failure("skill-manifests", f"registry entry `{entry_id}` must declare `{field}`"))
|
|
1317
|
+
|
|
1318
|
+
manifest_path = entry.get("manifest")
|
|
1319
|
+
if not isinstance(manifest_path, str) or not manifest_path:
|
|
1320
|
+
continue
|
|
1321
|
+
manifest_file = registry_path.parent / manifest_path
|
|
1322
|
+
if not manifest_file.exists():
|
|
1323
|
+
failures.append(Failure("skill-manifests", f"registry entry `{entry_id}` points to missing manifest `{manifest_path}`"))
|
|
1324
|
+
continue
|
|
1325
|
+
executable_path = entry.get("executable")
|
|
1326
|
+
if isinstance(executable_path, str) and executable_path:
|
|
1327
|
+
if not (registry_path.parent / executable_path).resolve().exists():
|
|
1328
|
+
failures.append(
|
|
1329
|
+
Failure("skill-manifests", f"registry entry `{entry_id}` points to missing executable `{executable_path}`")
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
try:
|
|
1333
|
+
contract = load_json_file(manifest_file)
|
|
1334
|
+
except json.JSONDecodeError as exc:
|
|
1335
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` is invalid JSON: {exc.msg}"))
|
|
1336
|
+
continue
|
|
1337
|
+
if not isinstance(contract, dict):
|
|
1338
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` must be a JSON object"))
|
|
1339
|
+
continue
|
|
1340
|
+
|
|
1341
|
+
if contract.get("id") != entry_id:
|
|
1342
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` id must match registry entry `{entry_id}`"))
|
|
1343
|
+
if contract.get("role") != entry.get("role"):
|
|
1344
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` role must match registry entry `{entry_id}`"))
|
|
1345
|
+
if contract.get("contract_version") != entry.get("contract_version"):
|
|
1346
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` contract version must match registry entry `{entry_id}`"))
|
|
1347
|
+
|
|
1348
|
+
contract_root = contract.get("root_entry")
|
|
1349
|
+
if entry_id == root_entry:
|
|
1350
|
+
if contract_root is not True:
|
|
1351
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `root_entry: true`"))
|
|
1352
|
+
elif contract_root is not False:
|
|
1353
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `root_entry: false`"))
|
|
1354
|
+
|
|
1355
|
+
entrypoint = contract.get("entrypoint")
|
|
1356
|
+
if not isinstance(entrypoint, dict):
|
|
1357
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `entrypoint`"))
|
|
1358
|
+
else:
|
|
1359
|
+
required_entrypoint_keys = {"skill_markdown", "adapter_metadata"}
|
|
1360
|
+
if entry_id == "loom-init":
|
|
1361
|
+
required_entrypoint_keys.add("bootstrap_cli")
|
|
1362
|
+
required_entrypoint_keys.add("route_cli")
|
|
1363
|
+
else:
|
|
1364
|
+
required_entrypoint_keys.add("orchestration_cli")
|
|
1365
|
+
for key in required_entrypoint_keys:
|
|
1366
|
+
value = entrypoint.get(key)
|
|
1367
|
+
if not isinstance(value, str) or not value:
|
|
1368
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` missing `entrypoint.{key}`"))
|
|
1369
|
+
continue
|
|
1370
|
+
if not (manifest_file.parent / value).exists():
|
|
1371
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` points `entrypoint.{key}` to missing `{value}`"))
|
|
1372
|
+
|
|
1373
|
+
for section in ("input_contract", "output_contract", "routing"):
|
|
1374
|
+
value = contract.get(section)
|
|
1375
|
+
if not isinstance(value, dict):
|
|
1376
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `{section}`"))
|
|
1377
|
+
continue
|
|
1378
|
+
reference = value.get("reference")
|
|
1379
|
+
if not isinstance(reference, str) or not reference:
|
|
1380
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `{section}.reference`"))
|
|
1381
|
+
continue
|
|
1382
|
+
if not (manifest_file.parent / reference).exists():
|
|
1383
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` points `{section}.reference` to missing `{reference}`"))
|
|
1384
|
+
|
|
1385
|
+
output_contract = contract.get("output_contract")
|
|
1386
|
+
if isinstance(output_contract, dict) and entry_id in GOVERNANCE_SURFACE_CONTRACT_SKILLS:
|
|
1387
|
+
required_sections = output_contract.get("required_sections")
|
|
1388
|
+
if not isinstance(required_sections, list):
|
|
1389
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `output_contract.required_sections`"))
|
|
1390
|
+
elif "governance_surface" not in required_sections:
|
|
1391
|
+
failures.append(
|
|
1392
|
+
Failure(
|
|
1393
|
+
"skill-manifests",
|
|
1394
|
+
f"`{manifest_path}` must require `governance_surface` in `output_contract.required_sections`",
|
|
1395
|
+
)
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
installation = contract.get("installation")
|
|
1399
|
+
if not isinstance(installation, dict):
|
|
1400
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `installation`"))
|
|
1401
|
+
else:
|
|
1402
|
+
for field in ("registry", "upgrade_contract", "layout_manifest"):
|
|
1403
|
+
value = installation.get(field)
|
|
1404
|
+
if not isinstance(value, str) or not value:
|
|
1405
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` must declare `installation.{field}`"))
|
|
1406
|
+
continue
|
|
1407
|
+
if not (manifest_file.parent / value).exists():
|
|
1408
|
+
failures.append(Failure("skill-manifests", f"`{manifest_path}` points `installation.{field}` to missing `{value}`"))
|
|
1409
|
+
|
|
1410
|
+
if root_registry_entry is None:
|
|
1411
|
+
failures.append(Failure("skill-manifests", f"`skills/registry.json` root entry `{root_entry}` does not match any declared entry"))
|
|
1412
|
+
return failures
|
|
1413
|
+
if seen_ids != set(expected_entries):
|
|
1414
|
+
missing = sorted(set(expected_entries) - seen_ids)
|
|
1415
|
+
extra = sorted(seen_ids - set(expected_entries))
|
|
1416
|
+
if missing:
|
|
1417
|
+
failures.append(Failure("skill-manifests", f"registry is missing first-wave entries: {', '.join(missing)}"))
|
|
1418
|
+
if extra:
|
|
1419
|
+
failures.append(Failure("skill-manifests", f"registry contains unexpected first-wave entries: {', '.join(extra)}"))
|
|
1420
|
+
if upgrade_reference != "upgrade-contract.json":
|
|
1421
|
+
failures.append(Failure("skill-manifests", "`skills/registry.json` must point to `upgrade-contract.json`"))
|
|
1422
|
+
|
|
1423
|
+
upgrade_root = upgrade_contract.get("root_entry")
|
|
1424
|
+
current_contract_version = upgrade_contract.get("current_contract_version")
|
|
1425
|
+
upgrade_policy = upgrade_contract.get("upgrade_policy")
|
|
1426
|
+
if upgrade_root != root_entry:
|
|
1427
|
+
failures.append(
|
|
1428
|
+
Failure(
|
|
1429
|
+
"skill-manifests",
|
|
1430
|
+
f"`skills/upgrade-contract.json` root entry `{upgrade_root}` does not match registry root `{root_entry}`",
|
|
1431
|
+
)
|
|
1432
|
+
)
|
|
1433
|
+
if current_contract_version != root_registry_entry.get("contract_version"):
|
|
1434
|
+
failures.append(
|
|
1435
|
+
Failure(
|
|
1436
|
+
"skill-manifests",
|
|
1437
|
+
"`skills/upgrade-contract.json` current contract version must match the registry entry version",
|
|
1438
|
+
)
|
|
1439
|
+
)
|
|
1440
|
+
if not isinstance(upgrade_policy, dict):
|
|
1441
|
+
failures.append(Failure("skill-manifests", "`skills/upgrade-contract.json` must declare `upgrade_policy`"))
|
|
1442
|
+
else:
|
|
1443
|
+
if upgrade_policy.get("mode") != "explicit":
|
|
1444
|
+
failures.append(Failure("skill-manifests", "`upgrade_policy.mode` must be `explicit`"))
|
|
1445
|
+
refresh_required = upgrade_policy.get("refresh_required")
|
|
1446
|
+
if not isinstance(refresh_required, list) or not refresh_required:
|
|
1447
|
+
failures.append(Failure("skill-manifests", "`upgrade_policy.refresh_required` must be a non-empty list"))
|
|
1448
|
+
else:
|
|
1449
|
+
required = {"registry", "manifest", "executable", "referenced_resources", "layout_manifest"}
|
|
1450
|
+
if not required.issubset(set(refresh_required)):
|
|
1451
|
+
failures.append(
|
|
1452
|
+
Failure(
|
|
1453
|
+
"skill-manifests",
|
|
1454
|
+
"`upgrade_policy.refresh_required` must cover registry, manifest, executable, referenced_resources, and layout_manifest",
|
|
1455
|
+
)
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
return failures
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
def check_skill_routing(root: Path) -> list[Failure]:
|
|
1462
|
+
failures: list[Failure] = []
|
|
1463
|
+
target = root / "examples/new-project"
|
|
1464
|
+
tool_path = root / "tools/loom_init.py"
|
|
1465
|
+
if not tool_path.exists() or not target.exists():
|
|
1466
|
+
return failures
|
|
1467
|
+
|
|
1468
|
+
registry = load_json_file(root / "skills/registry.json")
|
|
1469
|
+
if not isinstance(registry, dict):
|
|
1470
|
+
return failures
|
|
1471
|
+
entries = registry.get("entries")
|
|
1472
|
+
if not isinstance(entries, list):
|
|
1473
|
+
return failures
|
|
1474
|
+
explicit_skills = [
|
|
1475
|
+
entry.get("id")
|
|
1476
|
+
for entry in entries
|
|
1477
|
+
if isinstance(entry, dict) and isinstance(entry.get("id"), str) and entry.get("id")
|
|
1478
|
+
]
|
|
1479
|
+
for skill_id in explicit_skills:
|
|
1480
|
+
payload, error = load_command_json(
|
|
1481
|
+
root,
|
|
1482
|
+
["python3", "tools/loom_init.py", "route", "--target", "examples/new-project", "--skill", skill_id],
|
|
1483
|
+
)
|
|
1484
|
+
if error:
|
|
1485
|
+
failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` failed: {error}"))
|
|
1486
|
+
continue
|
|
1487
|
+
if payload.get("command") != "route":
|
|
1488
|
+
failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must report `command: route`"))
|
|
1489
|
+
if payload.get("result") != "pass":
|
|
1490
|
+
failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must pass"))
|
|
1491
|
+
if payload.get("selected_skill") != skill_id:
|
|
1492
|
+
failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` selected `{payload.get('selected_skill')}`"))
|
|
1493
|
+
if payload.get("mode") != "explicit":
|
|
1494
|
+
failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must report `mode: explicit`"))
|
|
1495
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
1496
|
+
failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must include `summary`"))
|
|
1497
|
+
if not isinstance(payload.get("matched_signals"), list):
|
|
1498
|
+
failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must include `matched_signals`"))
|
|
1499
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
1500
|
+
failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must include `missing_inputs`"))
|
|
1501
|
+
if payload.get("fallback_to") != "loom-init":
|
|
1502
|
+
failures.append(Failure("skill-routing", f"explicit route for `{skill_id}` must keep `fallback_to: loom-init`"))
|
|
1503
|
+
if skill_id in GOVERNANCE_SURFACE_ROUTE_SKILLS and payload.get("result") == "pass":
|
|
1504
|
+
require_governance_surface(
|
|
1505
|
+
failures,
|
|
1506
|
+
category="skill-routing",
|
|
1507
|
+
context=f"explicit route for `{skill_id}`",
|
|
1508
|
+
payload=payload,
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
implicit_cases = (
|
|
1512
|
+
("请初始化这个新项目并接入 Loom", "loom-adopt"),
|
|
1513
|
+
("请接手当前事项并恢复上下文后继续推进", "loom-resume"),
|
|
1514
|
+
("请在进入 review 前做统一检查", "loom-pre-review"),
|
|
1515
|
+
("请对当前事项做正式 review 并给出审查结论", "loom-review"),
|
|
1516
|
+
("请准备交接并回写停点", "loom-handoff"),
|
|
1517
|
+
("请清理并 retire 当前事项现场", "loom-retire"),
|
|
1518
|
+
("请确认这个事项是否 merge-ready", "loom-merge-ready"),
|
|
1519
|
+
)
|
|
1520
|
+
for task, skill_id in implicit_cases:
|
|
1521
|
+
payload, error = load_command_json(
|
|
1522
|
+
root,
|
|
1523
|
+
["python3", "tools/loom_init.py", "route", "--target", "examples/new-project", "--task", task],
|
|
1524
|
+
)
|
|
1525
|
+
if error:
|
|
1526
|
+
failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` failed: {error}"))
|
|
1527
|
+
continue
|
|
1528
|
+
if payload.get("command") != "route":
|
|
1529
|
+
failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must report `command: route`"))
|
|
1530
|
+
if payload.get("result") != "pass":
|
|
1531
|
+
failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must pass"))
|
|
1532
|
+
if payload.get("selected_skill") != skill_id:
|
|
1533
|
+
failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` selected `{payload.get('selected_skill')}`"))
|
|
1534
|
+
if payload.get("mode") != "implicit":
|
|
1535
|
+
failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must report `mode: implicit`"))
|
|
1536
|
+
if not isinstance(payload.get("matched_signals"), list) or not payload.get("matched_signals"):
|
|
1537
|
+
failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must include matched signals"))
|
|
1538
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
1539
|
+
failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must include `summary`"))
|
|
1540
|
+
if payload.get("fallback_to") != "loom-init":
|
|
1541
|
+
failures.append(Failure("skill-routing", f"implicit route for `{skill_id}` must keep `fallback_to: loom-init`"))
|
|
1542
|
+
if skill_id in GOVERNANCE_SURFACE_ROUTE_SKILLS and payload.get("result") == "pass":
|
|
1543
|
+
require_governance_surface(
|
|
1544
|
+
failures,
|
|
1545
|
+
category="skill-routing",
|
|
1546
|
+
context=f"implicit route for `{skill_id}`",
|
|
1547
|
+
payload=payload,
|
|
1548
|
+
)
|
|
1549
|
+
|
|
1550
|
+
fallback_payload, error = load_command_json(
|
|
1551
|
+
root,
|
|
1552
|
+
["python3", "tools/loom_init.py", "route", "--target", "examples/new-project", "--task", "请帮我看看这个仓库"],
|
|
1553
|
+
)
|
|
1554
|
+
if error:
|
|
1555
|
+
failures.append(Failure("skill-routing", f"fallback route failed: {error}"))
|
|
1556
|
+
else:
|
|
1557
|
+
if fallback_payload.get("result") != "fallback":
|
|
1558
|
+
failures.append(Failure("skill-routing", "ambiguous task must return `fallback`"))
|
|
1559
|
+
if fallback_payload.get("selected_skill") != "loom-init":
|
|
1560
|
+
failures.append(Failure("skill-routing", "fallback route must select `loom-init`"))
|
|
1561
|
+
if not isinstance(fallback_payload.get("missing_inputs"), list) or not fallback_payload.get("missing_inputs"):
|
|
1562
|
+
failures.append(Failure("skill-routing", "fallback route must include `missing_inputs`"))
|
|
1563
|
+
|
|
1564
|
+
ambiguous_payload, error = load_command_json(
|
|
1565
|
+
root,
|
|
1566
|
+
[
|
|
1567
|
+
"python3",
|
|
1568
|
+
"tools/loom_init.py",
|
|
1569
|
+
"route",
|
|
1570
|
+
"--target",
|
|
1571
|
+
"examples/new-project",
|
|
1572
|
+
"--task",
|
|
1573
|
+
"请接手当前事项并在 review 前检查",
|
|
1574
|
+
],
|
|
1575
|
+
)
|
|
1576
|
+
if error:
|
|
1577
|
+
failures.append(Failure("skill-routing", f"ambiguous route failed: {error}"))
|
|
1578
|
+
else:
|
|
1579
|
+
if ambiguous_payload.get("result") != "fallback":
|
|
1580
|
+
failures.append(Failure("skill-routing", "multi-match task must return `fallback`"))
|
|
1581
|
+
if ambiguous_payload.get("selected_skill") != "loom-init":
|
|
1582
|
+
failures.append(Failure("skill-routing", "multi-match route must select `loom-init`"))
|
|
1583
|
+
if not isinstance(ambiguous_payload.get("matched_signals"), list) or len(ambiguous_payload.get("matched_signals", [])) < 2:
|
|
1584
|
+
failures.append(Failure("skill-routing", "multi-match route must expose matched signals"))
|
|
1585
|
+
|
|
1586
|
+
unknown_payload, error = load_command_json(
|
|
1587
|
+
root,
|
|
1588
|
+
["python3", "tools/loom_init.py", "route", "--target", "examples/new-project", "--skill", "not-a-skill"],
|
|
1589
|
+
)
|
|
1590
|
+
if error:
|
|
1591
|
+
failures.append(Failure("skill-routing", f"unknown explicit route failed: {error}"))
|
|
1592
|
+
else:
|
|
1593
|
+
if unknown_payload.get("result") != "block":
|
|
1594
|
+
failures.append(Failure("skill-routing", "unknown explicit skill must block"))
|
|
1595
|
+
if unknown_payload.get("selected_skill") != "loom-init":
|
|
1596
|
+
failures.append(Failure("skill-routing", "unknown explicit skill must fall back to `loom-init`"))
|
|
1597
|
+
|
|
1598
|
+
return failures
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def check_demo_assets(root: Path) -> list[Failure]:
|
|
1602
|
+
failures = check_required_paths(root, "demo-assets", DEMO_ASSETS)
|
|
1603
|
+
|
|
1604
|
+
init_result_path = root / "examples/new-project/.loom/bootstrap/init-result.json"
|
|
1605
|
+
if init_result_path.exists():
|
|
1606
|
+
try:
|
|
1607
|
+
init_result = load_json_file(init_result_path)
|
|
1608
|
+
except json.JSONDecodeError as exc:
|
|
1609
|
+
failures.append(Failure("demo-assets", f"demo init-result is invalid JSON: {exc.msg}"))
|
|
1610
|
+
return failures
|
|
1611
|
+
if not isinstance(init_result, dict):
|
|
1612
|
+
failures.append(Failure("demo-assets", "demo init-result must be a JSON object"))
|
|
1613
|
+
return failures
|
|
1614
|
+
run = init_result.get("run")
|
|
1615
|
+
if not isinstance(run, dict) or run.get("scenario_key") != "new":
|
|
1616
|
+
failures.append(Failure("demo-assets", "demo init-result must keep `scenario_key` as `new`"))
|
|
1617
|
+
return failures
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
def check_demo_fact_chain(root: Path) -> list[Failure]:
|
|
1621
|
+
target = root / "examples/new-project"
|
|
1622
|
+
if not target.exists():
|
|
1623
|
+
return []
|
|
1624
|
+
|
|
1625
|
+
report, errors = inspect_fact_chain(target)
|
|
1626
|
+
failures: list[Failure] = []
|
|
1627
|
+
for detail in errors:
|
|
1628
|
+
failures.append(Failure("demo-fact-chain", detail))
|
|
1629
|
+
if report and report.get("fact_chain", {}).get("entry_points", {}).get("status_surface") != ".loom/status/current.md":
|
|
1630
|
+
failures.append(Failure("demo-fact-chain", "demo fact chain must point status_surface to `.loom/status/current.md`"))
|
|
1631
|
+
return failures
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
def check_demo_repo_local_cli(root: Path) -> list[Failure]:
|
|
1635
|
+
failures: list[Failure] = []
|
|
1636
|
+
target = root / "examples/new-project"
|
|
1637
|
+
if not target.exists():
|
|
1638
|
+
return failures
|
|
1639
|
+
|
|
1640
|
+
repo_local_commands = [
|
|
1641
|
+
(
|
|
1642
|
+
"repo-local-verify",
|
|
1643
|
+
["python3", ".loom/bin/loom_init.py", "verify", "--target", "."],
|
|
1644
|
+
"ok",
|
|
1645
|
+
),
|
|
1646
|
+
(
|
|
1647
|
+
"repo-local-fact-chain",
|
|
1648
|
+
["python3", ".loom/bin/loom_init.py", "fact-chain", "--target", "."],
|
|
1649
|
+
"ok",
|
|
1650
|
+
),
|
|
1651
|
+
]
|
|
1652
|
+
for label, args, expected_key in repo_local_commands:
|
|
1653
|
+
payload, error = load_command_json(root, args, cwd=target)
|
|
1654
|
+
if error:
|
|
1655
|
+
failures.append(Failure("demo-repo-local-cli", f"`{label}` failed: {error}"))
|
|
1656
|
+
continue
|
|
1657
|
+
if payload.get(expected_key) is not True:
|
|
1658
|
+
failures.append(Failure("demo-repo-local-cli", f"`{label}` must report `{expected_key}: true`"))
|
|
1659
|
+
return failures
|
|
1660
|
+
|
|
1661
|
+
|
|
1662
|
+
def check_deep_existing_repo_bootstrap(root: Path) -> list[Failure]:
|
|
1663
|
+
failures: list[Failure] = []
|
|
1664
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-deep-existing-") as tmp:
|
|
1665
|
+
tmp_root = Path(tmp)
|
|
1666
|
+
|
|
1667
|
+
def write_repo(target: Path, *, validation_entry: bool, pr_template: bool, workflow_doc: bool) -> None:
|
|
1668
|
+
(target / ".github" / "workflows").mkdir(parents=True, exist_ok=True)
|
|
1669
|
+
(target / "scripts").mkdir(parents=True, exist_ok=True)
|
|
1670
|
+
(target / "src").mkdir(parents=True, exist_ok=True)
|
|
1671
|
+
(target / "README.md").write_text("# Sample Repo\n", encoding="utf-8")
|
|
1672
|
+
(target / "AGENTS.md").write_text("# Root Rules\n", encoding="utf-8")
|
|
1673
|
+
(target / "src" / "main.py").write_text("print('ok')\n", encoding="utf-8")
|
|
1674
|
+
(target / "scripts" / "governance_status.py").write_text("print('ok')\n", encoding="utf-8")
|
|
1675
|
+
(target / ".github" / "workflows" / "ci.yml").write_text("name: ci\n", encoding="utf-8")
|
|
1676
|
+
if workflow_doc:
|
|
1677
|
+
(target / "WORKFLOW.md").write_text("# Workflow\n", encoding="utf-8")
|
|
1678
|
+
if validation_entry:
|
|
1679
|
+
(target / "Makefile").write_text("check:\n\t@echo ok\n", encoding="utf-8")
|
|
1680
|
+
if pr_template:
|
|
1681
|
+
(target / ".github" / "PULL_REQUEST_TEMPLATE.md").write_text("## Summary\n", encoding="utf-8")
|
|
1682
|
+
|
|
1683
|
+
deep_target = tmp_root / "deep-existing"
|
|
1684
|
+
write_repo(deep_target, validation_entry=True, pr_template=True, workflow_doc=True)
|
|
1685
|
+
deep_payload, deep_error = load_command_json(
|
|
1686
|
+
root,
|
|
1687
|
+
[
|
|
1688
|
+
"python3",
|
|
1689
|
+
"tools/loom_init.py",
|
|
1690
|
+
"bootstrap",
|
|
1691
|
+
"--target",
|
|
1692
|
+
str(deep_target),
|
|
1693
|
+
"--write",
|
|
1694
|
+
"--force",
|
|
1695
|
+
"--verify",
|
|
1696
|
+
"--install-pr-template",
|
|
1697
|
+
],
|
|
1698
|
+
)
|
|
1699
|
+
if deep_error:
|
|
1700
|
+
failures.append(Failure("deep-existing-bootstrap", f"`deep-existing bootstrap` failed: {deep_error}"))
|
|
1701
|
+
else:
|
|
1702
|
+
recommended = deep_payload.get("recommended_adoption")
|
|
1703
|
+
verification = deep_payload.get("verification")
|
|
1704
|
+
governance_surface = deep_payload.get("governance_surface")
|
|
1705
|
+
if not isinstance(recommended, dict) or recommended.get("path") != "deep-existing-repo":
|
|
1706
|
+
failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must select `recommended_adoption.path = deep-existing-repo`"))
|
|
1707
|
+
run = deep_payload.get("run")
|
|
1708
|
+
if not isinstance(run, dict) or run.get("scenario_key") != "complex-existing":
|
|
1709
|
+
failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must keep `scenario_key = complex-existing`"))
|
|
1710
|
+
if not isinstance(verification, dict) or verification.get("ok") is not True:
|
|
1711
|
+
failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must verify successfully"))
|
|
1712
|
+
if not isinstance(governance_surface, dict) or governance_surface.get("repository_mode") != "complex-existing":
|
|
1713
|
+
failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must keep `governance_surface.repository_mode = complex-existing`"))
|
|
1714
|
+
for required in (
|
|
1715
|
+
".loom/companion/README.md",
|
|
1716
|
+
".loom/companion/checkpoints.md",
|
|
1717
|
+
".loom/companion/review.md",
|
|
1718
|
+
".loom/companion/merge-ready.md",
|
|
1719
|
+
".loom/companion/closeout.md",
|
|
1720
|
+
):
|
|
1721
|
+
if not (deep_target / required).exists():
|
|
1722
|
+
failures.append(Failure("deep-existing-bootstrap", f"`deep-existing bootstrap` is missing `{required}`"))
|
|
1723
|
+
for forbidden in (
|
|
1724
|
+
".loom/work-items/INIT-0001.md",
|
|
1725
|
+
".loom/progress/INIT-0001.md",
|
|
1726
|
+
".loom/status/current.md",
|
|
1727
|
+
):
|
|
1728
|
+
if (deep_target / forbidden).exists():
|
|
1729
|
+
failures.append(Failure("deep-existing-bootstrap", f"`deep-existing bootstrap` must not generate `{forbidden}`"))
|
|
1730
|
+
fact_chain = deep_payload.get("fact_chain")
|
|
1731
|
+
if not isinstance(fact_chain, dict) or fact_chain.get("mode") != "repo-native attach-only":
|
|
1732
|
+
failures.append(Failure("deep-existing-bootstrap", "`deep-existing bootstrap` must keep `fact_chain.mode = repo-native attach-only`"))
|
|
1733
|
+
|
|
1734
|
+
full_target = tmp_root / "full-bootstrap"
|
|
1735
|
+
write_repo(full_target, validation_entry=False, pr_template=False, workflow_doc=False)
|
|
1736
|
+
full_payload, full_error = load_command_json(
|
|
1737
|
+
root,
|
|
1738
|
+
[
|
|
1739
|
+
"python3",
|
|
1740
|
+
"tools/loom_init.py",
|
|
1741
|
+
"bootstrap",
|
|
1742
|
+
"--target",
|
|
1743
|
+
str(full_target),
|
|
1744
|
+
"--write",
|
|
1745
|
+
"--force",
|
|
1746
|
+
"--verify",
|
|
1747
|
+
"--install-pr-template",
|
|
1748
|
+
],
|
|
1749
|
+
)
|
|
1750
|
+
if full_error:
|
|
1751
|
+
failures.append(Failure("deep-existing-bootstrap", f"`full-bootstrap fallback sample` failed: {full_error}"))
|
|
1752
|
+
else:
|
|
1753
|
+
recommended = full_payload.get("recommended_adoption")
|
|
1754
|
+
if not isinstance(recommended, dict) or recommended.get("path") != "full-bootstrap":
|
|
1755
|
+
failures.append(Failure("deep-existing-bootstrap", "complex existing sample without overload must keep `recommended_adoption.path = full-bootstrap`"))
|
|
1756
|
+
for required in (
|
|
1757
|
+
".loom/work-items/INIT-0001.md",
|
|
1758
|
+
".loom/progress/INIT-0001.md",
|
|
1759
|
+
".loom/status/current.md",
|
|
1760
|
+
):
|
|
1761
|
+
if not (full_target / required).exists():
|
|
1762
|
+
failures.append(Failure("deep-existing-bootstrap", f"`full-bootstrap fallback sample` must generate `{required}`"))
|
|
1763
|
+
return failures
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
def check_daily_execution_cli(root: Path) -> list[Failure]:
|
|
1767
|
+
failures: list[Failure] = []
|
|
1768
|
+
example_target = root / "examples/new-project"
|
|
1769
|
+
tool_path = root / "tools/loom_flow.py"
|
|
1770
|
+
if not tool_path.exists() or not example_target.exists():
|
|
1771
|
+
return failures
|
|
1772
|
+
|
|
1773
|
+
demo_commands = [
|
|
1774
|
+
(
|
|
1775
|
+
"runtime-state-init",
|
|
1776
|
+
["python3", "tools/loom_init.py", "runtime-state", "--target", "."],
|
|
1777
|
+
{"pass"},
|
|
1778
|
+
),
|
|
1779
|
+
(
|
|
1780
|
+
"runtime-state-flow",
|
|
1781
|
+
["python3", "tools/loom_flow.py", "runtime-state", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1782
|
+
{"pass"},
|
|
1783
|
+
),
|
|
1784
|
+
(
|
|
1785
|
+
"fact-chain",
|
|
1786
|
+
["python3", "tools/loom_flow.py", "fact-chain", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1787
|
+
{"pass"},
|
|
1788
|
+
),
|
|
1789
|
+
(
|
|
1790
|
+
"runtime-evidence",
|
|
1791
|
+
[
|
|
1792
|
+
"python3",
|
|
1793
|
+
"tools/loom_flow.py",
|
|
1794
|
+
"runtime-evidence",
|
|
1795
|
+
"--target",
|
|
1796
|
+
"examples/new-project",
|
|
1797
|
+
"--item",
|
|
1798
|
+
"INIT-0001",
|
|
1799
|
+
],
|
|
1800
|
+
{"pass"},
|
|
1801
|
+
),
|
|
1802
|
+
(
|
|
1803
|
+
"state-check",
|
|
1804
|
+
["python3", "tools/loom_flow.py", "state-check", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1805
|
+
{"pass"},
|
|
1806
|
+
),
|
|
1807
|
+
(
|
|
1808
|
+
"flow-pre-review",
|
|
1809
|
+
[
|
|
1810
|
+
"python3",
|
|
1811
|
+
"tools/loom_flow.py",
|
|
1812
|
+
"flow",
|
|
1813
|
+
"pre-review",
|
|
1814
|
+
"--target",
|
|
1815
|
+
"examples/new-project",
|
|
1816
|
+
"--item",
|
|
1817
|
+
"INIT-0001",
|
|
1818
|
+
],
|
|
1819
|
+
{"pass", "block", "fallback"},
|
|
1820
|
+
),
|
|
1821
|
+
(
|
|
1822
|
+
"flow-review",
|
|
1823
|
+
[
|
|
1824
|
+
"python3",
|
|
1825
|
+
"tools/loom_flow.py",
|
|
1826
|
+
"flow",
|
|
1827
|
+
"review",
|
|
1828
|
+
"--target",
|
|
1829
|
+
"examples/new-project",
|
|
1830
|
+
"--item",
|
|
1831
|
+
"INIT-0001",
|
|
1832
|
+
],
|
|
1833
|
+
{"pass", "block", "fallback"},
|
|
1834
|
+
),
|
|
1835
|
+
(
|
|
1836
|
+
"flow-resume",
|
|
1837
|
+
[
|
|
1838
|
+
"python3",
|
|
1839
|
+
"tools/loom_flow.py",
|
|
1840
|
+
"flow",
|
|
1841
|
+
"resume",
|
|
1842
|
+
"--target",
|
|
1843
|
+
"examples/new-project",
|
|
1844
|
+
"--item",
|
|
1845
|
+
"INIT-0001",
|
|
1846
|
+
],
|
|
1847
|
+
{"pass"},
|
|
1848
|
+
),
|
|
1849
|
+
(
|
|
1850
|
+
"flow-handoff",
|
|
1851
|
+
[
|
|
1852
|
+
"python3",
|
|
1853
|
+
"tools/loom_flow.py",
|
|
1854
|
+
"flow",
|
|
1855
|
+
"handoff",
|
|
1856
|
+
"--target",
|
|
1857
|
+
"examples/new-project",
|
|
1858
|
+
"--item",
|
|
1859
|
+
"INIT-0001",
|
|
1860
|
+
],
|
|
1861
|
+
{"pass", "block"},
|
|
1862
|
+
),
|
|
1863
|
+
(
|
|
1864
|
+
"flow-merge-ready",
|
|
1865
|
+
[
|
|
1866
|
+
"python3",
|
|
1867
|
+
"tools/loom_flow.py",
|
|
1868
|
+
"flow",
|
|
1869
|
+
"merge-ready",
|
|
1870
|
+
"--target",
|
|
1871
|
+
"examples/new-project",
|
|
1872
|
+
"--item",
|
|
1873
|
+
"INIT-0001",
|
|
1874
|
+
],
|
|
1875
|
+
{"pass", "block", "fallback"},
|
|
1876
|
+
),
|
|
1877
|
+
(
|
|
1878
|
+
"admission",
|
|
1879
|
+
["python3", "tools/loom_flow.py", "checkpoint", "admission", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1880
|
+
{"pass"},
|
|
1881
|
+
),
|
|
1882
|
+
(
|
|
1883
|
+
"build",
|
|
1884
|
+
["python3", "tools/loom_flow.py", "checkpoint", "build", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1885
|
+
{"pass", "block", "fallback"},
|
|
1886
|
+
),
|
|
1887
|
+
(
|
|
1888
|
+
"merge",
|
|
1889
|
+
["python3", "tools/loom_flow.py", "checkpoint", "merge", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1890
|
+
{"pass", "block", "fallback"},
|
|
1891
|
+
),
|
|
1892
|
+
(
|
|
1893
|
+
"locate",
|
|
1894
|
+
["python3", "tools/loom_flow.py", "workspace", "locate", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1895
|
+
{"pass"},
|
|
1896
|
+
),
|
|
1897
|
+
(
|
|
1898
|
+
"review-read",
|
|
1899
|
+
["python3", "tools/loom_flow.py", "review", "read", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1900
|
+
{"pass"},
|
|
1901
|
+
),
|
|
1902
|
+
(
|
|
1903
|
+
"host-lifecycle",
|
|
1904
|
+
["python3", "tools/loom_flow.py", "host-lifecycle", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1905
|
+
{"pass"},
|
|
1906
|
+
),
|
|
1907
|
+
(
|
|
1908
|
+
"closeout-check",
|
|
1909
|
+
["python3", "tools/loom_flow.py", "closeout", "check", "--target", ".", "--skip-gate"],
|
|
1910
|
+
{"pass"},
|
|
1911
|
+
),
|
|
1912
|
+
(
|
|
1913
|
+
"closeout-sync",
|
|
1914
|
+
["python3", "tools/loom_flow.py", "closeout", "sync", "--target", ".", "--skip-gate"],
|
|
1915
|
+
{"pass"},
|
|
1916
|
+
),
|
|
1917
|
+
(
|
|
1918
|
+
"reconciliation-audit",
|
|
1919
|
+
["python3", "tools/loom_flow.py", "reconciliation", "audit", "--target", "."],
|
|
1920
|
+
{"block"},
|
|
1921
|
+
),
|
|
1922
|
+
(
|
|
1923
|
+
"purity",
|
|
1924
|
+
["python3", "tools/loom_flow.py", "purity-check", "--target", "examples/new-project", "--item", "INIT-0001"],
|
|
1925
|
+
{"pass"},
|
|
1926
|
+
),
|
|
1927
|
+
]
|
|
1928
|
+
for label, args, allowed_results in demo_commands:
|
|
1929
|
+
payload, error = load_command_json(root, args)
|
|
1930
|
+
if error:
|
|
1931
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` command failed: {error}"))
|
|
1932
|
+
continue
|
|
1933
|
+
result = payload.get("result")
|
|
1934
|
+
if result not in allowed_results:
|
|
1935
|
+
failures.append(
|
|
1936
|
+
Failure(
|
|
1937
|
+
"daily-execution-cli",
|
|
1938
|
+
f"`{label}` returned unexpected result `{result}`",
|
|
1939
|
+
)
|
|
1940
|
+
)
|
|
1941
|
+
if label == "runtime-state-init":
|
|
1942
|
+
if payload.get("command") != "runtime-state":
|
|
1943
|
+
failures.append(Failure("daily-execution-cli", "`loom-init runtime-state` must report `command: runtime-state`"))
|
|
1944
|
+
require_runtime_state_payload(
|
|
1945
|
+
failures,
|
|
1946
|
+
category="daily-execution-cli",
|
|
1947
|
+
context="`loom-init runtime-state`",
|
|
1948
|
+
payload=payload.get("runtime_state"),
|
|
1949
|
+
expected_scene="repo-local-demo",
|
|
1950
|
+
expected_carrier="repo-local-wrapper",
|
|
1951
|
+
allowed_results={"pass"},
|
|
1952
|
+
)
|
|
1953
|
+
if label == "runtime-state-flow":
|
|
1954
|
+
if payload.get("command") != "runtime-state":
|
|
1955
|
+
failures.append(Failure("daily-execution-cli", "`loom-flow runtime-state` must report `command: runtime-state`"))
|
|
1956
|
+
require_runtime_state_payload(
|
|
1957
|
+
failures,
|
|
1958
|
+
category="daily-execution-cli",
|
|
1959
|
+
context="`loom-flow runtime-state`",
|
|
1960
|
+
payload=payload.get("runtime_state"),
|
|
1961
|
+
expected_scene="repo-local-demo",
|
|
1962
|
+
expected_carrier="repo-local-wrapper",
|
|
1963
|
+
allowed_results={"pass"},
|
|
1964
|
+
)
|
|
1965
|
+
if label == "runtime-evidence":
|
|
1966
|
+
require_runtime_state_payload(
|
|
1967
|
+
failures,
|
|
1968
|
+
category="daily-execution-cli",
|
|
1969
|
+
context="`runtime-evidence`",
|
|
1970
|
+
payload=payload.get("runtime_state"),
|
|
1971
|
+
expected_scene="repo-local-demo",
|
|
1972
|
+
expected_carrier="repo-local-wrapper",
|
|
1973
|
+
allowed_results={"pass"},
|
|
1974
|
+
)
|
|
1975
|
+
if label == "state-check":
|
|
1976
|
+
require_runtime_state_payload(
|
|
1977
|
+
failures,
|
|
1978
|
+
category="daily-execution-cli",
|
|
1979
|
+
context="`state-check`",
|
|
1980
|
+
payload=payload.get("runtime_state"),
|
|
1981
|
+
expected_scene="repo-local-demo",
|
|
1982
|
+
expected_carrier="repo-local-wrapper",
|
|
1983
|
+
allowed_results={"pass"},
|
|
1984
|
+
)
|
|
1985
|
+
if label == "flow-pre-review":
|
|
1986
|
+
require_runtime_state_payload(
|
|
1987
|
+
failures,
|
|
1988
|
+
category="daily-execution-cli",
|
|
1989
|
+
context="`flow pre-review`",
|
|
1990
|
+
payload=payload.get("runtime_state"),
|
|
1991
|
+
expected_scene="repo-local-demo",
|
|
1992
|
+
expected_carrier="repo-local-wrapper",
|
|
1993
|
+
allowed_results={"pass"},
|
|
1994
|
+
)
|
|
1995
|
+
steps = payload.get("steps")
|
|
1996
|
+
if isinstance(steps, list):
|
|
1997
|
+
step_names = [step.get("name") for step in steps if isinstance(step, dict)]
|
|
1998
|
+
if step_names != [
|
|
1999
|
+
"runtime-state",
|
|
2000
|
+
"fact-chain",
|
|
2001
|
+
"state-check",
|
|
2002
|
+
"runtime-evidence",
|
|
2003
|
+
"checkpoint-admission",
|
|
2004
|
+
"workspace-locate",
|
|
2005
|
+
]:
|
|
2006
|
+
failures.append(
|
|
2007
|
+
Failure(
|
|
2008
|
+
"daily-execution-cli",
|
|
2009
|
+
"`flow pre-review` must run runtime-state, fact-chain, state-check, runtime-evidence, checkpoint-admission, and workspace-locate in order",
|
|
2010
|
+
)
|
|
2011
|
+
)
|
|
2012
|
+
if label == "purity":
|
|
2013
|
+
purity = payload.get("purity")
|
|
2014
|
+
if not isinstance(purity, dict):
|
|
2015
|
+
failures.append(Failure("daily-execution-cli", "`purity` output must include a `purity` object"))
|
|
2016
|
+
continue
|
|
2017
|
+
require_runtime_state_payload(
|
|
2018
|
+
failures,
|
|
2019
|
+
category="daily-execution-cli",
|
|
2020
|
+
context="`purity`",
|
|
2021
|
+
payload=payload.get("runtime_state"),
|
|
2022
|
+
expected_scene="repo-local-demo",
|
|
2023
|
+
expected_carrier="repo-local-wrapper",
|
|
2024
|
+
allowed_results={"pass"},
|
|
2025
|
+
)
|
|
2026
|
+
scope_assessment = purity.get("scope_assessment")
|
|
2027
|
+
if not isinstance(scope_assessment, dict):
|
|
2028
|
+
failures.append(Failure("daily-execution-cli", "`purity` output must include `scope_assessment`"))
|
|
2029
|
+
continue
|
|
2030
|
+
mode = scope_assessment.get("mode")
|
|
2031
|
+
if mode not in {"constrained", "unconstrained"}:
|
|
2032
|
+
failures.append(
|
|
2033
|
+
Failure("daily-execution-cli", "`scope_assessment.mode` must be `constrained` or `unconstrained`")
|
|
2034
|
+
)
|
|
2035
|
+
if label == "flow-resume":
|
|
2036
|
+
if payload.get("command") != "flow":
|
|
2037
|
+
failures.append(Failure("daily-execution-cli", "`flow resume` must report `command: flow`"))
|
|
2038
|
+
if payload.get("operation") != "resume":
|
|
2039
|
+
failures.append(Failure("daily-execution-cli", "`flow resume` must report `operation: resume`"))
|
|
2040
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
2041
|
+
failures.append(Failure("daily-execution-cli", "`flow resume` must include a non-empty `summary`"))
|
|
2042
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
2043
|
+
failures.append(Failure("daily-execution-cli", "`flow resume` must include `missing_inputs`"))
|
|
2044
|
+
if payload.get("fallback_to") not in {None, "admission"}:
|
|
2045
|
+
failures.append(Failure("daily-execution-cli", "`flow resume` fallback must be `null` or `admission`"))
|
|
2046
|
+
for key in ("item", "workspace", "recovery", "checkpoint", "state_check"):
|
|
2047
|
+
if not isinstance(payload.get(key), dict):
|
|
2048
|
+
failures.append(Failure("daily-execution-cli", f"`flow resume` must include `{key}`"))
|
|
2049
|
+
require_runtime_state_payload(
|
|
2050
|
+
failures,
|
|
2051
|
+
category="daily-execution-cli",
|
|
2052
|
+
context="`flow resume`",
|
|
2053
|
+
payload=payload.get("runtime_state"),
|
|
2054
|
+
expected_scene="repo-local-demo",
|
|
2055
|
+
expected_carrier="repo-local-wrapper",
|
|
2056
|
+
allowed_results={"pass"},
|
|
2057
|
+
)
|
|
2058
|
+
require_governance_surface(
|
|
2059
|
+
failures,
|
|
2060
|
+
category="daily-execution-cli",
|
|
2061
|
+
context="`flow resume`",
|
|
2062
|
+
payload=payload,
|
|
2063
|
+
)
|
|
2064
|
+
steps = payload.get("steps")
|
|
2065
|
+
if not isinstance(steps, list):
|
|
2066
|
+
failures.append(Failure("daily-execution-cli", "`flow resume` must include `steps`"))
|
|
2067
|
+
continue
|
|
2068
|
+
step_names = [step.get("name") for step in steps if isinstance(step, dict)]
|
|
2069
|
+
if step_names != ["runtime-state", "fact-chain", "state-check", "workspace-locate"]:
|
|
2070
|
+
failures.append(
|
|
2071
|
+
Failure(
|
|
2072
|
+
"daily-execution-cli",
|
|
2073
|
+
"`flow resume` must run runtime-state, fact-chain, state-check, and workspace-locate in order",
|
|
2074
|
+
)
|
|
2075
|
+
)
|
|
2076
|
+
recovery = payload.get("recovery")
|
|
2077
|
+
if isinstance(recovery, dict):
|
|
2078
|
+
for field in ("current_stop", "next_step", "blockers", "latest_validation_summary"):
|
|
2079
|
+
value = recovery.get(field)
|
|
2080
|
+
if not isinstance(value, str) or not value:
|
|
2081
|
+
failures.append(
|
|
2082
|
+
Failure("daily-execution-cli", f"`flow resume` recovery must include non-empty `{field}`")
|
|
2083
|
+
)
|
|
2084
|
+
checkpoint = payload.get("checkpoint")
|
|
2085
|
+
if isinstance(checkpoint, dict):
|
|
2086
|
+
if checkpoint.get("normalized") not in {"admission", "build", "merge", "retired"}:
|
|
2087
|
+
failures.append(
|
|
2088
|
+
Failure("daily-execution-cli", "`flow resume` checkpoint must include a known normalized value")
|
|
2089
|
+
)
|
|
2090
|
+
state_check = payload.get("state_check")
|
|
2091
|
+
if isinstance(state_check, dict):
|
|
2092
|
+
if state_check.get("result") not in {"pass", "block"}:
|
|
2093
|
+
failures.append(
|
|
2094
|
+
Failure("daily-execution-cli", "`flow resume` state_check.result must be `pass` or `block`")
|
|
2095
|
+
)
|
|
2096
|
+
if not isinstance(state_check.get("checks"), dict):
|
|
2097
|
+
failures.append(Failure("daily-execution-cli", "`flow resume` must include `state_check.checks`"))
|
|
2098
|
+
if label == "flow-handoff":
|
|
2099
|
+
if payload.get("command") != "flow":
|
|
2100
|
+
failures.append(Failure("daily-execution-cli", "`flow handoff` must report `command: flow`"))
|
|
2101
|
+
if payload.get("operation") != "handoff":
|
|
2102
|
+
failures.append(Failure("daily-execution-cli", "`flow handoff` must report `operation: handoff`"))
|
|
2103
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
2104
|
+
failures.append(Failure("daily-execution-cli", "`flow handoff` must include a non-empty `summary`"))
|
|
2105
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
2106
|
+
failures.append(Failure("daily-execution-cli", "`flow handoff` must include `missing_inputs`"))
|
|
2107
|
+
if payload.get("fallback_to") not in {None, "admission"}:
|
|
2108
|
+
failures.append(Failure("daily-execution-cli", "`flow handoff` fallback must be `null` or `admission`"))
|
|
2109
|
+
for key in ("item", "workspace", "checkpoint", "state_check"):
|
|
2110
|
+
if not isinstance(payload.get(key), dict):
|
|
2111
|
+
failures.append(Failure("daily-execution-cli", f"`flow handoff` must include `{key}`"))
|
|
2112
|
+
require_runtime_state_payload(
|
|
2113
|
+
failures,
|
|
2114
|
+
category="daily-execution-cli",
|
|
2115
|
+
context="`flow handoff`",
|
|
2116
|
+
payload=payload.get("runtime_state"),
|
|
2117
|
+
expected_scene="repo-local-demo",
|
|
2118
|
+
expected_carrier="repo-local-wrapper",
|
|
2119
|
+
allowed_results={"pass"},
|
|
2120
|
+
)
|
|
2121
|
+
for key in (
|
|
2122
|
+
"recovery_entry",
|
|
2123
|
+
"status_surface",
|
|
2124
|
+
"current_stop",
|
|
2125
|
+
"next_step",
|
|
2126
|
+
"blockers",
|
|
2127
|
+
"latest_validation_summary",
|
|
2128
|
+
):
|
|
2129
|
+
value = payload.get(key)
|
|
2130
|
+
if not isinstance(value, str) or not value:
|
|
2131
|
+
failures.append(Failure("daily-execution-cli", f"`flow handoff` must include non-empty `{key}`"))
|
|
2132
|
+
if payload.get("fallback_target") not in {None, "admission"}:
|
|
2133
|
+
failures.append(Failure("daily-execution-cli", "`flow handoff` fallback_target must be `null` or `admission`"))
|
|
2134
|
+
writeback_fields = payload.get("writeback_fields")
|
|
2135
|
+
if writeback_fields != [
|
|
2136
|
+
"current_stop",
|
|
2137
|
+
"next_step",
|
|
2138
|
+
"blockers",
|
|
2139
|
+
"latest_validation_summary",
|
|
2140
|
+
]:
|
|
2141
|
+
failures.append(
|
|
2142
|
+
Failure(
|
|
2143
|
+
"daily-execution-cli",
|
|
2144
|
+
"`flow handoff` must report the stable writeback field list in order",
|
|
2145
|
+
)
|
|
2146
|
+
)
|
|
2147
|
+
steps = payload.get("steps")
|
|
2148
|
+
if not isinstance(steps, list):
|
|
2149
|
+
failures.append(Failure("daily-execution-cli", "`flow handoff` must include `steps`"))
|
|
2150
|
+
continue
|
|
2151
|
+
step_names = [step.get("name") for step in steps if isinstance(step, dict)]
|
|
2152
|
+
if step_names != ["runtime-state", "fact-chain", "state-check", "workspace-locate"]:
|
|
2153
|
+
failures.append(
|
|
2154
|
+
Failure(
|
|
2155
|
+
"daily-execution-cli",
|
|
2156
|
+
"`flow handoff` must run runtime-state, fact-chain, state-check, and workspace-locate in order",
|
|
2157
|
+
)
|
|
2158
|
+
)
|
|
2159
|
+
state_check = payload.get("state_check")
|
|
2160
|
+
if isinstance(state_check, dict):
|
|
2161
|
+
if state_check.get("result") not in {"pass", "block"}:
|
|
2162
|
+
failures.append(
|
|
2163
|
+
Failure("daily-execution-cli", "`flow handoff` state_check.result must be `pass` or `block`")
|
|
2164
|
+
)
|
|
2165
|
+
if not isinstance(state_check.get("checks"), dict):
|
|
2166
|
+
failures.append(Failure("daily-execution-cli", "`flow handoff` must include `state_check.checks`"))
|
|
2167
|
+
if label == "flow-review":
|
|
2168
|
+
if payload.get("command") != "flow":
|
|
2169
|
+
failures.append(Failure("daily-execution-cli", "`flow review` must report `command: flow`"))
|
|
2170
|
+
if payload.get("operation") != "review":
|
|
2171
|
+
failures.append(Failure("daily-execution-cli", "`flow review` must report `operation: review`"))
|
|
2172
|
+
for key in ("item", "state_check", "runtime_evidence", "build_checkpoint", "review", "current_checkpoint", "repo_specific_requirements"):
|
|
2173
|
+
if not isinstance(payload.get(key), dict):
|
|
2174
|
+
failures.append(Failure("daily-execution-cli", f"`flow review` must include `{key}`"))
|
|
2175
|
+
require_runtime_state_payload(
|
|
2176
|
+
failures,
|
|
2177
|
+
category="daily-execution-cli",
|
|
2178
|
+
context="`flow review`",
|
|
2179
|
+
payload=payload.get("runtime_state"),
|
|
2180
|
+
expected_scene="repo-local-demo",
|
|
2181
|
+
expected_carrier="repo-local-wrapper",
|
|
2182
|
+
allowed_results={"pass"},
|
|
2183
|
+
)
|
|
2184
|
+
steps = payload.get("steps")
|
|
2185
|
+
if not isinstance(steps, list):
|
|
2186
|
+
failures.append(Failure("daily-execution-cli", "`flow review` must include `steps`"))
|
|
2187
|
+
continue
|
|
2188
|
+
step_names = [step.get("name") for step in steps if isinstance(step, dict)]
|
|
2189
|
+
if step_names != [
|
|
2190
|
+
"runtime-state",
|
|
2191
|
+
"fact-chain",
|
|
2192
|
+
"state-check",
|
|
2193
|
+
"runtime-evidence",
|
|
2194
|
+
"checkpoint-build",
|
|
2195
|
+
"review-entry",
|
|
2196
|
+
]:
|
|
2197
|
+
failures.append(
|
|
2198
|
+
Failure(
|
|
2199
|
+
"daily-execution-cli",
|
|
2200
|
+
"`flow review` must run runtime-state, fact-chain, state-check, runtime-evidence, checkpoint-build, and review-entry in order",
|
|
2201
|
+
)
|
|
2202
|
+
)
|
|
2203
|
+
review = payload.get("review")
|
|
2204
|
+
if isinstance(review, dict):
|
|
2205
|
+
require_review_record_contract(
|
|
2206
|
+
failures,
|
|
2207
|
+
category="daily-execution-cli",
|
|
2208
|
+
context="`flow review` review.record",
|
|
2209
|
+
payload=review.get("record"),
|
|
2210
|
+
)
|
|
2211
|
+
require_repo_specific_requirements_payload(
|
|
2212
|
+
failures,
|
|
2213
|
+
category="daily-execution-cli",
|
|
2214
|
+
context="`flow review` repo_specific_requirements",
|
|
2215
|
+
payload=payload.get("repo_specific_requirements"),
|
|
2216
|
+
expected_surface="review",
|
|
2217
|
+
)
|
|
2218
|
+
if label == "review-read":
|
|
2219
|
+
if payload.get("command") != "review":
|
|
2220
|
+
failures.append(Failure("daily-execution-cli", "`review read` must report `command: review`"))
|
|
2221
|
+
if payload.get("operation") != "read":
|
|
2222
|
+
failures.append(Failure("daily-execution-cli", "`review read` must report `operation: read`"))
|
|
2223
|
+
review = payload.get("review")
|
|
2224
|
+
if not isinstance(review, dict):
|
|
2225
|
+
failures.append(Failure("daily-execution-cli", "`review read` must include a `review` object"))
|
|
2226
|
+
elif not isinstance(review.get("record"), dict):
|
|
2227
|
+
failures.append(Failure("daily-execution-cli", "`review read` must include `review.record`"))
|
|
2228
|
+
else:
|
|
2229
|
+
require_review_record_contract(
|
|
2230
|
+
failures,
|
|
2231
|
+
category="daily-execution-cli",
|
|
2232
|
+
context="`review read` review.record",
|
|
2233
|
+
payload=review.get("record"),
|
|
2234
|
+
)
|
|
2235
|
+
if label == "host-lifecycle":
|
|
2236
|
+
if payload.get("command") != "host-lifecycle":
|
|
2237
|
+
failures.append(Failure("daily-execution-cli", "`host-lifecycle` must report `command: host-lifecycle`"))
|
|
2238
|
+
require_host_lifecycle_payload(
|
|
2239
|
+
failures,
|
|
2240
|
+
category="daily-execution-cli",
|
|
2241
|
+
context="`host-lifecycle`",
|
|
2242
|
+
payload=payload,
|
|
2243
|
+
)
|
|
2244
|
+
if label in {"closeout-check", "closeout-sync"}:
|
|
2245
|
+
if payload.get("command") != "closeout":
|
|
2246
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` must report `command: closeout`"))
|
|
2247
|
+
expected_operation = "check" if label == "closeout-check" else "sync"
|
|
2248
|
+
if payload.get("operation") != expected_operation:
|
|
2249
|
+
failures.append(
|
|
2250
|
+
Failure("daily-execution-cli", f"`{label}` must report `operation: {expected_operation}`")
|
|
2251
|
+
)
|
|
2252
|
+
repo = payload.get("repo")
|
|
2253
|
+
if not isinstance(repo, dict):
|
|
2254
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` must include `repo`"))
|
|
2255
|
+
else:
|
|
2256
|
+
if not isinstance(repo.get("owner"), str) or not repo.get("owner"):
|
|
2257
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` must include `repo.owner`"))
|
|
2258
|
+
if not isinstance(repo.get("name"), str) or not repo.get("name"):
|
|
2259
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` must include `repo.name`"))
|
|
2260
|
+
require_runtime_state_payload(
|
|
2261
|
+
failures,
|
|
2262
|
+
category="daily-execution-cli",
|
|
2263
|
+
context=f"`{label}`",
|
|
2264
|
+
payload=payload.get("runtime_state"),
|
|
2265
|
+
expected_scene="repo-local-demo",
|
|
2266
|
+
expected_carrier="repo-local-wrapper",
|
|
2267
|
+
allowed_results={"pass"},
|
|
2268
|
+
)
|
|
2269
|
+
require_closeout_reconciliation_contract(
|
|
2270
|
+
failures,
|
|
2271
|
+
category="daily-execution-cli",
|
|
2272
|
+
context=f"`{label}`",
|
|
2273
|
+
payload=payload,
|
|
2274
|
+
)
|
|
2275
|
+
require_repo_specific_requirements_payload(
|
|
2276
|
+
failures,
|
|
2277
|
+
category="daily-execution-cli",
|
|
2278
|
+
context=f"`{label}` repo_specific_requirements",
|
|
2279
|
+
payload=payload.get("repo_specific_requirements"),
|
|
2280
|
+
expected_surface="closeout",
|
|
2281
|
+
)
|
|
2282
|
+
if label == "reconciliation-audit":
|
|
2283
|
+
if payload.get("command") != "reconciliation":
|
|
2284
|
+
failures.append(Failure("daily-execution-cli", "`reconciliation audit` must report `command: reconciliation`"))
|
|
2285
|
+
if payload.get("operation") != "audit":
|
|
2286
|
+
failures.append(Failure("daily-execution-cli", "`reconciliation audit` must report `operation: audit`"))
|
|
2287
|
+
require_runtime_state_payload(
|
|
2288
|
+
failures,
|
|
2289
|
+
category="daily-execution-cli",
|
|
2290
|
+
context="`reconciliation audit`",
|
|
2291
|
+
payload=payload.get("runtime_state"),
|
|
2292
|
+
expected_scene="repo-local-demo",
|
|
2293
|
+
expected_carrier="repo-local-wrapper",
|
|
2294
|
+
allowed_results={"pass"},
|
|
2295
|
+
)
|
|
2296
|
+
require_reconciliation_payload(
|
|
2297
|
+
failures,
|
|
2298
|
+
category="daily-execution-cli",
|
|
2299
|
+
context="`reconciliation audit`",
|
|
2300
|
+
payload=payload,
|
|
2301
|
+
)
|
|
2302
|
+
if label == "flow-merge-ready":
|
|
2303
|
+
if payload.get("command") != "flow":
|
|
2304
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must report `command: flow`"))
|
|
2305
|
+
if payload.get("operation") != "merge-ready":
|
|
2306
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must report `operation: merge-ready`"))
|
|
2307
|
+
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
2308
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include a non-empty `summary`"))
|
|
2309
|
+
if not isinstance(payload.get("missing_inputs"), list):
|
|
2310
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `missing_inputs`"))
|
|
2311
|
+
if payload.get("fallback_to") not in {None, "admission", "build", "merge", "retired"}:
|
|
2312
|
+
failures.append(
|
|
2313
|
+
Failure(
|
|
2314
|
+
"daily-execution-cli",
|
|
2315
|
+
"`flow merge-ready` fallback must be `null` or a known checkpoint",
|
|
2316
|
+
)
|
|
2317
|
+
)
|
|
2318
|
+
for key in ("item", "runtime_state", "state_check", "runtime_evidence", "build_checkpoint", "merge_checkpoint", "current_checkpoint", "repo_specific_requirements"):
|
|
2319
|
+
if not isinstance(payload.get(key), dict):
|
|
2320
|
+
failures.append(Failure("daily-execution-cli", f"`flow merge-ready` must include `{key}`"))
|
|
2321
|
+
require_runtime_state_payload(
|
|
2322
|
+
failures,
|
|
2323
|
+
category="daily-execution-cli",
|
|
2324
|
+
context="`flow merge-ready`",
|
|
2325
|
+
payload=payload.get("runtime_state"),
|
|
2326
|
+
expected_scene="repo-local-demo",
|
|
2327
|
+
expected_carrier="repo-local-wrapper",
|
|
2328
|
+
allowed_results={"pass"},
|
|
2329
|
+
)
|
|
2330
|
+
if not isinstance(payload.get("current_lane"), str) or not payload.get("current_lane"):
|
|
2331
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `current_lane`"))
|
|
2332
|
+
if not isinstance(payload.get("latest_validation_summary"), str) or not payload.get("latest_validation_summary"):
|
|
2333
|
+
failures.append(
|
|
2334
|
+
Failure("daily-execution-cli", "`flow merge-ready` must include `latest_validation_summary`")
|
|
2335
|
+
)
|
|
2336
|
+
steps = payload.get("steps")
|
|
2337
|
+
if not isinstance(steps, list):
|
|
2338
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `steps`"))
|
|
2339
|
+
continue
|
|
2340
|
+
step_names = [step.get("name") for step in steps if isinstance(step, dict)]
|
|
2341
|
+
if step_names != [
|
|
2342
|
+
"runtime-state",
|
|
2343
|
+
"fact-chain",
|
|
2344
|
+
"state-check",
|
|
2345
|
+
"runtime-evidence",
|
|
2346
|
+
"checkpoint-build",
|
|
2347
|
+
"checkpoint-merge",
|
|
2348
|
+
]:
|
|
2349
|
+
failures.append(
|
|
2350
|
+
Failure(
|
|
2351
|
+
"daily-execution-cli",
|
|
2352
|
+
"`flow merge-ready` must run runtime-state, fact-chain, state-check, runtime-evidence, checkpoint-build, and checkpoint-merge in order",
|
|
2353
|
+
)
|
|
2354
|
+
)
|
|
2355
|
+
state_check = payload.get("state_check")
|
|
2356
|
+
if isinstance(state_check, dict):
|
|
2357
|
+
if state_check.get("result") not in {"pass", "block"}:
|
|
2358
|
+
failures.append(
|
|
2359
|
+
Failure("daily-execution-cli", "`flow merge-ready` state_check.result must be `pass` or `block`")
|
|
2360
|
+
)
|
|
2361
|
+
if not isinstance(state_check.get("checks"), dict):
|
|
2362
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `state_check.checks`"))
|
|
2363
|
+
runtime_evidence = payload.get("runtime_evidence")
|
|
2364
|
+
if isinstance(runtime_evidence, dict):
|
|
2365
|
+
for field in ("run_entry", "logs_entry", "diagnostics_entry", "verification_entry", "lane_entry"):
|
|
2366
|
+
if not isinstance(runtime_evidence.get(field), dict):
|
|
2367
|
+
failures.append(
|
|
2368
|
+
Failure("daily-execution-cli", f"`flow merge-ready` must include runtime evidence field `{field}`")
|
|
2369
|
+
)
|
|
2370
|
+
for key in ("build_checkpoint", "merge_checkpoint"):
|
|
2371
|
+
checkpoint = payload.get(key)
|
|
2372
|
+
if isinstance(checkpoint, dict):
|
|
2373
|
+
if checkpoint.get("result") not in {"pass", "block", "fallback"}:
|
|
2374
|
+
failures.append(
|
|
2375
|
+
Failure(
|
|
2376
|
+
"daily-execution-cli",
|
|
2377
|
+
f"`flow merge-ready` {key}.result must be `pass`, `block`, or `fallback`",
|
|
2378
|
+
)
|
|
2379
|
+
)
|
|
2380
|
+
if not isinstance(checkpoint.get("missing_inputs"), list):
|
|
2381
|
+
failures.append(
|
|
2382
|
+
Failure("daily-execution-cli", f"`flow merge-ready` {key} must include `missing_inputs`")
|
|
2383
|
+
)
|
|
2384
|
+
merge_checkpoint = payload.get("merge_checkpoint")
|
|
2385
|
+
if isinstance(merge_checkpoint, dict) and not isinstance(merge_checkpoint.get("pr_template"), dict):
|
|
2386
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must include `merge_checkpoint.pr_template`"))
|
|
2387
|
+
require_repo_specific_requirements_payload(
|
|
2388
|
+
failures,
|
|
2389
|
+
category="daily-execution-cli",
|
|
2390
|
+
context="`flow merge-ready` repo_specific_requirements",
|
|
2391
|
+
payload=payload.get("repo_specific_requirements"),
|
|
2392
|
+
expected_surface="merge_ready",
|
|
2393
|
+
)
|
|
2394
|
+
if payload.get("result") != "fallback":
|
|
2395
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must return `fallback` for the bootstrap demo"))
|
|
2396
|
+
if payload.get("fallback_to") != "admission":
|
|
2397
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` must fall back to `admission` for the bootstrap demo"))
|
|
2398
|
+
if isinstance(payload.get("build_checkpoint"), dict) and payload["build_checkpoint"].get("fallback_to") != "admission":
|
|
2399
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` build checkpoint must fall back to `admission` for the bootstrap demo"))
|
|
2400
|
+
if isinstance(payload.get("merge_checkpoint"), dict) and payload["merge_checkpoint"].get("fallback_to") != "admission":
|
|
2401
|
+
failures.append(Failure("daily-execution-cli", "`flow merge-ready` merge checkpoint must fall back to `admission` for the bootstrap demo"))
|
|
2402
|
+
|
|
2403
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-review-run-") as tmp:
|
|
2404
|
+
source_snapshot = Path(tmp) / "source-snapshot"
|
|
2405
|
+
review_target = Path(tmp) / "new-project"
|
|
2406
|
+
fake_bin = Path(tmp) / "bin"
|
|
2407
|
+
fake_bin.mkdir(parents=True, exist_ok=True)
|
|
2408
|
+
shutil.copytree(root, source_snapshot, ignore=shutil.ignore_patterns(".git", ".DS_Store", "__pycache__"))
|
|
2409
|
+
|
|
2410
|
+
def prepare_review_target(target: Path, label: str) -> bool:
|
|
2411
|
+
shutil.copytree(source_snapshot, target)
|
|
2412
|
+
for args in (
|
|
2413
|
+
["git", "init"],
|
|
2414
|
+
["git", "config", "user.email", "loom-check@example.com"],
|
|
2415
|
+
["git", "config", "user.name", "loom-check"],
|
|
2416
|
+
):
|
|
2417
|
+
result = run_command(root, args, cwd=target)
|
|
2418
|
+
if result.returncode != 0:
|
|
2419
|
+
detail = result.stderr.strip() or result.stdout.strip() or "git setup failed"
|
|
2420
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` setup failed: {detail}"))
|
|
2421
|
+
return False
|
|
2422
|
+
payload, error = load_command_json(
|
|
2423
|
+
root,
|
|
2424
|
+
[
|
|
2425
|
+
"python3",
|
|
2426
|
+
"tools/loom_init.py",
|
|
2427
|
+
"bootstrap",
|
|
2428
|
+
"--target",
|
|
2429
|
+
".",
|
|
2430
|
+
"--write",
|
|
2431
|
+
"--force",
|
|
2432
|
+
"--verify",
|
|
2433
|
+
"--install-pr-template",
|
|
2434
|
+
],
|
|
2435
|
+
cwd=target,
|
|
2436
|
+
)
|
|
2437
|
+
if error:
|
|
2438
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` bootstrap failed: {error}"))
|
|
2439
|
+
return False
|
|
2440
|
+
verification = payload.get("verification")
|
|
2441
|
+
if not isinstance(verification, dict) or verification.get("ok") is not True:
|
|
2442
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` bootstrap must verify successfully"))
|
|
2443
|
+
return False
|
|
2444
|
+
for args in (
|
|
2445
|
+
["git", "add", "."],
|
|
2446
|
+
["git", "add", "-f", ".loom"],
|
|
2447
|
+
["git", "commit", "-m", "review-run baseline"],
|
|
2448
|
+
):
|
|
2449
|
+
result = run_command(root, args, cwd=target)
|
|
2450
|
+
if result.returncode != 0:
|
|
2451
|
+
detail = result.stderr.strip() or result.stdout.strip() or "git baseline commit failed"
|
|
2452
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` setup failed: {detail}"))
|
|
2453
|
+
return False
|
|
2454
|
+
return True
|
|
2455
|
+
|
|
2456
|
+
prepare_review_target(review_target, "review run positive chain")
|
|
2457
|
+
write_fake_codex(fake_bin / "codex", mode="success")
|
|
2458
|
+
success_env = prepend_path_env(fake_bin)
|
|
2459
|
+
|
|
2460
|
+
payload, error = load_command_json(
|
|
2461
|
+
root,
|
|
2462
|
+
[
|
|
2463
|
+
"python3",
|
|
2464
|
+
"tools/loom_flow.py",
|
|
2465
|
+
"review",
|
|
2466
|
+
"run",
|
|
2467
|
+
"--target",
|
|
2468
|
+
str(review_target),
|
|
2469
|
+
"--item",
|
|
2470
|
+
"INIT-0001",
|
|
2471
|
+
],
|
|
2472
|
+
env=success_env,
|
|
2473
|
+
)
|
|
2474
|
+
if error:
|
|
2475
|
+
failures.append(Failure("daily-execution-cli", f"`review run` positive chain failed: {error}"))
|
|
2476
|
+
else:
|
|
2477
|
+
require_review_run_payload(
|
|
2478
|
+
failures,
|
|
2479
|
+
category="daily-execution-cli",
|
|
2480
|
+
context="`review run` positive chain",
|
|
2481
|
+
payload=payload,
|
|
2482
|
+
expected_result={"pass"},
|
|
2483
|
+
)
|
|
2484
|
+
|
|
2485
|
+
engine_missing_target = Path(tmp) / "engine-missing"
|
|
2486
|
+
prepare_review_target(engine_missing_target, "review run engine unavailable")
|
|
2487
|
+
payload, error = load_command_json(
|
|
2488
|
+
root,
|
|
2489
|
+
[
|
|
2490
|
+
"python3",
|
|
2491
|
+
"tools/loom_flow.py",
|
|
2492
|
+
"review",
|
|
2493
|
+
"run",
|
|
2494
|
+
"--target",
|
|
2495
|
+
str(engine_missing_target),
|
|
2496
|
+
"--item",
|
|
2497
|
+
"INIT-0001",
|
|
2498
|
+
],
|
|
2499
|
+
env={"PATH": "/usr/bin:/bin"},
|
|
2500
|
+
)
|
|
2501
|
+
if error:
|
|
2502
|
+
failures.append(Failure("daily-execution-cli", f"`review run` engine unavailable failed: {error}"))
|
|
2503
|
+
elif payload.get("result") != "block":
|
|
2504
|
+
failures.append(Failure("daily-execution-cli", "`review run` must block when the default engine is unavailable"))
|
|
2505
|
+
else:
|
|
2506
|
+
require_review_run_payload(
|
|
2507
|
+
failures,
|
|
2508
|
+
category="daily-execution-cli",
|
|
2509
|
+
context="`review run` engine unavailable",
|
|
2510
|
+
payload=payload,
|
|
2511
|
+
expected_result={"block"},
|
|
2512
|
+
)
|
|
2513
|
+
engine = payload.get("engine")
|
|
2514
|
+
if isinstance(engine, dict) and engine.get("failure_reason") != "engine_unavailable":
|
|
2515
|
+
failures.append(Failure("daily-execution-cli", "`review run` must report `engine_unavailable` when Codex is missing"))
|
|
2516
|
+
if payload.get("fallback_to") is not None:
|
|
2517
|
+
failures.append(Failure("daily-execution-cli", "`review run` must not convert engine failure into checkpoint fallback"))
|
|
2518
|
+
|
|
2519
|
+
schema_target = Path(tmp) / "schema-drift"
|
|
2520
|
+
prepare_review_target(schema_target, "review run schema drift")
|
|
2521
|
+
write_fake_codex(fake_bin / "codex", mode="schema_drift")
|
|
2522
|
+
payload, error = load_command_json(
|
|
2523
|
+
root,
|
|
2524
|
+
[
|
|
2525
|
+
"python3",
|
|
2526
|
+
"tools/loom_flow.py",
|
|
2527
|
+
"review",
|
|
2528
|
+
"run",
|
|
2529
|
+
"--target",
|
|
2530
|
+
str(schema_target),
|
|
2531
|
+
"--item",
|
|
2532
|
+
"INIT-0001",
|
|
2533
|
+
],
|
|
2534
|
+
env=success_env,
|
|
2535
|
+
)
|
|
2536
|
+
if error:
|
|
2537
|
+
failures.append(Failure("daily-execution-cli", f"`review run` schema drift failed: {error}"))
|
|
2538
|
+
elif payload.get("result") != "block":
|
|
2539
|
+
failures.append(Failure("daily-execution-cli", "`review run` must block on schema drift"))
|
|
2540
|
+
else:
|
|
2541
|
+
require_review_run_payload(
|
|
2542
|
+
failures,
|
|
2543
|
+
category="daily-execution-cli",
|
|
2544
|
+
context="`review run` schema drift",
|
|
2545
|
+
payload=payload,
|
|
2546
|
+
expected_result={"block"},
|
|
2547
|
+
)
|
|
2548
|
+
engine = payload.get("engine")
|
|
2549
|
+
if isinstance(engine, dict) and engine.get("failure_reason") != "schema_drift":
|
|
2550
|
+
failures.append(Failure("daily-execution-cli", "`review run` must report `schema_drift` for invalid engine output"))
|
|
2551
|
+
|
|
2552
|
+
dirty_target = Path(tmp) / "tracked-edit"
|
|
2553
|
+
prepare_review_target(dirty_target, "review run tracked edit")
|
|
2554
|
+
write_fake_codex(fake_bin / "codex", mode="tracked_edit", tracked_edit_target=".loom/status/current.md")
|
|
2555
|
+
payload, error = load_command_json(
|
|
2556
|
+
root,
|
|
2557
|
+
[
|
|
2558
|
+
"python3",
|
|
2559
|
+
"tools/loom_flow.py",
|
|
2560
|
+
"review",
|
|
2561
|
+
"run",
|
|
2562
|
+
"--target",
|
|
2563
|
+
str(dirty_target),
|
|
2564
|
+
"--item",
|
|
2565
|
+
"INIT-0001",
|
|
2566
|
+
],
|
|
2567
|
+
env=success_env,
|
|
2568
|
+
)
|
|
2569
|
+
if error:
|
|
2570
|
+
failures.append(Failure("daily-execution-cli", f"`review run` tracked edit failed: {error}"))
|
|
2571
|
+
elif payload.get("result") != "block":
|
|
2572
|
+
failures.append(Failure("daily-execution-cli", "`review run` must block when engine modifies tracked repo content"))
|
|
2573
|
+
else:
|
|
2574
|
+
require_review_run_payload(
|
|
2575
|
+
failures,
|
|
2576
|
+
category="daily-execution-cli",
|
|
2577
|
+
context="`review run` tracked edit",
|
|
2578
|
+
payload=payload,
|
|
2579
|
+
expected_result={"block"},
|
|
2580
|
+
)
|
|
2581
|
+
engine = payload.get("engine")
|
|
2582
|
+
if isinstance(engine, dict) and engine.get("failure_reason") != "repo_diff_detected":
|
|
2583
|
+
failures.append(Failure("daily-execution-cli", "`review run` must report `repo_diff_detected` when tracked files change"))
|
|
2584
|
+
|
|
2585
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-flow-") as tmp:
|
|
2586
|
+
lifecycle_target = Path(tmp) / "new-project"
|
|
2587
|
+
shutil.copytree(example_target, lifecycle_target)
|
|
2588
|
+
temp_root = lifecycle_target / ".loom/flow/tmp"
|
|
2589
|
+
temp_root.mkdir(parents=True, exist_ok=True)
|
|
2590
|
+
(temp_root / "sentinel.txt").write_text("temp\n", encoding="utf-8")
|
|
2591
|
+
|
|
2592
|
+
for operation in ("create", "cleanup", "retire"):
|
|
2593
|
+
payload, error = load_command_json(
|
|
2594
|
+
root,
|
|
2595
|
+
[
|
|
2596
|
+
"python3",
|
|
2597
|
+
"tools/loom_flow.py",
|
|
2598
|
+
"workspace",
|
|
2599
|
+
operation,
|
|
2600
|
+
"--target",
|
|
2601
|
+
str(lifecycle_target),
|
|
2602
|
+
"--item",
|
|
2603
|
+
"INIT-0001",
|
|
2604
|
+
],
|
|
2605
|
+
)
|
|
2606
|
+
if error:
|
|
2607
|
+
failures.append(Failure("daily-execution-cli", f"`workspace {operation}` failed: {error}"))
|
|
2608
|
+
continue
|
|
2609
|
+
if payload.get("result") != "pass":
|
|
2610
|
+
failures.append(
|
|
2611
|
+
Failure(
|
|
2612
|
+
"daily-execution-cli",
|
|
2613
|
+
f"`workspace {operation}` must pass on a clean temp copy, got `{payload.get('result')}`",
|
|
2614
|
+
)
|
|
2615
|
+
)
|
|
2616
|
+
|
|
2617
|
+
locate_payload, error = load_command_json(
|
|
2618
|
+
root,
|
|
2619
|
+
[
|
|
2620
|
+
"python3",
|
|
2621
|
+
"tools/loom_flow.py",
|
|
2622
|
+
"workspace",
|
|
2623
|
+
"locate",
|
|
2624
|
+
"--target",
|
|
2625
|
+
str(lifecycle_target),
|
|
2626
|
+
"--item",
|
|
2627
|
+
"INIT-0001",
|
|
2628
|
+
],
|
|
2629
|
+
)
|
|
2630
|
+
if error:
|
|
2631
|
+
failures.append(Failure("daily-execution-cli", f"`workspace locate` after retire failed: {error}"))
|
|
2632
|
+
elif (
|
|
2633
|
+
not isinstance(locate_payload.get("checkpoint"), dict)
|
|
2634
|
+
or locate_payload["checkpoint"].get("normalized") != "retired"
|
|
2635
|
+
):
|
|
2636
|
+
failures.append(Failure("daily-execution-cli", "`workspace retire` must leave the copied sample in `retired` state"))
|
|
2637
|
+
|
|
2638
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-authoring-") as tmp:
|
|
2639
|
+
authoring_target = Path(tmp) / "new-project"
|
|
2640
|
+
shutil.copytree(example_target, authoring_target)
|
|
2641
|
+
|
|
2642
|
+
payload, error = load_command_json(
|
|
2643
|
+
root,
|
|
2644
|
+
[
|
|
2645
|
+
"python3",
|
|
2646
|
+
"tools/loom_flow.py",
|
|
2647
|
+
"recovery",
|
|
2648
|
+
"writeback",
|
|
2649
|
+
"--target",
|
|
2650
|
+
str(authoring_target),
|
|
2651
|
+
"--item",
|
|
2652
|
+
"INIT-0001",
|
|
2653
|
+
"--current-stop",
|
|
2654
|
+
"Bootstrap review has started.",
|
|
2655
|
+
"--next-step",
|
|
2656
|
+
"Record the first formal review conclusion.",
|
|
2657
|
+
"--latest-validation-summary",
|
|
2658
|
+
"Bootstrap artifacts verified and ready for semantic review.",
|
|
2659
|
+
],
|
|
2660
|
+
)
|
|
2661
|
+
if error:
|
|
2662
|
+
failures.append(Failure("daily-execution-cli", f"`recovery writeback` failed: {error}"))
|
|
2663
|
+
elif payload.get("result") != "pass":
|
|
2664
|
+
failures.append(Failure("daily-execution-cli", "`recovery writeback` must pass on a clean temp copy"))
|
|
2665
|
+
|
|
2666
|
+
payload, error = load_command_json(
|
|
2667
|
+
root,
|
|
2668
|
+
[
|
|
2669
|
+
"python3",
|
|
2670
|
+
"tools/loom_flow.py",
|
|
2671
|
+
"work-item",
|
|
2672
|
+
"create",
|
|
2673
|
+
"--target",
|
|
2674
|
+
str(authoring_target),
|
|
2675
|
+
"--item",
|
|
2676
|
+
"NEXT-0001",
|
|
2677
|
+
"--goal",
|
|
2678
|
+
"Validate work item authoring",
|
|
2679
|
+
"--scope",
|
|
2680
|
+
"Limit changes to `.loom/` artifacts for this temp check",
|
|
2681
|
+
"--execution-path",
|
|
2682
|
+
"execution/support",
|
|
2683
|
+
"--workspace-entry",
|
|
2684
|
+
".",
|
|
2685
|
+
"--validation-entry",
|
|
2686
|
+
"python3 .loom/bin/loom_init.py verify --target .",
|
|
2687
|
+
"--closing-condition",
|
|
2688
|
+
"The authored work item can be activated and read mechanically.",
|
|
2689
|
+
"--init-recovery",
|
|
2690
|
+
"--activate",
|
|
2691
|
+
],
|
|
2692
|
+
)
|
|
2693
|
+
if error:
|
|
2694
|
+
failures.append(Failure("daily-execution-cli", f"`work-item create` failed: {error}"))
|
|
2695
|
+
elif payload.get("result") != "pass":
|
|
2696
|
+
failures.append(Failure("daily-execution-cli", "`work-item create --activate` must pass on a clean temp copy"))
|
|
2697
|
+
|
|
2698
|
+
payload, error = load_command_json(
|
|
2699
|
+
root,
|
|
2700
|
+
[
|
|
2701
|
+
"python3",
|
|
2702
|
+
"tools/loom_flow.py",
|
|
2703
|
+
"work-item",
|
|
2704
|
+
"update",
|
|
2705
|
+
"--target",
|
|
2706
|
+
str(authoring_target),
|
|
2707
|
+
"--item",
|
|
2708
|
+
"NEXT-0001",
|
|
2709
|
+
"--scope",
|
|
2710
|
+
"Keep the temp authoring check constrained to `.loom/` files",
|
|
2711
|
+
"--add-artifact",
|
|
2712
|
+
".loom/reviews/NEXT-0001.json",
|
|
2713
|
+
],
|
|
2714
|
+
)
|
|
2715
|
+
if error:
|
|
2716
|
+
failures.append(Failure("daily-execution-cli", f"`work-item update` failed: {error}"))
|
|
2717
|
+
elif payload.get("result") != "pass":
|
|
2718
|
+
failures.append(Failure("daily-execution-cli", "`work-item update` must pass on a clean temp copy"))
|
|
2719
|
+
|
|
2720
|
+
findings_path = authoring_target / ".loom" / "review-findings.json"
|
|
2721
|
+
findings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2722
|
+
findings_path.write_text(
|
|
2723
|
+
json.dumps(
|
|
2724
|
+
[
|
|
2725
|
+
{
|
|
2726
|
+
"id": "compat-block-1",
|
|
2727
|
+
"summary": "Formal review has not approved the item yet.",
|
|
2728
|
+
"severity": "block",
|
|
2729
|
+
"rebuttal": None,
|
|
2730
|
+
"disposition": {
|
|
2731
|
+
"status": "rejected",
|
|
2732
|
+
"summary": "The finding remains open until the missing approval signal is resolved."
|
|
2733
|
+
},
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
"id": "compat-warn-1",
|
|
2737
|
+
"summary": "Re-run formal review after the missing approval signal is resolved.",
|
|
2738
|
+
"severity": "warn",
|
|
2739
|
+
"rebuttal": "A follow-up review will be recorded after the blocking issue is resolved.",
|
|
2740
|
+
"disposition": {
|
|
2741
|
+
"status": "deferred",
|
|
2742
|
+
"summary": "This follow-up stays open until the next formal review."
|
|
2743
|
+
},
|
|
2744
|
+
},
|
|
2745
|
+
],
|
|
2746
|
+
ensure_ascii=False,
|
|
2747
|
+
indent=2,
|
|
2748
|
+
)
|
|
2749
|
+
+ "\n",
|
|
2750
|
+
encoding="utf-8",
|
|
2751
|
+
)
|
|
2752
|
+
|
|
2753
|
+
payload, error = load_command_json(
|
|
2754
|
+
root,
|
|
2755
|
+
[
|
|
2756
|
+
"python3",
|
|
2757
|
+
"tools/loom_flow.py",
|
|
2758
|
+
"review",
|
|
2759
|
+
"record",
|
|
2760
|
+
"--target",
|
|
2761
|
+
str(authoring_target),
|
|
2762
|
+
"--item",
|
|
2763
|
+
"NEXT-0001",
|
|
2764
|
+
"--decision",
|
|
2765
|
+
"fallback",
|
|
2766
|
+
"--kind",
|
|
2767
|
+
"code_review",
|
|
2768
|
+
"--summary",
|
|
2769
|
+
"Formal review has not approved the item yet.",
|
|
2770
|
+
"--reviewer",
|
|
2771
|
+
"loom-check",
|
|
2772
|
+
"--fallback-to",
|
|
2773
|
+
"admission",
|
|
2774
|
+
"--findings-file",
|
|
2775
|
+
".loom/review-findings.json",
|
|
2776
|
+
],
|
|
2777
|
+
)
|
|
2778
|
+
if error:
|
|
2779
|
+
failures.append(Failure("daily-execution-cli", f"`review record` failed: {error}"))
|
|
2780
|
+
elif payload.get("result") != "pass":
|
|
2781
|
+
failures.append(Failure("daily-execution-cli", "`review record` must pass for an authored fallback decision"))
|
|
2782
|
+
else:
|
|
2783
|
+
review = payload.get("review")
|
|
2784
|
+
if isinstance(review, dict):
|
|
2785
|
+
require_review_record_contract(
|
|
2786
|
+
failures,
|
|
2787
|
+
category="daily-execution-cli",
|
|
2788
|
+
context="`review record` review.record",
|
|
2789
|
+
payload=review.get("record"),
|
|
2790
|
+
)
|
|
2791
|
+
|
|
2792
|
+
if shutil.which("git") is not None:
|
|
2793
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-purity-") as tmp:
|
|
2794
|
+
dirty_target = Path(tmp) / "new-project"
|
|
2795
|
+
shutil.copytree(example_target, dirty_target)
|
|
2796
|
+
run_command(root, ["git", "init"], cwd=dirty_target)
|
|
2797
|
+
run_command(root, ["git", "config", "user.email", "loom-check@example.com"], cwd=dirty_target)
|
|
2798
|
+
run_command(root, ["git", "config", "user.name", "loom-check"], cwd=dirty_target)
|
|
2799
|
+
run_command(root, ["git", "add", "."], cwd=dirty_target)
|
|
2800
|
+
run_command(root, ["git", "commit", "-m", "baseline"], cwd=dirty_target)
|
|
2801
|
+
(dirty_target / "untriaged.txt").write_text("pending\n", encoding="utf-8")
|
|
2802
|
+
payload, error = load_command_json(
|
|
2803
|
+
root,
|
|
2804
|
+
[
|
|
2805
|
+
"python3",
|
|
2806
|
+
"tools/loom_flow.py",
|
|
2807
|
+
"purity-check",
|
|
2808
|
+
"--target",
|
|
2809
|
+
str(dirty_target),
|
|
2810
|
+
"--item",
|
|
2811
|
+
"INIT-0001",
|
|
2812
|
+
],
|
|
2813
|
+
)
|
|
2814
|
+
if error:
|
|
2815
|
+
failures.append(Failure("daily-execution-cli", f"`purity-check` negative sample failed: {error}"))
|
|
2816
|
+
elif payload.get("result") != "block":
|
|
2817
|
+
failures.append(
|
|
2818
|
+
Failure(
|
|
2819
|
+
"daily-execution-cli",
|
|
2820
|
+
f"`purity-check` negative sample must block, got `{payload.get('result')}`",
|
|
2821
|
+
)
|
|
2822
|
+
)
|
|
2823
|
+
state_payload, error = load_command_json(
|
|
2824
|
+
root,
|
|
2825
|
+
[
|
|
2826
|
+
"python3",
|
|
2827
|
+
"tools/loom_flow.py",
|
|
2828
|
+
"state-check",
|
|
2829
|
+
"--target",
|
|
2830
|
+
str(dirty_target),
|
|
2831
|
+
"--item",
|
|
2832
|
+
"INIT-0001",
|
|
2833
|
+
],
|
|
2834
|
+
)
|
|
2835
|
+
if error:
|
|
2836
|
+
failures.append(Failure("daily-execution-cli", f"`state-check` negative sample failed: {error}"))
|
|
2837
|
+
elif state_payload.get("result") != "block":
|
|
2838
|
+
failures.append(
|
|
2839
|
+
Failure(
|
|
2840
|
+
"daily-execution-cli",
|
|
2841
|
+
f"`state-check` negative sample must block, got `{state_payload.get('result')}`",
|
|
2842
|
+
)
|
|
2843
|
+
)
|
|
2844
|
+
|
|
2845
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-runtime-state-") as tmp:
|
|
2846
|
+
tmp_root = Path(tmp)
|
|
2847
|
+
install_root = tmp_root / "installed" / "skills"
|
|
2848
|
+
target_root = tmp_root / "target"
|
|
2849
|
+
bootstrap_target = tmp_root / "bootstrapped-target"
|
|
2850
|
+
shutil.copytree(root / "skills", install_root)
|
|
2851
|
+
target_root.mkdir(parents=True, exist_ok=True)
|
|
2852
|
+
shutil.copytree(example_target, bootstrap_target)
|
|
2853
|
+
|
|
2854
|
+
payload, error = load_command_json(
|
|
2855
|
+
root,
|
|
2856
|
+
["python3", str(install_root / "loom-init" / "scripts" / "loom-init.py"), "runtime-state", "--target", str(target_root)],
|
|
2857
|
+
)
|
|
2858
|
+
if error:
|
|
2859
|
+
failures.append(Failure("daily-execution-cli", f"`installed loom-init runtime-state` failed: {error}"))
|
|
2860
|
+
else:
|
|
2861
|
+
require_runtime_state_payload(
|
|
2862
|
+
failures,
|
|
2863
|
+
category="daily-execution-cli",
|
|
2864
|
+
context="`installed loom-init runtime-state`",
|
|
2865
|
+
payload=payload.get("runtime_state"),
|
|
2866
|
+
expected_scene="installed-runtime",
|
|
2867
|
+
expected_carrier="installed-skills-root",
|
|
2868
|
+
allowed_results={"pass"},
|
|
2869
|
+
)
|
|
2870
|
+
|
|
2871
|
+
payload, error = load_command_json(
|
|
2872
|
+
root,
|
|
2873
|
+
["python3", str(install_root / "shared" / "scripts" / "loom_flow.py"), "runtime-state", "--target", str(target_root)],
|
|
2874
|
+
env={"LOOM_RUNTIME_SCENE": "upgrade-rehearsal"},
|
|
2875
|
+
)
|
|
2876
|
+
if error:
|
|
2877
|
+
failures.append(Failure("daily-execution-cli", f"`installed loom-flow runtime-state -- rehearsal` failed: {error}"))
|
|
2878
|
+
else:
|
|
2879
|
+
require_runtime_state_payload(
|
|
2880
|
+
failures,
|
|
2881
|
+
category="daily-execution-cli",
|
|
2882
|
+
context="`installed loom-flow runtime-state -- rehearsal`",
|
|
2883
|
+
payload=payload.get("runtime_state"),
|
|
2884
|
+
expected_scene="upgrade-rehearsal",
|
|
2885
|
+
expected_carrier="installed-skills-root",
|
|
2886
|
+
allowed_results={"pass"},
|
|
2887
|
+
)
|
|
2888
|
+
|
|
2889
|
+
broken_install = tmp_root / "broken-install" / "skills"
|
|
2890
|
+
shutil.copytree(root / "skills", broken_install)
|
|
2891
|
+
(broken_install / "shared" / "scripts" / "loom_flow.py").unlink()
|
|
2892
|
+
payload, error = load_command_json(
|
|
2893
|
+
root,
|
|
2894
|
+
["python3", str(broken_install / "loom-init" / "scripts" / "loom-init.py"), "runtime-state", "--target", str(target_root)],
|
|
2895
|
+
)
|
|
2896
|
+
if error:
|
|
2897
|
+
failures.append(Failure("daily-execution-cli", f"`installed runtime-state` missing shared runtime failed unexpectedly: {error}"))
|
|
2898
|
+
elif payload.get("result") != "block":
|
|
2899
|
+
failures.append(Failure("daily-execution-cli", "`installed runtime-state` must block when shared runtime is missing"))
|
|
2900
|
+
|
|
2901
|
+
drift_install = tmp_root / "drift-install" / "skills"
|
|
2902
|
+
shutil.copytree(root / "skills", drift_install)
|
|
2903
|
+
(drift_install / "install-layout.json").unlink()
|
|
2904
|
+
payload, error = load_command_json(
|
|
2905
|
+
root,
|
|
2906
|
+
["python3", str(drift_install / "loom-init" / "scripts" / "loom-init.py"), "runtime-state", "--target", str(target_root)],
|
|
2907
|
+
)
|
|
2908
|
+
if error:
|
|
2909
|
+
failures.append(Failure("daily-execution-cli", f"`installed runtime-state` missing install-layout failed unexpectedly: {error}"))
|
|
2910
|
+
elif payload.get("result") != "block":
|
|
2911
|
+
failures.append(Failure("daily-execution-cli", "`installed runtime-state` must block when install-layout is missing"))
|
|
2912
|
+
|
|
2913
|
+
payload, error = load_command_json(
|
|
2914
|
+
root,
|
|
2915
|
+
["python3", str(install_root / "shared" / "scripts" / "loom_flow.py"), "runtime-state", "--target", str(target_root)],
|
|
2916
|
+
env={"LOOM_RUNTIME_SCENE": "repo-local-demo"},
|
|
2917
|
+
)
|
|
2918
|
+
if error:
|
|
2919
|
+
failures.append(Failure("daily-execution-cli", f"`installed runtime-state` scene conflict failed unexpectedly: {error}"))
|
|
2920
|
+
elif payload.get("result") != "block":
|
|
2921
|
+
failures.append(Failure("daily-execution-cli", "`installed runtime-state` must block on scene/carrier conflict"))
|
|
2922
|
+
|
|
2923
|
+
payload, error = load_command_json(
|
|
2924
|
+
root,
|
|
2925
|
+
["python3", ".loom/bin/loom_init.py", "runtime-state", "--target", "."],
|
|
2926
|
+
cwd=bootstrap_target,
|
|
2927
|
+
)
|
|
2928
|
+
if error:
|
|
2929
|
+
failures.append(Failure("daily-execution-cli", f"`bootstrapped loom-init runtime-state` failed: {error}"))
|
|
2930
|
+
else:
|
|
2931
|
+
require_runtime_state_payload(
|
|
2932
|
+
failures,
|
|
2933
|
+
category="daily-execution-cli",
|
|
2934
|
+
context="`bootstrapped loom-init runtime-state`",
|
|
2935
|
+
payload=payload.get("runtime_state"),
|
|
2936
|
+
expected_scene="installed-runtime",
|
|
2937
|
+
expected_carrier="bootstrapped-target-runtime",
|
|
2938
|
+
allowed_results={"pass"},
|
|
2939
|
+
)
|
|
2940
|
+
|
|
2941
|
+
broken_bootstrap = tmp_root / "broken-bootstrapped-target"
|
|
2942
|
+
shutil.copytree(example_target, broken_bootstrap)
|
|
2943
|
+
manifest_path = broken_bootstrap / ".loom" / "bootstrap" / "manifest.json"
|
|
2944
|
+
manifest = load_json_file(manifest_path)
|
|
2945
|
+
if isinstance(manifest, dict):
|
|
2946
|
+
artifacts = manifest.get("artifacts")
|
|
2947
|
+
if isinstance(artifacts, list):
|
|
2948
|
+
for artifact in artifacts:
|
|
2949
|
+
if isinstance(artifact, dict) and artifact.get("path") == ".loom/bin/runtime_state.py":
|
|
2950
|
+
artifact["source"] = "broken/source.py"
|
|
2951
|
+
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
2952
|
+
payload, error = load_command_json(
|
|
2953
|
+
root,
|
|
2954
|
+
["python3", ".loom/bin/loom_init.py", "runtime-state", "--target", "."],
|
|
2955
|
+
cwd=broken_bootstrap,
|
|
2956
|
+
)
|
|
2957
|
+
if error:
|
|
2958
|
+
failures.append(Failure("daily-execution-cli", f"`bootstrapped runtime-state` manifest drift failed unexpectedly: {error}"))
|
|
2959
|
+
elif payload.get("result") != "block":
|
|
2960
|
+
failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when the bootstrap manifest drifts"))
|
|
2961
|
+
|
|
2962
|
+
if shutil.which("git") is not None:
|
|
2963
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-installed-pre-merge-") as tmp:
|
|
2964
|
+
tmp_root = Path(tmp)
|
|
2965
|
+
install_root = tmp_root / "installed" / "skills"
|
|
2966
|
+
source_snapshot = tmp_root / "source-snapshot"
|
|
2967
|
+
positive_target = tmp_root / "positive-target"
|
|
2968
|
+
review_fallback_target = tmp_root / "review-fallback-target"
|
|
2969
|
+
fake_bin = tmp_root / "bin"
|
|
2970
|
+
fake_bin.mkdir(parents=True, exist_ok=True)
|
|
2971
|
+
write_fake_codex(fake_bin / "codex", mode="success")
|
|
2972
|
+
installed_review_env = prepend_path_env(fake_bin)
|
|
2973
|
+
shutil.copytree(root / "skills", install_root)
|
|
2974
|
+
shutil.copytree(root, source_snapshot, ignore=shutil.ignore_patterns(".git", ".DS_Store", "__pycache__"))
|
|
2975
|
+
|
|
2976
|
+
def prepare_target(target: Path) -> tuple[str | None, list[str]]:
|
|
2977
|
+
errors: list[str] = []
|
|
2978
|
+
shutil.copytree(source_snapshot, target)
|
|
2979
|
+
for args in (
|
|
2980
|
+
["git", "init"],
|
|
2981
|
+
["git", "config", "user.email", "loom-check@example.com"],
|
|
2982
|
+
["git", "config", "user.name", "loom-check"],
|
|
2983
|
+
):
|
|
2984
|
+
result = run_command(root, args, cwd=target)
|
|
2985
|
+
if result.returncode != 0:
|
|
2986
|
+
detail = result.stderr.strip() or result.stdout.strip() or "git setup failed"
|
|
2987
|
+
errors.append(detail)
|
|
2988
|
+
return None, errors
|
|
2989
|
+
|
|
2990
|
+
payload, error = load_command_json(
|
|
2991
|
+
root,
|
|
2992
|
+
[
|
|
2993
|
+
"python3",
|
|
2994
|
+
str(install_root / "loom-init" / "scripts" / "loom-init.py"),
|
|
2995
|
+
"bootstrap",
|
|
2996
|
+
"--target",
|
|
2997
|
+
str(target),
|
|
2998
|
+
"--write",
|
|
2999
|
+
"--force",
|
|
3000
|
+
"--verify",
|
|
3001
|
+
"--install-pr-template",
|
|
3002
|
+
],
|
|
3003
|
+
)
|
|
3004
|
+
if error:
|
|
3005
|
+
errors.append(error)
|
|
3006
|
+
return None, errors
|
|
3007
|
+
verification = payload.get("verification")
|
|
3008
|
+
if not isinstance(verification, dict) or verification.get("ok") is not True:
|
|
3009
|
+
errors.append("installed bootstrap must verify successfully before the pre-merge chain starts")
|
|
3010
|
+
return None, errors
|
|
3011
|
+
|
|
3012
|
+
git_add = run_command(root, ["git", "add", "."], cwd=target)
|
|
3013
|
+
if git_add.returncode != 0:
|
|
3014
|
+
detail = git_add.stderr.strip() or git_add.stdout.strip() or "git add failed"
|
|
3015
|
+
errors.append(detail)
|
|
3016
|
+
return None, errors
|
|
3017
|
+
git_commit = run_command(root, ["git", "commit", "-m", "bootstrap baseline for #209"], cwd=target)
|
|
3018
|
+
if git_commit.returncode != 0:
|
|
3019
|
+
detail = git_commit.stderr.strip() or git_commit.stdout.strip() or "git commit failed"
|
|
3020
|
+
errors.append(detail)
|
|
3021
|
+
return None, errors
|
|
3022
|
+
|
|
3023
|
+
resume_payload, resume_error = load_command_json(
|
|
3024
|
+
root,
|
|
3025
|
+
[
|
|
3026
|
+
"python3",
|
|
3027
|
+
str(install_root / "loom-resume" / "scripts" / "loom-resume.py"),
|
|
3028
|
+
"flow",
|
|
3029
|
+
"resume",
|
|
3030
|
+
"--target",
|
|
3031
|
+
str(target),
|
|
3032
|
+
"--item",
|
|
3033
|
+
"INIT-0001",
|
|
3034
|
+
],
|
|
3035
|
+
)
|
|
3036
|
+
if resume_error:
|
|
3037
|
+
errors.append(resume_error)
|
|
3038
|
+
return None, errors
|
|
3039
|
+
recovery = resume_payload.get("recovery")
|
|
3040
|
+
if not isinstance(recovery, dict):
|
|
3041
|
+
errors.append("resume payload must include `recovery`")
|
|
3042
|
+
return None, errors
|
|
3043
|
+
summary = recovery.get("latest_validation_summary")
|
|
3044
|
+
if not isinstance(summary, str) or not summary:
|
|
3045
|
+
errors.append("resume payload must expose a non-empty `latest_validation_summary`")
|
|
3046
|
+
return None, errors
|
|
3047
|
+
return summary, errors
|
|
3048
|
+
|
|
3049
|
+
positive_summary, positive_setup_errors = prepare_target(positive_target)
|
|
3050
|
+
if positive_setup_errors:
|
|
3051
|
+
failures.append(
|
|
3052
|
+
Failure(
|
|
3053
|
+
"daily-execution-cli",
|
|
3054
|
+
f"`installed pre-merge chain` setup failed: {'; '.join(positive_setup_errors)}",
|
|
3055
|
+
)
|
|
3056
|
+
)
|
|
3057
|
+
else:
|
|
3058
|
+
task_signals = {
|
|
3059
|
+
"resume": "请接手当前事项并恢复上下文后继续推进",
|
|
3060
|
+
"pre-review": "请在进入 review 前做统一检查",
|
|
3061
|
+
"review": "请对当前事项做正式 review 并给出审查结论",
|
|
3062
|
+
"merge-ready": "请做 merge-ready 最终放行前预检并确认是否可以合并",
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
payload, error = load_command_json(
|
|
3066
|
+
root,
|
|
3067
|
+
[
|
|
3068
|
+
"python3",
|
|
3069
|
+
str(install_root / "loom-init" / "scripts" / "loom-init.py"),
|
|
3070
|
+
"route",
|
|
3071
|
+
"--target",
|
|
3072
|
+
str(positive_target),
|
|
3073
|
+
"--task",
|
|
3074
|
+
task_signals["resume"],
|
|
3075
|
+
],
|
|
3076
|
+
)
|
|
3077
|
+
if error:
|
|
3078
|
+
failures.append(Failure("daily-execution-cli", f"`installed route resume` failed: {error}"))
|
|
3079
|
+
else:
|
|
3080
|
+
require_route_payload(
|
|
3081
|
+
failures,
|
|
3082
|
+
category="daily-execution-cli",
|
|
3083
|
+
context="`installed route resume`",
|
|
3084
|
+
payload=payload,
|
|
3085
|
+
expected_skill="loom-resume",
|
|
3086
|
+
expected_mode="implicit",
|
|
3087
|
+
expected_runtime_scene="installed-runtime",
|
|
3088
|
+
expected_runtime_carrier="installed-skills-root",
|
|
3089
|
+
)
|
|
3090
|
+
|
|
3091
|
+
resume_payload, error = load_command_json(
|
|
3092
|
+
root,
|
|
3093
|
+
[
|
|
3094
|
+
"python3",
|
|
3095
|
+
str(install_root / "loom-resume" / "scripts" / "loom-resume.py"),
|
|
3096
|
+
"flow",
|
|
3097
|
+
"resume",
|
|
3098
|
+
"--target",
|
|
3099
|
+
str(positive_target),
|
|
3100
|
+
"--item",
|
|
3101
|
+
"INIT-0001",
|
|
3102
|
+
],
|
|
3103
|
+
)
|
|
3104
|
+
if error:
|
|
3105
|
+
failures.append(Failure("daily-execution-cli", f"`installed flow resume` failed: {error}"))
|
|
3106
|
+
elif resume_payload.get("result") != "pass":
|
|
3107
|
+
failures.append(Failure("daily-execution-cli", "`installed flow resume` must pass for the positive chain"))
|
|
3108
|
+
else:
|
|
3109
|
+
require_runtime_state_payload(
|
|
3110
|
+
failures,
|
|
3111
|
+
category="daily-execution-cli",
|
|
3112
|
+
context="`installed flow resume`",
|
|
3113
|
+
payload=resume_payload.get("runtime_state"),
|
|
3114
|
+
expected_scene="installed-runtime",
|
|
3115
|
+
expected_carrier="installed-skills-root",
|
|
3116
|
+
allowed_results={"pass"},
|
|
3117
|
+
)
|
|
3118
|
+
|
|
3119
|
+
payload, error = load_command_json(
|
|
3120
|
+
root,
|
|
3121
|
+
[
|
|
3122
|
+
"python3",
|
|
3123
|
+
str(install_root / "loom-init" / "scripts" / "loom-init.py"),
|
|
3124
|
+
"route",
|
|
3125
|
+
"--target",
|
|
3126
|
+
str(positive_target),
|
|
3127
|
+
"--task",
|
|
3128
|
+
task_signals["pre-review"],
|
|
3129
|
+
],
|
|
3130
|
+
)
|
|
3131
|
+
if error:
|
|
3132
|
+
failures.append(Failure("daily-execution-cli", f"`installed route pre-review` failed: {error}"))
|
|
3133
|
+
else:
|
|
3134
|
+
require_route_payload(
|
|
3135
|
+
failures,
|
|
3136
|
+
category="daily-execution-cli",
|
|
3137
|
+
context="`installed route pre-review`",
|
|
3138
|
+
payload=payload,
|
|
3139
|
+
expected_skill="loom-pre-review",
|
|
3140
|
+
expected_mode="implicit",
|
|
3141
|
+
expected_runtime_scene="installed-runtime",
|
|
3142
|
+
expected_runtime_carrier="installed-skills-root",
|
|
3143
|
+
)
|
|
3144
|
+
|
|
3145
|
+
payload, error = load_command_json(
|
|
3146
|
+
root,
|
|
3147
|
+
[
|
|
3148
|
+
"python3",
|
|
3149
|
+
str(install_root / "loom-pre-review" / "scripts" / "loom-pre-review.py"),
|
|
3150
|
+
"flow",
|
|
3151
|
+
"pre-review",
|
|
3152
|
+
"--target",
|
|
3153
|
+
str(positive_target),
|
|
3154
|
+
"--item",
|
|
3155
|
+
"INIT-0001",
|
|
3156
|
+
],
|
|
3157
|
+
)
|
|
3158
|
+
if error:
|
|
3159
|
+
failures.append(Failure("daily-execution-cli", f"`installed flow pre-review` failed: {error}"))
|
|
3160
|
+
elif payload.get("result") != "pass":
|
|
3161
|
+
failures.append(Failure("daily-execution-cli", "`installed flow pre-review` must pass for the positive chain"))
|
|
3162
|
+
|
|
3163
|
+
payload, error = load_command_json(
|
|
3164
|
+
root,
|
|
3165
|
+
[
|
|
3166
|
+
"python3",
|
|
3167
|
+
str(install_root / "loom-init" / "scripts" / "loom-init.py"),
|
|
3168
|
+
"route",
|
|
3169
|
+
"--target",
|
|
3170
|
+
str(positive_target),
|
|
3171
|
+
"--task",
|
|
3172
|
+
task_signals["review"],
|
|
3173
|
+
],
|
|
3174
|
+
)
|
|
3175
|
+
if error:
|
|
3176
|
+
failures.append(Failure("daily-execution-cli", f"`installed route review` failed: {error}"))
|
|
3177
|
+
else:
|
|
3178
|
+
require_route_payload(
|
|
3179
|
+
failures,
|
|
3180
|
+
category="daily-execution-cli",
|
|
3181
|
+
context="`installed route review`",
|
|
3182
|
+
payload=payload,
|
|
3183
|
+
expected_skill="loom-review",
|
|
3184
|
+
expected_mode="implicit",
|
|
3185
|
+
expected_runtime_scene="installed-runtime",
|
|
3186
|
+
expected_runtime_carrier="installed-skills-root",
|
|
3187
|
+
)
|
|
3188
|
+
|
|
3189
|
+
review_flow_payload, error = load_command_json(
|
|
3190
|
+
root,
|
|
3191
|
+
[
|
|
3192
|
+
"python3",
|
|
3193
|
+
str(install_root / "loom-review" / "scripts" / "loom-review.py"),
|
|
3194
|
+
"flow",
|
|
3195
|
+
"review",
|
|
3196
|
+
"--target",
|
|
3197
|
+
str(positive_target),
|
|
3198
|
+
"--item",
|
|
3199
|
+
"INIT-0001",
|
|
3200
|
+
],
|
|
3201
|
+
)
|
|
3202
|
+
if error:
|
|
3203
|
+
failures.append(Failure("daily-execution-cli", f"`installed flow review` failed: {error}"))
|
|
3204
|
+
elif review_flow_payload.get("result") != "pass":
|
|
3205
|
+
failures.append(Failure("daily-execution-cli", "`installed flow review` must pass for the positive chain"))
|
|
3206
|
+
else:
|
|
3207
|
+
review = review_flow_payload.get("review")
|
|
3208
|
+
if isinstance(review, dict):
|
|
3209
|
+
require_review_record_contract(
|
|
3210
|
+
failures,
|
|
3211
|
+
category="daily-execution-cli",
|
|
3212
|
+
context="`installed flow review` review.record",
|
|
3213
|
+
payload=review.get("record"),
|
|
3214
|
+
)
|
|
3215
|
+
|
|
3216
|
+
review_run_payload: dict[str, object] | None = None
|
|
3217
|
+
review_record_input: dict[str, object] | None = None
|
|
3218
|
+
review_run_payload, error = load_command_json(
|
|
3219
|
+
root,
|
|
3220
|
+
[
|
|
3221
|
+
"python3",
|
|
3222
|
+
str(install_root / "shared" / "scripts" / "loom_flow.py"),
|
|
3223
|
+
"review",
|
|
3224
|
+
"run",
|
|
3225
|
+
"--target",
|
|
3226
|
+
str(positive_target),
|
|
3227
|
+
"--item",
|
|
3228
|
+
"INIT-0001",
|
|
3229
|
+
],
|
|
3230
|
+
env=installed_review_env,
|
|
3231
|
+
)
|
|
3232
|
+
if error:
|
|
3233
|
+
failures.append(Failure("daily-execution-cli", f"`installed review run` failed: {error}"))
|
|
3234
|
+
elif review_run_payload.get("result") != "pass":
|
|
3235
|
+
failures.append(Failure("daily-execution-cli", "`installed review run` must pass for the positive chain"))
|
|
3236
|
+
else:
|
|
3237
|
+
require_review_run_payload(
|
|
3238
|
+
failures,
|
|
3239
|
+
category="daily-execution-cli",
|
|
3240
|
+
context="`installed review run`",
|
|
3241
|
+
payload=review_run_payload,
|
|
3242
|
+
expected_result={"pass"},
|
|
3243
|
+
)
|
|
3244
|
+
review_record_input = review_run_payload.get("review_record_input") if isinstance(review_run_payload, dict) else None
|
|
3245
|
+
review_record_payload, error = load_command_json(
|
|
3246
|
+
root,
|
|
3247
|
+
[
|
|
3248
|
+
"python3",
|
|
3249
|
+
str(install_root / "loom-review" / "scripts" / "loom-review.py"),
|
|
3250
|
+
"review",
|
|
3251
|
+
"record",
|
|
3252
|
+
"--target",
|
|
3253
|
+
str(positive_target),
|
|
3254
|
+
"--item",
|
|
3255
|
+
"INIT-0001",
|
|
3256
|
+
"--decision",
|
|
3257
|
+
str(review_record_input.get("decision", "allow")) if isinstance(review_record_input, dict) else "allow",
|
|
3258
|
+
"--kind",
|
|
3259
|
+
str(review_record_input.get("kind", "code_review")) if isinstance(review_record_input, dict) else "code_review",
|
|
3260
|
+
"--summary",
|
|
3261
|
+
str(review_record_input.get("summary", "Installed pre-merge chain is ready for merge checkpoint consumption."))
|
|
3262
|
+
if isinstance(review_record_input, dict)
|
|
3263
|
+
else "Installed pre-merge chain is ready for merge checkpoint consumption.",
|
|
3264
|
+
"--reviewer",
|
|
3265
|
+
str(review_record_input.get("reviewer", "loom-check")) if isinstance(review_record_input, dict) else "loom-check",
|
|
3266
|
+
"--findings-file",
|
|
3267
|
+
str(review_record_input.get("findings_file", ".loom/review-findings.json")) if isinstance(review_record_input, dict) else ".loom/review-findings.json",
|
|
3268
|
+
"--engine-adapter",
|
|
3269
|
+
str(review_record_input.get("engine_adapter", "loom/default-codex")) if isinstance(review_record_input, dict) else "loom/default-codex",
|
|
3270
|
+
"--engine-evidence",
|
|
3271
|
+
str(review_record_input.get("engine_evidence", ".loom/runtime/review/INIT-0001/unknown-head/engine-result.json"))
|
|
3272
|
+
if isinstance(review_record_input, dict)
|
|
3273
|
+
else ".loom/runtime/review/INIT-0001/unknown-head/engine-result.json",
|
|
3274
|
+
"--normalized-findings",
|
|
3275
|
+
str(review_record_input.get("normalized_findings", ".loom/review-findings.json"))
|
|
3276
|
+
if isinstance(review_record_input, dict)
|
|
3277
|
+
else ".loom/review-findings.json",
|
|
3278
|
+
],
|
|
3279
|
+
)
|
|
3280
|
+
if error:
|
|
3281
|
+
failures.append(Failure("daily-execution-cli", f"`installed review record allow` failed: {error}"))
|
|
3282
|
+
elif review_record_payload.get("result") != "pass":
|
|
3283
|
+
failures.append(Failure("daily-execution-cli", "`installed review record allow` must pass"))
|
|
3284
|
+
else:
|
|
3285
|
+
review = review_record_payload.get("review")
|
|
3286
|
+
if isinstance(review, dict):
|
|
3287
|
+
require_review_record_contract(
|
|
3288
|
+
failures,
|
|
3289
|
+
category="daily-execution-cli",
|
|
3290
|
+
context="`installed review record allow` review.record",
|
|
3291
|
+
payload=review.get("record"),
|
|
3292
|
+
)
|
|
3293
|
+
|
|
3294
|
+
payload, error = load_command_json(
|
|
3295
|
+
root,
|
|
3296
|
+
[
|
|
3297
|
+
"python3",
|
|
3298
|
+
str(install_root / "shared" / "scripts" / "loom_flow.py"),
|
|
3299
|
+
"recovery",
|
|
3300
|
+
"writeback",
|
|
3301
|
+
"--target",
|
|
3302
|
+
str(positive_target),
|
|
3303
|
+
"--item",
|
|
3304
|
+
"INIT-0001",
|
|
3305
|
+
"--current-checkpoint",
|
|
3306
|
+
"merge checkpoint",
|
|
3307
|
+
"--current-stop",
|
|
3308
|
+
"Installed review completed and merge-ready validation is next.",
|
|
3309
|
+
"--next-step",
|
|
3310
|
+
"Run merge-ready and checkpoint merge from installed skills.",
|
|
3311
|
+
"--latest-validation-summary",
|
|
3312
|
+
positive_summary,
|
|
3313
|
+
],
|
|
3314
|
+
)
|
|
3315
|
+
if error:
|
|
3316
|
+
failures.append(Failure("daily-execution-cli", f"`installed recovery writeback for merge` failed: {error}"))
|
|
3317
|
+
elif payload.get("result") != "pass":
|
|
3318
|
+
failures.append(Failure("daily-execution-cli", "`installed recovery writeback for merge` must pass"))
|
|
3319
|
+
|
|
3320
|
+
git_add = run_command(
|
|
3321
|
+
root,
|
|
3322
|
+
[
|
|
3323
|
+
"git",
|
|
3324
|
+
"add",
|
|
3325
|
+
"-f",
|
|
3326
|
+
".loom/progress/INIT-0001.md",
|
|
3327
|
+
".loom/status/current.md",
|
|
3328
|
+
".loom/reviews/INIT-0001.json",
|
|
3329
|
+
],
|
|
3330
|
+
cwd=positive_target,
|
|
3331
|
+
)
|
|
3332
|
+
if git_add.returncode != 0:
|
|
3333
|
+
detail = git_add.stderr.strip() or git_add.stdout.strip() or "git add failed"
|
|
3334
|
+
failures.append(Failure("daily-execution-cli", f"`installed pre-merge carrier commit` add failed: {detail}"))
|
|
3335
|
+
else:
|
|
3336
|
+
git_commit = run_command(
|
|
3337
|
+
root,
|
|
3338
|
+
["git", "commit", "-m", "author installed pre-merge carriers for #209"],
|
|
3339
|
+
cwd=positive_target,
|
|
3340
|
+
)
|
|
3341
|
+
if git_commit.returncode != 0:
|
|
3342
|
+
detail = git_commit.stderr.strip() or git_commit.stdout.strip() or "git commit failed"
|
|
3343
|
+
failures.append(Failure("daily-execution-cli", f"`installed pre-merge carrier commit` failed: {detail}"))
|
|
3344
|
+
|
|
3345
|
+
payload, error = load_command_json(
|
|
3346
|
+
root,
|
|
3347
|
+
[
|
|
3348
|
+
"python3",
|
|
3349
|
+
str(install_root / "loom-init" / "scripts" / "loom-init.py"),
|
|
3350
|
+
"route",
|
|
3351
|
+
"--target",
|
|
3352
|
+
str(positive_target),
|
|
3353
|
+
"--task",
|
|
3354
|
+
task_signals["merge-ready"],
|
|
3355
|
+
],
|
|
3356
|
+
)
|
|
3357
|
+
if error:
|
|
3358
|
+
failures.append(Failure("daily-execution-cli", f"`installed route merge-ready` failed: {error}"))
|
|
3359
|
+
else:
|
|
3360
|
+
require_route_payload(
|
|
3361
|
+
failures,
|
|
3362
|
+
category="daily-execution-cli",
|
|
3363
|
+
context="`installed route merge-ready`",
|
|
3364
|
+
payload=payload,
|
|
3365
|
+
expected_skill="loom-merge-ready",
|
|
3366
|
+
expected_mode="implicit",
|
|
3367
|
+
expected_runtime_scene="installed-runtime",
|
|
3368
|
+
expected_runtime_carrier="installed-skills-root",
|
|
3369
|
+
)
|
|
3370
|
+
|
|
3371
|
+
merge_ready_payload, error = load_command_json(
|
|
3372
|
+
root,
|
|
3373
|
+
[
|
|
3374
|
+
"python3",
|
|
3375
|
+
str(install_root / "loom-merge-ready" / "scripts" / "loom-merge-ready.py"),
|
|
3376
|
+
"flow",
|
|
3377
|
+
"merge-ready",
|
|
3378
|
+
"--target",
|
|
3379
|
+
str(positive_target),
|
|
3380
|
+
"--item",
|
|
3381
|
+
"INIT-0001",
|
|
3382
|
+
],
|
|
3383
|
+
)
|
|
3384
|
+
if error:
|
|
3385
|
+
failures.append(Failure("daily-execution-cli", f"`installed flow merge-ready` failed: {error}"))
|
|
3386
|
+
elif merge_ready_payload.get("result") != "pass":
|
|
3387
|
+
failures.append(Failure("daily-execution-cli", "`installed flow merge-ready` must pass for the positive chain"))
|
|
3388
|
+
else:
|
|
3389
|
+
merge_checkpoint = merge_ready_payload.get("merge_checkpoint")
|
|
3390
|
+
if not isinstance(merge_checkpoint, dict) or merge_checkpoint.get("result") != "pass":
|
|
3391
|
+
failures.append(Failure("daily-execution-cli", "`installed flow merge-ready` must expose `merge_checkpoint.result = pass`"))
|
|
3392
|
+
|
|
3393
|
+
checkpoint_merge_payload, error = load_command_json(
|
|
3394
|
+
root,
|
|
3395
|
+
[
|
|
3396
|
+
"python3",
|
|
3397
|
+
str(install_root / "shared" / "scripts" / "loom_flow.py"),
|
|
3398
|
+
"checkpoint",
|
|
3399
|
+
"merge",
|
|
3400
|
+
"--target",
|
|
3401
|
+
str(positive_target),
|
|
3402
|
+
"--item",
|
|
3403
|
+
"INIT-0001",
|
|
3404
|
+
],
|
|
3405
|
+
)
|
|
3406
|
+
if error:
|
|
3407
|
+
failures.append(Failure("daily-execution-cli", f"`installed checkpoint merge` failed: {error}"))
|
|
3408
|
+
elif checkpoint_merge_payload.get("result") != "pass":
|
|
3409
|
+
failures.append(Failure("daily-execution-cli", "`installed checkpoint merge` must pass for the positive chain"))
|
|
3410
|
+
|
|
3411
|
+
broken_install = tmp_root / "broken-install" / "skills"
|
|
3412
|
+
shutil.copytree(root / "skills", broken_install)
|
|
3413
|
+
(broken_install / "install-layout.json").unlink()
|
|
3414
|
+
payload, error = load_command_json(
|
|
3415
|
+
root,
|
|
3416
|
+
[
|
|
3417
|
+
"python3",
|
|
3418
|
+
str(broken_install / "loom-init" / "scripts" / "loom-init.py"),
|
|
3419
|
+
"route",
|
|
3420
|
+
"--target",
|
|
3421
|
+
str(positive_target),
|
|
3422
|
+
"--task",
|
|
3423
|
+
task_signals["resume"],
|
|
3424
|
+
],
|
|
3425
|
+
)
|
|
3426
|
+
if error:
|
|
3427
|
+
failures.append(Failure("daily-execution-cli", f"`installed route` missing install-layout failed unexpectedly: {error}"))
|
|
3428
|
+
else:
|
|
3429
|
+
require_route_payload(
|
|
3430
|
+
failures,
|
|
3431
|
+
category="daily-execution-cli",
|
|
3432
|
+
context="`installed route` missing install-layout",
|
|
3433
|
+
payload=payload,
|
|
3434
|
+
expected_skill="loom-init",
|
|
3435
|
+
expected_mode="fallback",
|
|
3436
|
+
expected_runtime_scene="installed-runtime",
|
|
3437
|
+
expected_runtime_carrier="installed-skills-root",
|
|
3438
|
+
allowed_results={"block"},
|
|
3439
|
+
)
|
|
3440
|
+
|
|
3441
|
+
payload, error = load_command_json(
|
|
3442
|
+
root,
|
|
3443
|
+
[
|
|
3444
|
+
"python3",
|
|
3445
|
+
str(broken_install / "loom-pre-review" / "scripts" / "loom-pre-review.py"),
|
|
3446
|
+
"flow",
|
|
3447
|
+
"pre-review",
|
|
3448
|
+
"--target",
|
|
3449
|
+
str(positive_target),
|
|
3450
|
+
"--item",
|
|
3451
|
+
"INIT-0001",
|
|
3452
|
+
],
|
|
3453
|
+
)
|
|
3454
|
+
if error:
|
|
3455
|
+
failures.append(Failure("daily-execution-cli", f"`installed flow pre-review` missing install-layout failed unexpectedly: {error}"))
|
|
3456
|
+
elif payload.get("result") != "block":
|
|
3457
|
+
failures.append(Failure("daily-execution-cli", "`installed flow pre-review` must block when install-layout is missing"))
|
|
3458
|
+
else:
|
|
3459
|
+
require_runtime_state_payload(
|
|
3460
|
+
failures,
|
|
3461
|
+
category="daily-execution-cli",
|
|
3462
|
+
context="`installed flow pre-review` missing install-layout",
|
|
3463
|
+
payload=payload.get("runtime_state"),
|
|
3464
|
+
expected_scene="installed-runtime",
|
|
3465
|
+
expected_carrier="installed-skills-root",
|
|
3466
|
+
allowed_results={"block"},
|
|
3467
|
+
)
|
|
3468
|
+
|
|
3469
|
+
review_fallback_summary, review_fallback_errors = prepare_target(review_fallback_target)
|
|
3470
|
+
if review_fallback_errors:
|
|
3471
|
+
failures.append(
|
|
3472
|
+
Failure(
|
|
3473
|
+
"daily-execution-cli",
|
|
3474
|
+
f"`installed review baseline fallback` setup failed: {'; '.join(review_fallback_errors)}",
|
|
3475
|
+
)
|
|
3476
|
+
)
|
|
3477
|
+
else:
|
|
3478
|
+
payload, error = load_command_json(
|
|
3479
|
+
root,
|
|
3480
|
+
[
|
|
3481
|
+
"python3",
|
|
3482
|
+
str(install_root / "shared" / "scripts" / "loom_flow.py"),
|
|
3483
|
+
"recovery",
|
|
3484
|
+
"writeback",
|
|
3485
|
+
"--target",
|
|
3486
|
+
str(review_fallback_target),
|
|
3487
|
+
"--item",
|
|
3488
|
+
"INIT-0001",
|
|
3489
|
+
"--current-checkpoint",
|
|
3490
|
+
"admission checkpoint",
|
|
3491
|
+
"--current-stop",
|
|
3492
|
+
"Installed review baseline is still at admission.",
|
|
3493
|
+
"--next-step",
|
|
3494
|
+
"Promote the target repo to build checkpoint before review.",
|
|
3495
|
+
"--latest-validation-summary",
|
|
3496
|
+
review_fallback_summary,
|
|
3497
|
+
],
|
|
3498
|
+
)
|
|
3499
|
+
if error:
|
|
3500
|
+
failures.append(Failure("daily-execution-cli", f"`installed recovery writeback for admission fallback` failed: {error}"))
|
|
3501
|
+
elif payload.get("result") != "pass":
|
|
3502
|
+
failures.append(Failure("daily-execution-cli", "`installed recovery writeback for admission fallback` must pass"))
|
|
3503
|
+
|
|
3504
|
+
git_add = run_command(
|
|
3505
|
+
root,
|
|
3506
|
+
["git", "add", "-f", ".loom/progress/INIT-0001.md", ".loom/status/current.md"],
|
|
3507
|
+
cwd=review_fallback_target,
|
|
3508
|
+
)
|
|
3509
|
+
if git_add.returncode != 0:
|
|
3510
|
+
detail = git_add.stderr.strip() or git_add.stdout.strip() or "git add failed"
|
|
3511
|
+
failures.append(Failure("daily-execution-cli", f"`installed review baseline fallback` add failed: {detail}"))
|
|
3512
|
+
else:
|
|
3513
|
+
git_commit = run_command(
|
|
3514
|
+
root,
|
|
3515
|
+
["git", "commit", "-m", "lower checkpoint to admission for #209 fallback"],
|
|
3516
|
+
cwd=review_fallback_target,
|
|
3517
|
+
)
|
|
3518
|
+
if git_commit.returncode != 0:
|
|
3519
|
+
detail = git_commit.stderr.strip() or git_commit.stdout.strip() or "git commit failed"
|
|
3520
|
+
failures.append(Failure("daily-execution-cli", f"`installed review baseline fallback` commit failed: {detail}"))
|
|
3521
|
+
|
|
3522
|
+
payload, error = load_command_json(
|
|
3523
|
+
root,
|
|
3524
|
+
[
|
|
3525
|
+
"python3",
|
|
3526
|
+
str(install_root / "loom-review" / "scripts" / "loom-review.py"),
|
|
3527
|
+
"flow",
|
|
3528
|
+
"review",
|
|
3529
|
+
"--target",
|
|
3530
|
+
str(review_fallback_target),
|
|
3531
|
+
"--item",
|
|
3532
|
+
"INIT-0001",
|
|
3533
|
+
],
|
|
3534
|
+
)
|
|
3535
|
+
if error:
|
|
3536
|
+
failures.append(Failure("daily-execution-cli", f"`installed flow review` admission fallback failed: {error}"))
|
|
3537
|
+
elif payload.get("result") != "fallback" or payload.get("fallback_to") != "admission":
|
|
3538
|
+
failures.append(Failure("daily-execution-cli", "`installed flow review` must fall back to `admission` when build checkpoint is missing"))
|
|
3539
|
+
|
|
3540
|
+
payload, error = load_command_json(
|
|
3541
|
+
root,
|
|
3542
|
+
[
|
|
3543
|
+
"python3",
|
|
3544
|
+
str(install_root / "loom-merge-ready" / "scripts" / "loom-merge-ready.py"),
|
|
3545
|
+
"flow",
|
|
3546
|
+
"merge-ready",
|
|
3547
|
+
"--target",
|
|
3548
|
+
str(review_fallback_target),
|
|
3549
|
+
"--item",
|
|
3550
|
+
"INIT-0001",
|
|
3551
|
+
],
|
|
3552
|
+
)
|
|
3553
|
+
if error:
|
|
3554
|
+
failures.append(Failure("daily-execution-cli", f"`installed flow merge-ready` review-baseline fallback failed: {error}"))
|
|
3555
|
+
elif payload.get("result") not in {"fallback", "block"}:
|
|
3556
|
+
failures.append(Failure("daily-execution-cli", "`installed flow merge-ready` must fail closed when review baseline is missing"))
|
|
3557
|
+
|
|
3558
|
+
readme_path = positive_target / "README.md"
|
|
3559
|
+
readme_path.write_text(readme_path.read_text(encoding="utf-8") + "\n# review-head-drift\n", encoding="utf-8")
|
|
3560
|
+
git_add = run_command(root, ["git", "add", "README.md"], cwd=positive_target)
|
|
3561
|
+
if git_add.returncode != 0:
|
|
3562
|
+
detail = git_add.stderr.strip() or git_add.stdout.strip() or "git add failed"
|
|
3563
|
+
failures.append(Failure("daily-execution-cli", f"`installed merge-ready drift` add failed: {detail}"))
|
|
3564
|
+
else:
|
|
3565
|
+
git_commit = run_command(
|
|
3566
|
+
root,
|
|
3567
|
+
["git", "commit", "-m", "introduce non-carrier drift after review for #209"],
|
|
3568
|
+
cwd=positive_target,
|
|
3569
|
+
)
|
|
3570
|
+
if git_commit.returncode != 0:
|
|
3571
|
+
detail = git_commit.stderr.strip() or git_commit.stdout.strip() or "git commit failed"
|
|
3572
|
+
failures.append(Failure("daily-execution-cli", f"`installed merge-ready drift` commit failed: {detail}"))
|
|
3573
|
+
|
|
3574
|
+
payload, error = load_command_json(
|
|
3575
|
+
root,
|
|
3576
|
+
[
|
|
3577
|
+
"python3",
|
|
3578
|
+
str(install_root / "shared" / "scripts" / "loom_flow.py"),
|
|
3579
|
+
"checkpoint",
|
|
3580
|
+
"merge",
|
|
3581
|
+
"--target",
|
|
3582
|
+
str(positive_target),
|
|
3583
|
+
"--item",
|
|
3584
|
+
"INIT-0001",
|
|
3585
|
+
],
|
|
3586
|
+
)
|
|
3587
|
+
if error:
|
|
3588
|
+
failures.append(Failure("daily-execution-cli", f"`installed checkpoint merge` drift negative failed: {error}"))
|
|
3589
|
+
elif payload.get("result") != "block":
|
|
3590
|
+
failures.append(Failure("daily-execution-cli", "`installed checkpoint merge` must block when HEAD drifts beyond Loom carriers"))
|
|
3591
|
+
|
|
3592
|
+
gh_auth_ready = shutil.which("gh") is not None and run_command(root, ["gh", "auth", "status"]).returncode == 0
|
|
3593
|
+
if gh_auth_ready:
|
|
3594
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-installed-post-merge-") as tmp:
|
|
3595
|
+
tmp_root = Path(tmp)
|
|
3596
|
+
install_root = tmp_root / "installed" / "skills"
|
|
3597
|
+
retire_target = tmp_root / "retire-target"
|
|
3598
|
+
dirty_target = tmp_root / "dirty-target"
|
|
3599
|
+
broken_install = tmp_root / "broken-install" / "skills"
|
|
3600
|
+
shutil.copytree(root / "skills", install_root)
|
|
3601
|
+
|
|
3602
|
+
for label, args in (
|
|
3603
|
+
(
|
|
3604
|
+
"installed reconciliation audit",
|
|
3605
|
+
[
|
|
3606
|
+
"python3",
|
|
3607
|
+
str(install_root / "shared" / "scripts" / "loom_flow.py"),
|
|
3608
|
+
"reconciliation",
|
|
3609
|
+
"audit",
|
|
3610
|
+
"--target",
|
|
3611
|
+
str(root),
|
|
3612
|
+
"--issue",
|
|
3613
|
+
"131",
|
|
3614
|
+
"--pr",
|
|
3615
|
+
"138",
|
|
3616
|
+
"--project",
|
|
3617
|
+
"5",
|
|
3618
|
+
],
|
|
3619
|
+
),
|
|
3620
|
+
(
|
|
3621
|
+
"installed reconciliation sync dry-run",
|
|
3622
|
+
[
|
|
3623
|
+
"python3",
|
|
3624
|
+
str(install_root / "shared" / "scripts" / "loom_flow.py"),
|
|
3625
|
+
"reconciliation",
|
|
3626
|
+
"sync",
|
|
3627
|
+
"--target",
|
|
3628
|
+
str(root),
|
|
3629
|
+
"--issue",
|
|
3630
|
+
"131",
|
|
3631
|
+
"--pr",
|
|
3632
|
+
"138",
|
|
3633
|
+
"--project",
|
|
3634
|
+
"5",
|
|
3635
|
+
"--dry-run",
|
|
3636
|
+
],
|
|
3637
|
+
),
|
|
3638
|
+
(
|
|
3639
|
+
"installed closeout check",
|
|
3640
|
+
[
|
|
3641
|
+
"python3",
|
|
3642
|
+
str(install_root / "shared" / "scripts" / "loom_flow.py"),
|
|
3643
|
+
"closeout",
|
|
3644
|
+
"check",
|
|
3645
|
+
"--target",
|
|
3646
|
+
str(root),
|
|
3647
|
+
"--issue",
|
|
3648
|
+
"131",
|
|
3649
|
+
"--pr",
|
|
3650
|
+
"138",
|
|
3651
|
+
"--project",
|
|
3652
|
+
"5",
|
|
3653
|
+
"--skip-gate",
|
|
3654
|
+
],
|
|
3655
|
+
),
|
|
3656
|
+
(
|
|
3657
|
+
"installed closeout sync",
|
|
3658
|
+
[
|
|
3659
|
+
"python3",
|
|
3660
|
+
str(install_root / "shared" / "scripts" / "loom_flow.py"),
|
|
3661
|
+
"closeout",
|
|
3662
|
+
"sync",
|
|
3663
|
+
"--target",
|
|
3664
|
+
str(root),
|
|
3665
|
+
"--issue",
|
|
3666
|
+
"131",
|
|
3667
|
+
"--pr",
|
|
3668
|
+
"138",
|
|
3669
|
+
"--project",
|
|
3670
|
+
"5",
|
|
3671
|
+
"--skip-gate",
|
|
3672
|
+
],
|
|
3673
|
+
),
|
|
3674
|
+
):
|
|
3675
|
+
payload, error = load_command_json_with_retry(
|
|
3676
|
+
root,
|
|
3677
|
+
args,
|
|
3678
|
+
timeout_seconds=30,
|
|
3679
|
+
retries=3,
|
|
3680
|
+
)
|
|
3681
|
+
if error:
|
|
3682
|
+
if label in {"installed closeout check", "installed closeout sync"} and "command timed out" in error:
|
|
3683
|
+
continue
|
|
3684
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` failed: {error}"))
|
|
3685
|
+
continue
|
|
3686
|
+
rate_limited = payload_has_github_rate_limit(payload)
|
|
3687
|
+
if label == "installed reconciliation audit":
|
|
3688
|
+
if payload.get("result") != "pass" and not rate_limited:
|
|
3689
|
+
failures.append(Failure("daily-execution-cli", "`installed reconciliation audit` must pass on the historical closeout sample"))
|
|
3690
|
+
require_runtime_state_payload(
|
|
3691
|
+
failures,
|
|
3692
|
+
category="daily-execution-cli",
|
|
3693
|
+
context="`installed reconciliation audit`",
|
|
3694
|
+
payload=payload.get("runtime_state"),
|
|
3695
|
+
expected_scene="installed-runtime",
|
|
3696
|
+
expected_carrier="installed-skills-root",
|
|
3697
|
+
allowed_results={"pass"},
|
|
3698
|
+
)
|
|
3699
|
+
require_reconciliation_payload(
|
|
3700
|
+
failures,
|
|
3701
|
+
category="daily-execution-cli",
|
|
3702
|
+
context="`installed reconciliation audit`",
|
|
3703
|
+
payload=payload,
|
|
3704
|
+
)
|
|
3705
|
+
elif label == "installed reconciliation sync dry-run":
|
|
3706
|
+
if payload.get("result") != "pass" and not rate_limited:
|
|
3707
|
+
failures.append(Failure("daily-execution-cli", "`installed reconciliation sync --dry-run` must pass on an already aligned sample"))
|
|
3708
|
+
require_runtime_state_payload(
|
|
3709
|
+
failures,
|
|
3710
|
+
category="daily-execution-cli",
|
|
3711
|
+
context="`installed reconciliation sync --dry-run`",
|
|
3712
|
+
payload=payload.get("runtime_state"),
|
|
3713
|
+
expected_scene="installed-runtime",
|
|
3714
|
+
expected_carrier="installed-skills-root",
|
|
3715
|
+
allowed_results={"pass"},
|
|
3716
|
+
)
|
|
3717
|
+
else:
|
|
3718
|
+
if payload.get("result") != "pass" and not rate_limited:
|
|
3719
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` must pass on the historical closeout sample"))
|
|
3720
|
+
require_runtime_state_payload(
|
|
3721
|
+
failures,
|
|
3722
|
+
category="daily-execution-cli",
|
|
3723
|
+
context=f"`{label}`",
|
|
3724
|
+
payload=payload.get("runtime_state"),
|
|
3725
|
+
expected_scene="installed-runtime",
|
|
3726
|
+
expected_carrier="installed-skills-root",
|
|
3727
|
+
allowed_results={"pass"},
|
|
3728
|
+
)
|
|
3729
|
+
require_closeout_reconciliation_contract(
|
|
3730
|
+
failures,
|
|
3731
|
+
category="daily-execution-cli",
|
|
3732
|
+
context=f"`{label}`",
|
|
3733
|
+
payload=payload,
|
|
3734
|
+
)
|
|
3735
|
+
|
|
3736
|
+
for target in (retire_target, dirty_target):
|
|
3737
|
+
shutil.copytree(example_target, target)
|
|
3738
|
+
for args in (
|
|
3739
|
+
["git", "init"],
|
|
3740
|
+
["git", "config", "user.email", "loom-check@example.com"],
|
|
3741
|
+
["git", "config", "user.name", "loom-check"],
|
|
3742
|
+
["git", "add", "."],
|
|
3743
|
+
["git", "commit", "-m", "baseline"],
|
|
3744
|
+
):
|
|
3745
|
+
result = run_command(root, args, cwd=target)
|
|
3746
|
+
if result.returncode != 0:
|
|
3747
|
+
detail = result.stderr.strip() or result.stdout.strip() or "git setup failed"
|
|
3748
|
+
failures.append(Failure("daily-execution-cli", f"`installed retire` setup failed: {detail}"))
|
|
3749
|
+
break
|
|
3750
|
+
|
|
3751
|
+
purity_payload, error = load_command_json(
|
|
3752
|
+
root,
|
|
3753
|
+
[
|
|
3754
|
+
"python3",
|
|
3755
|
+
str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
|
|
3756
|
+
"purity-check",
|
|
3757
|
+
"--target",
|
|
3758
|
+
str(retire_target),
|
|
3759
|
+
"--item",
|
|
3760
|
+
"INIT-0001",
|
|
3761
|
+
],
|
|
3762
|
+
)
|
|
3763
|
+
if error:
|
|
3764
|
+
failures.append(Failure("daily-execution-cli", f"`installed purity-check` failed: {error}"))
|
|
3765
|
+
elif purity_payload.get("result") != "pass":
|
|
3766
|
+
failures.append(Failure("daily-execution-cli", "`installed purity-check` must pass on a clean retire target"))
|
|
3767
|
+
else:
|
|
3768
|
+
require_runtime_state_payload(
|
|
3769
|
+
failures,
|
|
3770
|
+
category="daily-execution-cli",
|
|
3771
|
+
context="`installed purity-check`",
|
|
3772
|
+
payload=purity_payload.get("runtime_state"),
|
|
3773
|
+
expected_scene="installed-runtime",
|
|
3774
|
+
expected_carrier="installed-skills-root",
|
|
3775
|
+
allowed_results={"pass"},
|
|
3776
|
+
)
|
|
3777
|
+
|
|
3778
|
+
temp_root = retire_target / ".loom" / ".tmp"
|
|
3779
|
+
temp_root.mkdir(parents=True, exist_ok=True)
|
|
3780
|
+
(temp_root / "sentinel.txt").write_text("temp\n", encoding="utf-8")
|
|
3781
|
+
|
|
3782
|
+
cleanup_payload, error = load_command_json(
|
|
3783
|
+
root,
|
|
3784
|
+
[
|
|
3785
|
+
"python3",
|
|
3786
|
+
str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
|
|
3787
|
+
"workspace",
|
|
3788
|
+
"cleanup",
|
|
3789
|
+
"--target",
|
|
3790
|
+
str(retire_target),
|
|
3791
|
+
"--item",
|
|
3792
|
+
"INIT-0001",
|
|
3793
|
+
],
|
|
3794
|
+
)
|
|
3795
|
+
if error:
|
|
3796
|
+
failures.append(Failure("daily-execution-cli", f"`installed workspace cleanup` failed: {error}"))
|
|
3797
|
+
elif cleanup_payload.get("result") != "pass":
|
|
3798
|
+
failures.append(Failure("daily-execution-cli", "`installed workspace cleanup` must pass for Loom-owned residue"))
|
|
3799
|
+
else:
|
|
3800
|
+
require_runtime_state_payload(
|
|
3801
|
+
failures,
|
|
3802
|
+
category="daily-execution-cli",
|
|
3803
|
+
context="`installed workspace cleanup`",
|
|
3804
|
+
payload=cleanup_payload.get("runtime_state"),
|
|
3805
|
+
expected_scene="installed-runtime",
|
|
3806
|
+
expected_carrier="installed-skills-root",
|
|
3807
|
+
allowed_results={"pass"},
|
|
3808
|
+
)
|
|
3809
|
+
|
|
3810
|
+
retire_payload, error = load_command_json(
|
|
3811
|
+
root,
|
|
3812
|
+
[
|
|
3813
|
+
"python3",
|
|
3814
|
+
str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
|
|
3815
|
+
"workspace",
|
|
3816
|
+
"retire",
|
|
3817
|
+
"--target",
|
|
3818
|
+
str(retire_target),
|
|
3819
|
+
"--item",
|
|
3820
|
+
"INIT-0001",
|
|
3821
|
+
],
|
|
3822
|
+
)
|
|
3823
|
+
if error:
|
|
3824
|
+
failures.append(Failure("daily-execution-cli", f"`installed workspace retire` failed: {error}"))
|
|
3825
|
+
elif retire_payload.get("result") != "pass":
|
|
3826
|
+
failures.append(Failure("daily-execution-cli", "`installed workspace retire` must pass after cleanup"))
|
|
3827
|
+
else:
|
|
3828
|
+
require_runtime_state_payload(
|
|
3829
|
+
failures,
|
|
3830
|
+
category="daily-execution-cli",
|
|
3831
|
+
context="`installed workspace retire`",
|
|
3832
|
+
payload=retire_payload.get("runtime_state"),
|
|
3833
|
+
expected_scene="installed-runtime",
|
|
3834
|
+
expected_carrier="installed-skills-root",
|
|
3835
|
+
allowed_results={"pass"},
|
|
3836
|
+
)
|
|
3837
|
+
checkpoint = retire_payload.get("checkpoint")
|
|
3838
|
+
if not isinstance(checkpoint, dict) or checkpoint.get("normalized") != "retired":
|
|
3839
|
+
failures.append(Failure("daily-execution-cli", "`installed workspace retire` must leave the target in `retired` state"))
|
|
3840
|
+
|
|
3841
|
+
(dirty_target / "foreign-residue.txt").write_text("pending\n", encoding="utf-8")
|
|
3842
|
+
dirty_add = run_command(root, ["git", "add", "foreign-residue.txt"], cwd=dirty_target)
|
|
3843
|
+
if dirty_add.returncode != 0:
|
|
3844
|
+
detail = dirty_add.stderr.strip() or dirty_add.stdout.strip() or "git add failed"
|
|
3845
|
+
failures.append(Failure("daily-execution-cli", f"`installed retire` dirty sample setup failed: {detail}"))
|
|
3846
|
+
|
|
3847
|
+
for label, args in (
|
|
3848
|
+
(
|
|
3849
|
+
"installed purity-check dirty sample",
|
|
3850
|
+
[
|
|
3851
|
+
"python3",
|
|
3852
|
+
str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
|
|
3853
|
+
"purity-check",
|
|
3854
|
+
"--target",
|
|
3855
|
+
str(dirty_target),
|
|
3856
|
+
"--item",
|
|
3857
|
+
"INIT-0001",
|
|
3858
|
+
],
|
|
3859
|
+
),
|
|
3860
|
+
(
|
|
3861
|
+
"installed workspace cleanup dirty sample",
|
|
3862
|
+
[
|
|
3863
|
+
"python3",
|
|
3864
|
+
str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
|
|
3865
|
+
"workspace",
|
|
3866
|
+
"cleanup",
|
|
3867
|
+
"--target",
|
|
3868
|
+
str(dirty_target),
|
|
3869
|
+
"--item",
|
|
3870
|
+
"INIT-0001",
|
|
3871
|
+
],
|
|
3872
|
+
),
|
|
3873
|
+
(
|
|
3874
|
+
"installed workspace retire dirty sample",
|
|
3875
|
+
[
|
|
3876
|
+
"python3",
|
|
3877
|
+
str(install_root / "loom-retire" / "scripts" / "loom-retire.py"),
|
|
3878
|
+
"workspace",
|
|
3879
|
+
"retire",
|
|
3880
|
+
"--target",
|
|
3881
|
+
str(dirty_target),
|
|
3882
|
+
"--item",
|
|
3883
|
+
"INIT-0001",
|
|
3884
|
+
],
|
|
3885
|
+
),
|
|
3886
|
+
):
|
|
3887
|
+
payload, error = load_command_json(root, args)
|
|
3888
|
+
if error:
|
|
3889
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` failed: {error}"))
|
|
3890
|
+
continue
|
|
3891
|
+
if payload.get("result") != "block":
|
|
3892
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` must block when non-Loom residue is present"))
|
|
3893
|
+
require_runtime_state_payload(
|
|
3894
|
+
failures,
|
|
3895
|
+
category="daily-execution-cli",
|
|
3896
|
+
context=f"`{label}`",
|
|
3897
|
+
payload=payload.get("runtime_state"),
|
|
3898
|
+
expected_scene="installed-runtime",
|
|
3899
|
+
expected_carrier="installed-skills-root",
|
|
3900
|
+
allowed_results={"pass"},
|
|
3901
|
+
)
|
|
3902
|
+
|
|
3903
|
+
shutil.copytree(root / "skills", broken_install)
|
|
3904
|
+
(broken_install / "install-layout.json").unlink()
|
|
3905
|
+
for label, args in (
|
|
3906
|
+
(
|
|
3907
|
+
"installed closeout check missing install-layout",
|
|
3908
|
+
[
|
|
3909
|
+
"python3",
|
|
3910
|
+
str(broken_install / "shared" / "scripts" / "loom_flow.py"),
|
|
3911
|
+
"closeout",
|
|
3912
|
+
"check",
|
|
3913
|
+
"--target",
|
|
3914
|
+
str(root),
|
|
3915
|
+
"--issue",
|
|
3916
|
+
"131",
|
|
3917
|
+
"--pr",
|
|
3918
|
+
"138",
|
|
3919
|
+
"--skip-gate",
|
|
3920
|
+
],
|
|
3921
|
+
),
|
|
3922
|
+
(
|
|
3923
|
+
"installed purity-check missing install-layout",
|
|
3924
|
+
[
|
|
3925
|
+
"python3",
|
|
3926
|
+
str(broken_install / "loom-retire" / "scripts" / "loom-retire.py"),
|
|
3927
|
+
"purity-check",
|
|
3928
|
+
"--target",
|
|
3929
|
+
str(retire_target),
|
|
3930
|
+
"--item",
|
|
3931
|
+
"INIT-0001",
|
|
3932
|
+
],
|
|
3933
|
+
),
|
|
3934
|
+
):
|
|
3935
|
+
payload, error = load_command_json(root, args)
|
|
3936
|
+
if error:
|
|
3937
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` failed unexpectedly: {error}"))
|
|
3938
|
+
continue
|
|
3939
|
+
if payload.get("result") != "block":
|
|
3940
|
+
failures.append(Failure("daily-execution-cli", f"`{label}` must block when install-layout is missing"))
|
|
3941
|
+
require_runtime_state_payload(
|
|
3942
|
+
failures,
|
|
3943
|
+
category="daily-execution-cli",
|
|
3944
|
+
context=f"`{label}`",
|
|
3945
|
+
payload=payload.get("runtime_state"),
|
|
3946
|
+
expected_scene="installed-runtime",
|
|
3947
|
+
expected_carrier="installed-skills-root",
|
|
3948
|
+
allowed_results={"block"},
|
|
3949
|
+
)
|
|
3950
|
+
|
|
3951
|
+
fail_closed_payloads = [
|
|
3952
|
+
(
|
|
3953
|
+
"closeout-fix-needed-fail-open",
|
|
3954
|
+
{
|
|
3955
|
+
"result": "pass",
|
|
3956
|
+
"fallback_to": None,
|
|
3957
|
+
"reconciliation": {
|
|
3958
|
+
"command": "reconciliation",
|
|
3959
|
+
"operation": "audit",
|
|
3960
|
+
"result": "fix-needed",
|
|
3961
|
+
"summary": "fix-needed",
|
|
3962
|
+
"missing_inputs": [],
|
|
3963
|
+
"fallback_to": "manual-reconciliation",
|
|
3964
|
+
"findings": [
|
|
3965
|
+
{
|
|
3966
|
+
"kind": "absorbed_but_open",
|
|
3967
|
+
"severity": "fix-needed",
|
|
3968
|
+
"subject": "issue #177",
|
|
3969
|
+
"evidence": {},
|
|
3970
|
+
"recommended_action": "run reconciliation sync",
|
|
3971
|
+
}
|
|
3972
|
+
],
|
|
3973
|
+
},
|
|
3974
|
+
},
|
|
3975
|
+
),
|
|
3976
|
+
(
|
|
3977
|
+
"closeout-block-fallback-drift",
|
|
3978
|
+
{
|
|
3979
|
+
"result": "block",
|
|
3980
|
+
"fallback_to": "merge",
|
|
3981
|
+
"reconciliation": {
|
|
3982
|
+
"command": "reconciliation",
|
|
3983
|
+
"operation": "audit",
|
|
3984
|
+
"result": "block",
|
|
3985
|
+
"summary": "block",
|
|
3986
|
+
"missing_inputs": ["issue/pr/project"],
|
|
3987
|
+
"fallback_to": "manual-reconciliation",
|
|
3988
|
+
"findings": [
|
|
3989
|
+
{
|
|
3990
|
+
"kind": "parent_drift",
|
|
3991
|
+
"severity": "block",
|
|
3992
|
+
"subject": "parent issue #148",
|
|
3993
|
+
"evidence": {},
|
|
3994
|
+
"recommended_action": "manual reconciliation",
|
|
3995
|
+
}
|
|
3996
|
+
],
|
|
3997
|
+
},
|
|
3998
|
+
},
|
|
3999
|
+
),
|
|
4000
|
+
(
|
|
4001
|
+
"closeout-malformed-reconciliation",
|
|
4002
|
+
{
|
|
4003
|
+
"result": "pass",
|
|
4004
|
+
"fallback_to": None,
|
|
4005
|
+
"reconciliation": {
|
|
4006
|
+
"command": "reconciliation",
|
|
4007
|
+
"operation": "audit",
|
|
4008
|
+
"summary": "broken",
|
|
4009
|
+
"missing_inputs": "bad",
|
|
4010
|
+
"findings": "bad",
|
|
4011
|
+
},
|
|
4012
|
+
},
|
|
4013
|
+
),
|
|
4014
|
+
]
|
|
4015
|
+
for label, payload in fail_closed_payloads:
|
|
4016
|
+
sample_failures: list[Failure] = []
|
|
4017
|
+
require_closeout_reconciliation_contract(
|
|
4018
|
+
sample_failures,
|
|
4019
|
+
category="daily-execution-cli",
|
|
4020
|
+
context=f"`{label}`",
|
|
4021
|
+
payload=payload,
|
|
4022
|
+
)
|
|
4023
|
+
if not sample_failures:
|
|
4024
|
+
failures.append(
|
|
4025
|
+
Failure(
|
|
4026
|
+
"daily-execution-cli",
|
|
4027
|
+
f"`{label}` synthetic payload must fail closeout reconciliation validation",
|
|
4028
|
+
)
|
|
4029
|
+
)
|
|
4030
|
+
|
|
4031
|
+
warn_payload_failures: list[Failure] = []
|
|
4032
|
+
require_closeout_reconciliation_contract(
|
|
4033
|
+
warn_payload_failures,
|
|
4034
|
+
category="daily-execution-cli",
|
|
4035
|
+
context="`closeout-warn-does-not-block`",
|
|
4036
|
+
payload={
|
|
4037
|
+
"result": "pass",
|
|
4038
|
+
"fallback_to": None,
|
|
4039
|
+
"reconciliation": {
|
|
4040
|
+
"command": "reconciliation",
|
|
4041
|
+
"operation": "audit",
|
|
4042
|
+
"result": "warn",
|
|
4043
|
+
"summary": "warn",
|
|
4044
|
+
"missing_inputs": [],
|
|
4045
|
+
"fallback_to": "manual-reconciliation",
|
|
4046
|
+
"findings": [
|
|
4047
|
+
{
|
|
4048
|
+
"kind": "project_drift",
|
|
4049
|
+
"severity": "warn",
|
|
4050
|
+
"subject": "project 5",
|
|
4051
|
+
"evidence": {},
|
|
4052
|
+
"recommended_action": "review warning",
|
|
4053
|
+
}
|
|
4054
|
+
],
|
|
4055
|
+
},
|
|
4056
|
+
},
|
|
4057
|
+
)
|
|
4058
|
+
if warn_payload_failures:
|
|
4059
|
+
failures.append(
|
|
4060
|
+
Failure(
|
|
4061
|
+
"daily-execution-cli",
|
|
4062
|
+
"`closeout-warn-does-not-block` synthetic payload must allow non-blocking reconciliation warnings",
|
|
4063
|
+
)
|
|
4064
|
+
)
|
|
4065
|
+
|
|
4066
|
+
return failures
|
|
4067
|
+
|
|
4068
|
+
|
|
4069
|
+
def check_repo_companion_interface_contracts(root: Path) -> list[Failure]:
|
|
4070
|
+
failures: list[Failure] = []
|
|
4071
|
+
example_target = root / "examples/new-project"
|
|
4072
|
+
if not example_target.exists():
|
|
4073
|
+
return failures
|
|
4074
|
+
|
|
4075
|
+
def write_json(path: Path, payload: object) -> None:
|
|
4076
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
4077
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
4078
|
+
|
|
4079
|
+
def install_companion(
|
|
4080
|
+
target: Path,
|
|
4081
|
+
*,
|
|
4082
|
+
manifest: dict[str, object] | None = None,
|
|
4083
|
+
repo_interface: dict[str, object] | None = None,
|
|
4084
|
+
legacy_docs_only: bool = False,
|
|
4085
|
+
) -> None:
|
|
4086
|
+
companion_dir = target / ".loom" / "companion"
|
|
4087
|
+
companion_dir.mkdir(parents=True, exist_ok=True)
|
|
4088
|
+
if legacy_docs_only:
|
|
4089
|
+
(companion_dir / "README.md").write_text("# Legacy Companion Docs\n", encoding="utf-8")
|
|
4090
|
+
return
|
|
4091
|
+
(companion_dir / "README.md").write_text("# Repo Companion\n", encoding="utf-8")
|
|
4092
|
+
for doc in (
|
|
4093
|
+
"review.md",
|
|
4094
|
+
"merge-ready.md",
|
|
4095
|
+
"closeout.md",
|
|
4096
|
+
"specialized-gates.md",
|
|
4097
|
+
"checkpoints.md",
|
|
4098
|
+
"metadata-contract.md",
|
|
4099
|
+
"context-schema.md",
|
|
4100
|
+
):
|
|
4101
|
+
(companion_dir / doc).write_text(f"# {doc}\n", encoding="utf-8")
|
|
4102
|
+
if manifest is not None:
|
|
4103
|
+
write_json(companion_dir / "manifest.json", manifest)
|
|
4104
|
+
if repo_interface is not None:
|
|
4105
|
+
write_json(companion_dir / "repo-interface.json", repo_interface)
|
|
4106
|
+
|
|
4107
|
+
valid_manifest = {
|
|
4108
|
+
"schema_version": "loom-repo-companion-manifest/v1",
|
|
4109
|
+
"companion_entry": ".loom/companion/README.md",
|
|
4110
|
+
"repo_interface": ".loom/companion/repo-interface.json",
|
|
4111
|
+
}
|
|
4112
|
+
valid_interface_v1 = {
|
|
4113
|
+
"schema_version": "loom-repo-interface/v1",
|
|
4114
|
+
"companion_entry": ".loom/companion/README.md",
|
|
4115
|
+
"repo_specific_requirements": {
|
|
4116
|
+
"review": [
|
|
4117
|
+
{
|
|
4118
|
+
"id": "review-specialized-gate",
|
|
4119
|
+
"summary": "Run the repo-specific semantic review checklist.",
|
|
4120
|
+
"locator": ".loom/companion/review.md",
|
|
4121
|
+
"enforcement": "blocking",
|
|
4122
|
+
}
|
|
4123
|
+
],
|
|
4124
|
+
"merge_ready": [
|
|
4125
|
+
{
|
|
4126
|
+
"id": "merge-ready-advisory-note",
|
|
4127
|
+
"summary": "Review the repo-specific merge advisory note.",
|
|
4128
|
+
"locator": ".loom/companion/merge-ready.md",
|
|
4129
|
+
"enforcement": "advisory",
|
|
4130
|
+
}
|
|
4131
|
+
],
|
|
4132
|
+
"closeout": [
|
|
4133
|
+
{
|
|
4134
|
+
"id": "closeout-specialized-gate",
|
|
4135
|
+
"summary": "Confirm the repo-specific closeout checklist.",
|
|
4136
|
+
"locator": ".loom/companion/closeout.md",
|
|
4137
|
+
"enforcement": "blocking",
|
|
4138
|
+
}
|
|
4139
|
+
],
|
|
4140
|
+
},
|
|
4141
|
+
"specialized_gates": [
|
|
4142
|
+
{
|
|
4143
|
+
"id": "specialized-release-gate",
|
|
4144
|
+
"summary": "Companion-owned release judgment.",
|
|
4145
|
+
"locator": ".loom/companion/specialized-gates.md",
|
|
4146
|
+
}
|
|
4147
|
+
],
|
|
4148
|
+
}
|
|
4149
|
+
valid_interface_v2 = {
|
|
4150
|
+
"schema_version": "loom-repo-interface/v2",
|
|
4151
|
+
"companion_entry": ".loom/companion/README.md",
|
|
4152
|
+
"repo_specific_requirements": valid_interface_v1["repo_specific_requirements"],
|
|
4153
|
+
"specialized_gates": [
|
|
4154
|
+
{
|
|
4155
|
+
"id": "specialized-review-gate",
|
|
4156
|
+
"summary": "Companion-owned review specialization.",
|
|
4157
|
+
"locator": ".loom/companion/specialized-gates.md",
|
|
4158
|
+
"gate_type": "review",
|
|
4159
|
+
}
|
|
4160
|
+
],
|
|
4161
|
+
"metadata_contract": {
|
|
4162
|
+
"fields": [
|
|
4163
|
+
{
|
|
4164
|
+
"id": "integration_check",
|
|
4165
|
+
"summary": "Declare repo-specific integration metadata.",
|
|
4166
|
+
"applicability_locator": ".loom/companion/metadata-contract.md",
|
|
4167
|
+
"authority_locator": ".loom/companion/review.md",
|
|
4168
|
+
"enforcement": "blocking",
|
|
4169
|
+
}
|
|
4170
|
+
]
|
|
4171
|
+
},
|
|
4172
|
+
"context_schema": {
|
|
4173
|
+
"fields": [
|
|
4174
|
+
{
|
|
4175
|
+
"id": "item_key",
|
|
4176
|
+
"summary": "Repo-native item key.",
|
|
4177
|
+
"type": "string",
|
|
4178
|
+
"required": True,
|
|
4179
|
+
"mapping_rule_locator": ".loom/companion/context-schema.md",
|
|
4180
|
+
}
|
|
4181
|
+
]
|
|
4182
|
+
},
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-repo-companion-") as tmp:
|
|
4186
|
+
base = Path(tmp)
|
|
4187
|
+
|
|
4188
|
+
absent_target = base / "absent"
|
|
4189
|
+
shutil.copytree(example_target, absent_target)
|
|
4190
|
+
absent_surface = build_governance_surface(absent_target)
|
|
4191
|
+
repo_interface = absent_surface.get("repo_interface")
|
|
4192
|
+
require_repo_interface_payload(
|
|
4193
|
+
failures,
|
|
4194
|
+
category="repo-companion",
|
|
4195
|
+
context="absent repo companion",
|
|
4196
|
+
payload=repo_interface,
|
|
4197
|
+
)
|
|
4198
|
+
if not isinstance(repo_interface, dict) or repo_interface.get("availability") != "absent":
|
|
4199
|
+
failures.append(Failure("repo-companion", "absent repo companion sample must report `availability: absent`"))
|
|
4200
|
+
|
|
4201
|
+
docs_only_target = base / "docs-only"
|
|
4202
|
+
shutil.copytree(example_target, docs_only_target)
|
|
4203
|
+
install_companion(docs_only_target, legacy_docs_only=True)
|
|
4204
|
+
docs_only_surface = build_governance_surface(docs_only_target)
|
|
4205
|
+
docs_only_interface = docs_only_surface.get("repo_interface")
|
|
4206
|
+
require_repo_interface_payload(
|
|
4207
|
+
failures,
|
|
4208
|
+
category="repo-companion",
|
|
4209
|
+
context="docs-only repo companion",
|
|
4210
|
+
payload=docs_only_interface,
|
|
4211
|
+
)
|
|
4212
|
+
if not isinstance(docs_only_interface, dict) or docs_only_interface.get("availability") != "companion_docs_only":
|
|
4213
|
+
failures.append(Failure("repo-companion", "docs-only repo companion sample must report `availability: companion_docs_only`"))
|
|
4214
|
+
|
|
4215
|
+
incomplete_target = base / "incomplete"
|
|
4216
|
+
shutil.copytree(example_target, incomplete_target)
|
|
4217
|
+
install_companion(
|
|
4218
|
+
incomplete_target,
|
|
4219
|
+
manifest={
|
|
4220
|
+
**valid_manifest,
|
|
4221
|
+
"current_stop": "forbidden authored state",
|
|
4222
|
+
"repo_interface": ".loom/companion/missing-interface.json",
|
|
4223
|
+
},
|
|
4224
|
+
)
|
|
4225
|
+
incomplete_surface = build_governance_surface(incomplete_target)
|
|
4226
|
+
incomplete_interface = incomplete_surface.get("repo_interface")
|
|
4227
|
+
require_repo_interface_payload(
|
|
4228
|
+
failures,
|
|
4229
|
+
category="repo-companion",
|
|
4230
|
+
context="incomplete repo companion",
|
|
4231
|
+
payload=incomplete_interface,
|
|
4232
|
+
)
|
|
4233
|
+
if not isinstance(incomplete_interface, dict) or incomplete_interface.get("availability") != "incomplete":
|
|
4234
|
+
failures.append(Failure("repo-companion", "incomplete repo companion sample must report `availability: incomplete`"))
|
|
4235
|
+
|
|
4236
|
+
invalid_interface_target = base / "invalid-interface"
|
|
4237
|
+
shutil.copytree(example_target, invalid_interface_target)
|
|
4238
|
+
install_companion(
|
|
4239
|
+
invalid_interface_target,
|
|
4240
|
+
manifest=valid_manifest,
|
|
4241
|
+
repo_interface={
|
|
4242
|
+
"schema_version": "loom-repo-interface/v1",
|
|
4243
|
+
"companion_entry": ".loom/companion/README.md",
|
|
4244
|
+
"repo_specific_requirements": {
|
|
4245
|
+
"review": [
|
|
4246
|
+
{
|
|
4247
|
+
"id": "bad-enforcement",
|
|
4248
|
+
"summary": "Broken requirement",
|
|
4249
|
+
"locator": ".loom/companion/review.md",
|
|
4250
|
+
"enforcement": "required",
|
|
4251
|
+
}
|
|
4252
|
+
],
|
|
4253
|
+
"merge_ready": [],
|
|
4254
|
+
},
|
|
4255
|
+
"specialized_gates": [],
|
|
4256
|
+
},
|
|
4257
|
+
)
|
|
4258
|
+
invalid_interface_surface = build_governance_surface(invalid_interface_target)
|
|
4259
|
+
invalid_interface = invalid_interface_surface.get("repo_interface")
|
|
4260
|
+
require_repo_interface_payload(
|
|
4261
|
+
failures,
|
|
4262
|
+
category="repo-companion",
|
|
4263
|
+
context="invalid repo companion interface",
|
|
4264
|
+
payload=invalid_interface,
|
|
4265
|
+
)
|
|
4266
|
+
if not isinstance(invalid_interface, dict) or invalid_interface.get("availability") != "incomplete":
|
|
4267
|
+
failures.append(Failure("repo-companion", "invalid repo companion interface sample must report `availability: incomplete`"))
|
|
4268
|
+
|
|
4269
|
+
invalid_v2_target = base / "invalid-v2-interface"
|
|
4270
|
+
shutil.copytree(example_target, invalid_v2_target)
|
|
4271
|
+
install_companion(
|
|
4272
|
+
invalid_v2_target,
|
|
4273
|
+
manifest=valid_manifest,
|
|
4274
|
+
repo_interface={
|
|
4275
|
+
"schema_version": "loom-repo-interface/v2",
|
|
4276
|
+
"companion_entry": ".loom/companion/README.md",
|
|
4277
|
+
"repo_specific_requirements": valid_interface_v1["repo_specific_requirements"],
|
|
4278
|
+
"specialized_gates": [
|
|
4279
|
+
{
|
|
4280
|
+
"id": "bad-gate-type",
|
|
4281
|
+
"summary": "Broken gate type",
|
|
4282
|
+
"locator": ".loom/companion/specialized-gates.md",
|
|
4283
|
+
"gate_type": "guardian",
|
|
4284
|
+
}
|
|
4285
|
+
],
|
|
4286
|
+
"metadata_contract": {
|
|
4287
|
+
"fields": [
|
|
4288
|
+
{
|
|
4289
|
+
"id": "bad-metadata",
|
|
4290
|
+
"summary": "Broken metadata field",
|
|
4291
|
+
"applicability_locator": ".loom/companion/metadata-contract.md",
|
|
4292
|
+
"authority_locator": ".loom/companion/review.md",
|
|
4293
|
+
"enforcement": "required",
|
|
4294
|
+
}
|
|
4295
|
+
]
|
|
4296
|
+
},
|
|
4297
|
+
"context_schema": {
|
|
4298
|
+
"fields": [
|
|
4299
|
+
{
|
|
4300
|
+
"id": "bad-context",
|
|
4301
|
+
"summary": "Broken context field",
|
|
4302
|
+
"type": "object",
|
|
4303
|
+
"required": "yes",
|
|
4304
|
+
"mapping_rule_locator": ".loom/companion/context-schema.md",
|
|
4305
|
+
}
|
|
4306
|
+
]
|
|
4307
|
+
},
|
|
4308
|
+
},
|
|
4309
|
+
)
|
|
4310
|
+
invalid_v2_surface = build_governance_surface(invalid_v2_target)
|
|
4311
|
+
invalid_v2_interface = invalid_v2_surface.get("repo_interface")
|
|
4312
|
+
require_repo_interface_payload(
|
|
4313
|
+
failures,
|
|
4314
|
+
category="repo-companion",
|
|
4315
|
+
context="invalid v2 repo companion interface",
|
|
4316
|
+
payload=invalid_v2_interface,
|
|
4317
|
+
)
|
|
4318
|
+
if not isinstance(invalid_v2_interface, dict) or invalid_v2_interface.get("availability") != "incomplete":
|
|
4319
|
+
failures.append(Failure("repo-companion", "invalid v2 repo companion interface sample must report `availability: incomplete`"))
|
|
4320
|
+
|
|
4321
|
+
present_v1_target = base / "present-v1"
|
|
4322
|
+
shutil.copytree(example_target, present_v1_target)
|
|
4323
|
+
install_companion(
|
|
4324
|
+
present_v1_target,
|
|
4325
|
+
manifest=valid_manifest,
|
|
4326
|
+
repo_interface=valid_interface_v1,
|
|
4327
|
+
)
|
|
4328
|
+
present_v1_surface = build_governance_surface(present_v1_target)
|
|
4329
|
+
present_v1_interface = present_v1_surface.get("repo_interface")
|
|
4330
|
+
require_repo_interface_payload(
|
|
4331
|
+
failures,
|
|
4332
|
+
category="repo-companion",
|
|
4333
|
+
context="present v1 repo companion",
|
|
4334
|
+
payload=present_v1_interface,
|
|
4335
|
+
)
|
|
4336
|
+
if not isinstance(present_v1_interface, dict) or present_v1_interface.get("availability") != "present":
|
|
4337
|
+
failures.append(Failure("repo-companion", "present v1 repo companion sample must report `availability: present`"))
|
|
4338
|
+
|
|
4339
|
+
present_target = base / "present-v2"
|
|
4340
|
+
shutil.copytree(example_target, present_target)
|
|
4341
|
+
install_companion(
|
|
4342
|
+
present_target,
|
|
4343
|
+
manifest=valid_manifest,
|
|
4344
|
+
repo_interface=valid_interface_v2,
|
|
4345
|
+
)
|
|
4346
|
+
present_surface = build_governance_surface(present_target)
|
|
4347
|
+
present_interface = present_surface.get("repo_interface")
|
|
4348
|
+
require_repo_interface_payload(
|
|
4349
|
+
failures,
|
|
4350
|
+
category="repo-companion",
|
|
4351
|
+
context="present v2 repo companion",
|
|
4352
|
+
payload=present_interface,
|
|
4353
|
+
)
|
|
4354
|
+
if not isinstance(present_interface, dict) or present_interface.get("availability") != "present":
|
|
4355
|
+
failures.append(Failure("repo-companion", "present v2 repo companion sample must report `availability: present`"))
|
|
4356
|
+
|
|
4357
|
+
review_requirements = repo_specific_requirements_payload(
|
|
4358
|
+
present_interface,
|
|
4359
|
+
target_root=present_target,
|
|
4360
|
+
surface="review",
|
|
4361
|
+
)
|
|
4362
|
+
require_repo_specific_requirements_payload(
|
|
4363
|
+
failures,
|
|
4364
|
+
category="repo-companion",
|
|
4365
|
+
context="present repo companion review requirements",
|
|
4366
|
+
payload=review_requirements,
|
|
4367
|
+
expected_surface="review",
|
|
4368
|
+
)
|
|
4369
|
+
if review_requirements.get("result") != "block":
|
|
4370
|
+
failures.append(Failure("repo-companion", "blocking review requirements must fail closed"))
|
|
4371
|
+
|
|
4372
|
+
merge_requirements = repo_specific_requirements_payload(
|
|
4373
|
+
present_interface,
|
|
4374
|
+
target_root=present_target,
|
|
4375
|
+
surface="merge_ready",
|
|
4376
|
+
)
|
|
4377
|
+
require_repo_specific_requirements_payload(
|
|
4378
|
+
failures,
|
|
4379
|
+
category="repo-companion",
|
|
4380
|
+
context="present repo companion merge-ready requirements",
|
|
4381
|
+
payload=merge_requirements,
|
|
4382
|
+
expected_surface="merge_ready",
|
|
4383
|
+
)
|
|
4384
|
+
if merge_requirements.get("result") != "pass":
|
|
4385
|
+
failures.append(Failure("repo-companion", "advisory merge-ready requirements must remain non-blocking"))
|
|
4386
|
+
|
|
4387
|
+
closeout_requirements = repo_specific_requirements_payload(
|
|
4388
|
+
present_interface,
|
|
4389
|
+
target_root=present_target,
|
|
4390
|
+
surface="closeout",
|
|
4391
|
+
)
|
|
4392
|
+
require_repo_specific_requirements_payload(
|
|
4393
|
+
failures,
|
|
4394
|
+
category="repo-companion",
|
|
4395
|
+
context="present repo companion closeout requirements",
|
|
4396
|
+
payload=closeout_requirements,
|
|
4397
|
+
expected_surface="closeout",
|
|
4398
|
+
)
|
|
4399
|
+
if closeout_requirements.get("result") != "block":
|
|
4400
|
+
failures.append(Failure("repo-companion", "blocking closeout requirements must fail closed"))
|
|
4401
|
+
|
|
4402
|
+
flow_review_payload, error = load_command_json(
|
|
4403
|
+
root,
|
|
4404
|
+
[
|
|
4405
|
+
"python3",
|
|
4406
|
+
"tools/loom_flow.py",
|
|
4407
|
+
"flow",
|
|
4408
|
+
"review",
|
|
4409
|
+
"--target",
|
|
4410
|
+
str(present_target),
|
|
4411
|
+
"--item",
|
|
4412
|
+
"INIT-0001",
|
|
4413
|
+
],
|
|
4414
|
+
)
|
|
4415
|
+
if error:
|
|
4416
|
+
failures.append(Failure("repo-companion", f"`flow review` companion sample failed: {error}"))
|
|
4417
|
+
elif flow_review_payload.get("result") != "block":
|
|
4418
|
+
failures.append(Failure("repo-companion", "`flow review` must block when repo companion declares blocking review requirements"))
|
|
4419
|
+
|
|
4420
|
+
flow_merge_ready_payload, error = load_command_json(
|
|
4421
|
+
root,
|
|
4422
|
+
[
|
|
4423
|
+
"python3",
|
|
4424
|
+
"tools/loom_flow.py",
|
|
4425
|
+
"flow",
|
|
4426
|
+
"merge-ready",
|
|
4427
|
+
"--target",
|
|
4428
|
+
str(present_target),
|
|
4429
|
+
"--item",
|
|
4430
|
+
"INIT-0001",
|
|
4431
|
+
],
|
|
4432
|
+
)
|
|
4433
|
+
if error:
|
|
4434
|
+
failures.append(Failure("repo-companion", f"`flow merge-ready` companion sample failed: {error}"))
|
|
4435
|
+
elif not isinstance(flow_merge_ready_payload.get("repo_specific_requirements"), dict):
|
|
4436
|
+
failures.append(Failure("repo-companion", "`flow merge-ready` companion sample must include `repo_specific_requirements`"))
|
|
4437
|
+
elif flow_merge_ready_payload["repo_specific_requirements"].get("result") != "pass":
|
|
4438
|
+
failures.append(Failure("repo-companion", "`flow merge-ready` advisory companion requirements must stay non-blocking"))
|
|
4439
|
+
|
|
4440
|
+
return failures
|
|
4441
|
+
|
|
4442
|
+
|
|
4443
|
+
def check_repo_interop_contracts(root: Path) -> list[Failure]:
|
|
4444
|
+
example_target = root / "examples/new-project"
|
|
4445
|
+
if not example_target.exists():
|
|
4446
|
+
return []
|
|
4447
|
+
|
|
4448
|
+
failures: list[Failure] = []
|
|
4449
|
+
|
|
4450
|
+
def write_json(path: Path, payload: object) -> None:
|
|
4451
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
4452
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
4453
|
+
|
|
4454
|
+
def install_interop(
|
|
4455
|
+
target: Path,
|
|
4456
|
+
*,
|
|
4457
|
+
interop: dict[str, object] | None = None,
|
|
4458
|
+
) -> None:
|
|
4459
|
+
companion_dir = target / ".loom" / "companion"
|
|
4460
|
+
companion_dir.mkdir(parents=True, exist_ok=True)
|
|
4461
|
+
(target / "host").mkdir(parents=True, exist_ok=True)
|
|
4462
|
+
(target / "native").mkdir(parents=True, exist_ok=True)
|
|
4463
|
+
(target / ".loom" / "shadow").mkdir(parents=True, exist_ok=True)
|
|
4464
|
+
(target / "native" / "status").mkdir(parents=True, exist_ok=True)
|
|
4465
|
+
for relative, payload in {
|
|
4466
|
+
".loom/shadow/admission-loom.json": {"result": "pass"},
|
|
4467
|
+
".loom/shadow/admission-repo.json": {"result": "pass"},
|
|
4468
|
+
".loom/shadow/review-loom.json": {"decision": "allow"},
|
|
4469
|
+
".loom/shadow/review-repo.json": {"decision": "allow"},
|
|
4470
|
+
".loom/shadow/merge-ready-loom.json": {"status": "pass"},
|
|
4471
|
+
".loom/shadow/merge-ready-repo.json": {"status": "pass"},
|
|
4472
|
+
".loom/shadow/closeout-loom.json": {"status": "done"},
|
|
4473
|
+
".loom/shadow/closeout-repo.json": {"status": "done"},
|
|
4474
|
+
"host/guardian-review.json": {"verdict": "allow"},
|
|
4475
|
+
}.items():
|
|
4476
|
+
write_json(target / relative, payload)
|
|
4477
|
+
if interop is not None:
|
|
4478
|
+
write_json(companion_dir / "interop.json", interop)
|
|
4479
|
+
|
|
4480
|
+
valid_interop = {
|
|
4481
|
+
"schema_version": "loom-repo-interop/v1",
|
|
4482
|
+
"host_adapters": [
|
|
4483
|
+
{
|
|
4484
|
+
"id": "guardian-review",
|
|
4485
|
+
"summary": "Read guardian review verdicts without reimplementing the host action.",
|
|
4486
|
+
"surfaces": ["review", "merge_ready"],
|
|
4487
|
+
"locator": "host/guardian-review.json",
|
|
4488
|
+
}
|
|
4489
|
+
],
|
|
4490
|
+
"repo_native_carriers": [
|
|
4491
|
+
{
|
|
4492
|
+
"id": "governance-status",
|
|
4493
|
+
"summary": "Read repo-native governance status output without migrating carriers.",
|
|
4494
|
+
"surfaces": ["admission", "review", "merge_ready", "closeout"],
|
|
4495
|
+
"locator": "native/status",
|
|
4496
|
+
}
|
|
4497
|
+
],
|
|
4498
|
+
"shadow_surfaces": {
|
|
4499
|
+
"admission": {
|
|
4500
|
+
"summary": "Compare admission parity.",
|
|
4501
|
+
"loom_locator": ".loom/shadow/admission-loom.json",
|
|
4502
|
+
"repo_locator": ".loom/shadow/admission-repo.json",
|
|
4503
|
+
},
|
|
4504
|
+
"review": {
|
|
4505
|
+
"summary": "Compare review parity.",
|
|
4506
|
+
"loom_locator": ".loom/shadow/review-loom.json",
|
|
4507
|
+
"repo_locator": ".loom/shadow/review-repo.json",
|
|
4508
|
+
},
|
|
4509
|
+
"merge_ready": {
|
|
4510
|
+
"summary": "Compare merge-ready parity.",
|
|
4511
|
+
"loom_locator": ".loom/shadow/merge-ready-loom.json",
|
|
4512
|
+
"repo_locator": ".loom/shadow/merge-ready-repo.json",
|
|
4513
|
+
},
|
|
4514
|
+
"closeout": {
|
|
4515
|
+
"summary": "Compare closeout parity.",
|
|
4516
|
+
"loom_locator": ".loom/shadow/closeout-loom.json",
|
|
4517
|
+
"repo_locator": ".loom/shadow/closeout-repo.json",
|
|
4518
|
+
},
|
|
4519
|
+
},
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
with tempfile.TemporaryDirectory(prefix="loom-check-repo-interop-") as tmp:
|
|
4523
|
+
base = Path(tmp)
|
|
4524
|
+
|
|
4525
|
+
absent_target = base / "absent"
|
|
4526
|
+
shutil.copytree(example_target, absent_target)
|
|
4527
|
+
absent_surface = build_governance_surface(absent_target)
|
|
4528
|
+
repo_interop = absent_surface.get("repo_interop")
|
|
4529
|
+
require_repo_interop_payload(
|
|
4530
|
+
failures,
|
|
4531
|
+
category="repo-interop",
|
|
4532
|
+
context="absent repo interop",
|
|
4533
|
+
payload=repo_interop,
|
|
4534
|
+
)
|
|
4535
|
+
if not isinstance(repo_interop, dict) or repo_interop.get("availability") != "absent":
|
|
4536
|
+
failures.append(Failure("repo-interop", "absent repo interop sample must report `availability: absent`"))
|
|
4537
|
+
|
|
4538
|
+
invalid_target = base / "invalid"
|
|
4539
|
+
shutil.copytree(example_target, invalid_target)
|
|
4540
|
+
install_interop(
|
|
4541
|
+
invalid_target,
|
|
4542
|
+
interop={
|
|
4543
|
+
"schema_version": "loom-repo-interop/v1",
|
|
4544
|
+
"host_adapters": [
|
|
4545
|
+
{
|
|
4546
|
+
"id": "bad-adapter",
|
|
4547
|
+
"summary": "Broken adapter",
|
|
4548
|
+
"surfaces": ["guardian"],
|
|
4549
|
+
"locator": "host/missing.json",
|
|
4550
|
+
}
|
|
4551
|
+
],
|
|
4552
|
+
"repo_native_carriers": [],
|
|
4553
|
+
"shadow_surfaces": {
|
|
4554
|
+
"admission": {
|
|
4555
|
+
"summary": "Compare admission parity.",
|
|
4556
|
+
"loom_locator": ".loom/shadow/admission-loom.json",
|
|
4557
|
+
"repo_locator": ".loom/shadow/admission-repo.json",
|
|
4558
|
+
}
|
|
4559
|
+
},
|
|
4560
|
+
},
|
|
4561
|
+
)
|
|
4562
|
+
invalid_surface = build_governance_surface(invalid_target)
|
|
4563
|
+
invalid_interop = invalid_surface.get("repo_interop")
|
|
4564
|
+
require_repo_interop_payload(
|
|
4565
|
+
failures,
|
|
4566
|
+
category="repo-interop",
|
|
4567
|
+
context="invalid repo interop",
|
|
4568
|
+
payload=invalid_interop,
|
|
4569
|
+
)
|
|
4570
|
+
if not isinstance(invalid_interop, dict) or invalid_interop.get("availability") != "incomplete":
|
|
4571
|
+
failures.append(Failure("repo-interop", "invalid repo interop sample must report `availability: incomplete`"))
|
|
4572
|
+
|
|
4573
|
+
present_target = base / "present"
|
|
4574
|
+
shutil.copytree(example_target, present_target)
|
|
4575
|
+
install_interop(present_target, interop=valid_interop)
|
|
4576
|
+
present_surface = build_governance_surface(present_target)
|
|
4577
|
+
present_interop = present_surface.get("repo_interop")
|
|
4578
|
+
require_repo_interop_payload(
|
|
4579
|
+
failures,
|
|
4580
|
+
category="repo-interop",
|
|
4581
|
+
context="present repo interop",
|
|
4582
|
+
payload=present_interop,
|
|
4583
|
+
)
|
|
4584
|
+
if not isinstance(present_interop, dict) or present_interop.get("availability") != "present":
|
|
4585
|
+
failures.append(Failure("repo-interop", "present repo interop sample must report `availability: present`"))
|
|
4586
|
+
|
|
4587
|
+
parity_payload, error = load_command_json(
|
|
4588
|
+
root,
|
|
4589
|
+
["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(present_target)],
|
|
4590
|
+
)
|
|
4591
|
+
if error:
|
|
4592
|
+
failures.append(Failure("repo-interop", f"`shadow-parity` sample failed: {error}"))
|
|
4593
|
+
else:
|
|
4594
|
+
require_shadow_parity_payload(
|
|
4595
|
+
failures,
|
|
4596
|
+
category="repo-interop",
|
|
4597
|
+
context="`shadow-parity` present sample",
|
|
4598
|
+
payload=parity_payload,
|
|
4599
|
+
expected_reports=4,
|
|
4600
|
+
)
|
|
4601
|
+
if parity_payload.get("result") != "pass":
|
|
4602
|
+
failures.append(Failure("repo-interop", "`shadow-parity` must pass when all declared surfaces match"))
|
|
4603
|
+
|
|
4604
|
+
mismatch_target = base / "mismatch"
|
|
4605
|
+
shutil.copytree(present_target, mismatch_target)
|
|
4606
|
+
write_json(mismatch_target / ".loom/shadow/review-repo.json", {"decision": "block"})
|
|
4607
|
+
mismatch_payload, error = load_command_json(
|
|
4608
|
+
root,
|
|
4609
|
+
["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(mismatch_target), "--surface", "review"],
|
|
4610
|
+
)
|
|
4611
|
+
if error:
|
|
4612
|
+
failures.append(Failure("repo-interop", f"`shadow-parity` mismatch sample failed: {error}"))
|
|
4613
|
+
else:
|
|
4614
|
+
require_shadow_parity_payload(
|
|
4615
|
+
failures,
|
|
4616
|
+
category="repo-interop",
|
|
4617
|
+
context="`shadow-parity` mismatch sample",
|
|
4618
|
+
payload=mismatch_payload,
|
|
4619
|
+
expected_reports=1,
|
|
4620
|
+
)
|
|
4621
|
+
reports = mismatch_payload.get("reports")
|
|
4622
|
+
if not isinstance(reports, list) or not reports or reports[0].get("result") != "mismatch":
|
|
4623
|
+
failures.append(Failure("repo-interop", "`shadow-parity` mismatch sample must report `mismatch`"))
|
|
4624
|
+
|
|
4625
|
+
return failures
|
|
4626
|
+
|
|
4627
|
+
|
|
4628
|
+
def check_node_installer(root: Path) -> list[Failure]:
|
|
4629
|
+
category = "node-installer"
|
|
4630
|
+
failures: list[Failure] = []
|
|
4631
|
+
package_root = root / "packages/loom-installer"
|
|
4632
|
+
if not package_root.exists():
|
|
4633
|
+
return [Failure(category, "missing `packages/loom-installer`")]
|
|
4634
|
+
npm_bin = shutil.which("npm")
|
|
4635
|
+
if not npm_bin:
|
|
4636
|
+
return [Failure(category, "`npm` is required to validate the Node installer")]
|
|
4637
|
+
|
|
4638
|
+
commands = (
|
|
4639
|
+
["npm", "ci"],
|
|
4640
|
+
["npm", "test"],
|
|
4641
|
+
["npm", "pack", "--dry-run"],
|
|
4642
|
+
)
|
|
4643
|
+
for args in commands:
|
|
4644
|
+
try:
|
|
4645
|
+
result = run_command(root, args, cwd=package_root, timeout_seconds=300)
|
|
4646
|
+
except subprocess.TimeoutExpired:
|
|
4647
|
+
failures.append(Failure(category, f"`{' '.join(args)}` timed out"))
|
|
4648
|
+
continue
|
|
4649
|
+
if result.returncode != 0:
|
|
4650
|
+
detail = result.stderr.strip() or result.stdout.strip() or "command failed without output"
|
|
4651
|
+
failures.append(Failure(category, f"`{' '.join(args)}` failed: {detail}"))
|
|
4652
|
+
return failures
|
|
4653
|
+
|
|
4654
|
+
|
|
4655
|
+
def is_within(path: Path, root: Path) -> bool:
|
|
4656
|
+
try:
|
|
4657
|
+
path.relative_to(root)
|
|
4658
|
+
return True
|
|
4659
|
+
except ValueError:
|
|
4660
|
+
return False
|
|
4661
|
+
|
|
4662
|
+
|
|
4663
|
+
def collect_failures(root: Path) -> list[Failure]:
|
|
4664
|
+
failures: list[Failure] = []
|
|
4665
|
+
failures.extend(check_required_paths(root, "top-level-dirs", TOP_LEVEL_DIRS))
|
|
4666
|
+
failures.extend(check_required_paths(root, "top-level-files", TOP_LEVEL_FILES))
|
|
4667
|
+
failures.extend(check_required_paths(root, "area-readmes", AREA_READMES))
|
|
4668
|
+
failures.extend(check_required_paths(root, "core-docs", CORE_DOCS))
|
|
4669
|
+
failures.extend(
|
|
4670
|
+
check_required_paths(root, "automation-frontload-templates", AUTOMATION_FRONTLOAD_TEMPLATES)
|
|
4671
|
+
)
|
|
4672
|
+
failures.extend(check_required_paths(root, "automation-frontload-skills", AUTOMATION_FRONTLOAD_SKILLS))
|
|
4673
|
+
failures.extend(
|
|
4674
|
+
check_required_paths(
|
|
4675
|
+
root,
|
|
4676
|
+
"automation-frontload-execution-support",
|
|
4677
|
+
AUTOMATION_FRONTLOAD_EXECUTION_SUPPORT,
|
|
4678
|
+
)
|
|
4679
|
+
)
|
|
4680
|
+
failures.extend(check_skill_manifests(root))
|
|
4681
|
+
failures.extend(check_skill_routing(root))
|
|
4682
|
+
failures.extend(check_demo_assets(root))
|
|
4683
|
+
failures.extend(check_demo_fact_chain(root))
|
|
4684
|
+
failures.extend(check_demo_repo_local_cli(root))
|
|
4685
|
+
failures.extend(check_deep_existing_repo_bootstrap(root))
|
|
4686
|
+
failures.extend(check_daily_execution_cli(root))
|
|
4687
|
+
failures.extend(check_repo_companion_interface_contracts(root))
|
|
4688
|
+
failures.extend(check_repo_interop_contracts(root))
|
|
4689
|
+
failures.extend(check_node_installer(root))
|
|
4690
|
+
failures.extend(check_markdown_links(root))
|
|
4691
|
+
return failures
|
|
4692
|
+
|
|
4693
|
+
|
|
4694
|
+
def print_report(root: Path, failures: list[Failure]) -> None:
|
|
4695
|
+
categories_checked = 16
|
|
4696
|
+
if not failures:
|
|
4697
|
+
print(f"loom_check: OK ({root})")
|
|
4698
|
+
print(f"checked {categories_checked} surfaces")
|
|
4699
|
+
return
|
|
4700
|
+
|
|
4701
|
+
grouped: dict[str, list[str]] = defaultdict(list)
|
|
4702
|
+
for failure in failures:
|
|
4703
|
+
grouped[failure.category].append(failure.detail)
|
|
4704
|
+
|
|
4705
|
+
print(f"loom_check: FAILED ({root})")
|
|
4706
|
+
for category in sorted(grouped):
|
|
4707
|
+
print(f"- {category}")
|
|
4708
|
+
for detail in grouped[category]:
|
|
4709
|
+
print(f" - {detail}")
|
|
4710
|
+
print(f"failures: {len(failures)} across {len(grouped)} categories")
|
|
4711
|
+
|
|
4712
|
+
|
|
4713
|
+
def main(argv: list[str]) -> int:
|
|
4714
|
+
root = repo_root_from_argv(argv)
|
|
4715
|
+
if not root.exists():
|
|
4716
|
+
print(f"loom_check: repo root does not exist: {root}", file=sys.stderr)
|
|
4717
|
+
return 2
|
|
4718
|
+
failures = collect_failures(root)
|
|
4719
|
+
print_report(root, failures)
|
|
4720
|
+
return 1 if failures else 0
|
|
4721
|
+
|
|
4722
|
+
|
|
4723
|
+
if __name__ == "__main__":
|
|
4724
|
+
sys.exit(main(sys.argv))
|