devlyn-cli 3.2.1 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +12 -102
- package/bin/devlyn.js +13 -225
- package/bin/instruction-templates.json +8 -0
- 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 +351 -25
- package/config/skills/_shared/task-complete.py +660 -150
- 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 +47 -38
- 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 -238
|
@@ -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,17 +678,13 @@ 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
|
-
if mode == "auto":
|
|
641
|
-
require(info["mergeCommitAllowed"], "repository disallows merge commits; use pr mode or resolve policy with repository owner")
|
|
642
688
|
if receipt.get("pr_number"):
|
|
643
689
|
pr = json.loads(gh(receipt, "pr", "view", str(receipt["pr_number"]), "--json", PR_FIELDS))
|
|
644
690
|
else:
|
|
@@ -649,11 +695,11 @@ def complete(args):
|
|
|
649
695
|
validate_pr(receipt, pr)
|
|
650
696
|
actual = remote_head(receipt, receipt["branch"])
|
|
651
697
|
if not pr or pr["state"] != "MERGED":
|
|
698
|
+
inspect_workspace(receipt)
|
|
699
|
+
verify_files(Path(receipt["worktree"]), receipt["files"])
|
|
652
700
|
require(actual in {None, receipt["publish_sha"]}, "remote task head changed; refusing to overwrite or merge")
|
|
653
701
|
require(actual is not None or (not pr and not receipt.get("pushed")), "published task ref disappeared; retain and inspect")
|
|
654
702
|
if actual is None:
|
|
655
|
-
inspect_workspace(receipt)
|
|
656
|
-
verify_files(Path(receipt["worktree"]), receipt["files"])
|
|
657
703
|
gref(receipt, "push", receipt["remote"], receipt["publish_sha"]+":refs/heads/"+receipt["branch"])
|
|
658
704
|
receipt["pushed"] = True
|
|
659
705
|
atomic_json(path, receipt)
|
|
@@ -665,21 +711,33 @@ def complete(args):
|
|
|
665
711
|
validate_pr(receipt, pr)
|
|
666
712
|
receipt.update(pr_number=pr["number"], pr_url=pr["url"])
|
|
667
713
|
atomic_json(path, receipt)
|
|
668
|
-
if mode == "pr":
|
|
669
|
-
|
|
714
|
+
if mode == "pr" and pr["state"] == "OPEN":
|
|
715
|
+
if pr.get("autoMergeRequest"):
|
|
716
|
+
gh(receipt, "pr", "merge", str(pr["number"]), "--disable-auto")
|
|
717
|
+
pr = json.loads(gh(receipt, "pr", "view", str(pr["number"]), "--json", PR_FIELDS))
|
|
718
|
+
validate_pr(receipt, pr)
|
|
719
|
+
require(not pr.get("autoMergeRequest"), "auto-merge request remains on owned PR; retain and inspect")
|
|
720
|
+
if pr["state"] == "OPEN":
|
|
721
|
+
return result("PR")
|
|
670
722
|
if pr["state"] != "MERGED":
|
|
671
723
|
inspect_workspace(receipt)
|
|
672
724
|
verify_files(Path(receipt["worktree"]), receipt["files"])
|
|
673
725
|
require(remote_head(receipt, receipt["branch"]) == receipt["publish_sha"], "remote head race before merge")
|
|
726
|
+
refused = None
|
|
674
727
|
if not pr.get("autoMergeRequest"):
|
|
675
|
-
|
|
728
|
+
try:
|
|
729
|
+
require(info["mergeCommitAllowed"], "repository disallows merge commits")
|
|
730
|
+
gh(receipt, "pr", "merge", str(pr["number"]), "--auto", "--merge", "--match-head-commit", receipt["publish_sha"])
|
|
731
|
+
except CompletionError as error:
|
|
732
|
+
refused = str(error)
|
|
676
733
|
pr = json.loads(gh(receipt, "pr", "view", str(pr["number"]), "--json", PR_FIELDS))
|
|
677
734
|
validate_pr(receipt, pr)
|
|
678
735
|
if pr["state"] != "MERGED":
|
|
679
|
-
|
|
736
|
+
# Merge only when the repository allows it; a refused request leaves the PR for a person.
|
|
737
|
+
return dict(result("PR"), merge_refused=refused) if refused and not pr.get("autoMergeRequest") else result("PENDING")
|
|
680
738
|
receipt["merge"] = pr
|
|
681
739
|
atomic_json(path, receipt)
|
|
682
|
-
cleanup(receipt, path, pr
|
|
740
|
+
cleanup(receipt, path, pr)
|
|
683
741
|
return result("COMPLETE")
|
|
684
742
|
|
|
685
743
|
|
|
@@ -692,7 +750,7 @@ def main():
|
|
|
692
750
|
for name in ("task", "branch", "repository", "base"):
|
|
693
751
|
allocation.add_argument("--"+name, required=True)
|
|
694
752
|
allocation.add_argument("--remote", default="origin")
|
|
695
|
-
allocation.add_argument("--worktree")
|
|
753
|
+
allocation.add_argument("--worktree", required=True)
|
|
696
754
|
completion = actions.add_parser("complete")
|
|
697
755
|
completion.add_argument("--receipt", required=True)
|
|
698
756
|
completion.add_argument("--acceptance")
|
|
@@ -742,7 +800,7 @@ def remote(ref):
|
|
|
742
800
|
return r.stdout.strip() if r.returncode == 0 else None
|
|
743
801
|
if a[:2] == ['repo', 'view']:
|
|
744
802
|
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)}))
|
|
803
|
+
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
804
|
elif a[:2] == ['pr', 'list']:
|
|
747
805
|
assert '--repo' in a and a[a.index('--repo')+1] == 'github.com/test/project'
|
|
748
806
|
print(json.dumps([d['pr']] if d.get('pr') else []))
|
|
@@ -756,6 +814,10 @@ elif a[:2] == ['pr', 'create']:
|
|
|
756
814
|
if d.pop('interrupt_create',False): save(); sys.exit(1)
|
|
757
815
|
elif a[:2] == ['pr', 'view']:
|
|
758
816
|
print(json.dumps(d['pr']))
|
|
817
|
+
elif a[:2] == ['pr', 'merge'] and '--disable-auto' in a:
|
|
818
|
+
d['disables'] = d.get('disables',0)+1
|
|
819
|
+
d['pr']['autoMergeRequest'] = None
|
|
820
|
+
save()
|
|
759
821
|
elif a[:2] == ['pr', 'merge']:
|
|
760
822
|
assert '--admin' not in a and '--delete-branch' not in a
|
|
761
823
|
assert '--auto' in a and '--merge' in a and '--match-head-commit' in a
|
|
@@ -837,7 +899,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
837
899
|
self.env["PATH"] = str(self.bin) + os.pathsep + os.environ["PATH"]
|
|
838
900
|
self.run_cmd(["git", "init", "--bare", "--initial-branch=main", str(self.bare)])
|
|
839
901
|
self.run_cmd(["git", "init", "--initial-branch=main", str(self.work)])
|
|
840
|
-
(self.work / ".gitignore").write_text(".devlyn/\nignored/\n", encoding="utf-8")
|
|
902
|
+
(self.work / ".gitignore").write_text(".devlyn/\nignored/\nnode_modules/\n.env\n", encoding="utf-8")
|
|
841
903
|
(self.work / "product").write_text("baseline\n", encoding="utf-8")
|
|
842
904
|
self.g("add", ".")
|
|
843
905
|
self.g("commit", "-m", "baseline")
|
|
@@ -862,33 +924,61 @@ class CompletionTests(unittest.TestCase):
|
|
|
862
924
|
r = self.run_cmd([sys.executable, str(Path(__file__).resolve()), *map(str, args)], success=success, cwd=cwd)
|
|
863
925
|
return json.loads(r.stdout), r
|
|
864
926
|
|
|
865
|
-
def allocate(self,
|
|
866
|
-
args = ["allocate", "--repo", self.work, "--task",
|
|
867
|
-
|
|
868
|
-
args += ["--worktree", self.root / "linked"]
|
|
927
|
+
def allocate(self, name="fixture", base="main"):
|
|
928
|
+
args = ["allocate", "--repo", self.work, "--task", name, "--branch", "task/"+name, "--repository", "test/project", "--base", base,
|
|
929
|
+
"--worktree", self.root / ("linked" if name == "fixture" else name)]
|
|
869
930
|
result, _ = self.cli(*args)
|
|
870
931
|
self.receipt = Path(result["receipt"])
|
|
871
932
|
self.task = Path(result["worktree"])
|
|
872
933
|
return result
|
|
873
934
|
|
|
935
|
+
def allocate_legacy(self):
|
|
936
|
+
common = Path(self.g("rev-parse", "--absolute-git-dir"))
|
|
937
|
+
key = hashlib.sha256(b"task/fixture").hexdigest()[:24]
|
|
938
|
+
receipt = {"task": "fixture", "repository": "test/project", "remote": "origin", "base": "main", "branch": "task/fixture",
|
|
939
|
+
"common_gitdir": str(common), "anchor": str(self.work), "linked": False, "allocation": "owned",
|
|
940
|
+
"baseline": self.g("rev-parse", "HEAD"), "worktree": str(self.work), "id": key,
|
|
941
|
+
"recovery_ref": "refs/devlyn/completed/"+key, "worktree_gitdir": str(common),
|
|
942
|
+
"remote_url": dict.fromkeys(("literal", "fetch", "push"), "https://github.com/test/project.git")}
|
|
943
|
+
self.receipt = common / "devlyn-completion" / key / "receipt.json"
|
|
944
|
+
self.receipt.parent.mkdir(parents=True)
|
|
945
|
+
scratch = self.receipt.parent / "scratch"
|
|
946
|
+
scratch.mkdir()
|
|
947
|
+
receipt["scratch_identity"] = workspace_identity(scratch, self.receipt.parent)
|
|
948
|
+
self.g("switch", "-c", "task/fixture")
|
|
949
|
+
receipt["worktree_identity"] = workspace_identity(self.work, common)
|
|
950
|
+
self.receipt.write_text(json.dumps(receipt), encoding="utf-8")
|
|
951
|
+
self.task = self.work
|
|
952
|
+
|
|
953
|
+
def merge_pr(self):
|
|
954
|
+
self.configure(pending=False)
|
|
955
|
+
self.run_cmd(["gh", "pr", "merge", "1", "--auto", "--merge", "--match-head-commit", self.sha])
|
|
956
|
+
|
|
957
|
+
def anchor_state(self, work=None):
|
|
958
|
+
work = work or self.work
|
|
959
|
+
return (self.g("symbolic-ref", "HEAD", work=work), self.g("rev-parse", "HEAD", work=work),
|
|
960
|
+
self.g("status", "--porcelain", "--untracked-files=all", work=work), (work / "product").read_bytes(),
|
|
961
|
+
(Path(self.g("rev-parse", "--absolute-git-dir", work=work)) / "index").read_bytes())
|
|
962
|
+
|
|
874
963
|
def test_allocate_base_with_same_named_tag(self):
|
|
875
964
|
self.g("tag", "main")
|
|
876
|
-
result = self.allocate(
|
|
965
|
+
result = self.allocate()
|
|
877
966
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
878
967
|
self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], "main")
|
|
879
968
|
|
|
880
969
|
def test_allocate_base_preserves_unicode_whitespace(self):
|
|
881
970
|
base = "main\u00a0"
|
|
882
971
|
self.g("switch", "-c", base)
|
|
972
|
+
self.g("push", "origin", "HEAD:refs/heads/"+base)
|
|
883
973
|
result = self.allocate(base=base)
|
|
884
974
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
885
975
|
self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], base)
|
|
886
976
|
|
|
887
977
|
def assert_base_identity_rejected(self, actual, base):
|
|
888
978
|
result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
889
|
-
"--branch", "task/fixture", "--repository", "test/project", "--base", base, success=False)
|
|
979
|
+
"--branch", "task/fixture", "--repository", "test/project", "--base", base, "--worktree", self.root / "linked", success=False)
|
|
890
980
|
self.assertNotEqual(process.returncode, 0)
|
|
891
|
-
self.assertIn("
|
|
981
|
+
self.assertIn("refs/heads/"+base, result["reason"])
|
|
892
982
|
head = self.run_cmd(["git", "-C", str(self.work), "symbolic-ref", "HEAD"]).stdout
|
|
893
983
|
self.assertEqual(head, "refs/heads/" + actual + "\n")
|
|
894
984
|
self.assertFalse((self.work / ".git/devlyn-completion").exists())
|
|
@@ -897,6 +987,8 @@ class CompletionTests(unittest.TestCase):
|
|
|
897
987
|
|
|
898
988
|
def test_allocate_rejects_trimmed_base_alias(self):
|
|
899
989
|
self.g("switch", "-c", "main\u00a0")
|
|
990
|
+
self.g("push", "origin", "HEAD:refs/heads/main\u00a0")
|
|
991
|
+
self.run_cmd(["git", "--git-dir", str(self.bare), "update-ref", "-d", "refs/heads/main"])
|
|
900
992
|
self.assert_base_identity_rejected("main\u00a0", "main")
|
|
901
993
|
|
|
902
994
|
def test_allocate_rejects_short_base_alias(self):
|
|
@@ -904,6 +996,29 @@ class CompletionTests(unittest.TestCase):
|
|
|
904
996
|
self.g("tag", "main")
|
|
905
997
|
self.assert_base_identity_rejected("main", "heads/main")
|
|
906
998
|
|
|
999
|
+
def test_allocate_uses_remote_base_with_dirty_foreign_anchor(self):
|
|
1000
|
+
baseline = self.g("rev-parse", "main")
|
|
1001
|
+
(self.work / "product").write_text("remote advance\n", encoding="utf-8")
|
|
1002
|
+
self.g("commit", "-am", "remote advance")
|
|
1003
|
+
self.g("push", "origin", "main")
|
|
1004
|
+
remote = self.g("rev-parse", "main")
|
|
1005
|
+
self.g("switch", "-c", "busy")
|
|
1006
|
+
(self.work / "product").write_text("local advance\n", encoding="utf-8")
|
|
1007
|
+
self.g("commit", "-am", "local advance")
|
|
1008
|
+
ahead = self.g("rev-parse", "HEAD")
|
|
1009
|
+
(self.work / "product").write_text("busy edits\n", encoding="utf-8")
|
|
1010
|
+
(self.work / "untracked").write_bytes(b"owner data")
|
|
1011
|
+
for name, local in (("behind", baseline), ("ahead", ahead)):
|
|
1012
|
+
self.g("update-ref", "refs/heads/main", local)
|
|
1013
|
+
before = self.anchor_state()
|
|
1014
|
+
result = self.allocate(name)
|
|
1015
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1016
|
+
self.assertEqual(json.loads(self.receipt.read_text())["baseline"], remote)
|
|
1017
|
+
self.assertEqual(self.g("rev-parse", "HEAD", work=self.task), remote)
|
|
1018
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1019
|
+
self.assertEqual((self.work / "untracked").read_bytes(), b"owner data")
|
|
1020
|
+
self.assertFalse((self.work / ".git" / "FETCH_HEAD").exists())
|
|
1021
|
+
|
|
907
1022
|
@unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
|
|
908
1023
|
def test_disposable_scratch_cleans_for_local_only_and_preserves_source(self):
|
|
909
1024
|
result = self.allocate()
|
|
@@ -934,6 +1049,29 @@ class CompletionTests(unittest.TestCase):
|
|
|
934
1049
|
child.wait(timeout=5)
|
|
935
1050
|
self.cli("clean-scratch", "--receipt", self.receipt, "--writers-stopped")
|
|
936
1051
|
|
|
1052
|
+
def test_linux_writer_scan_continues_after_vanished_fd(self):
|
|
1053
|
+
from unittest.mock import patch
|
|
1054
|
+
process = Path("/proc") / str(os.getpid() + 1)
|
|
1055
|
+
vanished, active = process / "fd/0", process / "fd/1"
|
|
1056
|
+
iterdir, readlink = Path.iterdir, os.readlink
|
|
1057
|
+
def entries(path):
|
|
1058
|
+
if path == Path("/proc"):
|
|
1059
|
+
return iter([process])
|
|
1060
|
+
if path == process / "fd":
|
|
1061
|
+
return iter([vanished, active])
|
|
1062
|
+
return iterdir(path)
|
|
1063
|
+
def target(path, *args, **kwargs):
|
|
1064
|
+
if path == process / "cwd":
|
|
1065
|
+
return str(self.root)
|
|
1066
|
+
if path == vanished:
|
|
1067
|
+
raise FileNotFoundError(path)
|
|
1068
|
+
if path == active:
|
|
1069
|
+
return str(self.work / "product")
|
|
1070
|
+
return readlink(path, *args, **kwargs)
|
|
1071
|
+
with patch.object(sys, "platform", "linux"), patch.object(Path, "iterdir", entries), patch.object(os, "readlink", target):
|
|
1072
|
+
with self.assertRaisesRegex(CompletionError, "active process " + process.name):
|
|
1073
|
+
stopped_writers(self.work)
|
|
1074
|
+
|
|
937
1075
|
@unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
|
|
938
1076
|
def test_scratch_rejects_redirect_and_git_data_but_does_not_follow_child_links(self):
|
|
939
1077
|
scratch = Path(self.allocate()["scratch"])
|
|
@@ -988,7 +1126,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
988
1126
|
scratch = Path(self.allocate()["scratch"])
|
|
989
1127
|
self.accept()
|
|
990
1128
|
(scratch / ".git").mkdir()
|
|
991
|
-
result, _ = self.cli("complete", "--receipt", self.receipt, "--acceptance", self.acceptance, "--writers-stopped")
|
|
1129
|
+
result, _ = self.cli("complete", "--receipt", self.receipt, "--acceptance", self.acceptance, "--writers-stopped", "--mode", "auto")
|
|
992
1130
|
self.assertEqual(result["status"], "CLEANUP_PENDING")
|
|
993
1131
|
self.assertEqual(result["delivery_status"], "COMPLETE")
|
|
994
1132
|
self.assertEqual(result["scratch_cleanup"]["status"], "RETAINED")
|
|
@@ -1111,7 +1249,23 @@ class CompletionTests(unittest.TestCase):
|
|
|
1111
1249
|
result, _ = self.complete("--mode", "pr")
|
|
1112
1250
|
self.assertEqual(result["status"], "PR")
|
|
1113
1251
|
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1114
|
-
|
|
1252
|
+
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1253
|
+
self.assertEqual((d["pushs"], d.get("merges", 0), d["pr"]["autoMergeRequest"]), (1, 0, None))
|
|
1254
|
+
|
|
1255
|
+
def test_pr_mode_disables_only_owned_auto_merge(self):
|
|
1256
|
+
self.allocate(); self.accept()
|
|
1257
|
+
self.configure(pending=True)
|
|
1258
|
+
result, _ = self.complete("--mode", "auto")
|
|
1259
|
+
self.assertEqual(result["status"], "PENDING")
|
|
1260
|
+
result, _ = self.complete("--mode", "pr")
|
|
1261
|
+
self.assertEqual(result["status"], "PR")
|
|
1262
|
+
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1263
|
+
self.assertEqual((d["disables"], d["pr"]["autoMergeRequest"]), (1, None))
|
|
1264
|
+
self.configure(pr=dict(d["pr"], isCrossRepository=True, autoMergeRequest={"enabledAt": "now"}))
|
|
1265
|
+
_, r = self.complete("--mode", "pr", success=False)
|
|
1266
|
+
self.assertIn("PR head repository differs", json.loads(r.stdout)["reason"])
|
|
1267
|
+
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1268
|
+
self.assertEqual((d["disables"], d["pr"]["autoMergeRequest"]), (1, {"enabledAt": "now"}))
|
|
1115
1269
|
|
|
1116
1270
|
def test_remote_rewrite_after_allocation_is_rejected(self):
|
|
1117
1271
|
self.allocate(); self.accept()
|
|
@@ -1158,28 +1312,373 @@ class CompletionTests(unittest.TestCase):
|
|
|
1158
1312
|
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1159
1313
|
self.assertEqual((d["creates"], d["merges"], d["pushs"]), (1, 1, 1))
|
|
1160
1314
|
|
|
1161
|
-
def
|
|
1162
|
-
self.
|
|
1315
|
+
def test_legacy_in_place_retains_checked_out_branch_then_retires_refs(self):
|
|
1316
|
+
self.allocate_legacy(); self.accept()
|
|
1163
1317
|
(self.task / "ignored").mkdir()
|
|
1164
1318
|
(self.task / "ignored" / "user.txt").write_text("retain me", encoding="utf-8")
|
|
1319
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1320
|
+
self.merge_pr()
|
|
1321
|
+
before = self.anchor_state()
|
|
1322
|
+
receipt, task = self.receipt, self.task
|
|
1323
|
+
result = self.allocate("next")
|
|
1324
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1325
|
+
self.assertIn(str(task), result["reconciled"][0]["reason"])
|
|
1326
|
+
self.assertIn("switch", result["reconciled"][0]["reason"])
|
|
1327
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1328
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1329
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1330
|
+
self.g("switch", "main")
|
|
1331
|
+
(self.work / "product").write_text("owner edits\n", encoding="utf-8")
|
|
1332
|
+
(self.work / "untracked").write_bytes(b"owner data")
|
|
1333
|
+
before = self.anchor_state()
|
|
1334
|
+
result = self.allocate("retired")
|
|
1335
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1336
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1337
|
+
self.assertEqual((self.work / "untracked").read_bytes(), b"owner data")
|
|
1338
|
+
self.assertEqual((task / "ignored/user.txt").read_text(encoding="utf-8"), "retain me")
|
|
1339
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1340
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1341
|
+
self.receipt, self.task = receipt, task
|
|
1342
|
+
again, _ = self.complete(acceptance=False)
|
|
1343
|
+
self.assertEqual(again["status"], "COMPLETE")
|
|
1344
|
+
|
|
1345
|
+
def test_pr_mode_cleans_after_human_merge(self):
|
|
1346
|
+
self.allocate(); self.accept()
|
|
1347
|
+
self.complete("--mode", "pr")
|
|
1348
|
+
self.merge_pr()
|
|
1349
|
+
before = self.data.read_bytes()
|
|
1350
|
+
result, _ = self.complete("--mode", "pr", "--writers-stopped")
|
|
1351
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1352
|
+
self.assertFalse(self.task.exists())
|
|
1353
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1354
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1355
|
+
saved, current = json.loads(before), json.loads(self.data.read_bytes())
|
|
1356
|
+
self.assertEqual((current["merges"], current.get("disables", 0)), (saved["merges"], saved.get("disables", 0)))
|
|
1357
|
+
|
|
1358
|
+
@unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
|
|
1359
|
+
def test_nested_repository_or_worktree_retains_tree(self):
|
|
1360
|
+
self.allocate(); self.accept()
|
|
1361
|
+
_, r = self.cli("allocate", "--repo", self.work, "--task", "inner", "--branch", "task/inner", "--repository", "test/project",
|
|
1362
|
+
"--base", "main", "--worktree", self.task / "ignored" / "inner", success=False)
|
|
1363
|
+
self.assertIn("disjoint from every registered worktree", json.loads(r.stdout)["reason"])
|
|
1364
|
+
nested = self.task / "ignored" / "nested"
|
|
1365
|
+
self.g("worktree", "add", "-b", "nested/work", str(nested), "main")
|
|
1366
|
+
(nested / "product").write_text("unsaved nested work\n", encoding="utf-8")
|
|
1367
|
+
clone = self.task / "ignored" / "clone"
|
|
1368
|
+
self.run_cmd(["git", "init", "--initial-branch=main", str(clone)])
|
|
1369
|
+
for remove in (lambda: self.g("worktree", "remove", "--force", str(nested)), lambda: shutil.rmtree(clone)):
|
|
1370
|
+
_, r = self.complete("--mode", "auto", "--writers-stopped", success=False)
|
|
1371
|
+
self.assertIn("nested repository or worktree", json.loads(r.stdout)["reason"])
|
|
1372
|
+
self.assertTrue(self.task.exists())
|
|
1373
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1374
|
+
if nested.exists():
|
|
1375
|
+
self.assertEqual((nested / "product").read_text(encoding="utf-8"), "unsaved nested work\n")
|
|
1376
|
+
remove()
|
|
1377
|
+
result, _ = self.complete("--mode", "auto", "--writers-stopped")
|
|
1378
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1379
|
+
self.assertFalse(self.task.exists())
|
|
1380
|
+
|
|
1381
|
+
@unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
|
|
1382
|
+
def test_default_mode_merges_when_possible_and_cleans(self):
|
|
1383
|
+
self.allocate(); self.accept()
|
|
1165
1384
|
result, _ = self.complete("--writers-stopped")
|
|
1166
1385
|
self.assertEqual(result["status"], "COMPLETE")
|
|
1167
|
-
self.assertEqual(self.
|
|
1168
|
-
self.
|
|
1386
|
+
self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("merges"), 1)
|
|
1387
|
+
self.assertFalse(self.task.exists())
|
|
1388
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1389
|
+
|
|
1390
|
+
def test_release_is_refused_from_inside_the_task_tree(self):
|
|
1391
|
+
self.allocate(); self.accept()
|
|
1392
|
+
_, r = self.complete("--mode", "pr", "--writers-stopped", cwd=self.task, success=False)
|
|
1393
|
+
self.assertIn("caller cwd is inside", json.loads(r.stdout)["reason"])
|
|
1394
|
+
self.assertNotIn("writers_released", json.loads(self.receipt.read_text(encoding="utf-8")))
|
|
1395
|
+
|
|
1396
|
+
@unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
|
|
1397
|
+
def test_squash_merge_cleans_and_recovery_ref_keeps_commit(self):
|
|
1398
|
+
self.allocate(); self.accept()
|
|
1399
|
+
result, _ = self.complete("--mode", "pr", "--writers-stopped")
|
|
1400
|
+
self.assertEqual(result["status"], "PR")
|
|
1401
|
+
bare = ["git", "--git-dir", str(self.bare)]
|
|
1402
|
+
base = self.run_cmd(bare + ["rev-parse", "refs/heads/main"]).stdout.strip()
|
|
1403
|
+
tree = self.run_cmd(bare + ["rev-parse", self.sha + "^{tree}"]).stdout.strip()
|
|
1404
|
+
squash = self.run_cmd(bare + ["commit-tree", tree, "-p", base, "-m", "squash fixture"]).stdout.strip()
|
|
1405
|
+
self.run_cmd(bare + ["update-ref", "refs/heads/main", squash, base])
|
|
1406
|
+
pr = json.loads(self.data.read_text(encoding="utf-8"))["pr"]
|
|
1407
|
+
self.configure(pr=dict(pr, state="MERGED", mergedAt="now", mergeCommit={"oid": squash}))
|
|
1408
|
+
result, _ = self.complete()
|
|
1409
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1410
|
+
self.assertFalse(self.task.exists())
|
|
1169
1411
|
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1170
1412
|
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1171
|
-
|
|
1172
|
-
self.
|
|
1413
|
+
self.assertEqual(self.g("rev-parse", json.loads(self.receipt.read_text(encoding="utf-8"))["recovery_ref"]), self.sha)
|
|
1414
|
+
self.assertFalse((self.work / ".git" / "FETCH_HEAD").exists())
|
|
1415
|
+
|
|
1416
|
+
def test_linked_ignored_output_disposed_and_records_preserved(self):
|
|
1417
|
+
self.allocate(); self.accept()
|
|
1418
|
+
for relative, raw in (("node_modules/x", b"build"), (".env", b"SECRET=value\n"), (".devlyn/runs/extra/log", b"extra\x00bytes\n")):
|
|
1419
|
+
path = self.task / relative
|
|
1420
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1421
|
+
path.write_bytes(raw)
|
|
1422
|
+
path.chmod(0o640)
|
|
1423
|
+
records = snapshot_files(self.task, [".devlyn"])
|
|
1424
|
+
(self.task / "untracked").write_bytes(b"owner data")
|
|
1425
|
+
_, process = self.complete("--mode", "auto", "--writers-stopped", success=False)
|
|
1426
|
+
self.assertNotEqual(process.returncode, 0)
|
|
1427
|
+
self.assertTrue(self.task.exists())
|
|
1428
|
+
self.assertEqual((self.task / "untracked").read_bytes(), b"owner data")
|
|
1429
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1430
|
+
(self.task / "untracked").unlink()
|
|
1431
|
+
result, _ = self.complete("--mode", "auto", "--writers-stopped")
|
|
1432
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1433
|
+
self.assertFalse(self.task.exists())
|
|
1434
|
+
verify_files(self.receipt.parent / "records", records)
|
|
1435
|
+
self.assertEqual(snapshot_files(self.receipt.parent / "records", [".devlyn"]), records)
|
|
1436
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1437
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1438
|
+
|
|
1439
|
+
def test_reconcile_released_merge_and_retains_without_release(self):
|
|
1440
|
+
self.allocate(); self.accept()
|
|
1441
|
+
result, _ = self.complete("--mode", "pr")
|
|
1442
|
+
self.assertEqual(result["status"], "PR")
|
|
1443
|
+
receipt, task = self.receipt, self.task
|
|
1444
|
+
self.merge_pr()
|
|
1445
|
+
result = self.allocate("unreleased")
|
|
1446
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1447
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1448
|
+
self.assertIn("writers", result["reconciled"][0]["reason"])
|
|
1449
|
+
self.assertTrue(task.exists())
|
|
1450
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1451
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1452
|
+
# Reopen only the fixture PR to exercise the owner's pre-merge release.
|
|
1453
|
+
d = json.loads(self.data.read_text())
|
|
1454
|
+
self.configure(pr=dict(d["pr"], state="OPEN", mergedAt=None, mergeCommit=None))
|
|
1455
|
+
self.receipt, self.task = receipt, task
|
|
1456
|
+
result, _ = self.complete("--mode", "pr", "--writers-stopped")
|
|
1457
|
+
self.assertEqual(result["status"], "PR")
|
|
1458
|
+
self.assertTrue(json.loads(receipt.read_text())["writers_released"])
|
|
1459
|
+
self.merge_pr()
|
|
1460
|
+
server = json.loads(self.data.read_text())
|
|
1461
|
+
result = self.allocate("released")
|
|
1462
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1463
|
+
self.assertFalse(task.exists())
|
|
1464
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1465
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1466
|
+
current = json.loads(self.data.read_text())
|
|
1467
|
+
self.assertEqual((current["pushs"], current["creates"], current["merges"], current.get("disables", 0)),
|
|
1468
|
+
(server["pushs"], server["creates"], server["merges"], server.get("disables", 0)))
|
|
1469
|
+
|
|
1470
|
+
def test_reconcile_checks_repo_policy_before_cleanup(self):
|
|
1471
|
+
self.allocate(); self.accept()
|
|
1472
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1473
|
+
self.merge_pr()
|
|
1474
|
+
receipt, task = self.receipt, self.task
|
|
1475
|
+
self.configure(default_branch="changed")
|
|
1476
|
+
result = self.allocate("next")
|
|
1477
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "RETAINED")])
|
|
1478
|
+
self.assertIn("base/default branch changed", result["reconciled"][0]["reason"])
|
|
1479
|
+
self.assertTrue(task.exists())
|
|
1480
|
+
self.assertEqual(self.g("rev-parse", "task/fixture"), self.sha)
|
|
1481
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1482
|
+
|
|
1483
|
+
def test_reconcile_skips_open_and_unaccepted_and_retains_in_use_tree(self):
|
|
1484
|
+
self.allocate(); self.accept()
|
|
1485
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1486
|
+
receipt, task = self.receipt, self.task
|
|
1487
|
+
before = receipt.read_bytes()
|
|
1488
|
+
result = self.allocate("unaccepted")
|
|
1489
|
+
self.assertEqual(result["reconciled"], [])
|
|
1490
|
+
self.assertEqual(receipt.read_bytes(), before)
|
|
1491
|
+
unaccepted, unaccepted_tree = self.receipt, self.task
|
|
1492
|
+
saved = json.loads(unaccepted.read_text())
|
|
1493
|
+
saved.update(pr_number=1, writers_released=True)
|
|
1494
|
+
unaccepted.write_text(json.dumps(saved))
|
|
1495
|
+
unaccepted_before = unaccepted.read_bytes()
|
|
1496
|
+
pr = json.loads(self.data.read_text())["pr"]
|
|
1497
|
+
self.configure(pr=dict(pr, state="CLOSED"))
|
|
1498
|
+
result = self.allocate("closed")
|
|
1499
|
+
self.assertEqual(result["reconciled"], [])
|
|
1500
|
+
self.assertEqual(receipt.read_bytes(), before)
|
|
1501
|
+
self.assertTrue(task.exists())
|
|
1502
|
+
self.configure(pr=pr)
|
|
1503
|
+
self.merge_pr()
|
|
1504
|
+
self.g("worktree", "lock", str(task))
|
|
1505
|
+
result = self.allocate("locked")
|
|
1506
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1507
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1508
|
+
self.assertIn("locked", result["reconciled"][0]["reason"])
|
|
1509
|
+
self.assertTrue(task.exists())
|
|
1510
|
+
self.g("worktree", "unlock", str(task))
|
|
1511
|
+
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"], cwd=task)
|
|
1512
|
+
try:
|
|
1513
|
+
result = self.allocate("active")
|
|
1514
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1515
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1516
|
+
self.assertIn("active process", result["reconciled"][0]["reason"])
|
|
1517
|
+
self.assertTrue(task.exists())
|
|
1518
|
+
self.assertIsNone(child.poll())
|
|
1519
|
+
finally:
|
|
1520
|
+
child.terminate(); child.wait(timeout=5)
|
|
1521
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1522
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1523
|
+
self.assertEqual(unaccepted.read_bytes(), unaccepted_before)
|
|
1524
|
+
self.assertTrue(unaccepted_tree.exists())
|
|
1525
|
+
|
|
1526
|
+
def test_reconcile_already_retired_tree_and_branch(self):
|
|
1527
|
+
self.allocate(); self.accept()
|
|
1528
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1529
|
+
self.merge_pr()
|
|
1530
|
+
receipt, task = self.receipt, self.task
|
|
1531
|
+
self.g("worktree", "remove", str(task))
|
|
1532
|
+
self.g("update-ref", "-d", "refs/heads/task/fixture", self.sha)
|
|
1533
|
+
before = self.anchor_state()
|
|
1534
|
+
result = self.allocate("next")
|
|
1535
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1536
|
+
self.assertFalse(task.exists())
|
|
1537
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1538
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1539
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1540
|
+
|
|
1541
|
+
def test_reconcile_never_removes_allocation_anchor(self):
|
|
1542
|
+
self.allocate(); self.accept()
|
|
1543
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1544
|
+
self.merge_pr()
|
|
1545
|
+
before = self.anchor_state(self.task)
|
|
1546
|
+
result, _ = self.cli("allocate", "--repo", self.task, "--task", "next", "--branch", "task/next",
|
|
1547
|
+
"--repository", "test/project", "--base", "main", "--worktree", self.root / "next")
|
|
1548
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1549
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1550
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1551
|
+
self.assertIn("anchor", result["reconciled"][0]["reason"])
|
|
1552
|
+
self.assertEqual(self.anchor_state(self.task), before)
|
|
1553
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1554
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1555
|
+
|
|
1556
|
+
def test_reconcile_reports_scratch_cleanup_pending(self):
|
|
1557
|
+
scratch = Path(self.allocate()["scratch"])
|
|
1558
|
+
self.accept()
|
|
1559
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1560
|
+
self.merge_pr()
|
|
1561
|
+
receipt, task = self.receipt, self.task
|
|
1562
|
+
(scratch / "object").write_bytes(b"rebuildable")
|
|
1563
|
+
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"], cwd=scratch)
|
|
1564
|
+
try:
|
|
1565
|
+
result = self.allocate("next")
|
|
1566
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1567
|
+
row = result["reconciled"][0]
|
|
1568
|
+
self.assertEqual((row["status"], row["delivery_status"]), ("CLEANUP_PENDING", "COMPLETE"))
|
|
1569
|
+
self.assertEqual(row["scratch_cleanup"]["status"], "RETAINED")
|
|
1570
|
+
self.assertIn("active process", row["scratch_cleanup"]["reason"])
|
|
1571
|
+
self.assertEqual((scratch / "object").read_bytes(), b"rebuildable")
|
|
1572
|
+
self.assertIsNone(child.poll())
|
|
1573
|
+
finally:
|
|
1574
|
+
child.terminate(); child.wait(timeout=5)
|
|
1575
|
+
self.assertFalse(task.exists())
|
|
1576
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1577
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1578
|
+
self.configure(default_branch="changed")
|
|
1579
|
+
server = json.loads(self.data.read_text())
|
|
1580
|
+
result = self.allocate("retry")
|
|
1581
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1582
|
+
self.assertFalse(any(scratch.iterdir()))
|
|
1583
|
+
self.assertEqual(json.loads(self.data.read_text()), server)
|
|
1584
|
+
self.assertEqual(self.allocate("later")["reconciled"], [])
|
|
1585
|
+
|
|
1586
|
+
def test_reconcile_retries_unattempted_scratch_cleanup(self):
|
|
1587
|
+
scratch = Path(self.allocate()["scratch"])
|
|
1588
|
+
self.accept()
|
|
1589
|
+
self.complete("--mode", "auto", "--writers-stopped")
|
|
1590
|
+
receipt = self.receipt
|
|
1591
|
+
saved = json.loads(receipt.read_text())
|
|
1592
|
+
del saved["scratch_cleanup"]
|
|
1593
|
+
receipt.write_text(json.dumps(saved))
|
|
1594
|
+
(scratch / "object").write_bytes(b"rebuildable")
|
|
1595
|
+
result = self.allocate("next")
|
|
1596
|
+
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
1597
|
+
self.assertFalse(any(scratch.iterdir()))
|
|
1598
|
+
|
|
1599
|
+
def test_reconcile_retains_busy_receipt_without_waiting(self):
|
|
1600
|
+
self.allocate(); self.accept()
|
|
1601
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1602
|
+
self.merge_pr()
|
|
1603
|
+
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"
|
|
1604
|
+
child = subprocess.Popen([sys.executable, "-c", code, str(Path(__file__).resolve().with_name("platform-support.py")),
|
|
1605
|
+
str(self.receipt.parent / "lock")], stdout=subprocess.PIPE, text=True)
|
|
1606
|
+
try:
|
|
1607
|
+
self.assertEqual(child.stdout.readline().strip(), "locked")
|
|
1608
|
+
process = subprocess.run([sys.executable, str(Path(__file__).resolve()), "allocate", "--repo", str(self.work),
|
|
1609
|
+
"--task", "next", "--branch", "task/next", "--repository", "test/project", "--base", "main",
|
|
1610
|
+
"--worktree", str(self.root / "next")], cwd=self.root, env=self.env, capture_output=True, text=True, timeout=10)
|
|
1611
|
+
self.assertEqual(process.returncode, 0, process.stdout+process.stderr)
|
|
1612
|
+
result = json.loads(process.stdout)
|
|
1613
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1614
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1615
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1616
|
+
self.assertIn("lock", result["reconciled"][0]["reason"])
|
|
1617
|
+
self.assertTrue(self.task.exists())
|
|
1618
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1619
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1620
|
+
self.assertIsNone(child.poll())
|
|
1621
|
+
finally:
|
|
1622
|
+
child.terminate(); child.wait(timeout=5)
|
|
1623
|
+
child.stdout.close()
|
|
1624
|
+
|
|
1625
|
+
def test_reconcile_retains_malformed_receipt_without_failing_allocation(self):
|
|
1626
|
+
self.allocate()
|
|
1627
|
+
receipt, task = self.receipt, self.task
|
|
1628
|
+
receipt.write_text("[]", encoding="utf-8")
|
|
1629
|
+
result = self.allocate("next")
|
|
1630
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1631
|
+
self.assertTrue(self.task.exists())
|
|
1632
|
+
self.assertEqual(len(result["reconciled"]), 1)
|
|
1633
|
+
self.assertEqual(result["reconciled"][0]["status"], "RETAINED")
|
|
1634
|
+
self.assertEqual(result["reconciled"][0]["receipt"], str(receipt))
|
|
1635
|
+
self.assertTrue(result["reconciled"][0]["reason"])
|
|
1636
|
+
self.assertEqual(receipt.read_text(), "[]")
|
|
1637
|
+
self.assertTrue(task.exists())
|
|
1638
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1639
|
+
|
|
1640
|
+
def test_records_refuse_symlink_nonregular_and_tampered_resume(self):
|
|
1641
|
+
self.allocate(); self.accept()
|
|
1642
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1643
|
+
self.merge_pr()
|
|
1644
|
+
other = self.root / "owner-file"
|
|
1645
|
+
other.write_bytes(b"owner data")
|
|
1646
|
+
link = self.task / ".devlyn/link"
|
|
1647
|
+
link.symlink_to(other)
|
|
1648
|
+
result, _ = self.complete(success=False)
|
|
1649
|
+
self.assertIn("path escapes custody", result["reason"])
|
|
1650
|
+
self.assertTrue(self.task.exists())
|
|
1651
|
+
self.assertTrue(link.is_symlink())
|
|
1652
|
+
self.assertEqual(other.read_bytes(), b"owner data")
|
|
1653
|
+
link.unlink()
|
|
1654
|
+
if hasattr(os, "mkfifo"):
|
|
1655
|
+
os.mkfifo(link)
|
|
1656
|
+
result, _ = self.complete(success=False)
|
|
1657
|
+
self.assertIn("regular file", result["reason"])
|
|
1658
|
+
self.assertTrue(self.task.exists())
|
|
1659
|
+
link.unlink()
|
|
1660
|
+
saved = self.receipt.parent / "records/.devlyn"
|
|
1661
|
+
shutil.copytree(self.task / ".devlyn", saved)
|
|
1662
|
+
(saved / "checks.txt").write_bytes(b"tampered copy")
|
|
1663
|
+
result, _ = self.complete(success=False)
|
|
1664
|
+
self.assertIn("bytes/mode changed", result["reason"])
|
|
1665
|
+
self.assertEqual((saved / "checks.txt").read_bytes(), b"tampered copy")
|
|
1666
|
+
self.assertTrue(self.task.exists())
|
|
1667
|
+
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1668
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
|
|
1669
|
+
shutil.copy2(self.task / ".devlyn/checks.txt", saved / "checks.txt")
|
|
1670
|
+
result, _ = self.complete()
|
|
1671
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1672
|
+
self.assertFalse(self.task.exists())
|
|
1173
1673
|
|
|
1174
1674
|
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()
|
|
1675
|
+
self.allocate(); self.accept(pipeline=True)
|
|
1178
1676
|
self.configure(interrupt_remove=True)
|
|
1179
|
-
result, r = self.complete("--writers-stopped", success=False)
|
|
1677
|
+
result, r = self.complete("--writers-stopped", "--mode", "auto", success=False)
|
|
1180
1678
|
self.assertNotEqual(r.returncode, 0)
|
|
1181
1679
|
self.assertFalse(self.task.exists())
|
|
1182
1680
|
saved = json.loads(self.receipt.read_text(encoding="utf-8"))
|
|
1681
|
+
self.assertNotIn("cleanup_started", saved)
|
|
1183
1682
|
recovered = self.receipt.parent / "custody" / ".devlyn/runs/fixture-run/final-report.md"
|
|
1184
1683
|
self.assertEqual(hashlib.sha256(recovered.read_bytes()).hexdigest(), saved["files"][".devlyn/runs/fixture-run/final-report.md"]["sha256"])
|
|
1185
1684
|
result, _ = self.complete("--writers-stopped", acceptance=False)
|
|
@@ -1224,33 +1723,35 @@ class CompletionTests(unittest.TestCase):
|
|
|
1224
1723
|
|
|
1225
1724
|
def test_interrupted_external_effects(self):
|
|
1226
1725
|
self.allocate(); self.accept()
|
|
1227
|
-
for effect in ("push", "create"
|
|
1726
|
+
for effect in ("push", "create"):
|
|
1228
1727
|
self.configure(**{"interrupt_"+effect: True})
|
|
1229
|
-
_, r = self.complete("--writers-stopped", success=False)
|
|
1728
|
+
_, r = self.complete("--writers-stopped", "--mode", "auto", success=False)
|
|
1230
1729
|
self.assertNotEqual(r.returncode, 0)
|
|
1730
|
+
# A merge command that fails after merging is judged by the PR state, not its exit code.
|
|
1731
|
+
self.configure(interrupt_merge=True)
|
|
1231
1732
|
result, _ = self.complete("--writers-stopped")
|
|
1232
1733
|
self.assertEqual(result["status"], "COMPLETE")
|
|
1233
1734
|
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1234
1735
|
self.assertEqual((d["pushs"], d["creates"], d["merges"]), (1, 1, 1))
|
|
1235
1736
|
|
|
1236
|
-
def
|
|
1237
|
-
self.allocate(
|
|
1238
|
-
for case in ("
|
|
1239
|
-
path = self.task / ("
|
|
1240
|
-
if case in {"
|
|
1737
|
+
def test_retains_dirty_locked_and_active_tree(self):
|
|
1738
|
+
self.allocate(); self.accept()
|
|
1739
|
+
for case in ("no-yield", "dirty", "untracked", "locked", "cwd"):
|
|
1740
|
+
path = self.task / ("product" if case == "dirty" else "unknown")
|
|
1741
|
+
if case in {"dirty", "untracked"}:
|
|
1241
1742
|
path.parent.mkdir(exist_ok=True); path.write_text("do not delete", encoding="utf-8")
|
|
1242
1743
|
if case == "locked": self.g("worktree", "lock", str(self.task))
|
|
1243
1744
|
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)
|
|
1745
|
+
_, r = self.complete("--mode", "auto", *([] if case == "no-yield" else ["--writers-stopped"]), cwd=self.task if case == "cwd" else None, success=False)
|
|
1245
1746
|
self.assertNotEqual(r.returncode, 0)
|
|
1246
1747
|
self.assertTrue(self.task.exists())
|
|
1247
1748
|
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1248
|
-
if case
|
|
1749
|
+
if case == "untracked": path.unlink()
|
|
1249
1750
|
if case == "dirty": path.write_text("accepted\n", encoding="utf-8")
|
|
1250
1751
|
if case == "locked": self.g("worktree", "unlock", str(self.task))
|
|
1251
1752
|
|
|
1252
1753
|
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)
|
|
1754
|
+
_, r = self.cli("allocate", "--repo", self.work, "--task", "foreign", "--branch", "main", "--repository", "test/project", "--base", "main", "--worktree", self.root / "foreign", success=False)
|
|
1254
1755
|
self.assertNotEqual(r.returncode, 0)
|
|
1255
1756
|
self.allocate(); self.accept()
|
|
1256
1757
|
self.complete("--mode", "pr")
|
|
@@ -1261,7 +1762,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1261
1762
|
|
|
1262
1763
|
def test_concurrent_completions(self):
|
|
1263
1764
|
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)]
|
|
1765
|
+
args = [sys.executable, str(Path(__file__).resolve()), "complete", "--receipt", str(self.receipt), "--acceptance", str(self.acceptance), "--mode", "auto"]
|
|
1265
1766
|
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
1767
|
for proc in procs:
|
|
1267
1768
|
out, err = proc.communicate(timeout=30)
|
|
@@ -1270,7 +1771,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1270
1771
|
self.assertEqual((d["pushs"], d["creates"], d["merges"]), (1,1,1))
|
|
1271
1772
|
|
|
1272
1773
|
def test_custody_failure_and_evidence_tamper(self):
|
|
1273
|
-
self.allocate(
|
|
1774
|
+
self.allocate(); self.accept()
|
|
1274
1775
|
bad = self.receipt.parent / "custody/.devlyn/checks.txt"
|
|
1275
1776
|
bad.parent.mkdir(parents=True)
|
|
1276
1777
|
bad.write_text("partial interrupted copy", encoding="utf-8")
|
|
@@ -1287,10 +1788,10 @@ class CompletionTests(unittest.TestCase):
|
|
|
1287
1788
|
self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("merges",0), 0)
|
|
1288
1789
|
|
|
1289
1790
|
def test_actual_foreign_writer_and_registration(self):
|
|
1290
|
-
self.allocate(
|
|
1791
|
+
self.allocate(); self.accept()
|
|
1291
1792
|
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"], cwd=self.task)
|
|
1292
1793
|
try:
|
|
1293
|
-
_, r = self.complete("--writers-stopped", success=False)
|
|
1794
|
+
_, r = self.complete("--writers-stopped", "--mode", "auto", success=False)
|
|
1294
1795
|
self.assertNotEqual(r.returncode, 0)
|
|
1295
1796
|
self.assertTrue(self.task.exists())
|
|
1296
1797
|
self.assertIn("active process", json.loads(r.stdout)["reason"])
|
|
@@ -1307,7 +1808,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1307
1808
|
self.allocate(); self.accept()
|
|
1308
1809
|
race = self.g("rev-parse", "main")
|
|
1309
1810
|
self.configure(remote_delete_race=True, race_sha=race)
|
|
1310
|
-
_, r = self.complete("--writers-stopped", success=False)
|
|
1811
|
+
_, r = self.complete("--writers-stopped", "--mode", "auto", success=False)
|
|
1311
1812
|
self.assertNotEqual(r.returncode, 0)
|
|
1312
1813
|
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], race)
|
|
1313
1814
|
self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
@@ -1315,16 +1816,19 @@ class CompletionTests(unittest.TestCase):
|
|
|
1315
1816
|
def test_policy_and_local_only_persist(self):
|
|
1316
1817
|
self.allocate(); self.accept()
|
|
1317
1818
|
self.configure(merge_allowed=False)
|
|
1318
|
-
|
|
1319
|
-
self.
|
|
1320
|
-
|
|
1819
|
+
result, _ = self.complete()
|
|
1820
|
+
self.assertEqual((result["status"], result["merge_refused"]), ("PR", "repository disallows merge commits"))
|
|
1821
|
+
d = json.loads(self.data.read_text(encoding="utf-8"))
|
|
1822
|
+
self.assertEqual((d.get("pushs", 0), d.get("merges", 0)), (1, 0))
|
|
1321
1823
|
self.complete("--mode", "pr")
|
|
1322
1824
|
result, _ = self.complete()
|
|
1323
1825
|
self.assertEqual(result["status"], "PR")
|
|
1826
|
+
self.assertNotIn("merge_refused", result)
|
|
1324
1827
|
self.configure(merge_allowed=True, auto_allowed=False, pending=True)
|
|
1325
|
-
|
|
1326
|
-
self.
|
|
1327
|
-
self.assertIn("disallows auto merge",
|
|
1828
|
+
result, _ = self.complete("--mode", "auto")
|
|
1829
|
+
self.assertEqual(result["status"], "PR")
|
|
1830
|
+
self.assertIn("disallows auto merge", result["merge_refused"])
|
|
1831
|
+
self.assertTrue(self.task.exists())
|
|
1328
1832
|
result, _ = self.complete("--no-push")
|
|
1329
1833
|
self.assertEqual(result["status"], "LOCAL_ONLY")
|
|
1330
1834
|
result, _ = self.complete(acceptance=False)
|
|
@@ -1365,15 +1869,19 @@ class CompletionTests(unittest.TestCase):
|
|
|
1365
1869
|
result, _ = self.complete("--mode", "pr")
|
|
1366
1870
|
self.assertEqual(result["status"], "PR")
|
|
1367
1871
|
|
|
1368
|
-
def
|
|
1369
|
-
self.
|
|
1872
|
+
def test_legacy_in_place_resume_does_not_overwrite_ignored_collision(self):
|
|
1873
|
+
self.allocate_legacy(); self.accept()
|
|
1874
|
+
self.complete("--mode", "pr", "--writers-stopped")
|
|
1875
|
+
self.merge_pr()
|
|
1876
|
+
self.g("switch", "main")
|
|
1370
1877
|
user_file = self.task / "ignored/user.txt"
|
|
1371
1878
|
user_file.parent.mkdir()
|
|
1372
1879
|
user_file.write_text("retained user data", encoding="utf-8")
|
|
1880
|
+
before = self.anchor_state()
|
|
1373
1881
|
self.configure(interrupt_delete=True)
|
|
1374
|
-
_, r = self.complete(
|
|
1882
|
+
_, r = self.complete(acceptance=False, success=False)
|
|
1375
1883
|
self.assertNotEqual(r.returncode, 0)
|
|
1376
|
-
self.assertEqual(self.
|
|
1884
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1377
1885
|
other = self.root / "advance"
|
|
1378
1886
|
self.run_cmd(["git", "clone", str(self.bare), str(other)])
|
|
1379
1887
|
(other / "ignored").mkdir()
|
|
@@ -1381,13 +1889,15 @@ class CompletionTests(unittest.TestCase):
|
|
|
1381
1889
|
self.g("add", "-f", "ignored/user.txt", work=other)
|
|
1382
1890
|
self.g("commit", "-m", "upstream collision", work=other)
|
|
1383
1891
|
self.g("push", "origin", "main", work=other)
|
|
1384
|
-
|
|
1385
|
-
self.
|
|
1892
|
+
result, _ = self.complete(acceptance=False)
|
|
1893
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1894
|
+
self.assertEqual(self.anchor_state(), before)
|
|
1386
1895
|
self.assertEqual(user_file.read_text(encoding="utf-8"), "retained user data")
|
|
1387
|
-
self.
|
|
1896
|
+
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
1897
|
+
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1388
1898
|
|
|
1389
1899
|
def test_replaced_tree_and_unsafe_custody(self):
|
|
1390
|
-
self.allocate(
|
|
1900
|
+
self.allocate(); self.accept()
|
|
1391
1901
|
elsewhere = self.root / "elsewhere"
|
|
1392
1902
|
elsewhere.mkdir()
|
|
1393
1903
|
(self.receipt.parent / "custody").symlink_to(elsewhere, target_is_directory=True)
|