devlyn-cli 3.2.0 → 3.3.0
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/AGENTS.md +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +22 -113
- package/bin/devlyn.js +13 -225
- package/bin/instruction-templates.json +752 -1
- package/bin/instructions.js +101 -17
- package/config/skills/_shared/adapters/grok.md +1 -2
- package/config/skills/_shared/archive_run.py +15 -3
- package/config/skills/_shared/judge-role-evidence.py +21 -1
- package/config/skills/_shared/runtime-principles.md +1 -1
- package/config/skills/_shared/spec-verify-check.py +0 -653
- package/config/skills/_shared/state-phase-write.py +371 -30
- package/config/skills/_shared/task-complete.py +635 -143
- package/config/skills/_shared/terminal-claim-check.py +229 -4
- package/config/skills/_shared/verify-merge-findings.py +93 -2
- package/config/skills/devlyn:ideate/SKILL.md +3 -4
- package/config/skills/devlyn:ideate/references/elicitation.md +2 -35
- package/config/skills/devlyn:ideate/references/from-spec-mode.md +3 -18
- package/config/skills/devlyn:ideate/references/project-mode.md +1 -16
- package/config/skills/devlyn:ideate/references/spec-template.md +0 -2
- package/config/skills/devlyn:queue/SKILL.md +6 -4
- package/config/skills/devlyn:resolve/SKILL.md +16 -25
- package/config/skills/devlyn:resolve/references/free-form-mode.md +0 -14
- package/config/skills/devlyn:resolve/references/outer-loop.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/cleanup.md +3 -3
- package/config/skills/devlyn:resolve/references/phases/implement.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/probe-derive.md +1 -16
- package/config/skills/devlyn:resolve/references/phases/verify.md +4 -9
- package/config/skills/devlyn:resolve/references/state-schema.md +9 -7
- package/config/skills/devlyn:resolve/references/task-completion.md +46 -37
- package/{config/skills → optional-skills}/code-health-standards/SKILL.md +4 -13
- package/{config/skills → optional-skills}/code-review-standards/SKILL.md +4 -12
- package/{config/skills → optional-skills}/root-cause-analysis/SKILL.md +4 -12
- package/{config/skills → optional-skills}/ui-implementation-standards/SKILL.md +4 -14
- package/package.json +1 -57
- package/benchmark/auto-resolve/BENCHMARK-DESIGN.md +0 -289
- package/benchmark/auto-resolve/BENCHMARK-RESULTS.md +0 -341
- package/benchmark/auto-resolve/README.md +0 -696
- package/benchmark/auto-resolve/RUBRIC.md +0 -177
- package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/NOTES.md +0 -34
- package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/expected.json +0 -68
- package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/setup.sh +0 -4
- package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/spec.md +0 -44
- package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/task.ko.txt +0 -8
- package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/task.txt +0 -8
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/NOTES.md +0 -68
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/expected.json +0 -60
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/setup.sh +0 -17
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/spec.md +0 -51
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/task.txt +0 -9
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/verifiers/invalid.js +0 -29
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/verifiers/parallel.js +0 -50
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/NOTES.md +0 -75
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/expected.json +0 -52
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/setup.sh +0 -171
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/spec.md +0 -50
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/task.txt +0 -9
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/NOTES.md +0 -89
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/expected.json +0 -74
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/setup.sh +0 -251
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/spec.md +0 -57
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/task.txt +0 -13
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/verifiers/replay-malformed-body.js +0 -64
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/NOTES.md +0 -104
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/expected.json +0 -46
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/setup.sh +0 -336
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/spec.md +0 -51
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/task.txt +0 -9
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/NOTES.md +0 -38
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/expected.json +0 -64
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/setup.sh +0 -32
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/spec.md +0 -63
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/exact-success.js +0 -54
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/no-hardcoded-pricing.js +0 -47
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/stock-error.js +0 -45
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/NOTES.md +0 -57
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/expected-pair-plan-registry.json +0 -170
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/expected.json +0 -84
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/metadata.json +0 -21
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/pair-plan.sample-fail.json +0 -214
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/pair-plan.sample-pass.json +0 -223
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/setup.sh +0 -5
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/spec.md +0 -55
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/task.txt +0 -14
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/NOTES.md +0 -39
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/expected.json +0 -62
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/setup.sh +0 -2
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/spec.md +0 -67
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/verifiers/error-order.js +0 -55
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/verifiers/priority-blocked.js +0 -48
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/NOTES.md +0 -35
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/expected.json +0 -56
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/setup.sh +0 -2
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/spec.md +0 -64
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/verifiers/conflicting-duplicate.js +0 -34
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/verifiers/idempotent-close.js +0 -41
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/NOTES.md +0 -39
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/expected.json +0 -56
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/setup.sh +0 -2
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/spec.md +0 -76
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/priority-rollback.js +0 -64
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/single-warehouse-fefo.js +0 -66
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/NOTES.md +0 -40
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/expected.json +0 -66
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/setup.sh +0 -36
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/spec.md +0 -71
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/catalog-source.js +0 -57
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/exact-success.js +0 -63
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/stock-error.js +0 -34
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/NOTES.md +0 -31
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/expected.json +0 -68
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/setup.sh +0 -17
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/spec.md +0 -68
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/conflicting-duplicate.js +0 -29
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/exact-payout.js +0 -58
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/rules-source.js +0 -56
- package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/NOTES.md +0 -35
- package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/expected-pair-plan-registry.json +0 -162
- package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/expected.json +0 -67
- package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/metadata.json +0 -19
- package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/setup.sh +0 -4
- package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/spec.md +0 -55
- package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/task.txt +0 -9
- package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/NOTES.md +0 -34
- package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/expected.json +0 -57
- package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/setup.sh +0 -2
- package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/spec.md +0 -67
- package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/verifiers/duplicate-event-error.js +0 -35
- package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/verifiers/priority-transfer-rollback.js +0 -53
- package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/NOTES.md +0 -38
- package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/expected.json +0 -57
- package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/setup.sh +0 -2
- package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/spec.md +0 -70
- package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/task.txt +0 -3
- package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/verifiers/duplicate-renewal-error.js +0 -42
- package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/verifiers/priority-credit-rollback.js +0 -70
- package/benchmark/auto-resolve/fixtures/F4-web-browser-design/NOTES.md +0 -47
- package/benchmark/auto-resolve/fixtures/F4-web-browser-design/expected.json +0 -57
- package/benchmark/auto-resolve/fixtures/F4-web-browser-design/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F4-web-browser-design/setup.sh +0 -6
- package/benchmark/auto-resolve/fixtures/F4-web-browser-design/spec.md +0 -48
- package/benchmark/auto-resolve/fixtures/F4-web-browser-design/task.txt +0 -9
- package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/NOTES.md +0 -45
- package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/expected.json +0 -65
- package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/setup.sh +0 -55
- package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/spec.md +0 -48
- package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/NOTES.md +0 -43
- package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/expected.json +0 -77
- package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/setup.sh +0 -4
- package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/spec.md +0 -48
- package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/task.txt +0 -10
- package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/NOTES.md +0 -57
- package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/expected.json +0 -76
- package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/setup.sh +0 -36
- package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/spec.md +0 -45
- package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/NOTES.md +0 -53
- package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/expected.json +0 -63
- package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/setup.sh +0 -4
- package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/spec.md +0 -48
- package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/task.txt +0 -1
- package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/NOTES.md +0 -105
- package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/expected.json +0 -74
- package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/setup.sh +0 -28
- package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/spec.md +0 -59
- package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/task.txt +0 -5
- package/benchmark/auto-resolve/fixtures/SCHEMA.md +0 -188
- package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/NOTES.md +0 -37
- package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/RETIRED.md +0 -13
- package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/expected.json +0 -56
- package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/setup.sh +0 -18
- package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/spec.md +0 -69
- package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/task.txt +0 -7
- package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/verifiers/exact-proration.js +0 -48
- package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/verifiers/rules-source-and-conflict.js +0 -79
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/NOTES.md +0 -54
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/RETIRED.md +0 -7
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/expected.json +0 -67
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/setup.sh +0 -2
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/spec.md +0 -67
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/task.txt +0 -5
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/verifiers/policy-precedence.js +0 -72
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/verifiers/validation-and-immutability.js +0 -43
- package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/verifiers/validation-boundary.js +0 -116
- package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/NOTES.md +0 -35
- package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/RETIRED.md +0 -12
- package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/expected.json +0 -58
- package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/setup.sh +0 -2
- package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/spec.md +0 -73
- package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/task.txt +0 -17
- package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/verifiers/mixed-idempotent-settlement.js +0 -53
- package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/verifiers/rejection-boundaries.js +0 -74
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/NOTES.md +0 -28
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/RETIRED.md +0 -15
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/expected.json +0 -57
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/setup.sh +0 -50
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/spec.md +0 -59
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/task.txt +0 -9
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/verifiers/ordered-settlement.js +0 -88
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/verifiers/reject-invalid.js +0 -45
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/NOTES.md +0 -30
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/RETIRED.md +0 -16
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/expected.json +0 -64
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/setup.sh +0 -21
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/spec.md +0 -58
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/task.txt +0 -9
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/dup-op.js +0 -54
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/priority-apply.js +0 -82
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/rollback.js +0 -77
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/NOTES.md +0 -99
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/RETIRED.md +0 -18
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/expected.json +0 -64
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/setup.sh +0 -4
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/spec.md +0 -61
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/task.txt +0 -9
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/correctness-small.js +0 -135
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/scale.js +0 -100
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/validation.js +0 -75
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/NOTES.md +0 -93
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/RETIRED.md +0 -16
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/expected.json +0 -65
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/setup.sh +0 -18
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/spec.md +0 -66
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/task.txt +0 -9
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/correctness-small.js +0 -84
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/scale.js +0 -184
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/validation.js +0 -74
- package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/NOTES.md +0 -60
- package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/RETIRED.md +0 -29
- package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/expected.json +0 -73
- package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/metadata.json +0 -10
- package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/setup.sh +0 -28
- package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/spec.md +0 -58
- package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/task.txt +0 -5
- package/benchmark/auto-resolve/fixtures/test-repo/README.md +0 -12
- package/benchmark/auto-resolve/fixtures/test-repo/bin/cli.js +0 -63
- package/benchmark/auto-resolve/fixtures/test-repo/package-lock.json +0 -823
- package/benchmark/auto-resolve/fixtures/test-repo/package.json +0 -22
- package/benchmark/auto-resolve/fixtures/test-repo/playwright.config.js +0 -17
- package/benchmark/auto-resolve/fixtures/test-repo/server/index.js +0 -37
- package/benchmark/auto-resolve/fixtures/test-repo/tests/cli.test.js +0 -25
- package/benchmark/auto-resolve/fixtures/test-repo/tests/server.test.js +0 -58
- package/benchmark/auto-resolve/fixtures/test-repo/web/index.html +0 -37
- package/benchmark/auto-resolve/results/20260507-f10-f11-tier1-full-pipeline/headroom-gate.json +0 -25
- package/benchmark/auto-resolve/results/20260507-f10-f11-tier1-full-pipeline/headroom-gate.md +0 -10
- package/benchmark/auto-resolve/results/20260508-f22-exact-error-headroom/headroom-gate.json +0 -18
- package/benchmark/auto-resolve/results/20260508-f22-exact-error-headroom/headroom-gate.md +0 -9
- package/benchmark/auto-resolve/results/20260508-f26-headroom/headroom-gate.json +0 -18
- package/benchmark/auto-resolve/results/20260508-f26-headroom/headroom-gate.md +0 -9
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F16-cli-quote-tax-rules/l2_risk_probes/result.json +0 -28
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F23-cli-fulfillment-wave/l2_risk_probes/result.json +0 -28
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F25-cli-cart-promotion-rules/l2_risk_probes/result.json +0 -30
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/full-pipeline-pair-gate.json +0 -82
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/full-pipeline-pair-gate.md +0 -18
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/headroom-gate.json +0 -46
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/headroom-gate.md +0 -17
- package/benchmark/auto-resolve/results/20260511-f12-webhook-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260511-f12-webhook-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260511-f15-concurrency-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260511-f15-concurrency-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/F21-cli-scheduler-priority/l2_risk_probes/result.json +0 -32
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/full-pipeline-pair-gate.json +0 -44
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/full-pipeline-pair-gate.md +0 -16
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/headroom-gate.json +0 -18
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/headroom-gate.md +0 -9
- package/benchmark/auto-resolve/results/20260511-f3-http-error-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260511-f3-http-error-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260512-f2-medium-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260512-f2-medium-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260512-f31-seat-rebalance-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260512-f31-seat-rebalance-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260512-f32-subscription-renewal-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260512-f32-subscription-renewal-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260512-f4-web-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260512-f4-web-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260512-f5-fixloop-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260512-f5-fixloop-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260512-f6-checksum-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260512-f6-checksum-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260512-f7-scope-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260512-f7-scope-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/results/20260512-f9-e2e-headroom/headroom-gate.json +0 -28
- package/benchmark/auto-resolve/results/20260512-f9-e2e-headroom/headroom-gate.md +0 -15
- package/benchmark/auto-resolve/run-real-benchmark.md +0 -303
- package/benchmark/auto-resolve/scripts/audit-headroom-rejections.py +0 -441
- package/benchmark/auto-resolve/scripts/audit-pair-evidence.py +0 -1256
- package/benchmark/auto-resolve/scripts/build-pair-eligible-manifest.py +0 -306
- package/benchmark/auto-resolve/scripts/check-f9-artifacts.py +0 -268
- package/benchmark/auto-resolve/scripts/collect-swebench-predictions.py +0 -108
- package/benchmark/auto-resolve/scripts/compile-report.py +0 -493
- package/benchmark/auto-resolve/scripts/fetch-swebench-instances.py +0 -129
- package/benchmark/auto-resolve/scripts/frozen-verify-gate.py +0 -434
- package/benchmark/auto-resolve/scripts/full-pipeline-pair-gate.py +0 -612
- package/benchmark/auto-resolve/scripts/headroom-gate.py +0 -378
- package/benchmark/auto-resolve/scripts/iter-0033c-compare.py +0 -683
- package/benchmark/auto-resolve/scripts/iter-0033c-l1-summary.py +0 -97
- package/benchmark/auto-resolve/scripts/judge-opus-pass.sh +0 -542
- package/benchmark/auto-resolve/scripts/judge.sh +0 -565
- package/benchmark/auto-resolve/scripts/oracle-scope-tier-a.py +0 -256
- package/benchmark/auto-resolve/scripts/oracle-scope-tier-b.py +0 -287
- package/benchmark/auto-resolve/scripts/oracle-test-fidelity.py +0 -328
- package/benchmark/auto-resolve/scripts/pair-candidate-frontier.py +0 -469
- package/benchmark/auto-resolve/scripts/pair-plan-idgen.py +0 -401
- package/benchmark/auto-resolve/scripts/pair-plan-lint.py +0 -475
- package/benchmark/auto-resolve/scripts/pair-rejected-fixtures.sh +0 -103
- package/benchmark/auto-resolve/scripts/pair_evidence_contract.py +0 -270
- package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-case.py +0 -273
- package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-corpus.py +0 -148
- package/benchmark/auto-resolve/scripts/prepare-swebench-solver-worktree.py +0 -210
- package/benchmark/auto-resolve/scripts/recent-benchmark-summary.py +0 -232
- package/benchmark/auto-resolve/scripts/run-fixture.sh +0 -1024
- package/benchmark/auto-resolve/scripts/run-frozen-verify-pair.sh +0 -683
- package/benchmark/auto-resolve/scripts/run-full-pipeline-pair-candidate.sh +0 -458
- package/benchmark/auto-resolve/scripts/run-headroom-candidate.sh +0 -336
- package/benchmark/auto-resolve/scripts/run-iter-0033c.sh +0 -208
- package/benchmark/auto-resolve/scripts/run-suite.sh +0 -251
- package/benchmark/auto-resolve/scripts/run-swebench-frozen-corpus.sh +0 -310
- package/benchmark/auto-resolve/scripts/run-swebench-solver-batch.sh +0 -257
- package/benchmark/auto-resolve/scripts/ship-gate.py +0 -391
- package/benchmark/auto-resolve/scripts/solo-ceiling-avoidance.py +0 -53
- package/benchmark/auto-resolve/scripts/solo-headroom-hypothesis.py +0 -77
- package/benchmark/auto-resolve/scripts/swebench-frozen-matrix.py +0 -478
- package/benchmark/auto-resolve/scripts/test-audit-headroom-rejections.sh +0 -288
- package/benchmark/auto-resolve/scripts/test-audit-pair-evidence.sh +0 -1672
- package/benchmark/auto-resolve/scripts/test-benchmark-arg-parsing.sh +0 -1015
- package/benchmark/auto-resolve/scripts/test-build-pair-eligible-manifest.sh +0 -491
- package/benchmark/auto-resolve/scripts/test-check-f9-artifacts.sh +0 -91
- package/benchmark/auto-resolve/scripts/test-frozen-verify-gate.sh +0 -517
- package/benchmark/auto-resolve/scripts/test-full-pipeline-pair-gate.sh +0 -610
- package/benchmark/auto-resolve/scripts/test-headroom-gate.sh +0 -401
- package/benchmark/auto-resolve/scripts/test-iter-0033c-compare.sh +0 -525
- package/benchmark/auto-resolve/scripts/test-iter-0033c-l1-summary.sh +0 -254
- package/benchmark/auto-resolve/scripts/test-lint-fixtures.sh +0 -580
- package/benchmark/auto-resolve/scripts/test-pair-candidate-frontier.sh +0 -591
- package/benchmark/auto-resolve/scripts/test-run-full-pipeline-pair-candidate.sh +0 -497
- package/benchmark/auto-resolve/scripts/test-run-headroom-candidate.sh +0 -401
- package/benchmark/auto-resolve/scripts/test-run-swebench-solver-batch.sh +0 -111
- package/benchmark/auto-resolve/scripts/test-ship-gate.sh +0 -1189
- package/benchmark/auto-resolve/scripts/test-swebench-frozen-case.sh +0 -1221
- package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/NOTES.md +0 -28
- package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/expected.json +0 -63
- package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/metadata.json +0 -10
- package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/setup.sh +0 -3
- package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/spec.md +0 -47
- package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/task.txt +0 -1
- package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/NOTES.md +0 -34
- package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/expected.json +0 -53
- package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/metadata.json +0 -10
- package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/setup.sh +0 -3
- package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/spec.md +0 -50
- package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/task.txt +0 -1
- package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/verifiers/duplicate-order-error.js +0 -27
- package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/verifiers/priority-stock-reservation.js +0 -44
- package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/NOTES.md +0 -34
- package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/expected.json +0 -55
- package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/metadata.json +0 -10
- package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/setup.sh +0 -3
- package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/spec.md +0 -52
- package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/task.txt +0 -1
- package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/verifiers/duplicate-ticket-error.js +0 -29
- package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/verifiers/priority-agent-assignment.js +0 -48
- package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/NOTES.md +0 -34
- package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/expected.json +0 -55
- package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/metadata.json +0 -10
- package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/setup.sh +0 -3
- package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/spec.md +0 -55
- package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/task.txt +0 -1
- package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/verifiers/duplicate-return-error.js +0 -43
- package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/verifiers/priority-return-routing.js +0 -70
- package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/NOTES.md +0 -37
- package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/expected.json +0 -54
- package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/metadata.json +0 -10
- package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/setup.sh +0 -3
- package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/spec.md +0 -59
- package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/task.txt +0 -1
- package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/verifiers/credit-ledger-priority.js +0 -98
- package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/verifiers/duplicate-charge-error.js +0 -38
- package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/NOTES.md +0 -36
- package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/expected.json +0 -56
- package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/metadata.json +0 -10
- package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/setup.sh +0 -3
- package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/spec.md +0 -59
- package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/task.txt +0 -1
- package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/verifiers/duplicate-refund-error.js +0 -41
- package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/verifiers/priority-refund-ledger.js +0 -65
- package/config/skills/_shared/pair-plan-schema.md +0 -298
- package/scripts/lint-fixtures.sh +0 -352
- package/scripts/lint-shadow-fixtures.sh +0 -58
- package/scripts/lint-skills.sh +0 -4265
- package/scripts/test-owner-phases.py +0 -193
|
@@ -172,7 +172,7 @@ def policy(receipt, override):
|
|
|
172
172
|
require(config.returncode == 1 or config.stdout in {"auto\n", "pr\n"}, "invalid local devlyn.completionMode; use auto|pr")
|
|
173
173
|
value = config.stdout.strip() if config.returncode == 0 else None
|
|
174
174
|
require(override is None or override in {"auto", "pr"}, "invalid task mode; use auto|pr")
|
|
175
|
-
return override or value or "
|
|
175
|
+
return override or value or "pr"
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
def allocate(args):
|
|
@@ -185,18 +185,15 @@ def allocate(args):
|
|
|
185
185
|
require(re.fullmatch(r"[A-Za-z0-9_.-]+", args.remote), "unsafe remote name")
|
|
186
186
|
receipt = {"task": args.task, "repository": args.repository, "remote": args.remote,
|
|
187
187
|
"base": args.base, "branch": args.branch, "common_gitdir": str(common),
|
|
188
|
-
"anchor": str(work), "linked":
|
|
188
|
+
"anchor": str(work), "linked": True, "allocation": "allocating"}
|
|
189
189
|
policy(receipt, None)
|
|
190
190
|
receipt["remote_url"] = remote_url(receipt)
|
|
191
191
|
require("\n" not in receipt["remote_url"]["push"] and receipt["remote_url"]["push"] == receipt["remote_url"]["fetch"], "split/multiple remote URLs are unsupported")
|
|
192
192
|
require(not ref_sha(receipt, "refs/heads/"+args.branch), "existing branch cannot be adopted")
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
receipt["baseline"] = git(work, "rev-parse", "HEAD")
|
|
196
|
-
target = Path(args.worktree).absolute() if args.worktree else work
|
|
193
|
+
receipt["baseline"] = remote_base(receipt)
|
|
194
|
+
target = Path(args.worktree).absolute()
|
|
197
195
|
require(target == target.resolve(), "worktree path must not traverse symlinks")
|
|
198
|
-
|
|
199
|
-
require(not target.exists() and not target.is_relative_to(work) and not work.is_relative_to(target), "linked worktree must be an absent disjoint path")
|
|
196
|
+
require(not target.exists() and all(not target.is_relative_to(p) and not p.is_relative_to(target) for p in map(Path, registrations(receipt))), "linked worktree must be an absent path disjoint from every registered worktree")
|
|
200
197
|
receipt["worktree"] = str(target)
|
|
201
198
|
key = hashlib.sha256(args.branch.encode()).hexdigest()[:24]
|
|
202
199
|
directory = common / "devlyn-completion" / key
|
|
@@ -210,15 +207,13 @@ def allocate(args):
|
|
|
210
207
|
# Persist prospective intent BEFORE native creation. An interrupted allocation
|
|
211
208
|
# stays visibly incomplete; a later call may not adopt whatever now exists.
|
|
212
209
|
atomic_json(path, receipt)
|
|
213
|
-
|
|
214
|
-
git(work, "worktree", "add", "-b", args.branch, str(target), receipt["baseline"])
|
|
215
|
-
else:
|
|
216
|
-
git(work, "switch", "-c", args.branch, receipt["baseline"])
|
|
210
|
+
gref(receipt, "worktree", "add", "-b", args.branch, str(target), receipt["baseline"])
|
|
217
211
|
receipt["worktree_gitdir"] = git(target, "rev-parse", "--absolute-git-dir")
|
|
218
212
|
receipt["worktree_identity"] = workspace_identity(target, Path(receipt["worktree_gitdir"]))
|
|
219
213
|
receipt["allocation"] = "owned"
|
|
220
214
|
atomic_json(path, receipt)
|
|
221
|
-
return {"status": "ALLOCATED", "receipt": str(path), "worktree": str(target), "scratch": str(scratch)
|
|
215
|
+
return {"status": "ALLOCATED", "receipt": str(path), "worktree": str(target), "scratch": str(scratch),
|
|
216
|
+
"reconciled": reconcile(common, path, work)}
|
|
222
217
|
|
|
223
218
|
|
|
224
219
|
def pipeline_acceptance(work, acceptance, files, directory):
|
|
@@ -298,8 +293,6 @@ def bind_acceptance(receipt, path, supplied):
|
|
|
298
293
|
if supplied:
|
|
299
294
|
require(file_record(Path(supplied))["sha256"] == receipt["acceptance_digest"], "acceptance changed; resume with the original acceptance")
|
|
300
295
|
verify_files(path.parent / "custody", receipt["files"])
|
|
301
|
-
if work.exists() and not receipt.get("cleanup_started"):
|
|
302
|
-
verify_files(work, receipt["files"])
|
|
303
296
|
return
|
|
304
297
|
require(supplied, "first completion requires explicit root acceptance")
|
|
305
298
|
acceptance_path = Path(supplied).absolute()
|
|
@@ -377,6 +370,13 @@ def repo_policy(receipt):
|
|
|
377
370
|
return info
|
|
378
371
|
|
|
379
372
|
|
|
373
|
+
def remote_base(receipt):
|
|
374
|
+
# Never write FETCH_HEAD: a concurrent `git pull` in the anchor would merge ours.
|
|
375
|
+
tracking = "refs/remotes/" + receipt["remote"] + "/" + receipt["base"]
|
|
376
|
+
gref(receipt, "fetch", "--no-tags", "--no-write-fetch-head", receipt["remote"], "+refs/heads/" + receipt["base"] + ":" + tracking)
|
|
377
|
+
return gref(receipt, "rev-parse", "--verify", tracking)
|
|
378
|
+
|
|
379
|
+
|
|
380
380
|
def remote_head(receipt, branch):
|
|
381
381
|
rows = gref(receipt, "ls-remote", "--heads", receipt["remote"], "refs/heads/"+branch).splitlines()
|
|
382
382
|
require(len(rows) <= 1, "ambiguous remote ref")
|
|
@@ -395,24 +395,35 @@ def workspace_identity(work, gitdir):
|
|
|
395
395
|
return [[p.stat().st_dev, p.stat().st_ino] for p in (work, gitdir)]
|
|
396
396
|
|
|
397
397
|
|
|
398
|
-
def inspect_workspace(receipt
|
|
398
|
+
def inspect_workspace(receipt):
|
|
399
399
|
work = Path(receipt["worktree"])
|
|
400
400
|
row = registrations(receipt).get(str(work))
|
|
401
401
|
require(row and "locked" not in row and "prunable" not in row and "detached" not in row, "worktree missing, detached or locked; retain and inspect registration")
|
|
402
402
|
expected = "refs/heads/" + receipt["branch"]
|
|
403
|
-
|
|
404
|
-
require(row.get("branch") == expected or returned, "foreign worktree branch; retain workspace")
|
|
403
|
+
require(row.get("branch") == expected, "foreign worktree branch; retain workspace")
|
|
405
404
|
require(git(work, "rev-parse", "--absolute-git-dir") == receipt["worktree_gitdir"] and str(Path(git(work, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve()) == receipt["common_gitdir"], "worktree Gitdir ownership changed")
|
|
406
405
|
require(workspace_identity(work, Path(receipt["worktree_gitdir"])) == receipt["worktree_identity"], "worktree/registration was replaced; receipt no longer owns it")
|
|
407
|
-
|
|
408
|
-
require(git(work, "rev-parse", "HEAD") == receipt.get("publish_sha", receipt["baseline"]), "task HEAD changed")
|
|
406
|
+
require(git(work, "rev-parse", "HEAD") == receipt.get("publish_sha", receipt["baseline"]), "task HEAD changed")
|
|
409
407
|
require(not git(work, "status", "--porcelain", "--untracked-files=all"), "dirty/untracked workspace; retain files and commit only accepted scope")
|
|
410
|
-
return returned
|
|
411
408
|
|
|
412
409
|
|
|
413
|
-
def
|
|
414
|
-
require(not
|
|
415
|
-
require(not
|
|
410
|
+
def outside(work):
|
|
411
|
+
require(not Path.cwd().resolve().is_relative_to(work), "caller cwd is inside removable worktree; yield it and resume from outside")
|
|
412
|
+
require(not Path(__file__).resolve().is_relative_to(work), "invoke the installed helper outside the removable worktree so resume remains available")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def removable(receipt, work):
|
|
416
|
+
inspect_workspace(receipt)
|
|
417
|
+
verify_files(work, receipt["files"])
|
|
418
|
+
# Git lists a nested repository or worktree under an ignored path as `dir/`;
|
|
419
|
+
# native removal would delete it with its uncommitted work.
|
|
420
|
+
nested = [p for p in git(work, "ls-files", "--others", "--ignored", "--exclude-standard", "-z").split("\0") if p.endswith("/")]
|
|
421
|
+
require(not nested, "nested repository or worktree inside the task tree: " + ", ".join(nested) + "; retain")
|
|
422
|
+
stopped_writers(work)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def stopped_writers(work):
|
|
426
|
+
outside(work)
|
|
416
427
|
# The owner assertion covers its actual children; an OS observation catches
|
|
417
428
|
# other currently open files/cwds, not future writers or a universal lease.
|
|
418
429
|
if sys.platform == "darwin":
|
|
@@ -431,10 +442,15 @@ def stopped_writers(work, *, linked):
|
|
|
431
442
|
try:
|
|
432
443
|
links = [process / "cwd", *(process / "fd").iterdir()]
|
|
433
444
|
for link in links:
|
|
434
|
-
|
|
445
|
+
try:
|
|
446
|
+
target = Path(os.readlink(link))
|
|
447
|
+
except FileNotFoundError:
|
|
448
|
+
if link == process / "cwd":
|
|
449
|
+
break
|
|
450
|
+
continue
|
|
435
451
|
require(not target.is_absolute() or not target.is_relative_to(work), f"active process {process.name} uses task files; stop/yield it before resume")
|
|
436
452
|
except FileNotFoundError:
|
|
437
|
-
continue # Process
|
|
453
|
+
continue # Process exited during observation.
|
|
438
454
|
except PermissionError as exc:
|
|
439
455
|
raise CompletionError("unknown process access; retain tree until writer cessation can be established") from exc
|
|
440
456
|
else:
|
|
@@ -478,7 +494,7 @@ def clean_scratch(receipt, path, writers_stopped):
|
|
|
478
494
|
require(workspace_identity(scratch, path.parent) == identity, "scratch directory was replaced; retain")
|
|
479
495
|
require(shutil.rmtree.avoids_symlink_attacks, "safe scratch removal unsupported on this platform; retain")
|
|
480
496
|
require(not any(mount.is_relative_to(scratch) for mount in scratch_mounts()), "scratch contains a mounted filesystem; retain")
|
|
481
|
-
stopped_writers(scratch
|
|
497
|
+
stopped_writers(scratch)
|
|
482
498
|
size = 0
|
|
483
499
|
for directory, dirs, files in os.walk(scratch, followlinks=False, onerror=raise_walk_error):
|
|
484
500
|
require(not any(name.casefold() == ".git" for name in dirs + files), "scratch contains Git recovery data; move it to retained custody before cleanup")
|
|
@@ -487,7 +503,7 @@ def clean_scratch(receipt, path, writers_stopped):
|
|
|
487
503
|
info = item.lstat()
|
|
488
504
|
require(info.st_dev == identity[0][0], "scratch contains a mounted filesystem; retain")
|
|
489
505
|
size += info.st_size if stat.S_ISREG(info.st_mode) else 0
|
|
490
|
-
stopped_writers(scratch
|
|
506
|
+
stopped_writers(scratch)
|
|
491
507
|
require(workspace_identity(scratch, path.parent) == identity, "scratch changed before removal; retain")
|
|
492
508
|
require(not any(mount.is_relative_to(scratch) for mount in scratch_mounts()), "scratch mount appeared before removal; retain")
|
|
493
509
|
for item in scratch.iterdir():
|
|
@@ -510,57 +526,47 @@ def clean_scratch_command(args):
|
|
|
510
526
|
"receipt": str(path), "scratch_cleanup": result, "product_verdict_unchanged": True}
|
|
511
527
|
|
|
512
528
|
|
|
513
|
-
def cleanup(receipt, path, pr
|
|
529
|
+
def cleanup(receipt, path, pr):
|
|
514
530
|
require(pr["state"] == "MERGED" and pr.get("mergedAt") and pr.get("mergeCommit", {}).get("oid"), "actual matching merge evidence is required")
|
|
515
|
-
require(writers_stopped, "wait actual children, stop/yield known writers, then resume with --writers-stopped")
|
|
516
531
|
sha = receipt["publish_sha"]
|
|
517
532
|
branch_ref = "refs/heads/" + receipt["branch"]
|
|
533
|
+
# The recovery ref keeps the accepted commit reachable, so squash and
|
|
534
|
+
# rebase merges need only the merge commit on base.
|
|
518
535
|
require(ref_sha(receipt, receipt["recovery_ref"]) == sha, "recovery reachability changed; retain resources")
|
|
519
536
|
verify_files(path.parent / "custody", receipt["files"])
|
|
520
537
|
require(read_json(path.parent / "manifest.json") == receipt["files"], "custody manifest changed")
|
|
521
|
-
gref(receipt, "
|
|
522
|
-
base_sha = gref(receipt, "rev-parse", "FETCH_HEAD")
|
|
523
|
-
gref(receipt, "merge-base", "--is-ancestor", pr["mergeCommit"]["oid"], base_sha)
|
|
524
|
-
gref(receipt, "merge-base", "--is-ancestor", sha, pr["mergeCommit"]["oid"])
|
|
538
|
+
gref(receipt, "merge-base", "--is-ancestor", pr["mergeCommit"]["oid"], remote_base(receipt))
|
|
525
539
|
gref(receipt, "merge-base", "--is-ancestor", receipt["source_sha"], sha)
|
|
526
540
|
work = Path(receipt["worktree"])
|
|
527
541
|
local = ref_sha(receipt, branch_ref)
|
|
528
|
-
require(local
|
|
529
|
-
if
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
verify_files(work, receipt["files"])
|
|
533
|
-
if receipt["linked"]:
|
|
542
|
+
require(local in {None, sha}, "local task ref changed; retain")
|
|
543
|
+
if receipt["linked"]:
|
|
544
|
+
if work.exists():
|
|
545
|
+
require(receipt.get("writers_released"), "wait actual children, stop/yield known writers, then resume with --writers-stopped")
|
|
534
546
|
require(work != Path(receipt["anchor"]) and work != Path(receipt["common_gitdir"]).parent, "cannot remove retained/main checkout")
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
547
|
+
removable(receipt, work)
|
|
548
|
+
devlyn = safe_path(work, ".devlyn")
|
|
549
|
+
if devlyn.exists():
|
|
550
|
+
require(devlyn.is_dir(), ".devlyn records must be a directory; retain tree")
|
|
551
|
+
paths = [str((Path(directory) / name).relative_to(work))
|
|
552
|
+
for directory, dirs, files in os.walk(devlyn, onerror=raise_walk_error)
|
|
553
|
+
for name in files + [name for name in dirs if (Path(directory) / name).is_symlink()]]
|
|
554
|
+
if paths:
|
|
555
|
+
records = snapshot_files(work, paths)
|
|
556
|
+
destination = path.parent / "records"
|
|
557
|
+
if not destination.exists():
|
|
558
|
+
with tempfile.TemporaryDirectory(prefix=".records-", dir=path.parent) as temporary:
|
|
559
|
+
shutil.copytree(devlyn, Path(temporary) / ".devlyn", symlinks=True)
|
|
560
|
+
verify_files(Path(temporary), records)
|
|
561
|
+
Path(temporary).rename(destination)
|
|
562
|
+
verify_files(destination, records)
|
|
563
|
+
verify_files(work, records)
|
|
564
|
+
removable(receipt, work)
|
|
542
565
|
gref(receipt, "worktree", "remove", str(work))
|
|
543
566
|
else:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
base_ref = "refs/heads/" + receipt["base"]
|
|
548
|
-
old_base = ref_sha(receipt, base_ref)
|
|
549
|
-
require(old_base, "local base branch is missing")
|
|
550
|
-
gref(receipt, "merge-base", "--is-ancestor", old_base, base_sha)
|
|
551
|
-
receipt["cleanup_started"] = True
|
|
552
|
-
atomic_json(path, receipt)
|
|
553
|
-
inspect_workspace(receipt, allow_returned=True)
|
|
554
|
-
if not returned:
|
|
555
|
-
other = [row for p, row in registrations(receipt).items() if p != str(work) and row.get("branch") == base_ref]
|
|
556
|
-
require(not other, "base checked out elsewhere; retain task checkout")
|
|
557
|
-
gref(receipt, "update-ref", base_ref, base_sha, old_base)
|
|
558
|
-
git(work, "switch", "--no-overwrite-ignore", receipt["base"])
|
|
559
|
-
else:
|
|
560
|
-
git(work, "merge", "--no-overwrite-ignore", "--ff-only", base_sha)
|
|
561
|
-
else:
|
|
562
|
-
require(receipt["linked"] and receipt.get("cleanup_started") and str(work) not in registrations(receipt), "missing/replaced worktree was not removed by this receipt")
|
|
563
|
-
require(all(row.get("branch") != branch_ref for row in registrations(receipt).values()), "task branch is still checked out; retain ref")
|
|
567
|
+
require(str(work) not in registrations(receipt), "missing worktree is still registered; retain")
|
|
568
|
+
checked_out = [p for p, row in registrations(receipt).items() if row.get("branch") == branch_ref]
|
|
569
|
+
require(not checked_out, "task branch is still checked out at " + ", ".join(checked_out) + "; switch it off the task branch before cleanup")
|
|
564
570
|
# Check ownership/ref identity once more immediately before each deletion.
|
|
565
571
|
repo_policy(receipt)
|
|
566
572
|
require(remote_url(receipt) == receipt["remote_url"], "remote changed before deletion")
|
|
@@ -578,12 +584,12 @@ def cleanup(receipt, path, pr, writers_stopped):
|
|
|
578
584
|
|
|
579
585
|
|
|
580
586
|
@contextlib.contextmanager
|
|
581
|
-
def locked_receipt(path):
|
|
587
|
+
def locked_receipt(path, *, blocking=True):
|
|
582
588
|
require(path.name == "receipt.json" and path.parent.parent.name == "devlyn-completion" and path == path.resolve(), "receipt must be its original external nonsymlink path")
|
|
583
589
|
require(not (path.parent / "lock").is_symlink(), "receipt lock must not be a symlink")
|
|
584
590
|
with contextlib.ExitStack() as stack:
|
|
585
591
|
try:
|
|
586
|
-
stack.enter_context(shared("platform-support")["file_lock"](path.parent / "lock", blocking=
|
|
592
|
+
stack.enter_context(shared("platform-support")["file_lock"](path.parent / "lock", blocking=blocking))
|
|
587
593
|
except (ImportError, OSError, AttributeError) as exc:
|
|
588
594
|
raise CompletionError(f"receipt lock unavailable: {path}: {exc}") from exc
|
|
589
595
|
receipt = read_json(path)
|
|
@@ -593,29 +599,73 @@ def locked_receipt(path):
|
|
|
593
599
|
yield receipt
|
|
594
600
|
|
|
595
601
|
|
|
602
|
+
def completion_result(receipt, path, status):
|
|
603
|
+
released = receipt.get("writers_released", False)
|
|
604
|
+
resume = shlex.join([sys.executable, str(Path(__file__).resolve()), "complete", "--receipt", str(path)] +
|
|
605
|
+
(["--writers-stopped"] if released else []))
|
|
606
|
+
scratch = {"status": "NOT_OWNED"}
|
|
607
|
+
if "scratch_identity" in receipt:
|
|
608
|
+
try:
|
|
609
|
+
scratch = clean_scratch(receipt, path, released)
|
|
610
|
+
except (CompletionError, OSError, ValueError, KeyError, TypeError, IndexError) as error:
|
|
611
|
+
scratch = {"status": "RETAINED", "reason": str(error), "resume": shlex.join([
|
|
612
|
+
sys.executable, str(Path(__file__).resolve()), "clean-scratch", "--receipt", str(path), "--writers-stopped"])}
|
|
613
|
+
receipt["scratch_cleanup"] = scratch
|
|
614
|
+
try:
|
|
615
|
+
atomic_json(path, receipt)
|
|
616
|
+
except OSError as record_error:
|
|
617
|
+
scratch["record_error"] = str(record_error)
|
|
618
|
+
return {"status": "CLEANUP_PENDING" if status == "COMPLETE" and scratch["status"] == "RETAINED" else status,
|
|
619
|
+
"delivery_status": status, "receipt": str(path), "pr": receipt.get("pr_url"), "resume": resume,
|
|
620
|
+
"acceptance": (receipt.get("acceptance") or {}).get("kind"), "product_verdict_unchanged": True,
|
|
621
|
+
"scratch_path": str(path.parent / "scratch") if "scratch_identity" in receipt else None,
|
|
622
|
+
"scratch_cleanup": scratch}
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def reconcile(common, allocated, anchor):
|
|
626
|
+
results = []
|
|
627
|
+
for path in sorted((common / "devlyn-completion").glob("*/receipt.json")):
|
|
628
|
+
if path == allocated:
|
|
629
|
+
continue
|
|
630
|
+
try:
|
|
631
|
+
receipt = read_json(path)
|
|
632
|
+
if receipt.get("allocation") != "owned" or not receipt.get("acceptance") or not receipt.get("pr_number") or receipt.get("local_only"):
|
|
633
|
+
continue
|
|
634
|
+
if receipt.get("status") == "COMPLETE" and (not receipt.get("writers_released") or "scratch_identity" not in receipt or receipt.get("scratch_cleanup", {}).get("status") == "CLEAN"):
|
|
635
|
+
continue
|
|
636
|
+
with locked_receipt(path, blocking=False) as receipt:
|
|
637
|
+
if receipt.get("status") == "COMPLETE":
|
|
638
|
+
if receipt.get("writers_released") and "scratch_identity" in receipt and receipt.get("scratch_cleanup", {}).get("status") != "CLEAN":
|
|
639
|
+
results.append(completion_result(receipt, path, "COMPLETE"))
|
|
640
|
+
continue
|
|
641
|
+
if not receipt.get("acceptance") or not receipt.get("pr_number") or receipt.get("local_only"):
|
|
642
|
+
continue
|
|
643
|
+
pr = json.loads(gh(receipt, "pr", "view", str(receipt["pr_number"]), "--json", PR_FIELDS))
|
|
644
|
+
if pr["state"] != "MERGED":
|
|
645
|
+
continue
|
|
646
|
+
validate_pr(receipt, pr)
|
|
647
|
+
require(not receipt["linked"] or Path(receipt["worktree"]) != anchor, "cannot clean the current allocation's anchor checkout: " + str(anchor))
|
|
648
|
+
require(remote_url(receipt) == receipt["remote_url"], "remote URL changed since allocation")
|
|
649
|
+
repo_policy(receipt)
|
|
650
|
+
receipt["merge"] = pr
|
|
651
|
+
atomic_json(path, receipt)
|
|
652
|
+
cleanup(receipt, path, pr)
|
|
653
|
+
results.append(completion_result(receipt, path, "COMPLETE"))
|
|
654
|
+
except (Exception, SystemExit) as error:
|
|
655
|
+
results.append({"receipt": str(path), "status": "RETAINED", "reason": str(error)})
|
|
656
|
+
return results
|
|
657
|
+
|
|
658
|
+
|
|
596
659
|
def complete(args):
|
|
597
660
|
path = Path(args.receipt).absolute()
|
|
598
|
-
resume = shlex.join([sys.executable, str(Path(__file__).resolve()), "complete", "--receipt", str(path)] +
|
|
599
|
-
(["--writers-stopped"] if args.writers_stopped else []))
|
|
600
661
|
with locked_receipt(path) as receipt:
|
|
662
|
+
if args.writers_stopped:
|
|
663
|
+
if receipt["linked"]:
|
|
664
|
+
outside(Path(receipt["worktree"]))
|
|
665
|
+
receipt["writers_released"] = True
|
|
666
|
+
atomic_json(path, receipt)
|
|
601
667
|
def result(status):
|
|
602
|
-
|
|
603
|
-
if "scratch_identity" in receipt:
|
|
604
|
-
try:
|
|
605
|
-
scratch = clean_scratch(receipt, path, args.writers_stopped)
|
|
606
|
-
except (CompletionError, OSError, ValueError, KeyError, TypeError, IndexError) as error:
|
|
607
|
-
scratch = {"status": "RETAINED", "reason": str(error), "resume": shlex.join([
|
|
608
|
-
sys.executable, str(Path(__file__).resolve()), "clean-scratch", "--receipt", str(path), "--writers-stopped"])}
|
|
609
|
-
receipt["scratch_cleanup"] = scratch
|
|
610
|
-
try:
|
|
611
|
-
atomic_json(path, receipt)
|
|
612
|
-
except OSError as record_error:
|
|
613
|
-
scratch["record_error"] = str(record_error)
|
|
614
|
-
return {"status": "CLEANUP_PENDING" if status == "COMPLETE" and scratch["status"] == "RETAINED" else status,
|
|
615
|
-
"delivery_status": status, "receipt": str(path), "pr": receipt.get("pr_url"), "resume": resume,
|
|
616
|
-
"acceptance": (receipt.get("acceptance") or {}).get("kind"), "product_verdict_unchanged": True,
|
|
617
|
-
"scratch_path": str(path.parent / "scratch") if "scratch_identity" in receipt else None,
|
|
618
|
-
"scratch_cleanup": scratch}
|
|
668
|
+
return completion_result(receipt, path, status)
|
|
619
669
|
if args.local_only or receipt.get("local_only"):
|
|
620
670
|
receipt["local_only"] = True
|
|
621
671
|
atomic_json(path, receipt)
|
|
@@ -628,14 +678,12 @@ def complete(args):
|
|
|
628
678
|
if receipt.get("status") == "COMPLETE":
|
|
629
679
|
verify_files(path.parent / "custody", receipt["files"])
|
|
630
680
|
return result("COMPLETE")
|
|
631
|
-
if not receipt.get("
|
|
681
|
+
if not receipt.get("acceptance"):
|
|
632
682
|
# Before first binding the owner has committed the product; inspect
|
|
633
683
|
# uses the actual branch head until acceptance pins its exact SHA.
|
|
634
684
|
current = dict(receipt, publish_sha=ref_sha(receipt, "refs/heads/"+receipt["branch"]))
|
|
635
685
|
inspect_workspace(current)
|
|
636
686
|
bind_acceptance(receipt, path, args.acceptance)
|
|
637
|
-
if not receipt.get("cleanup_started"):
|
|
638
|
-
inspect_workspace(receipt)
|
|
639
687
|
info = repo_policy(receipt)
|
|
640
688
|
if mode == "auto":
|
|
641
689
|
require(info["mergeCommitAllowed"], "repository disallows merge commits; use pr mode or resolve policy with repository owner")
|
|
@@ -649,11 +697,11 @@ def complete(args):
|
|
|
649
697
|
validate_pr(receipt, pr)
|
|
650
698
|
actual = remote_head(receipt, receipt["branch"])
|
|
651
699
|
if not pr or pr["state"] != "MERGED":
|
|
700
|
+
inspect_workspace(receipt)
|
|
701
|
+
verify_files(Path(receipt["worktree"]), receipt["files"])
|
|
652
702
|
require(actual in {None, receipt["publish_sha"]}, "remote task head changed; refusing to overwrite or merge")
|
|
653
703
|
require(actual is not None or (not pr and not receipt.get("pushed")), "published task ref disappeared; retain and inspect")
|
|
654
704
|
if actual is None:
|
|
655
|
-
inspect_workspace(receipt)
|
|
656
|
-
verify_files(Path(receipt["worktree"]), receipt["files"])
|
|
657
705
|
gref(receipt, "push", receipt["remote"], receipt["publish_sha"]+":refs/heads/"+receipt["branch"])
|
|
658
706
|
receipt["pushed"] = True
|
|
659
707
|
atomic_json(path, receipt)
|
|
@@ -665,8 +713,14 @@ def complete(args):
|
|
|
665
713
|
validate_pr(receipt, pr)
|
|
666
714
|
receipt.update(pr_number=pr["number"], pr_url=pr["url"])
|
|
667
715
|
atomic_json(path, receipt)
|
|
668
|
-
if mode == "pr":
|
|
669
|
-
|
|
716
|
+
if mode == "pr" and pr["state"] == "OPEN":
|
|
717
|
+
if pr.get("autoMergeRequest"):
|
|
718
|
+
gh(receipt, "pr", "merge", str(pr["number"]), "--disable-auto")
|
|
719
|
+
pr = json.loads(gh(receipt, "pr", "view", str(pr["number"]), "--json", PR_FIELDS))
|
|
720
|
+
validate_pr(receipt, pr)
|
|
721
|
+
require(not pr.get("autoMergeRequest"), "auto-merge request remains on owned PR; retain and inspect")
|
|
722
|
+
if pr["state"] == "OPEN":
|
|
723
|
+
return result("PR")
|
|
670
724
|
if pr["state"] != "MERGED":
|
|
671
725
|
inspect_workspace(receipt)
|
|
672
726
|
verify_files(Path(receipt["worktree"]), receipt["files"])
|
|
@@ -679,7 +733,7 @@ def complete(args):
|
|
|
679
733
|
return result("PENDING")
|
|
680
734
|
receipt["merge"] = pr
|
|
681
735
|
atomic_json(path, receipt)
|
|
682
|
-
cleanup(receipt, path, pr
|
|
736
|
+
cleanup(receipt, path, pr)
|
|
683
737
|
return result("COMPLETE")
|
|
684
738
|
|
|
685
739
|
|
|
@@ -692,7 +746,7 @@ def main():
|
|
|
692
746
|
for name in ("task", "branch", "repository", "base"):
|
|
693
747
|
allocation.add_argument("--"+name, required=True)
|
|
694
748
|
allocation.add_argument("--remote", default="origin")
|
|
695
|
-
allocation.add_argument("--worktree")
|
|
749
|
+
allocation.add_argument("--worktree", required=True)
|
|
696
750
|
completion = actions.add_parser("complete")
|
|
697
751
|
completion.add_argument("--receipt", required=True)
|
|
698
752
|
completion.add_argument("--acceptance")
|
|
@@ -742,7 +796,7 @@ def remote(ref):
|
|
|
742
796
|
return r.stdout.strip() if r.returncode == 0 else None
|
|
743
797
|
if a[:2] == ['repo', 'view']:
|
|
744
798
|
assert a[2:] == ['github.com/test/project', '--json', 'nameWithOwner,url,defaultBranchRef,mergeCommitAllowed'], 'unsupported gh repo view arguments: '+str(a)
|
|
745
|
-
print(json.dumps({'nameWithOwner':'test/project', 'url':'https://github.com/test/project', 'defaultBranchRef':{'name':'main'}, 'mergeCommitAllowed':d.get('merge_allowed',True)}))
|
|
799
|
+
print(json.dumps({'nameWithOwner':'test/project', 'url':'https://github.com/test/project', 'defaultBranchRef':{'name':d.get('default_branch','main')}, 'mergeCommitAllowed':d.get('merge_allowed',True)}))
|
|
746
800
|
elif a[:2] == ['pr', 'list']:
|
|
747
801
|
assert '--repo' in a and a[a.index('--repo')+1] == 'github.com/test/project'
|
|
748
802
|
print(json.dumps([d['pr']] if d.get('pr') else []))
|
|
@@ -756,6 +810,10 @@ elif a[:2] == ['pr', 'create']:
|
|
|
756
810
|
if d.pop('interrupt_create',False): save(); sys.exit(1)
|
|
757
811
|
elif a[:2] == ['pr', 'view']:
|
|
758
812
|
print(json.dumps(d['pr']))
|
|
813
|
+
elif a[:2] == ['pr', 'merge'] and '--disable-auto' in a:
|
|
814
|
+
d['disables'] = d.get('disables',0)+1
|
|
815
|
+
d['pr']['autoMergeRequest'] = None
|
|
816
|
+
save()
|
|
759
817
|
elif a[:2] == ['pr', 'merge']:
|
|
760
818
|
assert '--admin' not in a and '--delete-branch' not in a
|
|
761
819
|
assert '--auto' in a and '--merge' in a and '--match-head-commit' in a
|
|
@@ -837,7 +895,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
837
895
|
self.env["PATH"] = str(self.bin) + os.pathsep + os.environ["PATH"]
|
|
838
896
|
self.run_cmd(["git", "init", "--bare", "--initial-branch=main", str(self.bare)])
|
|
839
897
|
self.run_cmd(["git", "init", "--initial-branch=main", str(self.work)])
|
|
840
|
-
(self.work / ".gitignore").write_text(".devlyn/\nignored/\n", encoding="utf-8")
|
|
898
|
+
(self.work / ".gitignore").write_text(".devlyn/\nignored/\nnode_modules/\n.env\n", encoding="utf-8")
|
|
841
899
|
(self.work / "product").write_text("baseline\n", encoding="utf-8")
|
|
842
900
|
self.g("add", ".")
|
|
843
901
|
self.g("commit", "-m", "baseline")
|
|
@@ -862,33 +920,61 @@ class CompletionTests(unittest.TestCase):
|
|
|
862
920
|
r = self.run_cmd([sys.executable, str(Path(__file__).resolve()), *map(str, args)], success=success, cwd=cwd)
|
|
863
921
|
return json.loads(r.stdout), r
|
|
864
922
|
|
|
865
|
-
def allocate(self,
|
|
866
|
-
args = ["allocate", "--repo", self.work, "--task",
|
|
867
|
-
|
|
868
|
-
args += ["--worktree", self.root / "linked"]
|
|
923
|
+
def allocate(self, name="fixture", base="main"):
|
|
924
|
+
args = ["allocate", "--repo", self.work, "--task", name, "--branch", "task/"+name, "--repository", "test/project", "--base", base,
|
|
925
|
+
"--worktree", self.root / ("linked" if name == "fixture" else name)]
|
|
869
926
|
result, _ = self.cli(*args)
|
|
870
927
|
self.receipt = Path(result["receipt"])
|
|
871
928
|
self.task = Path(result["worktree"])
|
|
872
929
|
return result
|
|
873
930
|
|
|
931
|
+
def allocate_legacy(self):
|
|
932
|
+
common = Path(self.g("rev-parse", "--absolute-git-dir"))
|
|
933
|
+
key = hashlib.sha256(b"task/fixture").hexdigest()[:24]
|
|
934
|
+
receipt = {"task": "fixture", "repository": "test/project", "remote": "origin", "base": "main", "branch": "task/fixture",
|
|
935
|
+
"common_gitdir": str(common), "anchor": str(self.work), "linked": False, "allocation": "owned",
|
|
936
|
+
"baseline": self.g("rev-parse", "HEAD"), "worktree": str(self.work), "id": key,
|
|
937
|
+
"recovery_ref": "refs/devlyn/completed/"+key, "worktree_gitdir": str(common),
|
|
938
|
+
"remote_url": dict.fromkeys(("literal", "fetch", "push"), "https://github.com/test/project.git")}
|
|
939
|
+
self.receipt = common / "devlyn-completion" / key / "receipt.json"
|
|
940
|
+
self.receipt.parent.mkdir(parents=True)
|
|
941
|
+
scratch = self.receipt.parent / "scratch"
|
|
942
|
+
scratch.mkdir()
|
|
943
|
+
receipt["scratch_identity"] = workspace_identity(scratch, self.receipt.parent)
|
|
944
|
+
self.g("switch", "-c", "task/fixture")
|
|
945
|
+
receipt["worktree_identity"] = workspace_identity(self.work, common)
|
|
946
|
+
self.receipt.write_text(json.dumps(receipt), encoding="utf-8")
|
|
947
|
+
self.task = self.work
|
|
948
|
+
|
|
949
|
+
def merge_pr(self):
|
|
950
|
+
self.configure(pending=False)
|
|
951
|
+
self.run_cmd(["gh", "pr", "merge", "1", "--auto", "--merge", "--match-head-commit", self.sha])
|
|
952
|
+
|
|
953
|
+
def anchor_state(self, work=None):
|
|
954
|
+
work = work or self.work
|
|
955
|
+
return (self.g("symbolic-ref", "HEAD", work=work), self.g("rev-parse", "HEAD", work=work),
|
|
956
|
+
self.g("status", "--porcelain", "--untracked-files=all", work=work), (work / "product").read_bytes(),
|
|
957
|
+
(Path(self.g("rev-parse", "--absolute-git-dir", work=work)) / "index").read_bytes())
|
|
958
|
+
|
|
874
959
|
def test_allocate_base_with_same_named_tag(self):
|
|
875
960
|
self.g("tag", "main")
|
|
876
|
-
result = self.allocate(
|
|
961
|
+
result = self.allocate()
|
|
877
962
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
878
963
|
self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], "main")
|
|
879
964
|
|
|
880
965
|
def test_allocate_base_preserves_unicode_whitespace(self):
|
|
881
966
|
base = "main\u00a0"
|
|
882
967
|
self.g("switch", "-c", base)
|
|
968
|
+
self.g("push", "origin", "HEAD:refs/heads/"+base)
|
|
883
969
|
result = self.allocate(base=base)
|
|
884
970
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
885
971
|
self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], base)
|
|
886
972
|
|
|
887
973
|
def assert_base_identity_rejected(self, actual, base):
|
|
888
974
|
result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
889
|
-
"--branch", "task/fixture", "--repository", "test/project", "--base", base, success=False)
|
|
975
|
+
"--branch", "task/fixture", "--repository", "test/project", "--base", base, "--worktree", self.root / "linked", success=False)
|
|
890
976
|
self.assertNotEqual(process.returncode, 0)
|
|
891
|
-
self.assertIn("
|
|
977
|
+
self.assertIn("refs/heads/"+base, result["reason"])
|
|
892
978
|
head = self.run_cmd(["git", "-C", str(self.work), "symbolic-ref", "HEAD"]).stdout
|
|
893
979
|
self.assertEqual(head, "refs/heads/" + actual + "\n")
|
|
894
980
|
self.assertFalse((self.work / ".git/devlyn-completion").exists())
|
|
@@ -897,6 +983,8 @@ class CompletionTests(unittest.TestCase):
|
|
|
897
983
|
|
|
898
984
|
def test_allocate_rejects_trimmed_base_alias(self):
|
|
899
985
|
self.g("switch", "-c", "main\u00a0")
|
|
986
|
+
self.g("push", "origin", "HEAD:refs/heads/main\u00a0")
|
|
987
|
+
self.run_cmd(["git", "--git-dir", str(self.bare), "update-ref", "-d", "refs/heads/main"])
|
|
900
988
|
self.assert_base_identity_rejected("main\u00a0", "main")
|
|
901
989
|
|
|
902
990
|
def test_allocate_rejects_short_base_alias(self):
|
|
@@ -904,6 +992,29 @@ class CompletionTests(unittest.TestCase):
|
|
|
904
992
|
self.g("tag", "main")
|
|
905
993
|
self.assert_base_identity_rejected("main", "heads/main")
|
|
906
994
|
|
|
995
|
+
def test_allocate_uses_remote_base_with_dirty_foreign_anchor(self):
|
|
996
|
+
baseline = self.g("rev-parse", "main")
|
|
997
|
+
(self.work / "product").write_text("remote advance\n", encoding="utf-8")
|
|
998
|
+
self.g("commit", "-am", "remote advance")
|
|
999
|
+
self.g("push", "origin", "main")
|
|
1000
|
+
remote = self.g("rev-parse", "main")
|
|
1001
|
+
self.g("switch", "-c", "busy")
|
|
1002
|
+
(self.work / "product").write_text("local advance\n", encoding="utf-8")
|
|
1003
|
+
self.g("commit", "-am", "local advance")
|
|
1004
|
+
ahead = self.g("rev-parse", "HEAD")
|
|
1005
|
+
(self.work / "product").write_text("busy edits\n", encoding="utf-8")
|
|
1006
|
+
(self.work / "untracked").write_bytes(b"owner data")
|
|
1007
|
+
for name, local in (("behind", baseline), ("ahead", ahead)):
|
|
1008
|
+
self.g("update-ref", "refs/heads/main", local)
|
|
1009
|
+
before = self.anchor_state()
|
|
1010
|
+
result = self.allocate(name)
|
|
1011
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1012
|
+
self.assertEqual(json.loads(self.receipt.read_text())["baseline"], remote)
|
|
1013
|
+
self.assertEqual(self.g("rev-parse", "HEAD", work=self.task), remote)
|
|
1014
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1015
|
+
self.assertEqual((self.work / "untracked").read_bytes(), b"owner data")
|
|
1016
|
+
self.assertFalse((self.work / ".git" / "FETCH_HEAD").exists())
|
|
1017
|
+
|
|
907
1018
|
@unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
|
|
908
1019
|
def test_disposable_scratch_cleans_for_local_only_and_preserves_source(self):
|
|
909
1020
|
result = self.allocate()
|
|
@@ -934,6 +1045,29 @@ class CompletionTests(unittest.TestCase):
|
|
|
934
1045
|
child.wait(timeout=5)
|
|
935
1046
|
self.cli("clean-scratch", "--receipt", self.receipt, "--writers-stopped")
|
|
936
1047
|
|
|
1048
|
+
def test_linux_writer_scan_continues_after_vanished_fd(self):
|
|
1049
|
+
from unittest.mock import patch
|
|
1050
|
+
process = Path("/proc") / str(os.getpid() + 1)
|
|
1051
|
+
vanished, active = process / "fd/0", process / "fd/1"
|
|
1052
|
+
iterdir, readlink = Path.iterdir, os.readlink
|
|
1053
|
+
def entries(path):
|
|
1054
|
+
if path == Path("/proc"):
|
|
1055
|
+
return iter([process])
|
|
1056
|
+
if path == process / "fd":
|
|
1057
|
+
return iter([vanished, active])
|
|
1058
|
+
return iterdir(path)
|
|
1059
|
+
def target(path, *args, **kwargs):
|
|
1060
|
+
if path == process / "cwd":
|
|
1061
|
+
return str(self.root)
|
|
1062
|
+
if path == vanished:
|
|
1063
|
+
raise FileNotFoundError(path)
|
|
1064
|
+
if path == active:
|
|
1065
|
+
return str(self.work / "product")
|
|
1066
|
+
return readlink(path, *args, **kwargs)
|
|
1067
|
+
with patch.object(sys, "platform", "linux"), patch.object(Path, "iterdir", entries), patch.object(os, "readlink", target):
|
|
1068
|
+
with self.assertRaisesRegex(CompletionError, "active process " + process.name):
|
|
1069
|
+
stopped_writers(self.work)
|
|
1070
|
+
|
|
937
1071
|
@unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
|
|
938
1072
|
def test_scratch_rejects_redirect_and_git_data_but_does_not_follow_child_links(self):
|
|
939
1073
|
scratch = Path(self.allocate()["scratch"])
|
|
@@ -988,7 +1122,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
988
1122
|
scratch = Path(self.allocate()["scratch"])
|
|
989
1123
|
self.accept()
|
|
990
1124
|
(scratch / ".git").mkdir()
|
|
991
|
-
result, _ = self.cli("complete", "--receipt", self.receipt, "--acceptance", self.acceptance, "--writers-stopped")
|
|
1125
|
+
result, _ = self.cli("complete", "--receipt", self.receipt, "--acceptance", self.acceptance, "--writers-stopped", "--mode", "auto")
|
|
992
1126
|
self.assertEqual(result["status"], "CLEANUP_PENDING")
|
|
993
1127
|
self.assertEqual(result["delivery_status"], "COMPLETE")
|
|
994
1128
|
self.assertEqual(result["scratch_cleanup"]["status"], "RETAINED")
|
|
@@ -1108,10 +1242,26 @@ class CompletionTests(unittest.TestCase):
|
|
|
1108
1242
|
self.g("config", "url." + alias + ".insteadOf", intended)
|
|
1109
1243
|
self.allocate(); self.accept()
|
|
1110
1244
|
self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["remote_url"], {"literal": intended, "fetch": alias, "push": alias})
|
|
1111
|
-
result, _ = self.complete(
|
|
1245
|
+
result, _ = self.complete()
|
|
1112
1246
|
self.assertEqual(result["status"], "PR")
|
|
1113
1247
|
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1114
|
-
|
|
1248
|
+
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1249
|
+
self.assertEqual((d["pushs"], d.get("merges", 0), d["pr"]["autoMergeRequest"]), (1, 0, None))
|
|
1250
|
+
|
|
1251
|
+
def test_pr_mode_disables_only_owned_auto_merge(self):
|
|
1252
|
+
self.allocate(); self.accept()
|
|
1253
|
+
self.configure(pending=True)
|
|
1254
|
+
result, _ = self.complete("--mode", "auto")
|
|
1255
|
+
self.assertEqual(result["status"], "PENDING")
|
|
1256
|
+
result, _ = self.complete("--mode", "pr")
|
|
1257
|
+
self.assertEqual(result["status"], "PR")
|
|
1258
|
+
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1259
|
+
self.assertEqual((d["disables"], d["pr"]["autoMergeRequest"]), (1, None))
|
|
1260
|
+
self.configure(pr=dict(d["pr"], isCrossRepository=True, autoMergeRequest={"enabledAt": "now"}))
|
|
1261
|
+
_, r = self.complete("--mode", "pr", success=False)
|
|
1262
|
+
self.assertIn("PR head repository differs", json.loads(r.stdout)["reason"])
|
|
1263
|
+
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1264
|
+
self.assertEqual((d["disables"], d["pr"]["autoMergeRequest"]), (1, {"enabledAt": "now"}))
|
|
1115
1265
|
|
|
1116
1266
|
def test_remote_rewrite_after_allocation_is_rejected(self):
|
|
1117
1267
|
self.allocate(); self.accept()
|
|
@@ -1158,28 +1308,364 @@ class CompletionTests(unittest.TestCase):
|
|
|
1158
1308
|
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1159
1309
|
self.assertEqual((d["creates"], d["merges"], d["pushs"]), (1, 1, 1))
|
|
1160
1310
|
|
|
1161
|
-
def
|
|
1162
|
-
self.
|
|
1311
|
+
def test_legacy_in_place_retains_checked_out_branch_then_retires_refs(self):
|
|
1312
|
+
self.allocate_legacy(); self.accept()
|
|
1163
1313
|
(self.task / "ignored").mkdir()
|
|
1164
1314
|
(self.task / "ignored" / "user.txt").write_text("retain me", encoding="utf-8")
|
|
1165
|
-
|
|
1166
|
-
self.
|
|
1167
|
-
self.
|
|
1168
|
-
|
|
1315
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1316
|
+
self.merge_pr()
|
|
1317
|
+
before = self.anchor_state()
|
|
1318
|
+
receipt, task = self.receipt, self.task
|
|
1319
|
+
result = self.allocate("next")
|
|
1320
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1321
|
+
self.assertIn(str(task), result["reconciled"][0]["reason"])
|
|
1322
|
+
self.assertIn("switch", result["reconciled"][0]["reason"])
|
|
1323
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1324
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1325
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1326
|
+
self.g("switch", "main")
|
|
1327
|
+
(self.work / "product").write_text("owner edits\n", encoding="utf-8")
|
|
1328
|
+
(self.work / "untracked").write_bytes(b"owner data")
|
|
1329
|
+
before = self.anchor_state()
|
|
1330
|
+
result = self.allocate("retired")
|
|
1331
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1332
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1333
|
+
self.assertEqual((self.work / "untracked").read_bytes(), b"owner data")
|
|
1334
|
+
self.assertEqual((task / "ignored/user.txt").read_text(encoding="utf-8"), "retain me")
|
|
1169
1335
|
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1170
1336
|
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1171
|
-
|
|
1337
|
+
self.receipt, self.task = receipt, task
|
|
1338
|
+
again, _ = self.complete(acceptance=False)
|
|
1172
1339
|
self.assertEqual(again["status"], "COMPLETE")
|
|
1173
1340
|
|
|
1341
|
+
def test_pr_mode_cleans_after_human_merge(self):
|
|
1342
|
+
self.allocate(); self.accept()
|
|
1343
|
+
self.complete("--mode", "pr")
|
|
1344
|
+
self.merge_pr()
|
|
1345
|
+
before = self.data.read_bytes()
|
|
1346
|
+
result, _ = self.complete("--mode", "pr", "--writers-stopped")
|
|
1347
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1348
|
+
self.assertFalse(self.task.exists())
|
|
1349
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1350
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1351
|
+
saved, current = json.loads(before), json.loads(self.data.read_bytes())
|
|
1352
|
+
self.assertEqual((current["merges"], current.get("disables", 0)), (saved["merges"], saved.get("disables", 0)))
|
|
1353
|
+
|
|
1354
|
+
@unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
|
|
1355
|
+
def test_nested_repository_or_worktree_retains_tree(self):
|
|
1356
|
+
self.allocate(); self.accept()
|
|
1357
|
+
_, r = self.cli("allocate", "--repo", self.work, "--task", "inner", "--branch", "task/inner", "--repository", "test/project",
|
|
1358
|
+
"--base", "main", "--worktree", self.task / "ignored" / "inner", success=False)
|
|
1359
|
+
self.assertIn("disjoint from every registered worktree", json.loads(r.stdout)["reason"])
|
|
1360
|
+
nested = self.task / "ignored" / "nested"
|
|
1361
|
+
self.g("worktree", "add", "-b", "nested/work", str(nested), "main")
|
|
1362
|
+
(nested / "product").write_text("unsaved nested work\n", encoding="utf-8")
|
|
1363
|
+
clone = self.task / "ignored" / "clone"
|
|
1364
|
+
self.run_cmd(["git", "init", "--initial-branch=main", str(clone)])
|
|
1365
|
+
for remove in (lambda: self.g("worktree", "remove", "--force", str(nested)), lambda: shutil.rmtree(clone)):
|
|
1366
|
+
_, r = self.complete("--mode", "auto", "--writers-stopped", success=False)
|
|
1367
|
+
self.assertIn("nested repository or worktree", json.loads(r.stdout)["reason"])
|
|
1368
|
+
self.assertTrue(self.task.exists())
|
|
1369
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1370
|
+
if nested.exists():
|
|
1371
|
+
self.assertEqual((nested / "product").read_text(encoding="utf-8"), "unsaved nested work\n")
|
|
1372
|
+
remove()
|
|
1373
|
+
result, _ = self.complete("--mode", "auto", "--writers-stopped")
|
|
1374
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1375
|
+
self.assertFalse(self.task.exists())
|
|
1376
|
+
|
|
1377
|
+
def test_release_is_refused_from_inside_the_task_tree(self):
|
|
1378
|
+
self.allocate(); self.accept()
|
|
1379
|
+
_, r = self.complete("--mode", "pr", "--writers-stopped", cwd=self.task, success=False)
|
|
1380
|
+
self.assertIn("caller cwd is inside", json.loads(r.stdout)["reason"])
|
|
1381
|
+
self.assertNotIn("writers_released", json.loads(self.receipt.read_text(encoding="utf-8")))
|
|
1382
|
+
|
|
1383
|
+
@unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
|
|
1384
|
+
def test_squash_merge_cleans_and_recovery_ref_keeps_commit(self):
|
|
1385
|
+
self.allocate(); self.accept()
|
|
1386
|
+
result, _ = self.complete("--mode", "pr", "--writers-stopped")
|
|
1387
|
+
self.assertEqual(result["status"], "PR")
|
|
1388
|
+
bare = ["git", "--git-dir", str(self.bare)]
|
|
1389
|
+
base = self.run_cmd(bare + ["rev-parse", "refs/heads/main"]).stdout.strip()
|
|
1390
|
+
tree = self.run_cmd(bare + ["rev-parse", self.sha + "^{tree}"]).stdout.strip()
|
|
1391
|
+
squash = self.run_cmd(bare + ["commit-tree", tree, "-p", base, "-m", "squash fixture"]).stdout.strip()
|
|
1392
|
+
self.run_cmd(bare + ["update-ref", "refs/heads/main", squash, base])
|
|
1393
|
+
pr = json.loads(self.data.read_text(encoding="utf-8"))["pr"]
|
|
1394
|
+
self.configure(pr=dict(pr, state="MERGED", mergedAt="now", mergeCommit={"oid": squash}))
|
|
1395
|
+
result, _ = self.complete()
|
|
1396
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1397
|
+
self.assertFalse(self.task.exists())
|
|
1398
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1399
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1400
|
+
self.assertEqual(self.g("rev-parse", json.loads(self.receipt.read_text(encoding="utf-8"))["recovery_ref"]), self.sha)
|
|
1401
|
+
self.assertFalse((self.work / ".git" / "FETCH_HEAD").exists())
|
|
1402
|
+
|
|
1403
|
+
def test_linked_ignored_output_disposed_and_records_preserved(self):
|
|
1404
|
+
self.allocate(); self.accept()
|
|
1405
|
+
for relative, raw in (("node_modules/x", b"build"), (".env", b"SECRET=value\n"), (".devlyn/runs/extra/log", b"extra\x00bytes\n")):
|
|
1406
|
+
path = self.task / relative
|
|
1407
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1408
|
+
path.write_bytes(raw)
|
|
1409
|
+
path.chmod(0o640)
|
|
1410
|
+
records = snapshot_files(self.task, [".devlyn"])
|
|
1411
|
+
(self.task / "untracked").write_bytes(b"owner data")
|
|
1412
|
+
_, process = self.complete("--mode", "auto", "--writers-stopped", success=False)
|
|
1413
|
+
self.assertNotEqual(process.returncode, 0)
|
|
1414
|
+
self.assertTrue(self.task.exists())
|
|
1415
|
+
self.assertEqual((self.task / "untracked").read_bytes(), b"owner data")
|
|
1416
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1417
|
+
(self.task / "untracked").unlink()
|
|
1418
|
+
result, _ = self.complete("--mode", "auto", "--writers-stopped")
|
|
1419
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1420
|
+
self.assertFalse(self.task.exists())
|
|
1421
|
+
verify_files(self.receipt.parent / "records", records)
|
|
1422
|
+
self.assertEqual(snapshot_files(self.receipt.parent / "records", [".devlyn"]), records)
|
|
1423
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1424
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1425
|
+
|
|
1426
|
+
def test_reconcile_released_merge_and_retains_without_release(self):
|
|
1427
|
+
self.allocate(); self.accept()
|
|
1428
|
+
result, _ = self.complete("--mode", "pr")
|
|
1429
|
+
self.assertEqual(result["status"], "PR")
|
|
1430
|
+
receipt, task = self.receipt, self.task
|
|
1431
|
+
self.merge_pr()
|
|
1432
|
+
result = self.allocate("unreleased")
|
|
1433
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1434
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1435
|
+
self.assertIn("writers", result["reconciled"][0]["reason"])
|
|
1436
|
+
self.assertTrue(task.exists())
|
|
1437
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1438
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1439
|
+
# Reopen only the fixture PR to exercise the owner's pre-merge release.
|
|
1440
|
+
d = json.loads(self.data.read_text())
|
|
1441
|
+
self.configure(pr=dict(d["pr"], state="OPEN", mergedAt=None, mergeCommit=None))
|
|
1442
|
+
self.receipt, self.task = receipt, task
|
|
1443
|
+
result, _ = self.complete("--mode", "pr", "--writers-stopped")
|
|
1444
|
+
self.assertEqual(result["status"], "PR")
|
|
1445
|
+
self.assertTrue(json.loads(receipt.read_text())["writers_released"])
|
|
1446
|
+
self.merge_pr()
|
|
1447
|
+
server = json.loads(self.data.read_text())
|
|
1448
|
+
result = self.allocate("released")
|
|
1449
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1450
|
+
self.assertFalse(task.exists())
|
|
1451
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1452
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1453
|
+
current = json.loads(self.data.read_text())
|
|
1454
|
+
self.assertEqual((current["pushs"], current["creates"], current["merges"], current.get("disables", 0)),
|
|
1455
|
+
(server["pushs"], server["creates"], server["merges"], server.get("disables", 0)))
|
|
1456
|
+
|
|
1457
|
+
def test_reconcile_checks_repo_policy_before_cleanup(self):
|
|
1458
|
+
self.allocate(); self.accept()
|
|
1459
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1460
|
+
self.merge_pr()
|
|
1461
|
+
receipt, task = self.receipt, self.task
|
|
1462
|
+
self.configure(default_branch="changed")
|
|
1463
|
+
result = self.allocate("next")
|
|
1464
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "RETAINED")])
|
|
1465
|
+
self.assertIn("base/default branch changed", result["reconciled"][0]["reason"])
|
|
1466
|
+
self.assertTrue(task.exists())
|
|
1467
|
+
self.assertEqual(self.g("rev-parse", "task/fixture"), self.sha)
|
|
1468
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1469
|
+
|
|
1470
|
+
def test_reconcile_skips_open_and_unaccepted_and_retains_in_use_tree(self):
|
|
1471
|
+
self.allocate(); self.accept()
|
|
1472
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1473
|
+
receipt, task = self.receipt, self.task
|
|
1474
|
+
before = receipt.read_bytes()
|
|
1475
|
+
result = self.allocate("unaccepted")
|
|
1476
|
+
self.assertEqual(result["reconciled"], [])
|
|
1477
|
+
self.assertEqual(receipt.read_bytes(), before)
|
|
1478
|
+
unaccepted, unaccepted_tree = self.receipt, self.task
|
|
1479
|
+
saved = json.loads(unaccepted.read_text())
|
|
1480
|
+
saved.update(pr_number=1, writers_released=True)
|
|
1481
|
+
unaccepted.write_text(json.dumps(saved))
|
|
1482
|
+
unaccepted_before = unaccepted.read_bytes()
|
|
1483
|
+
pr = json.loads(self.data.read_text())["pr"]
|
|
1484
|
+
self.configure(pr=dict(pr, state="CLOSED"))
|
|
1485
|
+
result = self.allocate("closed")
|
|
1486
|
+
self.assertEqual(result["reconciled"], [])
|
|
1487
|
+
self.assertEqual(receipt.read_bytes(), before)
|
|
1488
|
+
self.assertTrue(task.exists())
|
|
1489
|
+
self.configure(pr=pr)
|
|
1490
|
+
self.merge_pr()
|
|
1491
|
+
self.g("worktree", "lock", str(task))
|
|
1492
|
+
result = self.allocate("locked")
|
|
1493
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1494
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1495
|
+
self.assertIn("locked", result["reconciled"][0]["reason"])
|
|
1496
|
+
self.assertTrue(task.exists())
|
|
1497
|
+
self.g("worktree", "unlock", str(task))
|
|
1498
|
+
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"], cwd=task)
|
|
1499
|
+
try:
|
|
1500
|
+
result = self.allocate("active")
|
|
1501
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1502
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1503
|
+
self.assertIn("active process", result["reconciled"][0]["reason"])
|
|
1504
|
+
self.assertTrue(task.exists())
|
|
1505
|
+
self.assertIsNone(child.poll())
|
|
1506
|
+
finally:
|
|
1507
|
+
child.terminate(); child.wait(timeout=5)
|
|
1508
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1509
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1510
|
+
self.assertEqual(unaccepted.read_bytes(), unaccepted_before)
|
|
1511
|
+
self.assertTrue(unaccepted_tree.exists())
|
|
1512
|
+
|
|
1513
|
+
def test_reconcile_already_retired_tree_and_branch(self):
|
|
1514
|
+
self.allocate(); self.accept()
|
|
1515
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1516
|
+
self.merge_pr()
|
|
1517
|
+
receipt, task = self.receipt, self.task
|
|
1518
|
+
self.g("worktree", "remove", str(task))
|
|
1519
|
+
self.g("update-ref", "-d", "refs/heads/task/fixture", self.sha)
|
|
1520
|
+
before = self.anchor_state()
|
|
1521
|
+
result = self.allocate("next")
|
|
1522
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1523
|
+
self.assertFalse(task.exists())
|
|
1524
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1525
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1526
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1527
|
+
|
|
1528
|
+
def test_reconcile_never_removes_allocation_anchor(self):
|
|
1529
|
+
self.allocate(); self.accept()
|
|
1530
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1531
|
+
self.merge_pr()
|
|
1532
|
+
before = self.anchor_state(self.task)
|
|
1533
|
+
result, _ = self.cli("allocate", "--repo", self.task, "--task", "next", "--branch", "task/next",
|
|
1534
|
+
"--repository", "test/project", "--base", "main", "--worktree", self.root / "next")
|
|
1535
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1536
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1537
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1538
|
+
self.assertIn("anchor", result["reconciled"][0]["reason"])
|
|
1539
|
+
self.assertEqual(self.anchor_state(self.task), before)
|
|
1540
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1541
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1542
|
+
|
|
1543
|
+
def test_reconcile_reports_scratch_cleanup_pending(self):
|
|
1544
|
+
scratch = Path(self.allocate()["scratch"])
|
|
1545
|
+
self.accept()
|
|
1546
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1547
|
+
self.merge_pr()
|
|
1548
|
+
receipt, task = self.receipt, self.task
|
|
1549
|
+
(scratch / "object").write_bytes(b"rebuildable")
|
|
1550
|
+
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"], cwd=scratch)
|
|
1551
|
+
try:
|
|
1552
|
+
result = self.allocate("next")
|
|
1553
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1554
|
+
row = result["reconciled"][0]
|
|
1555
|
+
self.assertEqual((row["status"], row["delivery_status"]), ("CLEANUP_PENDING", "COMPLETE"))
|
|
1556
|
+
self.assertEqual(row["scratch_cleanup"]["status"], "RETAINED")
|
|
1557
|
+
self.assertIn("active process", row["scratch_cleanup"]["reason"])
|
|
1558
|
+
self.assertEqual((scratch / "object").read_bytes(), b"rebuildable")
|
|
1559
|
+
self.assertIsNone(child.poll())
|
|
1560
|
+
finally:
|
|
1561
|
+
child.terminate(); child.wait(timeout=5)
|
|
1562
|
+
self.assertFalse(task.exists())
|
|
1563
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1564
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1565
|
+
self.configure(default_branch="changed")
|
|
1566
|
+
server = json.loads(self.data.read_text())
|
|
1567
|
+
result = self.allocate("retry")
|
|
1568
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1569
|
+
self.assertFalse(any(scratch.iterdir()))
|
|
1570
|
+
self.assertEqual(json.loads(self.data.read_text()), server)
|
|
1571
|
+
self.assertEqual(self.allocate("later")["reconciled"], [])
|
|
1572
|
+
|
|
1573
|
+
def test_reconcile_retries_unattempted_scratch_cleanup(self):
|
|
1574
|
+
scratch = Path(self.allocate()["scratch"])
|
|
1575
|
+
self.accept()
|
|
1576
|
+
self.complete("--mode", "auto", "--writers-stopped")
|
|
1577
|
+
receipt = self.receipt
|
|
1578
|
+
saved = json.loads(receipt.read_text())
|
|
1579
|
+
del saved["scratch_cleanup"]
|
|
1580
|
+
receipt.write_text(json.dumps(saved))
|
|
1581
|
+
(scratch / "object").write_bytes(b"rebuildable")
|
|
1582
|
+
result = self.allocate("next")
|
|
1583
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1584
|
+
self.assertFalse(any(scratch.iterdir()))
|
|
1585
|
+
|
|
1586
|
+
def test_reconcile_retains_busy_receipt_without_waiting(self):
|
|
1587
|
+
self.allocate(); self.accept()
|
|
1588
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1589
|
+
self.merge_pr()
|
|
1590
|
+
code = "import pathlib, runpy, sys, time\nwith runpy.run_path(sys.argv[1])['file_lock'](pathlib.Path(sys.argv[2]), blocking=True):\n print('locked', flush=True)\n time.sleep(30)\n"
|
|
1591
|
+
child = subprocess.Popen([sys.executable, "-c", code, str(Path(__file__).resolve().with_name("platform-support.py")),
|
|
1592
|
+
str(self.receipt.parent / "lock")], stdout=subprocess.PIPE, text=True)
|
|
1593
|
+
try:
|
|
1594
|
+
self.assertEqual(child.stdout.readline().strip(), "locked")
|
|
1595
|
+
process = subprocess.run([sys.executable, str(Path(__file__).resolve()), "allocate", "--repo", str(self.work),
|
|
1596
|
+
"--task", "next", "--branch", "task/next", "--repository", "test/project", "--base", "main",
|
|
1597
|
+
"--worktree", str(self.root / "next")], cwd=self.root, env=self.env, capture_output=True, text=True, timeout=10)
|
|
1598
|
+
self.assertEqual(process.returncode, 0, process.stdout+process.stderr)
|
|
1599
|
+
result = json.loads(process.stdout)
|
|
1600
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1601
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1602
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1603
|
+
self.assertIn("lock", result["reconciled"][0]["reason"])
|
|
1604
|
+
self.assertTrue(self.task.exists())
|
|
1605
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1606
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1607
|
+
self.assertIsNone(child.poll())
|
|
1608
|
+
finally:
|
|
1609
|
+
child.terminate(); child.wait(timeout=5)
|
|
1610
|
+
child.stdout.close()
|
|
1611
|
+
|
|
1612
|
+
def test_reconcile_retains_malformed_receipt_without_failing_allocation(self):
|
|
1613
|
+
self.allocate()
|
|
1614
|
+
receipt, task = self.receipt, self.task
|
|
1615
|
+
receipt.write_text("[]", encoding="utf-8")
|
|
1616
|
+
result = self.allocate("next")
|
|
1617
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1618
|
+
self.assertTrue(self.task.exists())
|
|
1619
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1620
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1621
|
+
self.assertEqual(result["reconciled"][0]["receipt"], str(receipt))
|
|
1622
|
+
self.assertTrue(result["reconciled"][0]["reason"])
|
|
1623
|
+
self.assertEqual(receipt.read_text(), "[]")
|
|
1624
|
+
self.assertTrue(task.exists())
|
|
1625
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1626
|
+
|
|
1627
|
+
def test_records_refuse_symlink_nonregular_and_tampered_resume(self):
|
|
1628
|
+
self.allocate(); self.accept()
|
|
1629
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1630
|
+
self.merge_pr()
|
|
1631
|
+
other = self.root / "owner-file"
|
|
1632
|
+
other.write_bytes(b"owner data")
|
|
1633
|
+
link = self.task / ".devlyn/link"
|
|
1634
|
+
link.symlink_to(other)
|
|
1635
|
+
result, _ = self.complete(success=False)
|
|
1636
|
+
self.assertIn("path escapes custody", result["reason"])
|
|
1637
|
+
self.assertTrue(self.task.exists())
|
|
1638
|
+
self.assertTrue(link.is_symlink())
|
|
1639
|
+
self.assertEqual(other.read_bytes(), b"owner data")
|
|
1640
|
+
link.unlink()
|
|
1641
|
+
if hasattr(os, "mkfifo"):
|
|
1642
|
+
os.mkfifo(link)
|
|
1643
|
+
result, _ = self.complete(success=False)
|
|
1644
|
+
self.assertIn("regular file", result["reason"])
|
|
1645
|
+
self.assertTrue(self.task.exists())
|
|
1646
|
+
link.unlink()
|
|
1647
|
+
saved = self.receipt.parent / "records/.devlyn"
|
|
1648
|
+
shutil.copytree(self.task / ".devlyn", saved)
|
|
1649
|
+
(saved / "checks.txt").write_bytes(b"tampered copy")
|
|
1650
|
+
result, _ = self.complete(success=False)
|
|
1651
|
+
self.assertIn("bytes/mode changed", result["reason"])
|
|
1652
|
+
self.assertEqual((saved / "checks.txt").read_bytes(), b"tampered copy")
|
|
1653
|
+
self.assertTrue(self.task.exists())
|
|
1654
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1655
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1656
|
+
shutil.copy2(self.task / ".devlyn/checks.txt", saved / "checks.txt")
|
|
1657
|
+
result, _ = self.complete()
|
|
1658
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1659
|
+
self.assertFalse(self.task.exists())
|
|
1660
|
+
|
|
1174
1661
|
def test_linked_custody_and_interrupted_removal(self):
|
|
1175
|
-
self.allocate(
|
|
1176
|
-
# Direct evidence unrelated to the pipeline is unknown ignored content.
|
|
1177
|
-
(self.task / ".devlyn/checks.txt").unlink()
|
|
1662
|
+
self.allocate(); self.accept(pipeline=True)
|
|
1178
1663
|
self.configure(interrupt_remove=True)
|
|
1179
|
-
result, r = self.complete("--writers-stopped", success=False)
|
|
1664
|
+
result, r = self.complete("--writers-stopped", "--mode", "auto", success=False)
|
|
1180
1665
|
self.assertNotEqual(r.returncode, 0)
|
|
1181
1666
|
self.assertFalse(self.task.exists())
|
|
1182
1667
|
saved = json.loads(self.receipt.read_text(encoding="utf-8"))
|
|
1668
|
+
self.assertNotIn("cleanup_started", saved)
|
|
1183
1669
|
recovered = self.receipt.parent / "custody" / ".devlyn/runs/fixture-run/final-report.md"
|
|
1184
1670
|
self.assertEqual(hashlib.sha256(recovered.read_bytes()).hexdigest(), saved["files"][".devlyn/runs/fixture-run/final-report.md"]["sha256"])
|
|
1185
1671
|
result, _ = self.complete("--writers-stopped", acceptance=False)
|
|
@@ -1226,31 +1712,31 @@ class CompletionTests(unittest.TestCase):
|
|
|
1226
1712
|
self.allocate(); self.accept()
|
|
1227
1713
|
for effect in ("push", "create", "merge"):
|
|
1228
1714
|
self.configure(**{"interrupt_"+effect: True})
|
|
1229
|
-
_, r = self.complete("--writers-stopped", success=False)
|
|
1715
|
+
_, r = self.complete("--writers-stopped", "--mode", "auto", success=False)
|
|
1230
1716
|
self.assertNotEqual(r.returncode, 0)
|
|
1231
1717
|
result, _ = self.complete("--writers-stopped")
|
|
1232
1718
|
self.assertEqual(result["status"], "COMPLETE")
|
|
1233
1719
|
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1234
1720
|
self.assertEqual((d["pushs"], d["creates"], d["merges"]), (1, 1, 1))
|
|
1235
1721
|
|
|
1236
|
-
def
|
|
1237
|
-
self.allocate(
|
|
1238
|
-
for case in ("
|
|
1239
|
-
path = self.task / ("
|
|
1240
|
-
if case in {"
|
|
1722
|
+
def test_retains_dirty_locked_and_active_tree(self):
|
|
1723
|
+
self.allocate(); self.accept()
|
|
1724
|
+
for case in ("no-yield", "dirty", "untracked", "locked", "cwd"):
|
|
1725
|
+
path = self.task / ("product" if case == "dirty" else "unknown")
|
|
1726
|
+
if case in {"dirty", "untracked"}:
|
|
1241
1727
|
path.parent.mkdir(exist_ok=True); path.write_text("do not delete", encoding="utf-8")
|
|
1242
1728
|
if case == "locked": self.g("worktree", "lock", str(self.task))
|
|
1243
1729
|
with self.subTest(case=case):
|
|
1244
|
-
_, r = self.complete(*([] if case == "no-yield" else ["--writers-stopped"]), cwd=self.task if case == "cwd" else None, success=False)
|
|
1730
|
+
_, r = self.complete("--mode", "auto", *([] if case == "no-yield" else ["--writers-stopped"]), cwd=self.task if case == "cwd" else None, success=False)
|
|
1245
1731
|
self.assertNotEqual(r.returncode, 0)
|
|
1246
1732
|
self.assertTrue(self.task.exists())
|
|
1247
1733
|
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1248
|
-
if case
|
|
1734
|
+
if case == "untracked": path.unlink()
|
|
1249
1735
|
if case == "dirty": path.write_text("accepted\n", encoding="utf-8")
|
|
1250
1736
|
if case == "locked": self.g("worktree", "unlock", str(self.task))
|
|
1251
1737
|
|
|
1252
1738
|
def test_no_adoption_and_remote_head_race(self):
|
|
1253
|
-
_, r = self.cli("allocate", "--repo", self.work, "--task", "foreign", "--branch", "main", "--repository", "test/project", "--base", "main", success=False)
|
|
1739
|
+
_, r = self.cli("allocate", "--repo", self.work, "--task", "foreign", "--branch", "main", "--repository", "test/project", "--base", "main", "--worktree", self.root / "foreign", success=False)
|
|
1254
1740
|
self.assertNotEqual(r.returncode, 0)
|
|
1255
1741
|
self.allocate(); self.accept()
|
|
1256
1742
|
self.complete("--mode", "pr")
|
|
@@ -1261,7 +1747,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1261
1747
|
|
|
1262
1748
|
def test_concurrent_completions(self):
|
|
1263
1749
|
self.allocate(); self.accept(); self.configure(pending=True)
|
|
1264
|
-
args = [sys.executable, str(Path(__file__).resolve()), "complete", "--receipt", str(self.receipt), "--acceptance", str(self.acceptance)]
|
|
1750
|
+
args = [sys.executable, str(Path(__file__).resolve()), "complete", "--receipt", str(self.receipt), "--acceptance", str(self.acceptance), "--mode", "auto"]
|
|
1265
1751
|
procs = [subprocess.Popen(args, cwd=self.root, env=self.env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8") for _ in range(2)]
|
|
1266
1752
|
for proc in procs:
|
|
1267
1753
|
out, err = proc.communicate(timeout=30)
|
|
@@ -1270,7 +1756,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1270
1756
|
self.assertEqual((d["pushs"], d["creates"], d["merges"]), (1,1,1))
|
|
1271
1757
|
|
|
1272
1758
|
def test_custody_failure_and_evidence_tamper(self):
|
|
1273
|
-
self.allocate(
|
|
1759
|
+
self.allocate(); self.accept()
|
|
1274
1760
|
bad = self.receipt.parent / "custody/.devlyn/checks.txt"
|
|
1275
1761
|
bad.parent.mkdir(parents=True)
|
|
1276
1762
|
bad.write_text("partial interrupted copy", encoding="utf-8")
|
|
@@ -1287,10 +1773,10 @@ class CompletionTests(unittest.TestCase):
|
|
|
1287
1773
|
self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("merges",0), 0)
|
|
1288
1774
|
|
|
1289
1775
|
def test_actual_foreign_writer_and_registration(self):
|
|
1290
|
-
self.allocate(
|
|
1776
|
+
self.allocate(); self.accept()
|
|
1291
1777
|
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"], cwd=self.task)
|
|
1292
1778
|
try:
|
|
1293
|
-
_, r = self.complete("--writers-stopped", success=False)
|
|
1779
|
+
_, r = self.complete("--writers-stopped", "--mode", "auto", success=False)
|
|
1294
1780
|
self.assertNotEqual(r.returncode, 0)
|
|
1295
1781
|
self.assertTrue(self.task.exists())
|
|
1296
1782
|
self.assertIn("active process", json.loads(r.stdout)["reason"])
|
|
@@ -1307,7 +1793,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1307
1793
|
self.allocate(); self.accept()
|
|
1308
1794
|
race = self.g("rev-parse", "main")
|
|
1309
1795
|
self.configure(remote_delete_race=True, race_sha=race)
|
|
1310
|
-
_, r = self.complete("--writers-stopped", success=False)
|
|
1796
|
+
_, r = self.complete("--writers-stopped", "--mode", "auto", success=False)
|
|
1311
1797
|
self.assertNotEqual(r.returncode, 0)
|
|
1312
1798
|
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], race)
|
|
1313
1799
|
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
@@ -1315,7 +1801,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1315
1801
|
def test_policy_and_local_only_persist(self):
|
|
1316
1802
|
self.allocate(); self.accept()
|
|
1317
1803
|
self.configure(merge_allowed=False)
|
|
1318
|
-
_, r = self.complete(success=False)
|
|
1804
|
+
_, r = self.complete("--mode", "auto", success=False)
|
|
1319
1805
|
self.assertNotEqual(r.returncode, 0)
|
|
1320
1806
|
self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs",0), 0)
|
|
1321
1807
|
self.complete("--mode", "pr")
|
|
@@ -1365,15 +1851,19 @@ class CompletionTests(unittest.TestCase):
|
|
|
1365
1851
|
result, _ = self.complete("--mode", "pr")
|
|
1366
1852
|
self.assertEqual(result["status"], "PR")
|
|
1367
1853
|
|
|
1368
|
-
def
|
|
1369
|
-
self.
|
|
1854
|
+
def test_legacy_in_place_resume_does_not_overwrite_ignored_collision(self):
|
|
1855
|
+
self.allocate_legacy(); self.accept()
|
|
1856
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1857
|
+
self.merge_pr()
|
|
1858
|
+
self.g("switch", "main")
|
|
1370
1859
|
user_file = self.task / "ignored/user.txt"
|
|
1371
1860
|
user_file.parent.mkdir()
|
|
1372
1861
|
user_file.write_text("retained user data", encoding="utf-8")
|
|
1862
|
+
before = self.anchor_state()
|
|
1373
1863
|
self.configure(interrupt_delete=True)
|
|
1374
|
-
_, r = self.complete(
|
|
1864
|
+
_, r = self.complete(acceptance=False, success=False)
|
|
1375
1865
|
self.assertNotEqual(r.returncode, 0)
|
|
1376
|
-
self.assertEqual(self.
|
|
1866
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1377
1867
|
other = self.root / "advance"
|
|
1378
1868
|
self.run_cmd(["git", "clone", str(self.bare), str(other)])
|
|
1379
1869
|
(other / "ignored").mkdir()
|
|
@@ -1381,13 +1871,15 @@ class CompletionTests(unittest.TestCase):
|
|
|
1381
1871
|
self.g("add", "-f", "ignored/user.txt", work=other)
|
|
1382
1872
|
self.g("commit", "-m", "upstream collision", work=other)
|
|
1383
1873
|
self.g("push", "origin", "main", work=other)
|
|
1384
|
-
|
|
1385
|
-
self.
|
|
1874
|
+
result, _ = self.complete(acceptance=False)
|
|
1875
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1876
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1386
1877
|
self.assertEqual(user_file.read_text(encoding="utf-8"), "retained user data")
|
|
1387
|
-
self.
|
|
1878
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1879
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1388
1880
|
|
|
1389
1881
|
def test_replaced_tree_and_unsafe_custody(self):
|
|
1390
|
-
self.allocate(
|
|
1882
|
+
self.allocate(); self.accept()
|
|
1391
1883
|
elsewhere = self.root / "elsewhere"
|
|
1392
1884
|
elsewhere.mkdir()
|
|
1393
1885
|
(self.receipt.parent / "custody").symlink_to(elsewhere, target_is_directory=True)
|