@codyswann/lisa 4.54.13 → 4.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-contents/.gitattributes +8 -0
- package/all/copy-overwrite/scripts/check-orphaned-branches.mjs +78 -9
- package/all/copy-overwrite/scripts/check-third-party-action-pins.mjs +17 -1
- package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +194 -7
- package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +31 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +81 -2
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +293 -8
- package/all/copy-overwrite/scripts/lisa-hooks/block-instruction-file-edits.sh +21 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +78 -15
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +21 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-shell-json-parsing.sh +21 -0
- package/all/copy-overwrite/scripts/lisa-hooks/guard-dedupe.bash +340 -0
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +29 -1
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +183 -1
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.sh +21 -0
- package/all/copy-overwrite/scripts/lisa-postinstall.mjs +21 -1
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +100 -5
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +1085 -80
- package/all/create-only/.github/workflows/continuous-gates.yml +7 -0
- package/cdk/create-only/.github/workflows/ci.yml +7 -0
- package/cdk/create-only/.github/workflows/deploy.yml +21 -0
- package/dist/cli/apply.d.ts.map +1 -1
- package/dist/cli/apply.js +9 -1
- package/dist/cli/apply.js.map +1 -1
- package/dist/cli/doctor-apply-deletions.d.ts +61 -0
- package/dist/cli/doctor-apply-deletions.d.ts.map +1 -0
- package/dist/cli/doctor-apply-deletions.js +137 -0
- package/dist/cli/doctor-apply-deletions.js.map +1 -0
- package/dist/cli/doctor-cdk-preset-adoption.d.ts +15 -0
- package/dist/cli/doctor-cdk-preset-adoption.d.ts.map +1 -0
- package/dist/cli/doctor-cdk-preset-adoption.js +316 -0
- package/dist/cli/doctor-cdk-preset-adoption.js.map +1 -0
- package/dist/cli/doctor-config-shadowing.d.ts +22 -0
- package/dist/cli/doctor-config-shadowing.d.ts.map +1 -0
- package/dist/cli/doctor-config-shadowing.js +123 -0
- package/dist/cli/doctor-config-shadowing.js.map +1 -0
- package/dist/cli/doctor-rails-deploy-intent.d.ts +15 -0
- package/dist/cli/doctor-rails-deploy-intent.d.ts.map +1 -0
- package/dist/cli/doctor-rails-deploy-intent.js +197 -0
- package/dist/cli/doctor-rails-deploy-intent.js.map +1 -0
- package/dist/cli/doctor-seeded-artifacts.d.ts +12 -0
- package/dist/cli/doctor-seeded-artifacts.d.ts.map +1 -0
- package/dist/cli/doctor-seeded-artifacts.js +54 -0
- package/dist/cli/doctor-seeded-artifacts.js.map +1 -0
- package/dist/cli/doctor-stale-banner-scan.d.ts +44 -0
- package/dist/cli/doctor-stale-banner-scan.d.ts.map +1 -0
- package/dist/cli/doctor-stale-banner-scan.js +152 -0
- package/dist/cli/doctor-stale-banner-scan.js.map +1 -0
- package/dist/cli/doctor-stale-managed-banner.d.ts +19 -0
- package/dist/cli/doctor-stale-managed-banner.d.ts.map +1 -0
- package/dist/cli/doctor-stale-managed-banner.js +198 -0
- package/dist/cli/doctor-stale-managed-banner.js.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +16 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/ui-detected-stacks.d.ts.map +1 -1
- package/dist/cli/ui-detected-stacks.js +3 -2
- package/dist/cli/ui-detected-stacks.js.map +1 -1
- package/dist/cli/update-check.d.ts.map +1 -1
- package/dist/cli/update-check.js +14 -0
- package/dist/cli/update-check.js.map +1 -1
- package/dist/cli/worktree-liveness.d.ts +5 -0
- package/dist/cli/worktree-liveness.d.ts.map +1 -1
- package/dist/cli/worktree-liveness.js +7 -6
- package/dist/cli/worktree-liveness.js.map +1 -1
- package/dist/cli/worktree-ownership.d.ts +13 -0
- package/dist/cli/worktree-ownership.d.ts.map +1 -1
- package/dist/cli/worktree-ownership.js +14 -0
- package/dist/cli/worktree-ownership.js.map +1 -1
- package/dist/configs/eslint/base.d.ts +34 -0
- package/dist/configs/eslint/base.d.ts.map +1 -1
- package/dist/configs/eslint/base.js +34 -0
- package/dist/configs/eslint/base.js.map +1 -1
- package/dist/core/apply-receipt.d.ts +65 -0
- package/dist/core/apply-receipt.d.ts.map +1 -1
- package/dist/core/apply-receipt.js +62 -2
- package/dist/core/apply-receipt.js.map +1 -1
- package/dist/core/cdk-preset-adoption.d.ts +136 -0
- package/dist/core/cdk-preset-adoption.d.ts.map +1 -0
- package/dist/core/cdk-preset-adoption.js +135 -0
- package/dist/core/cdk-preset-adoption.js.map +1 -0
- package/dist/core/config-shadowing.d.ts +36 -0
- package/dist/core/config-shadowing.d.ts.map +1 -1
- package/dist/core/config-shadowing.js +38 -0
- package/dist/core/config-shadowing.js.map +1 -1
- package/dist/core/config.d.ts +11 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/learnings-merge-driver.d.ts +10 -0
- package/dist/core/learnings-merge-driver.d.ts.map +1 -1
- package/dist/core/learnings-merge-driver.js +18 -0
- package/dist/core/learnings-merge-driver.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +136 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/lisa.d.ts.map +1 -1
- package/dist/core/lisa.js +9 -1
- package/dist/core/lisa.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts +5 -5
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +7 -7
- package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
- package/dist/core/ownership-header.d.ts +74 -0
- package/dist/core/ownership-header.d.ts.map +1 -0
- package/dist/core/ownership-header.js +76 -0
- package/dist/core/ownership-header.js.map +1 -0
- package/dist/core/rails-deploy-production-intent.d.ts +36 -0
- package/dist/core/rails-deploy-production-intent.d.ts.map +1 -0
- package/dist/core/rails-deploy-production-intent.js +79 -0
- package/dist/core/rails-deploy-production-intent.js.map +1 -0
- package/dist/core/stale-managed-banner.d.ts +78 -0
- package/dist/core/stale-managed-banner.d.ts.map +1 -0
- package/dist/core/stale-managed-banner.js +118 -0
- package/dist/core/stale-managed-banner.js.map +1 -0
- package/dist/core/two-channel-delivery-scan.d.ts +33 -1
- package/dist/core/two-channel-delivery-scan.d.ts.map +1 -1
- package/dist/core/two-channel-delivery-scan.js +133 -6
- package/dist/core/two-channel-delivery-scan.js.map +1 -1
- package/dist/core/two-channel-delivery.d.ts +101 -4
- package/dist/core/two-channel-delivery.d.ts.map +1 -1
- package/dist/core/two-channel-delivery.js +121 -13
- package/dist/core/two-channel-delivery.js.map +1 -1
- package/dist/core/two-channel-staleness.d.ts +137 -0
- package/dist/core/two-channel-staleness.d.ts.map +1 -0
- package/dist/core/two-channel-staleness.js +268 -0
- package/dist/core/two-channel-staleness.js.map +1 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +265 -121
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/core/workflow-deletion-ownership.d.ts +6 -24
- package/dist/core/workflow-deletion-ownership.d.ts.map +1 -1
- package/dist/core/workflow-deletion-ownership.js +22 -42
- package/dist/core/workflow-deletion-ownership.js.map +1 -1
- package/dist/detection/detectors/cdk.d.ts.map +1 -1
- package/dist/detection/detectors/cdk.js +2 -1
- package/dist/detection/detectors/cdk.js.map +1 -1
- package/dist/health/template-inspection.d.ts.map +1 -1
- package/dist/health/template-inspection.js +2 -1
- package/dist/health/template-inspection.js.map +1 -1
- package/dist/migrations/ensure-lisa-postinstall.d.ts +16 -0
- package/dist/migrations/ensure-lisa-postinstall.d.ts.map +1 -1
- package/dist/migrations/ensure-lisa-postinstall.js +16 -0
- package/dist/migrations/ensure-lisa-postinstall.js.map +1 -1
- package/dist/migrations/ensure-pinned-reusable-workflow-refs.d.ts +2 -2
- package/dist/migrations/ensure-pinned-reusable-workflow-refs.d.ts.map +1 -1
- package/dist/migrations/ensure-pinned-reusable-workflow-refs.js +98 -34
- package/dist/migrations/ensure-pinned-reusable-workflow-refs.js.map +1 -1
- package/dist/migrations/ensure-playwright-dedicated-caller.d.ts.map +1 -1
- package/dist/migrations/ensure-playwright-dedicated-caller.js +1 -0
- package/dist/migrations/ensure-playwright-dedicated-caller.js.map +1 -1
- package/dist/opencode/hooks-installer.d.ts.map +1 -1
- package/dist/opencode/hooks-installer.js +1 -0
- package/dist/opencode/hooks-installer.js.map +1 -1
- package/dist/opencode/plugin-templates/block-managed-file-edits.sh +78 -15
- package/dist/opencode/plugin-templates/block-no-verify.sh +21 -0
- package/dist/opencode/plugin-templates/guard-dedupe.bash +337 -0
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +242 -55
- package/dist/opencode/plugin-templates/parity-safety-net.sh +29 -1
- package/dist/strategies/package-lisa.d.ts.map +1 -1
- package/dist/strategies/package-lisa.js +2 -1
- package/dist/strategies/package-lisa.js.map +1 -1
- package/dist/utils/path-utils.d.ts +29 -0
- package/dist/utils/path-utils.d.ts.map +1 -1
- package/dist/utils/path-utils.js +41 -0
- package/dist/utils/path-utils.js.map +1 -1
- package/expo/create-only/.github/workflows/ci.yml +21 -0
- package/expo/create-only/.github/workflows/deploy.yml +28 -0
- package/expo/create-only/.github/workflows/maestro-e2e.yml +7 -0
- package/expo/create-only/.github/workflows/nightly-e2e-bypass-reaper.yml +124 -13
- package/expo/create-only/.github/workflows/nightly-e2e-health.yml +7 -0
- package/expo/create-only/.github/workflows/nightly-e2e-report.yml +7 -0
- package/expo/create-only/.github/workflows/nightly-e2e-tracking.yml +8 -0
- package/expo/create-only/.github/workflows/playwright-e2e.yml +7 -0
- package/harper-fabric/copy-overwrite/.github/workflows/ci.yml +7 -0
- package/harper-fabric/create-only/.github/workflows/deploy.yml +14 -0
- package/nestjs/create-only/.github/workflows/ci.yml +14 -0
- package/nestjs/create-only/.github/workflows/deploy.yml +28 -0
- package/package.json +10 -5
- package/phaser/copy-overwrite/.github/workflows/ci.yml +7 -0
- package/plugins/lisa/.claude-plugin/plugin.json +28 -1
- package/plugins/lisa/.codex-plugin/hooks.json +27 -0
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
- package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +27 -4
- package/plugins/lisa/.codex-plugin/skills/lisa-github-prd-intake/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +22 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-github-verify/SKILL.md +14 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +51 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +27 -4
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-verify/SKILL.md +14 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +51 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +55 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +27 -4
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-prd-intake/SKILL.md +47 -30
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +22 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-verify/SKILL.md +14 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +54 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-qa-clear/SKILL.md +19 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-qa-fail/SKILL.md +28 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-qa-queue/SKILL.md +28 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +94 -24
- package/plugins/lisa/.codex-plugin/skills/lisa-rework-triage/SKILL.md +32 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-linear/SKILL.md +4 -4
- package/plugins/lisa/.codex-plugin/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-build-intake/SKILL.md +9 -0
- package/plugins/lisa/agents/linear-prd-intake.md +8 -8
- package/plugins/lisa/hooks/block-blind-automerge.sh +81 -2
- package/plugins/lisa/hooks/block-direct-issue-create.agy.sh +64 -16
- package/plugins/lisa/hooks/block-direct-issue-create.sh +293 -8
- package/plugins/lisa/hooks/block-instruction-file-edits.sh +21 -0
- package/plugins/lisa/hooks/block-managed-file-edits.sh +78 -15
- package/plugins/lisa/hooks/block-no-verify.sh +21 -0
- package/plugins/lisa/hooks/block-shell-json-parsing.sh +21 -0
- package/plugins/lisa/hooks/failure-signature-index.mjs +240 -5
- package/plugins/lisa/hooks/guard-dedupe.bash +337 -0
- package/plugins/lisa/hooks/inject-rules.sh +13 -5
- package/plugins/lisa/hooks/operational-hazards.mjs +736 -0
- package/plugins/lisa/hooks/operational-hazards.sh +28 -0
- package/plugins/lisa/hooks/parity-safety-net.sh +29 -1
- package/plugins/lisa/hooks/threshold-ratchet-compare.mjs +102 -21
- package/plugins/lisa/hooks/threshold-ratchet-families.mjs +130 -2
- package/plugins/lisa/hooks/threshold-ratchet.mjs +63 -13
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +183 -1
- package/plugins/lisa/hooks/worktree-binding-guard.sh +21 -0
- package/plugins/lisa/rules/eager/00-rule-index.md +1 -0
- package/plugins/lisa/rules/eager/operational-hazards.md +50 -0
- package/plugins/lisa/rules/eager/tracked-work.md +1 -1
- package/plugins/lisa/rules/reference/config-resolution.md +1 -1
- package/plugins/lisa/rules/reference/leaf-only-lifecycle.md +1 -1
- package/plugins/lisa/rules/reference/operational-hazards.md +115 -0
- package/plugins/lisa/rules/reference/work-item-trailer-definition.md +129 -0
- package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +407 -26
- package/plugins/lisa/scripts/qa-signal-lifecycle.mjs +507 -0
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
- package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +27 -4
- package/plugins/lisa/skills/lisa-github-prd-intake/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +22 -0
- package/plugins/lisa/skills/lisa-github-verify/SKILL.md +14 -0
- package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +51 -0
- package/plugins/lisa/skills/lisa-intake/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +27 -4
- package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
- package/plugins/lisa/skills/lisa-jira-verify/SKILL.md +14 -0
- package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +51 -0
- package/plugins/lisa/skills/lisa-linear-access/SKILL.md +55 -2
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +27 -4
- package/plugins/lisa/skills/lisa-linear-prd-intake/SKILL.md +48 -31
- package/plugins/lisa/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +22 -0
- package/plugins/lisa/skills/lisa-linear-verify/SKILL.md +14 -0
- package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +54 -3
- package/plugins/lisa/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa/skills/lisa-qa-clear/SKILL.md +19 -7
- package/plugins/lisa/skills/lisa-qa-fail/SKILL.md +28 -3
- package/plugins/lisa/skills/lisa-qa-queue/SKILL.md +28 -3
- package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +94 -24
- package/plugins/lisa/skills/lisa-rework-triage/SKILL.md +32 -2
- package/plugins/lisa/skills/lisa-setup-linear/SKILL.md +5 -5
- package/plugins/lisa/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-tracker-build-intake/SKILL.md +9 -0
- package/plugins/lisa-agy/agents/linear-prd-intake.md +8 -8
- package/plugins/lisa-agy/hooks/block-blind-automerge.sh +81 -2
- package/plugins/lisa-agy/hooks/block-direct-issue-create.agy.sh +64 -16
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +293 -8
- package/plugins/lisa-agy/hooks/block-instruction-file-edits.sh +21 -0
- package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +78 -15
- package/plugins/lisa-agy/hooks/block-shell-json-parsing.sh +21 -0
- package/plugins/lisa-agy/hooks/guard-dedupe.bash +337 -0
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +29 -1
- package/plugins/lisa-agy/hooks.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +407 -26
- package/plugins/lisa-agy/scripts/qa-signal-lifecycle.mjs +507 -0
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
- package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +27 -4
- package/plugins/lisa-agy/skills/lisa-github-prd-intake/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +22 -0
- package/plugins/lisa-agy/skills/lisa-github-verify/SKILL.md +14 -0
- package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +51 -0
- package/plugins/lisa-agy/skills/lisa-intake/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +27 -4
- package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
- package/plugins/lisa-agy/skills/lisa-jira-verify/SKILL.md +14 -0
- package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +51 -0
- package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +55 -2
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +27 -4
- package/plugins/lisa-agy/skills/lisa-linear-prd-intake/SKILL.md +48 -31
- package/plugins/lisa-agy/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +22 -0
- package/plugins/lisa-agy/skills/lisa-linear-verify/SKILL.md +14 -0
- package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +54 -3
- package/plugins/lisa-agy/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa-agy/skills/lisa-qa-clear/SKILL.md +19 -7
- package/plugins/lisa-agy/skills/lisa-qa-fail/SKILL.md +28 -3
- package/plugins/lisa-agy/skills/lisa-qa-queue/SKILL.md +28 -3
- package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +94 -24
- package/plugins/lisa-agy/skills/lisa-rework-triage/SKILL.md +32 -2
- package/plugins/lisa-agy/skills/lisa-setup-linear/SKILL.md +5 -5
- package/plugins/lisa-agy/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-tracker-build-intake/SKILL.md +9 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +19 -1
- package/plugins/lisa-copilot/agents/linear-prd-intake.agent.md +8 -8
- package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +81 -2
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +293 -8
- package/plugins/lisa-copilot/hooks/block-instruction-file-edits.sh +21 -0
- package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +78 -15
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +21 -0
- package/plugins/lisa-copilot/hooks/block-shell-json-parsing.sh +21 -0
- package/plugins/lisa-copilot/hooks/failure-signature-index.mjs +240 -5
- package/plugins/lisa-copilot/hooks/guard-dedupe.bash +337 -0
- package/plugins/lisa-copilot/hooks/inject-rules.sh +13 -5
- package/plugins/lisa-copilot/hooks/operational-hazards.mjs +736 -0
- package/plugins/lisa-copilot/hooks/operational-hazards.sh +28 -0
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +29 -1
- package/plugins/lisa-copilot/hooks/threshold-ratchet-compare.mjs +102 -21
- package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +130 -2
- package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +63 -13
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +183 -1
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.sh +21 -0
- package/plugins/lisa-copilot/rules/eager/00-rule-index.md +1 -0
- package/plugins/lisa-copilot/rules/eager/operational-hazards.md +50 -0
- package/plugins/lisa-copilot/rules/eager/tracked-work.md +1 -1
- package/plugins/lisa-copilot/rules/reference/config-resolution.md +1 -1
- package/plugins/lisa-copilot/rules/reference/leaf-only-lifecycle.md +1 -1
- package/plugins/lisa-copilot/rules/reference/operational-hazards.md +115 -0
- package/plugins/lisa-copilot/rules/reference/work-item-trailer-definition.md +129 -0
- package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +407 -26
- package/plugins/lisa-copilot/scripts/qa-signal-lifecycle.mjs +507 -0
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
- package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +27 -4
- package/plugins/lisa-copilot/skills/lisa-github-prd-intake/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-github-validate-issue/SKILL.md +22 -0
- package/plugins/lisa-copilot/skills/lisa-github-verify/SKILL.md +14 -0
- package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +51 -0
- package/plugins/lisa-copilot/skills/lisa-intake/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +27 -4
- package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
- package/plugins/lisa-copilot/skills/lisa-jira-verify/SKILL.md +14 -0
- package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +51 -0
- package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +55 -2
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +27 -4
- package/plugins/lisa-copilot/skills/lisa-linear-prd-intake/SKILL.md +48 -31
- package/plugins/lisa-copilot/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +22 -0
- package/plugins/lisa-copilot/skills/lisa-linear-verify/SKILL.md +14 -0
- package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +54 -3
- package/plugins/lisa-copilot/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa-copilot/skills/lisa-qa-clear/SKILL.md +19 -7
- package/plugins/lisa-copilot/skills/lisa-qa-fail/SKILL.md +28 -3
- package/plugins/lisa-copilot/skills/lisa-qa-queue/SKILL.md +28 -3
- package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +94 -24
- package/plugins/lisa-copilot/skills/lisa-rework-triage/SKILL.md +32 -2
- package/plugins/lisa-copilot/skills/lisa-setup-linear/SKILL.md +5 -5
- package/plugins/lisa-copilot/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-tracker-build-intake/SKILL.md +9 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/agents/linear-prd-intake.md +8 -8
- package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +81 -2
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +293 -8
- package/plugins/lisa-cursor/hooks/block-instruction-file-edits.sh +21 -0
- package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +78 -15
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +21 -0
- package/plugins/lisa-cursor/hooks/block-shell-json-parsing.sh +21 -0
- package/plugins/lisa-cursor/hooks/failure-signature-index.mjs +240 -5
- package/plugins/lisa-cursor/hooks/guard-dedupe.bash +337 -0
- package/plugins/lisa-cursor/hooks/hooks.json +10 -0
- package/plugins/lisa-cursor/hooks/operational-hazards.mjs +736 -0
- package/plugins/lisa-cursor/hooks/operational-hazards.sh +28 -0
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +29 -1
- package/plugins/lisa-cursor/hooks/threshold-ratchet-compare.mjs +102 -21
- package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +130 -2
- package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +63 -13
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +183 -1
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.sh +21 -0
- package/plugins/lisa-cursor/rules/00-rule-index.mdc +1 -0
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/leaf-only-lifecycle-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/operational-hazards-reference.mdc +120 -0
- package/plugins/lisa-cursor/rules/operational-hazards.mdc +55 -0
- package/plugins/lisa-cursor/rules/tracked-work.mdc +1 -1
- package/plugins/lisa-cursor/rules/work-item-trailer-definition-reference.mdc +134 -0
- package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +407 -26
- package/plugins/lisa-cursor/scripts/qa-signal-lifecycle.mjs +507 -0
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
- package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +27 -4
- package/plugins/lisa-cursor/skills/lisa-github-prd-intake/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +22 -0
- package/plugins/lisa-cursor/skills/lisa-github-verify/SKILL.md +14 -0
- package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +51 -0
- package/plugins/lisa-cursor/skills/lisa-intake/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +27 -4
- package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
- package/plugins/lisa-cursor/skills/lisa-jira-verify/SKILL.md +14 -0
- package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +51 -0
- package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +55 -2
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +27 -4
- package/plugins/lisa-cursor/skills/lisa-linear-prd-intake/SKILL.md +48 -31
- package/plugins/lisa-cursor/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +22 -0
- package/plugins/lisa-cursor/skills/lisa-linear-verify/SKILL.md +14 -0
- package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +54 -3
- package/plugins/lisa-cursor/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa-cursor/skills/lisa-qa-clear/SKILL.md +19 -7
- package/plugins/lisa-cursor/skills/lisa-qa-fail/SKILL.md +28 -3
- package/plugins/lisa-cursor/skills/lisa-qa-queue/SKILL.md +28 -3
- package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +94 -24
- package/plugins/lisa-cursor/skills/lisa-rework-triage/SKILL.md +32 -2
- package/plugins/lisa-cursor/skills/lisa-setup-linear/SKILL.md +5 -5
- package/plugins/lisa-cursor/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-tracker-build-intake/SKILL.md +9 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/hooks/inject-rules.sh +14 -3
- package/plugins/lisa-harper-fabric/rules/eager/harper-fabric.md +31 -0
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/hooks/inject-rules.sh +14 -3
- package/plugins/lisa-harper-fabric-copilot/rules/eager/harper-fabric.md +31 -0
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/rules/harper-fabric-reference.mdc +57 -0
- package/plugins/lisa-harper-fabric-cursor/rules/harper-fabric.mdc +32 -53
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/hooks/inject-rules.sh +14 -3
- package/plugins/lisa-phaser/rules/eager/phaser.md +44 -0
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/hooks/inject-rules.sh +14 -3
- package/plugins/lisa-phaser-copilot/rules/eager/phaser.md +44 -0
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/rules/phaser-reference.mdc +189 -0
- package/plugins/lisa-phaser-cursor/rules/phaser.mdc +45 -185
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/hooks/inject-rules.sh +14 -4
- package/plugins/lisa-rails/rules/eager/rails-conventions.md +23 -0
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/hooks/inject-rules.sh +14 -4
- package/plugins/lisa-rails-copilot/rules/eager/rails-conventions.md +23 -0
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/rules/rails-conventions-reference.mdc +181 -0
- package/plugins/lisa-rails-cursor/rules/rails-conventions.mdc +24 -177
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/materialized-artifacts.json +1 -0
- package/plugins/src/base/.claude-plugin/plugin.json +27 -0
- package/plugins/src/base/agents/linear-prd-intake.md +8 -8
- package/plugins/src/base/hooks/block-blind-automerge.sh +81 -2
- package/plugins/src/base/hooks/block-direct-issue-create.agy.sh +64 -16
- package/plugins/src/base/hooks/block-direct-issue-create.sh +293 -8
- package/plugins/src/base/hooks/block-instruction-file-edits.sh +21 -0
- package/plugins/src/base/hooks/block-managed-file-edits.sh +78 -15
- package/plugins/src/base/hooks/block-no-verify.sh +21 -0
- package/plugins/src/base/hooks/block-shell-json-parsing.sh +21 -0
- package/plugins/src/base/hooks/failure-signature-index.mjs +240 -5
- package/plugins/src/base/hooks/guard-dedupe.bash +337 -0
- package/plugins/src/base/hooks/inject-rules.sh +13 -5
- package/plugins/src/base/hooks/operational-hazards.mjs +736 -0
- package/plugins/src/base/hooks/operational-hazards.sh +28 -0
- package/plugins/src/base/hooks/parity-safety-net.sh +29 -1
- package/plugins/src/base/hooks/threshold-ratchet-compare.mjs +102 -21
- package/plugins/src/base/hooks/threshold-ratchet-families.mjs +130 -2
- package/plugins/src/base/hooks/threshold-ratchet.mjs +63 -13
- package/plugins/src/base/hooks/worktree-binding-guard.mjs +183 -1
- package/plugins/src/base/hooks/worktree-binding-guard.sh +21 -0
- package/plugins/src/base/rules/eager/00-rule-index.md +1 -0
- package/plugins/src/base/rules/eager/operational-hazards.md +50 -0
- package/plugins/src/base/rules/eager/tracked-work.md +1 -1
- package/plugins/src/base/rules/reference/config-resolution.md +1 -1
- package/plugins/src/base/rules/reference/leaf-only-lifecycle.md +1 -1
- package/plugins/src/base/rules/reference/operational-hazards.md +115 -0
- package/plugins/src/base/rules/reference/work-item-trailer-definition.md +129 -0
- package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +407 -26
- package/plugins/src/base/scripts/qa-signal-lifecycle.mjs +507 -0
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
- package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +27 -4
- package/plugins/src/base/skills/lisa-github-prd-intake/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +22 -0
- package/plugins/src/base/skills/lisa-github-verify/SKILL.md +14 -0
- package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +51 -0
- package/plugins/src/base/skills/lisa-intake/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +27 -4
- package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
- package/plugins/src/base/skills/lisa-jira-verify/SKILL.md +14 -0
- package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +51 -0
- package/plugins/src/base/skills/lisa-linear-access/SKILL.md +55 -2
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +27 -4
- package/plugins/src/base/skills/lisa-linear-prd-intake/SKILL.md +48 -31
- package/plugins/src/base/skills/lisa-linear-to-tracker/SKILL.md +3 -3
- package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +22 -0
- package/plugins/src/base/skills/lisa-linear-verify/SKILL.md +14 -0
- package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +54 -3
- package/plugins/src/base/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/src/base/skills/lisa-qa-clear/SKILL.md +19 -7
- package/plugins/src/base/skills/lisa-qa-fail/SKILL.md +28 -3
- package/plugins/src/base/skills/lisa-qa-queue/SKILL.md +28 -3
- package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +94 -24
- package/plugins/src/base/skills/lisa-rework-triage/SKILL.md +32 -2
- package/plugins/src/base/skills/lisa-setup-linear/SKILL.md +5 -5
- package/plugins/src/base/skills/lisa-track/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-tracker-build-intake/SKILL.md +9 -0
- package/plugins/src/harper-fabric/hooks/inject-rules.sh +14 -3
- package/plugins/src/harper-fabric/rules/eager/harper-fabric.md +31 -0
- package/plugins/src/phaser/hooks/inject-rules.sh +14 -3
- package/plugins/src/phaser/rules/eager/phaser.md +44 -0
- package/plugins/src/rails/hooks/inject-rules.sh +14 -4
- package/plugins/src/rails/rules/eager/rails-conventions.md +23 -0
- package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +63 -13
- package/rails/copy-overwrite/scripts/threshold-ratchet-compare.mjs +102 -21
- package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +130 -2
- package/rails/create-only/.github/workflows/ci.yml +8 -0
- package/rails/create-only/.github/workflows/deploy.yml +22 -0
- package/scripts/build-plugins.sh +2 -1
- package/scripts/check-engine-floor.mjs +361 -0
- package/scripts/check-guard-parity-notes.mjs +709 -0
- package/scripts/check-template-workflow-refs.mjs +9 -0
- package/scripts/check-third-party-action-pins.mjs +17 -1
- package/scripts/check-workflow-contract-assertions.mjs +480 -0
- package/scripts/copy-opencode-plugin-templates.mjs +1 -0
- package/scripts/deployed-guard-advice.mjs +636 -0
- package/scripts/generate-agy-plugin-artifacts.mjs +35 -10
- package/scripts/generate-export-surface.mjs +105 -19
- package/scripts/generate-two-channel-couplings.ts +143 -212
- package/scripts/lisa-enforcement-fallback.sh +31 -0
- package/scripts/measure-scripts-profile-gap.mts +361 -0
- package/scripts/mutation-performance-measure.mjs +21 -5
- package/scripts/reconcile-release-tags.mjs +65 -84
- package/scripts/two-channel-couplings.json +302 -70
- package/scripts/workflow-contract-assertion.sh +47 -0
- package/typescript/copy-overwrite/.lintstagedrc.json +1 -0
- package/typescript/copy-overwrite/eslint.ignore.config.json +1 -0
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +527 -53
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +301 -13
- package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +63 -13
- package/typescript/copy-overwrite/scripts/threshold-ratchet-compare.mjs +102 -21
- package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +130 -2
- package/typescript/create-only/.github/workflows/ci.yml +7 -0
- /package/plugins/lisa-harper-fabric/rules/{harper-fabric.md → reference/harper-fabric.md} +0 -0
- /package/plugins/lisa-harper-fabric-copilot/rules/{harper-fabric.md → reference/harper-fabric.md} +0 -0
- /package/plugins/lisa-phaser/rules/{phaser.md → reference/phaser.md} +0 -0
- /package/plugins/lisa-phaser-copilot/rules/{phaser.md → reference/phaser.md} +0 -0
- /package/plugins/lisa-rails/rules/{rails-conventions.md → reference/rails-conventions.md} +0 -0
- /package/plugins/lisa-rails-copilot/rules/{rails-conventions.md → reference/rails-conventions.md} +0 -0
- /package/plugins/src/harper-fabric/rules/{harper-fabric.md → reference/harper-fabric.md} +0 -0
- /package/plugins/src/phaser/rules/{phaser.md → reference/phaser.md} +0 -0
- /package/plugins/src/rails/rules/{rails-conventions.md → reference/rails-conventions.md} +0 -0
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* commit trailers, pull-request bodies, and the configured tracker.
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
11
12
|
import {
|
|
12
13
|
chmodSync,
|
|
13
14
|
existsSync,
|
|
@@ -65,6 +66,8 @@ export const WORK_ITEM_CONTRACT_VERSION = "1.0.0";
|
|
|
65
66
|
const RELEASE_SUBJECT =
|
|
66
67
|
/^chore\(release\): \d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)? \[skip ci\](?: \[skip-cd\])?$/;
|
|
67
68
|
const ZERO_OID = /^0+$/;
|
|
69
|
+
/** `git` pretty-format that prints a commit message body verbatim. */
|
|
70
|
+
const RAW_MESSAGE_FORMAT = "--format=%B";
|
|
68
71
|
const MARKER = "[lisa-pr-link]";
|
|
69
72
|
/**
|
|
70
73
|
* The deploy environment whose done role is terminal — the only one that
|
|
@@ -120,6 +123,218 @@ function workItemLineValue(line) {
|
|
|
120
123
|
const value = line.slice(match[0].length).trim();
|
|
121
124
|
return value === "" ? null : value;
|
|
122
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Is this a bare positive issue number — no sign, no padding, no suffix?
|
|
129
|
+
*
|
|
130
|
+
* A digit walk rather than a pattern. The shipped tree's lint refuses
|
|
131
|
+
* backtracking-prone quantifiers, and scanning characters cannot backtrack at
|
|
132
|
+
* all; the previous reader spelled this as part of a regex assembled from an
|
|
133
|
+
* escaped repository name, which is the construction that drifted.
|
|
134
|
+
* @param {string} text Candidate digits.
|
|
135
|
+
* @returns {boolean} True when `text` is `[1-9]` followed by digits only.
|
|
136
|
+
*/
|
|
137
|
+
function isIssueNumber(text) {
|
|
138
|
+
if (text.length === 0 || text[0] < "1" || text[0] > "9") return false;
|
|
139
|
+
for (let index = 1; index < text.length; index += 1) {
|
|
140
|
+
if (text[index] < "0" || text[index] > "9") return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* ## Lane attribution: which agent lane produced this, without saying who
|
|
146
|
+
*
|
|
147
|
+
* Every session in this fleet — and the human operator — pushes under ONE git
|
|
148
|
+
* identity, so `author.login` is a constant across every commit, branch and
|
|
149
|
+
* pull request. That is correct for accountability (the human IS the author of
|
|
150
|
+
* all of it) and useless for ROUTING: a green pull request nobody armed, a
|
|
151
|
+
* branch carrying finished work, an unpushed body found in a worktree sweep —
|
|
152
|
+
* none of them can be handed back to whoever should finish them, because the
|
|
153
|
+
* record does not say which lane made them. A survey has to guess from branch
|
|
154
|
+
* names and file mtimes, and guesses have been wrong in both directions.
|
|
155
|
+
*
|
|
156
|
+
* The obvious remedy — record the session URL — is itself a defect: this is a
|
|
157
|
+
* public repository and a published session identifier is an
|
|
158
|
+
* identifier-hygiene problem, filed separately. So the requirement is narrower
|
|
159
|
+
* than "add provenance":
|
|
160
|
+
*
|
|
161
|
+
* - it must distinguish WHICH lane to route to;
|
|
162
|
+
* - it must publish nothing that is itself an identifier;
|
|
163
|
+
* - it must survive the branch being deleted, which is when routing matters
|
|
164
|
+
* most, because pull requests auto-delete their branch on merge;
|
|
165
|
+
* - and it must not imply the human is not the accountable author.
|
|
166
|
+
*
|
|
167
|
+
* A lane id satisfies all four. It is `lane-` followed by twelve hex
|
|
168
|
+
* characters of a SHA-256 over an owner id the AGENT RUNTIME supplied — never
|
|
169
|
+
* the id itself. The digest is one-way over a high-entropy input, so it names
|
|
170
|
+
* no session, no URL, no person and no project; it is a stable handle that
|
|
171
|
+
* only a live lane can claim, by deriving its own and finding them equal.
|
|
172
|
+
*
|
|
173
|
+
* ### Written once, at the source
|
|
174
|
+
*
|
|
175
|
+
* The stamp goes on at `prepare-commit-msg`, in the process that is making the
|
|
176
|
+
* commit, from that process's own environment. An attribution reconstructed
|
|
177
|
+
* later — from a branch name, a timestamp, a report of a report — loses
|
|
178
|
+
* fidelity at every hop, and the sweep that motivated this is exactly that
|
|
179
|
+
* failure. `--if-exists=doNothing` means a rebase, an amend or a cherry-pick
|
|
180
|
+
* by a DIFFERENT lane preserves the original stamp rather than stealing the
|
|
181
|
+
* attribution.
|
|
182
|
+
*
|
|
183
|
+
* ### One primitive, three surfaces
|
|
184
|
+
*
|
|
185
|
+
* `renderLaneTrailer` / `parseLaneId` are deliberately text-in, text-out and
|
|
186
|
+
* carry no assumption that the text is a commit message. The same pair serves
|
|
187
|
+
* a commit trailer, a pull-request body marker and a tracker comment footer —
|
|
188
|
+
* the three places sibling work needs lane attribution — so the fleet has one
|
|
189
|
+
* token vocabulary rather than three bespoke ones.
|
|
190
|
+
*
|
|
191
|
+
* @see CodySwannGT/lisa#3771
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/** Trailer key that carries the lane id in a commit message or PR body. */
|
|
195
|
+
export const LANE_TRAILER = "Lane-Id";
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Environment variables consulted, in order, for the caller's owner id.
|
|
199
|
+
*
|
|
200
|
+
* Every one is supplied by the surrounding agent runtime. Lisa invents none of
|
|
201
|
+
* them: an id minted per invocation would differ from the one stamped on the
|
|
202
|
+
* commit and no lane could ever recognise its own work.
|
|
203
|
+
*
|
|
204
|
+
* `CLAUDE_CODE_SESSION_ID` is the variable Claude Code actually exports.
|
|
205
|
+
* `CLAUDE_SESSION_ID` is kept after it because the sibling list in
|
|
206
|
+
* `src/cli/worktree-ownership.ts` shipped with only that spelling, and a
|
|
207
|
+
* receipt written under it must keep resolving.
|
|
208
|
+
*
|
|
209
|
+
* A unit test asserts this list equals that one. Two readers of one fact drift
|
|
210
|
+
* silently otherwise, and the drift is invisible: both halves keep working,
|
|
211
|
+
* they just stop agreeing about who "mine" is.
|
|
212
|
+
*/
|
|
213
|
+
export const LANE_ID_VARIABLES = Object.freeze([
|
|
214
|
+
"LISA_OWNER_ID",
|
|
215
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
216
|
+
"CLAUDE_SESSION_ID",
|
|
217
|
+
"CODEX_SESSION_ID",
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
/** Fixed, human-readable prefix that makes a lane id greppable. */
|
|
221
|
+
const LANE_PREFIX = "lane-";
|
|
222
|
+
/**
|
|
223
|
+
* Hex characters of the digest kept.
|
|
224
|
+
*
|
|
225
|
+
* Twelve is 48 bits — collision-free at fleet scale by a wide margin, and
|
|
226
|
+
* short enough to read in a report line without wrapping.
|
|
227
|
+
*/
|
|
228
|
+
const LANE_DIGEST_LENGTH = 12;
|
|
229
|
+
/**
|
|
230
|
+
* The prefix only: an anchored, fixed-length literal with no quantifier, so it
|
|
231
|
+
* cannot backtrack over a commit message or pull-request body this script does
|
|
232
|
+
* not own. Same reasoning as `WORK_ITEM_PREFIX` above; the value is parsed by
|
|
233
|
+
* character scanning rather than by a second pattern.
|
|
234
|
+
*/
|
|
235
|
+
const LANE_PREFIX_MATCH = /^lane-id:/i;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Derive a lane id from an owner id supplied by the agent runtime.
|
|
239
|
+
*
|
|
240
|
+
* One-way by construction. The returned token cannot be turned back into the
|
|
241
|
+
* owner id, contains no character the owner id contributed, and is therefore
|
|
242
|
+
* safe to publish on a public repository — which is the whole reason it is a
|
|
243
|
+
* digest rather than the id.
|
|
244
|
+
* @param {unknown} ownerId Owner id from the agent runtime.
|
|
245
|
+
* @returns {string | undefined} The lane id, or undefined when there is no id.
|
|
246
|
+
*/
|
|
247
|
+
export function deriveLaneId(ownerId) {
|
|
248
|
+
const trimmed = typeof ownerId === "string" ? ownerId.trim() : "";
|
|
249
|
+
if (trimmed === "") return undefined;
|
|
250
|
+
const digest = createHash("sha256").update(trimmed, "utf8").digest("hex");
|
|
251
|
+
return LANE_PREFIX + digest.slice(0, LANE_DIGEST_LENGTH);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve the lane id of the process running this command.
|
|
256
|
+
*
|
|
257
|
+
* Undefined is a real answer and a restrictive one: a caller with no id can
|
|
258
|
+
* never match a stamp, so every attributed commit is somebody else's as far as
|
|
259
|
+
* it is concerned. "I could not tell" must never resolve in favour of taking
|
|
260
|
+
* over another lane's work.
|
|
261
|
+
* @param {NodeJS.ProcessEnv} [env] Environment to read.
|
|
262
|
+
* @returns {string | undefined} The caller's lane id, or undefined.
|
|
263
|
+
*/
|
|
264
|
+
export function resolveCallerLaneId(env = process.env) {
|
|
265
|
+
for (const variable of LANE_ID_VARIABLES) {
|
|
266
|
+
const lane = deriveLaneId(env[variable]);
|
|
267
|
+
if (lane !== undefined) return lane;
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Whether a value has the exact shape this module mints.
|
|
274
|
+
*
|
|
275
|
+
* Scanned character by character rather than matched with a quantified
|
|
276
|
+
* pattern, so the check is linear over untrusted text.
|
|
277
|
+
* @param {string} value Candidate token.
|
|
278
|
+
* @returns {boolean} True when the value is a well-formed lane id.
|
|
279
|
+
*/
|
|
280
|
+
function isLaneId(value) {
|
|
281
|
+
if (!value.startsWith(LANE_PREFIX)) return false;
|
|
282
|
+
const digest = value.slice(LANE_PREFIX.length);
|
|
283
|
+
if (digest.length !== LANE_DIGEST_LENGTH) return false;
|
|
284
|
+
for (const character of digest) {
|
|
285
|
+
const digit = character >= "0" && character <= "9";
|
|
286
|
+
const hex = character >= "a" && character <= "f";
|
|
287
|
+
if (!digit && !hex) return false;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Render the one marker line every surface uses.
|
|
294
|
+
*
|
|
295
|
+
* A commit trailer, a pull-request body line and a tracker comment footer are
|
|
296
|
+
* the same string here on purpose — see the module note above.
|
|
297
|
+
* @param {string} laneId Lane id to render.
|
|
298
|
+
* @returns {string} The marker line, without a trailing newline.
|
|
299
|
+
*/
|
|
300
|
+
export function renderLaneTrailer(laneId) {
|
|
301
|
+
return `${LANE_TRAILER}: ${laneId}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Read a lane id out of arbitrary text.
|
|
306
|
+
*
|
|
307
|
+
* Text-in, text-out: a commit message, a pull-request body and a tracker
|
|
308
|
+
* comment are all just text, and this makes no assumption about which it was
|
|
309
|
+
* handed.
|
|
310
|
+
* @param {string} text Commit message, pull-request body or comment body.
|
|
311
|
+
* @returns {string | undefined} The stamped lane id, or undefined.
|
|
312
|
+
*/
|
|
313
|
+
export function parseLaneId(text) {
|
|
314
|
+
for (const line of text.split("\n")) {
|
|
315
|
+
const match = LANE_PREFIX_MATCH.exec(line);
|
|
316
|
+
if (!match) continue;
|
|
317
|
+
const value = line.slice(match[0].length).trim();
|
|
318
|
+
if (isLaneId(value)) return value;
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Judge a stamp against the caller.
|
|
325
|
+
*
|
|
326
|
+
* A stamp naming somebody else is a hard "theirs" even when the caller has no
|
|
327
|
+
* lane of its own, for the same reason an unidentified cleaner may not delete
|
|
328
|
+
* a claimed worktree: an unidentified caller proves nothing.
|
|
329
|
+
* @param {string | undefined} markerLane Lane id found on the work.
|
|
330
|
+
* @param {string | undefined} callerLane Lane id of the running process.
|
|
331
|
+
* @returns {"mine" | "theirs" | "unattributed"} The routing verdict.
|
|
332
|
+
*/
|
|
333
|
+
export function judgeLane(markerLane, callerLane) {
|
|
334
|
+
if (markerLane === undefined) return "unattributed";
|
|
335
|
+
return markerLane === callerLane ? "mine" : "theirs";
|
|
336
|
+
}
|
|
337
|
+
|
|
123
338
|
const GUIDANCE = [
|
|
124
339
|
"Mention the ticket this work relates to, or ask Lisa to create one:",
|
|
125
340
|
" Work-Item: <configured-project-ticket>",
|
|
@@ -1008,16 +1223,206 @@ function writeState(ref, provider = trackerContract().provider, options = {}) {
|
|
|
1008
1223
|
* Comment lines and a verbose commit's diff are not a hazard: the prefix is
|
|
1009
1224
|
* anchored at column zero, so `# Work-Item: …` never matches, and every
|
|
1010
1225
|
* unified-diff line carries a space, `+` or `-` in that column.
|
|
1226
|
+
*
|
|
1227
|
+
* EXPORTED because the definition being private is what made it rediscoverable
|
|
1228
|
+
* as a bug. Anything in this fleet that reads a `Work-Item:` trailer imports
|
|
1229
|
+
* this; the alternative each new tool reached for — `%(trailers)` or an ad-hoc
|
|
1230
|
+
* regex — is Definition A, and Definition A misses 92% of this repository's
|
|
1231
|
+
* trailers. See the `work-item-trailer-definition` rule (#3747).
|
|
1232
|
+
*
|
|
1233
|
+
* It deliberately does NOT validate the shape of a value. It answers "what does
|
|
1234
|
+
* this text say?"; `canonicalizeRef` above it decides what is acceptable. A
|
|
1235
|
+
* reader that filtered malformed values out during the scan would make a bad
|
|
1236
|
+
* value invisible rather than refused — the #2672 failure re-created one layer
|
|
1237
|
+
* down. A filter a caller applies to this output is the CALLER's filter.
|
|
1011
1238
|
* @param {string} message Commit message or pull-request body.
|
|
1012
1239
|
* @returns {string[]} Every Work-Item value found, in order of appearance.
|
|
1013
1240
|
*/
|
|
1014
|
-
function workItemLines(message) {
|
|
1015
|
-
return
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1241
|
+
export function workItemLines(message) {
|
|
1242
|
+
return messageLines(message).flatMap(line => {
|
|
1243
|
+
const value = workItemLineValue(line);
|
|
1244
|
+
return value === null ? [] : [value];
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* One commit message or pull-request body, as lines.
|
|
1250
|
+
*
|
|
1251
|
+
* THE shared split, so every reader here means the same thing by "a line".
|
|
1252
|
+
* Definition B — the whole message, not git's final paragraph — is the whole
|
|
1253
|
+
* point of the `work-item-trailer-definition` rule (#3747), and it survives
|
|
1254
|
+
* only if the next reader that needs lines reuses this instead of writing a
|
|
1255
|
+
* third scanner with its own idea of where a message ends.
|
|
1256
|
+
* @param {string} message Commit message or pull-request body.
|
|
1257
|
+
* @returns {string[]} Every line, in order, without their terminators.
|
|
1258
|
+
*/
|
|
1259
|
+
function messageLines(message) {
|
|
1260
|
+
return String(message ?? "").split(/\r?\n/);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* ## The coding-session URL a commit message must not publish
|
|
1265
|
+
*
|
|
1266
|
+
* Measured on this fleet's own public history: 65 commits on the default
|
|
1267
|
+
* branch, and 54 more on one unmerged branch, carry a coding-session URL in
|
|
1268
|
+
* their message across nine session identifiers spanning a month
|
|
1269
|
+
* (CodySwannGT/lisa#3731). They are not credentials and nobody can use them.
|
|
1270
|
+
* They are internal identifiers, published in perpetuity, that partition a
|
|
1271
|
+
* public history by working session — metadata nobody decided to publish.
|
|
1272
|
+
*
|
|
1273
|
+
* **Nothing typed them.** The harness appended the trailer itself, below
|
|
1274
|
+
* whatever the agent wrote, after the agent had finished writing. That is why
|
|
1275
|
+
* the check lives at `commit-msg` and reads the file git is about to commit:
|
|
1276
|
+
* every instruction, rule and convention aimed at the agent is upstream of the
|
|
1277
|
+
* moment the text actually appears, so a guard placed there would be inert
|
|
1278
|
+
* against the one producer that has emitted every single instance. The
|
|
1279
|
+
* convention has since changed and no longer emits it — which is exactly the
|
|
1280
|
+
* state that existed before, and exactly the state that failed.
|
|
1281
|
+
*
|
|
1282
|
+
* Deliberately NOT wired into `validateMessage`: that reader also serves the
|
|
1283
|
+
* push and pull-request gates, which walk history. Refusing there would refuse
|
|
1284
|
+
* the 119 commits that already carry one — a published-history rewrite, which
|
|
1285
|
+
* is a human decision and explicitly out of scope. This ticket is about not
|
|
1286
|
+
* adding more.
|
|
1287
|
+
*/
|
|
1288
|
+
const SESSION_URL_MARK = "claude.ai/code/session_";
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* The shortest run of identifier characters this treats as a real session id.
|
|
1292
|
+
*
|
|
1293
|
+
* **This is the line between a leak and a sentence about leaks.** Real ids are
|
|
1294
|
+
* 24 characters of base62; prose that has to name the form writes
|
|
1295
|
+
* `claude.ai/code/session_<id>` or trails it off. A guard whose pattern matched
|
|
1296
|
+
* prose would refuse the commit that adds the guard, its tests and its
|
|
1297
|
+
* documentation — the failure this repository has walked into more than once —
|
|
1298
|
+
* so the discriminator is not "does the text mention it" but "does the text
|
|
1299
|
+
* carry an actual published identifier".
|
|
1300
|
+
*/
|
|
1301
|
+
const SESSION_ID_FLOOR = 16;
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Does `line` carry `needle` at `index`, comparing ASCII case-insensitively?
|
|
1305
|
+
*
|
|
1306
|
+
* A character walk rather than `line.toLowerCase().includes(needle)`. Case
|
|
1307
|
+
* folding is not length-preserving in Unicode — U+0130 expands to two code
|
|
1308
|
+
* units — so an index taken in a folded copy does not address the original,
|
|
1309
|
+
* and the id that follows the mark would be measured from the wrong place.
|
|
1310
|
+
* @param {string} line One line of a commit message.
|
|
1311
|
+
* @param {number} index Where in `line` to compare.
|
|
1312
|
+
* @param {string} needle Lowercase text to look for.
|
|
1313
|
+
* @returns {boolean} True when `line` carries `needle` there.
|
|
1314
|
+
*/
|
|
1315
|
+
function matchesFolded(line, index, needle) {
|
|
1316
|
+
if (index + needle.length > line.length) return false;
|
|
1317
|
+
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
1318
|
+
const code = line.charCodeAt(index + offset);
|
|
1319
|
+
const folded = code >= 65 && code <= 90 ? code + 32 : code;
|
|
1320
|
+
if (folded !== needle.charCodeAt(offset)) return false;
|
|
1321
|
+
}
|
|
1322
|
+
return true;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Is this one of the characters a session identifier is made of?
|
|
1327
|
+
* @param {string} character A single character.
|
|
1328
|
+
* @returns {boolean} True for `[0-9A-Za-z_-]`.
|
|
1329
|
+
*/
|
|
1330
|
+
function isSessionIdCharacter(character) {
|
|
1331
|
+
return (
|
|
1332
|
+
(character >= "0" && character <= "9") ||
|
|
1333
|
+
(character >= "a" && character <= "z") ||
|
|
1334
|
+
(character >= "A" && character <= "Z") ||
|
|
1335
|
+
character === "-" ||
|
|
1336
|
+
character === "_"
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Does this line carry a session URL with a real identifier on it?
|
|
1342
|
+
*
|
|
1343
|
+
* Scanned character by character, never matched with a pattern. The shipped
|
|
1344
|
+
* tree's lint refuses backtracking-prone quantifiers, and the input here is a
|
|
1345
|
+
* commit message — text this script does not get to trust the shape of.
|
|
1346
|
+
* @param {string} line One line of a commit message.
|
|
1347
|
+
* @returns {boolean} True when the line publishes a session identifier.
|
|
1348
|
+
*/
|
|
1349
|
+
function carriesSessionUrl(line) {
|
|
1350
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
1351
|
+
if (!matchesFolded(line, index, SESSION_URL_MARK)) continue;
|
|
1352
|
+
let end = index + SESSION_URL_MARK.length;
|
|
1353
|
+
while (end < line.length && isSessionIdCharacter(line[end])) end += 1;
|
|
1354
|
+
if (end - index - SESSION_URL_MARK.length >= SESSION_ID_FLOOR) return true;
|
|
1355
|
+
}
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Git's `--verbose` scissors, below which nothing reaches the commit.
|
|
1361
|
+
*
|
|
1362
|
+
* `git commit -v` appends the staged diff under this line and strips both
|
|
1363
|
+
* before recording the message. A guard that read past it would refuse a commit
|
|
1364
|
+
* for text that was never going to be published — including, precisely, the
|
|
1365
|
+
* commit that stages this guard's own test fixtures.
|
|
1366
|
+
*/
|
|
1367
|
+
const SCISSORS_MARK = ">8";
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* The part of a commit message that will actually be recorded.
|
|
1371
|
+
* @param {string} message The commit message file's contents.
|
|
1372
|
+
* @returns {string[]} Lines above the scissors, or every line when there is none.
|
|
1373
|
+
*/
|
|
1374
|
+
function publishedLines(message) {
|
|
1375
|
+
const lines = messageLines(message);
|
|
1376
|
+
const cut = lines.findIndex(
|
|
1377
|
+
line => line.trimStart().startsWith("#") && line.includes(SCISSORS_MARK)
|
|
1378
|
+
);
|
|
1379
|
+
return cut === -1 ? lines : lines.slice(0, cut);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Which lines of this message publish a coding-session URL.
|
|
1384
|
+
* @param {string} message The commit message file's contents.
|
|
1385
|
+
* @returns {number[]} One-based line numbers, in order.
|
|
1386
|
+
*/
|
|
1387
|
+
function sessionUrlLines(message) {
|
|
1388
|
+
return publishedLines(message).flatMap((line, index) =>
|
|
1389
|
+
carriesSessionUrl(line) ? [index + 1] : []
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Refuse a commit message that publishes a coding-session URL.
|
|
1395
|
+
*
|
|
1396
|
+
* The refusal names line numbers and the URL's PREFIX only. Echoing the
|
|
1397
|
+
* identifier back would copy it into CI logs and terminal scrollback, which is
|
|
1398
|
+
* the same act at a different address.
|
|
1399
|
+
* @param {string} message The commit message file's contents.
|
|
1400
|
+
* @throws {TrackingError} When the message carries a session identifier.
|
|
1401
|
+
*/
|
|
1402
|
+
function assertNoSessionUrl(message) {
|
|
1403
|
+
const lines = sessionUrlLines(message);
|
|
1404
|
+
if (lines.length === 0) return;
|
|
1405
|
+
const plural = lines.length === 1 ? "" : "s";
|
|
1406
|
+
const error = new TrackingError(
|
|
1407
|
+
[
|
|
1408
|
+
`This commit message publishes a coding-session URL on line${plural} ${lines.join(", ")}.`,
|
|
1409
|
+
"",
|
|
1410
|
+
`Remove every ${SESSION_URL_MARK}… occurrence from the message.`,
|
|
1411
|
+
"",
|
|
1412
|
+
"It is not a credential, and this refusal is hygiene rather than a",
|
|
1413
|
+
"breach: the identifier has no reader outside the session that made it.",
|
|
1414
|
+
"But this history is public and permanent, and a session id partitions",
|
|
1415
|
+
"it by working session — metadata nobody chose to publish. If something",
|
|
1416
|
+
"other than you is appending it, that is the thing to turn off; an",
|
|
1417
|
+
"instruction to stop cannot reach the code that writes it.",
|
|
1418
|
+
"",
|
|
1419
|
+
"To route a commit back to the run that produced it, use the",
|
|
1420
|
+
"non-identifying `Lane-Id:` trailer Lisa already stamps at",
|
|
1421
|
+
"prepare-commit-msg.",
|
|
1422
|
+
].join("\n")
|
|
1423
|
+
);
|
|
1424
|
+
error.selfExplanatory = true;
|
|
1425
|
+
throw error;
|
|
1021
1426
|
}
|
|
1022
1427
|
|
|
1023
1428
|
/**
|
|
@@ -1042,12 +1447,17 @@ function workItemLines(message) {
|
|
|
1042
1447
|
*
|
|
1043
1448
|
* Two DIFFERENT references is the real ambiguity — which one is this change
|
|
1044
1449
|
* about? — and it still fails, naming both.
|
|
1450
|
+
*
|
|
1451
|
+
* EXPORTED alongside `workItemLines` for the same reason: a tool that needs THE
|
|
1452
|
+
* work item rather than every line should get the canonicalizing, ambiguity-
|
|
1453
|
+
* refusing reader instead of writing its own. See the
|
|
1454
|
+
* `work-item-trailer-definition` rule (#3747).
|
|
1045
1455
|
* @param {string} text Commit message or pull-request body.
|
|
1046
1456
|
* @param {object} contract Resolved tracker contract.
|
|
1047
1457
|
* @param {string} subject What is being read, for the message.
|
|
1048
1458
|
* @returns {string} The canonical work-item reference.
|
|
1049
1459
|
*/
|
|
1050
|
-
function soleWorkItem(text, contract, subject) {
|
|
1460
|
+
export function soleWorkItem(text, contract, subject) {
|
|
1051
1461
|
const values = workItemLines(text);
|
|
1052
1462
|
if (values.length === 0) {
|
|
1053
1463
|
throw new TrackingError(`No Work-Item trailer anywhere in the ${subject}`);
|
|
@@ -1094,6 +1504,85 @@ function isMergeInProgress() {
|
|
|
1094
1504
|
);
|
|
1095
1505
|
}
|
|
1096
1506
|
|
|
1507
|
+
/**
|
|
1508
|
+
* Does this commit have more than one parent?
|
|
1509
|
+
*
|
|
1510
|
+
* The STRUCTURAL merge question, and the only one both gate paths ask. It is a
|
|
1511
|
+
* property of the commit rather than of the repository, so unlike
|
|
1512
|
+
* `isMergeInProgress` it survives the merge completing.
|
|
1513
|
+
* @param {string} sha Commit to inspect.
|
|
1514
|
+
* @param {object} [options] Passed to `run`; `allowFailure` makes an
|
|
1515
|
+
* unresolvable commit-ish answer "not a merge" instead of throwing.
|
|
1516
|
+
* @returns {boolean} Whether the commit is a merge.
|
|
1517
|
+
*/
|
|
1518
|
+
function isMergeCommit(sha, options = {}) {
|
|
1519
|
+
const result = run("git", ["rev-list", "--parents", "-n", "1", sha], options);
|
|
1520
|
+
return result.stdout.trim().split(/\s+/).length > 2;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* A commit message with git's own cleanup applied.
|
|
1525
|
+
*
|
|
1526
|
+
* `git stripspace` rather than a regular expression because the comment marker
|
|
1527
|
+
* is configurable — `core.commentChar`, and a whole `core.commentString` since
|
|
1528
|
+
* git 2.45 — so a hard-coded `#` reads a repository that chose `;` as carrying
|
|
1529
|
+
* comment lines that are really message text. This is the same pass git runs
|
|
1530
|
+
* on the file after the hook returns, so comparing two messages through it
|
|
1531
|
+
* compares what git will actually store.
|
|
1532
|
+
* @param {string} message Raw message text.
|
|
1533
|
+
* @returns {string} The cleaned message.
|
|
1534
|
+
*/
|
|
1535
|
+
function cleanedMessage(message) {
|
|
1536
|
+
const result = run("git", ["stripspace", "--strip-comments"], {
|
|
1537
|
+
allowFailure: true,
|
|
1538
|
+
input: message,
|
|
1539
|
+
});
|
|
1540
|
+
return (result.status === 0 ? result.stdout : message).trim();
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/**
|
|
1544
|
+
* Is this `commit-msg` run an amend of a merge that has already landed?
|
|
1545
|
+
*
|
|
1546
|
+
* The commit path used to ask only whether a merge was IN PROGRESS. That state
|
|
1547
|
+
* is transient: `git merge` removes `MERGE_HEAD` the moment the merge commits,
|
|
1548
|
+
* so `git commit --amend` on that same merge — the tidy way to fold in
|
|
1549
|
+
* generated artifacts the merge staled — reads as an ordinary commit and is
|
|
1550
|
+
* refused for lacking a trailer. The push path never had the defect, because it
|
|
1551
|
+
* counts parents. Two detectors, two different questions, and only one of them
|
|
1552
|
+
* survived the merge completing (#3875).
|
|
1553
|
+
*
|
|
1554
|
+
* At `commit-msg` time the commit does not exist yet, so ITS parents cannot be
|
|
1555
|
+
* counted — which is why the transient check was there. What can be read is
|
|
1556
|
+
* HEAD, and on an amend HEAD is the very commit being rewritten. So the
|
|
1557
|
+
* exemption needs both halves: HEAD is structurally a merge, AND the proposed
|
|
1558
|
+
* message is the message HEAD already carries.
|
|
1559
|
+
*
|
|
1560
|
+
* The second half is what keeps a NEW commit authored on top of a merge
|
|
1561
|
+
* checked. Treating every `commit-msg` invocation as an amend would exempt it,
|
|
1562
|
+
* and that is the strictly worse failure — an unlinked commit let through
|
|
1563
|
+
* rather than a linked one blocked. A new commit carries a new message.
|
|
1564
|
+
*
|
|
1565
|
+
* What remains is a new commit that reuses HEAD's merge message verbatim
|
|
1566
|
+
* (`git commit -C HEAD`). It is exempt here and NOT exempt at push time, where
|
|
1567
|
+
* `commitExemption` counts its single parent and demands the trailer. So the
|
|
1568
|
+
* looser commit-time answer is bounded by the structural gate downstream rather
|
|
1569
|
+
* than being the last word.
|
|
1570
|
+
* @param {string} message The proposed commit message.
|
|
1571
|
+
* @returns {boolean} Whether to treat this as an amend of a merge commit.
|
|
1572
|
+
*/
|
|
1573
|
+
function amendsMergeAtHead(message) {
|
|
1574
|
+
if (!isMergeCommit("HEAD", { allowFailure: true })) return false;
|
|
1575
|
+
const current = run("git", ["show", "-s", RAW_MESSAGE_FORMAT, "HEAD"], {
|
|
1576
|
+
allowFailure: true,
|
|
1577
|
+
});
|
|
1578
|
+
// probe-direction: fail-closed — a HEAD whose message cannot be read grants
|
|
1579
|
+
// no exemption, so an unreadable answer makes the gate stricter and the
|
|
1580
|
+
// author writes an ordinary trailered commit. The opposite default would
|
|
1581
|
+
// exempt on a failed probe, which is the shape this whole ticket is about.
|
|
1582
|
+
if (current.status !== 0) return false;
|
|
1583
|
+
return cleanedMessage(message) === cleanedMessage(current.stdout);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1097
1586
|
/**
|
|
1098
1587
|
* `.lisa.config.json` as it stands at one commit, or `{}` when it cannot be
|
|
1099
1588
|
* read there.
|
|
@@ -1202,8 +1691,7 @@ function protectedCommits(commits, configRef, remote) {
|
|
|
1202
1691
|
}
|
|
1203
1692
|
|
|
1204
1693
|
function commitExemption(sha, onProtectedBranch = new Set()) {
|
|
1205
|
-
|
|
1206
|
-
if (parents.length > 2) return "merge";
|
|
1694
|
+
if (isMergeCommit(sha)) return "merge";
|
|
1207
1695
|
if (RELEASE_SUBJECT.test(git(["show", "-s", "--format=%s", sha])))
|
|
1208
1696
|
return "release";
|
|
1209
1697
|
return onProtectedBranch.has(sha) ? "protected" : undefined;
|
|
@@ -1799,6 +2287,10 @@ function backlinkTokens(text) {
|
|
|
1799
2287
|
* field. Exported so the comparison can be asserted directly — a permissive
|
|
1800
2288
|
* comparison returns `true` rather than throwing, so nothing observable changes
|
|
1801
2289
|
* without an assertion on the returned boolean.
|
|
2290
|
+
*
|
|
2291
|
+
* READ-ONLY. Its permissiveness is correct here and only here: gate 5 should
|
|
2292
|
+
* pass when a human links the pull request in their own words. It must never
|
|
2293
|
+
* authorise a write — see `managedBacklinkTarget` for why that split exists.
|
|
1802
2294
|
* @param {unknown} value Comment body, or a structure containing one.
|
|
1803
2295
|
* @param {string} prUrl The pull request URL that must be linked.
|
|
1804
2296
|
* @returns {boolean} True when this exact pull request is linked.
|
|
@@ -1829,18 +2321,55 @@ function backlinkBody(prUrl) {
|
|
|
1829
2321
|
}
|
|
1830
2322
|
|
|
1831
2323
|
/**
|
|
1832
|
-
*
|
|
2324
|
+
* A comment body's visible text, whatever shape the provider returned.
|
|
2325
|
+
*
|
|
2326
|
+
* GitHub and Linear hand back a plain string; Jira hands back an Atlassian
|
|
2327
|
+
* Document Format tree whose prose lives in the `text` field of its leaf nodes.
|
|
2328
|
+
* Collecting those in document order reconstructs what a reader sees, which is
|
|
2329
|
+
* the only thing an identity test can honestly compare. Deliberately NOT
|
|
2330
|
+
* `JSON.stringify`: that would let structure — a link's `href`, a node type —
|
|
2331
|
+
* count as text the author wrote.
|
|
2332
|
+
* @param {unknown} body Comment body in whatever shape the provider returned.
|
|
2333
|
+
* @returns {string} The visible text, or the empty string when there is none.
|
|
2334
|
+
*/
|
|
2335
|
+
function bodyText(body) {
|
|
2336
|
+
if (typeof body === "string") return body;
|
|
2337
|
+
if (Array.isArray(body)) return body.map(bodyText).join(" ");
|
|
2338
|
+
if (body && typeof body === "object")
|
|
2339
|
+
return Object.entries(body)
|
|
2340
|
+
.map(([key, value]) =>
|
|
2341
|
+
key === "text" && typeof value === "string" ? value : bodyText(value)
|
|
2342
|
+
)
|
|
2343
|
+
.join(" ");
|
|
2344
|
+
return "";
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* The pull request a body is Lisa's managed backlink FOR, or null.
|
|
2349
|
+
*
|
|
2350
|
+
* This is an IDENTITY test, and the split from `textContainsBacklink` is the
|
|
2351
|
+
* whole point. That predicate answers "is this pull request linked from this
|
|
2352
|
+
* item?" — the right question for a reader, which should accept a human's link
|
|
2353
|
+
* however they wrote it. Reusing it to authorise a write turned a claim about
|
|
2354
|
+
* the ITEM into a claim about the COMMENT: any prose carrying the marker and
|
|
2355
|
+
* the URL as a bare token was treated as Lisa's to replace wholesale, and the
|
|
2356
|
+
* gate's own printed remedy contains both, so pasting a gate failure into a
|
|
2357
|
+
* comment was enough to have that comment flattened to one line.
|
|
1833
2358
|
*
|
|
1834
|
-
*
|
|
1835
|
-
*
|
|
1836
|
-
*
|
|
1837
|
-
*
|
|
1838
|
-
* still Lisa's comment to reuse rather than a second one to add.
|
|
2359
|
+
* So a body must BE `backlinkBody(...)`, not merely contain its parts.
|
|
2360
|
+
* Whitespace is normalised because a provider may hand back the body it stored
|
|
2361
|
+
* with a trailing newline, and refusing to recognise Lisa's own comment would
|
|
2362
|
+
* make every rerun post a duplicate — the opposite failure, equally wrong.
|
|
1839
2363
|
* @param {unknown} body Comment body in whatever shape the provider returned.
|
|
1840
|
-
* @returns {
|
|
2364
|
+
* @returns {string | null} The pull request URL it links, or null if the body
|
|
2365
|
+
* is somebody's writing rather than the managed comment.
|
|
1841
2366
|
*/
|
|
1842
|
-
function
|
|
1843
|
-
|
|
2367
|
+
function managedBacklinkTarget(body) {
|
|
2368
|
+
const text = bodyText(body).replace(/\s+/g, " ").trim();
|
|
2369
|
+
const prefix = `${MARKER} `;
|
|
2370
|
+
if (!text.startsWith(prefix)) return null;
|
|
2371
|
+
const target = text.slice(prefix.length);
|
|
2372
|
+
return target && !target.includes(" ") ? target : null;
|
|
1844
2373
|
}
|
|
1845
2374
|
|
|
1846
2375
|
/**
|
|
@@ -1863,6 +2392,12 @@ function carriesMarker(body) {
|
|
|
1863
2392
|
* and its replacement, a pull request reopened or recreated against a different
|
|
1864
2393
|
* base, and work split across repositories all put two pull requests on one
|
|
1865
2394
|
* item, and none of them involve a stack.
|
|
2395
|
+
*
|
|
2396
|
+
* Both arms run off `managedBacklinkTarget`, so a comment nobody managed is
|
|
2397
|
+
* neither overwritten nor counted. The count is the operator-visible half of
|
|
2398
|
+
* the same predicate: "1 other pull request already linked" said of a prose
|
|
2399
|
+
* comment is a false statement about the item, and it is the line that
|
|
2400
|
+
* surfaced this defect.
|
|
1866
2401
|
* @param {readonly unknown[]} comments Comments as the provider returned them.
|
|
1867
2402
|
* @param {string} prUrl The pull request being discharged.
|
|
1868
2403
|
* @param {(comment: unknown) => unknown} bodyOf Reads a comment's body.
|
|
@@ -1873,9 +2408,9 @@ function partitionBacklinks(comments, prUrl, bodyOf) {
|
|
|
1873
2408
|
const mine = [];
|
|
1874
2409
|
let others = 0;
|
|
1875
2410
|
for (const comment of comments) {
|
|
1876
|
-
const
|
|
1877
|
-
if (
|
|
1878
|
-
if (
|
|
2411
|
+
const target = managedBacklinkTarget(bodyOf(comment));
|
|
2412
|
+
if (target === null) continue;
|
|
2413
|
+
if (target === prUrl) mine.push(comment);
|
|
1879
2414
|
else others += 1;
|
|
1880
2415
|
}
|
|
1881
2416
|
return { mine: mine[0], others };
|
|
@@ -2037,9 +2572,10 @@ function linearBacklink(ref, prUrl, contract) {
|
|
|
2037
2572
|
* The managed comment as an Atlassian Document Format tree.
|
|
2038
2573
|
*
|
|
2039
2574
|
* A single text node, so the marker and the URL land in one string — which is
|
|
2040
|
-
* what `textContainsBacklink` walks the tree looking for
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
2575
|
+
* what `textContainsBacklink` walks the tree looking for, and what
|
|
2576
|
+
* `managedBacklinkTarget` reconstructs when deciding the comment is Lisa's to
|
|
2577
|
+
* update. Splitting them across nodes would write a comment the reader accepts
|
|
2578
|
+
* visually and the check rejects.
|
|
2043
2579
|
* @param {string} prUrl Pull request URL.
|
|
2044
2580
|
* @returns {object} The ADF document.
|
|
2045
2581
|
*/
|
|
@@ -2291,12 +2827,31 @@ function assertStateAmong(refs, contract) {
|
|
|
2291
2827
|
* take the fail-open path on every commit, and print the same success line: a
|
|
2292
2828
|
* second fail-open wearing the first fix's clothes, and strictly worse than
|
|
2293
2829
|
* the gap, because the gap would now be believed closed.
|
|
2830
|
+
* EXPORTED, AND TAKING THE BRANCH AS AN ARGUMENT, so the provider dispatch is
|
|
2831
|
+
* reachable in process. `githubBranchIssue` below was split out for that
|
|
2832
|
+
* reason and is measurable; the routing that decides whether it is called at
|
|
2833
|
+
* all was not. The CLI cases exercise these lines only by SPAWNING the script,
|
|
2834
|
+
* and a spawned child loads the file from disk rather than the instrumented
|
|
2835
|
+
* module, so a mutant here is activated in a process the assertions never
|
|
2836
|
+
* observe. It survives, and the gate reports a score rather than reporting
|
|
2837
|
+
* that it measured nothing — `if (false)` on the provider arm routes every
|
|
2838
|
+
* GitHub branch away from the extractor and restores the defect
|
|
2839
|
+
* CodySwannGT/lisa#3861 closed, while every end-to-end case still passes.
|
|
2840
|
+
* Measured after the split, over these lines only: 21 mutants, 19 killed,
|
|
2841
|
+
* 90.48%, where before the split every mutant here was unkillable. The one
|
|
2842
|
+
* survivor left inside this function is `if (!branch)` mutated to `if (false)`,
|
|
2843
|
+
* and it is EQUIVALENT rather than unproven: both arms already decline an
|
|
2844
|
+
* absent name on their own — the GitHub reading needs a slash-segment starting
|
|
2845
|
+
* with a digit, the key-based reading needs the configured key — so no input
|
|
2846
|
+
* distinguishes the two programs. The guard stays because it is the documented
|
|
2847
|
+
* fail-open and because the next arm added here may not be so forgiving; the
|
|
2848
|
+
* equivalence is recorded so a later reader does not mistake it for a gap.
|
|
2849
|
+
* @param {string|undefined} branch Active branch name, if any.
|
|
2294
2850
|
* @param {object} contract Resolved tracker contract.
|
|
2295
2851
|
* @returns {string|undefined} Canonical reference, or undefined when the
|
|
2296
2852
|
* branch encodes none.
|
|
2297
2853
|
*/
|
|
2298
|
-
function
|
|
2299
|
-
const branch = activeBranch();
|
|
2854
|
+
export function branchWorkItemFrom(branch, contract) {
|
|
2300
2855
|
if (!branch) return undefined;
|
|
2301
2856
|
if (contract.provider === "github")
|
|
2302
2857
|
return githubBranchIssue(branch, contract);
|
|
@@ -2313,6 +2868,28 @@ function branchWorkItem(contract) {
|
|
|
2313
2868
|
return match ? `${key}-${match[1]}` : undefined;
|
|
2314
2869
|
}
|
|
2315
2870
|
|
|
2871
|
+
/**
|
|
2872
|
+
* The work item the ACTIVE branch encodes, or undefined when it encodes none.
|
|
2873
|
+
*
|
|
2874
|
+
* The impure half of the split, and deliberately nothing beyond the
|
|
2875
|
+
* `activeBranch()` call — every decision, including the detached-HEAD
|
|
2876
|
+
* fail-open, lives in `branchWorkItemFrom`, which is pure and exported so the
|
|
2877
|
+
* mutation gate can see it.
|
|
2878
|
+
*
|
|
2879
|
+
* This one line is therefore the whole residue of the subprocess boundary: its
|
|
2880
|
+
* body-removal mutant survives, because the only cases that reach it spawn the
|
|
2881
|
+
* script. Those cases DO catch it — with this returning undefined the trailer
|
|
2882
|
+
* comparison silently fails open again and the mismatch cases stop refusing —
|
|
2883
|
+
* but the gate cannot observe a child process. Shrinking that blind spot from
|
|
2884
|
+
* the whole dispatch to one delegating call is the point of the split.
|
|
2885
|
+
* @param {object} contract Resolved tracker contract.
|
|
2886
|
+
* @returns {string|undefined} Canonical reference, or undefined when the
|
|
2887
|
+
* branch encodes none.
|
|
2888
|
+
*/
|
|
2889
|
+
function branchWorkItem(contract) {
|
|
2890
|
+
return branchWorkItemFrom(activeBranch(), contract);
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2316
2893
|
/**
|
|
2317
2894
|
* The GitHub issue a branch name encodes, canonicalized, or undefined.
|
|
2318
2895
|
*
|
|
@@ -2357,7 +2934,26 @@ export function githubBranchIssue(branch, contract) {
|
|
|
2357
2934
|
// Bounded on both sides: the number must fill the segment, so `4.33.1` and
|
|
2358
2935
|
// `se-7728` do not match, and `3463` is not read out of `34631`.
|
|
2359
2936
|
const match = /^[^/]+\/([1-9]\d*)(?:-|$)/.exec(branch);
|
|
2360
|
-
|
|
2937
|
+
if (!match) return undefined;
|
|
2938
|
+
// A LEADING date stamp escapes every other bound this rule has. The comment
|
|
2939
|
+
// above already declines `stack/queue-drain-20260903`, but only because the
|
|
2940
|
+
// digits trail there; `release/20260903-cutover` puts the same stamp at the
|
|
2941
|
+
// front of the segment, where it fills it exactly and reads as issue
|
|
2942
|
+
// 20260903. Nothing else here can tell the two apart, because by shape they
|
|
2943
|
+
// are the same token in the same position.
|
|
2944
|
+
//
|
|
2945
|
+
// Matched as a DATE rather than as "too many digits": a length bound would
|
|
2946
|
+
// be a guess about how many issues this fleet will ever file, and would
|
|
2947
|
+
// start refusing real numbers on the day it is wrong. `YYYYMMDD` with a
|
|
2948
|
+
// real month and a real day is narrow enough that the only issue number it
|
|
2949
|
+
// can cost is one no repository will reach.
|
|
2950
|
+
//
|
|
2951
|
+
// Declining fails OPEN — the trailer is then compared against nothing, which
|
|
2952
|
+
// is the state this whole fallback was built to improve on but is still
|
|
2953
|
+
// strictly safer than refusing a commit that is perfectly correct.
|
|
2954
|
+
if (/^(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])$/.test(match[1]))
|
|
2955
|
+
return undefined;
|
|
2956
|
+
return `${contract.repository}#${match[1]}`;
|
|
2361
2957
|
}
|
|
2362
2958
|
|
|
2363
2959
|
/**
|
|
@@ -2407,7 +3003,10 @@ function assertIdentityMatches(ref, contract) {
|
|
|
2407
3003
|
}
|
|
2408
3004
|
|
|
2409
3005
|
function validateMessage(message, options = {}) {
|
|
2410
|
-
if (
|
|
3006
|
+
if (
|
|
3007
|
+
options.allowMergeExemption &&
|
|
3008
|
+
(isMergeInProgress() || amendsMergeAtHead(message))
|
|
3009
|
+
)
|
|
2411
3010
|
return { exempt: "merge" };
|
|
2412
3011
|
if (RELEASE_SUBJECT.test(messageSubject(message)))
|
|
2413
3012
|
return { exempt: "release" };
|
|
@@ -2419,7 +3018,7 @@ function validateMessage(message, options = {}) {
|
|
|
2419
3018
|
}
|
|
2420
3019
|
|
|
2421
3020
|
function commitMessage(sha) {
|
|
2422
|
-
return git(["show", "-s",
|
|
3021
|
+
return git(["show", "-s", RAW_MESSAGE_FORMAT, sha]);
|
|
2423
3022
|
}
|
|
2424
3023
|
|
|
2425
3024
|
/**
|
|
@@ -2493,6 +3092,13 @@ function validateCommits(commits, configRef, remote) {
|
|
|
2493
3092
|
refs: list,
|
|
2494
3093
|
issue: ref ? issues.get(ref) : undefined,
|
|
2495
3094
|
issues,
|
|
3095
|
+
// De-duplicated on purpose, and it is the same `unique` every counter below
|
|
3096
|
+
// was computed from. A caller that compared a counter against the RAW
|
|
3097
|
+
// `commits.length` would read "some commits were exempt" out of a range in
|
|
3098
|
+
// which every one of them was, whenever the same sha appeared twice — which
|
|
3099
|
+
// a push range spanning several refs does routinely. The whole point of
|
|
3100
|
+
// this field is to be the denominator those counters actually add up to.
|
|
3101
|
+
examined: unique.length,
|
|
2496
3102
|
mergeExempt,
|
|
2497
3103
|
protectedExempt,
|
|
2498
3104
|
releaseExempt,
|
|
@@ -3160,6 +3766,61 @@ function reportMapping(findings, commitRefs, bodyRefs) {
|
|
|
3160
3766
|
});
|
|
3161
3767
|
}
|
|
3162
3768
|
|
|
3769
|
+
/**
|
|
3770
|
+
* True when this range introduced no authored work of its own, and at least one
|
|
3771
|
+
* of the things it did introduce was a merge.
|
|
3772
|
+
*
|
|
3773
|
+
* ## Why the condition is stated against `examined`
|
|
3774
|
+
*
|
|
3775
|
+
* The deferral used to read `relevant === 0 && mergeExempt > 0`. Both halves
|
|
3776
|
+
* are true of every range that should defer, but only one of them SAYS so.
|
|
3777
|
+
* `mergeExempt > 0` is satisfied by a range in which SOME commits are merges —
|
|
3778
|
+
* strictly weaker than the precondition the deferral means to assert, which is
|
|
3779
|
+
* that not one commit in the range authored anything. It coincided with the
|
|
3780
|
+
* right answer because `relevant === 0` was carrying that claim, so the two
|
|
3781
|
+
* agreed by accident rather than by construction, and a refactor that loosened
|
|
3782
|
+
* `relevant` would have taken the deferral with it silently
|
|
3783
|
+
* (CodySwannGT/lisa#3921).
|
|
3784
|
+
*
|
|
3785
|
+
* Stating it as "every commit examined was exempt" makes the predicate
|
|
3786
|
+
* self-sufficient: it reads the denominator directly instead of trusting a
|
|
3787
|
+
* counter that is incremented before the trailer is parsed.
|
|
3788
|
+
*
|
|
3789
|
+
* ## Why it is not `mergeExempt === examined`
|
|
3790
|
+
*
|
|
3791
|
+
* Because that is a DIFFERENT claim — "the range is nothing but merges" — and
|
|
3792
|
+
* it is strictly stronger than the one the deferral needs. A back-merge of a
|
|
3793
|
+
* non-default deploy branch legitimately drags release commits along with the
|
|
3794
|
+
* merge; those are exempt for their own reason, and a range of one merge plus
|
|
3795
|
+
* one `chore(release):` commit still introduced no authored work. Requiring
|
|
3796
|
+
* merges-only would refuse it with "no non-merge commit linked to a work item",
|
|
3797
|
+
* which is the #3851 deadlock returning through the door this closed.
|
|
3798
|
+
*
|
|
3799
|
+
* `mergeExempt > 0` stays as the second conjunct, and it is not the weak test
|
|
3800
|
+
* this replaced: it is what separates a deferral ("the subject is one level up,
|
|
3801
|
+
* at the merge's own pull request") from the release-only and deploy-chain
|
|
3802
|
+
* exemptions, which have their own branches and their own wording above.
|
|
3803
|
+
*
|
|
3804
|
+
* Exported so the predicate can be asserted directly. The push path reaches it
|
|
3805
|
+
* only through a spawned child, where the interesting case — a range whose
|
|
3806
|
+
* exemptions do not add up — cannot be built out of real commits at all.
|
|
3807
|
+
* @param {object} [result] Commit-side result from `validateCommits`.
|
|
3808
|
+
* @returns {boolean} True when the commit-side question has no subject here.
|
|
3809
|
+
*/
|
|
3810
|
+
export function mergeOnlyRange(result) {
|
|
3811
|
+
if (!result) return false;
|
|
3812
|
+
const examinedCount = result.examined ?? 0;
|
|
3813
|
+
const exempt =
|
|
3814
|
+
(result.mergeExempt ?? 0) +
|
|
3815
|
+
(result.releaseExempt ?? 0) +
|
|
3816
|
+
(result.protectedExempt ?? 0);
|
|
3817
|
+
return (
|
|
3818
|
+
examinedCount > 0 &&
|
|
3819
|
+
(result.mergeExempt ?? 0) > 0 &&
|
|
3820
|
+
exempt === examinedCount
|
|
3821
|
+
);
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3163
3824
|
/**
|
|
3164
3825
|
* Check every pull-request requirement and report all of the unmet ones.
|
|
3165
3826
|
*
|
|
@@ -3228,12 +3889,13 @@ function validatePrData(outcome, prUrl, prBody, rangeIsPartial = false) {
|
|
|
3228
3889
|
// Nothing to check HERE, and the subject is one level up. Scoped to a merge
|
|
3229
3890
|
// because that is the only way a partial range empties out while the pull
|
|
3230
3891
|
// request is attributed: the trailered commit is excluded for having been
|
|
3231
|
-
// validated on an earlier push. `
|
|
3232
|
-
// commit — `validateCommits`
|
|
3233
|
-
//
|
|
3234
|
-
//
|
|
3892
|
+
// validated on an earlier push. `mergeOnlyRange` cannot mask an untrailered
|
|
3893
|
+
// commit — `validateCommits` classifies one as neither merge, release nor
|
|
3894
|
+
// protected, so the exemptions no longer add up to the range and the
|
|
3895
|
+
// predicate is false; the missing trailer also raises through `outcome.error`
|
|
3896
|
+
// on a different branch entirely, and the control below still refuses it.
|
|
3235
3897
|
const deferredToPullRequest =
|
|
3236
|
-
rangeIsPartial === true && result
|
|
3898
|
+
rangeIsPartial === true && mergeOnlyRange(result);
|
|
3237
3899
|
// All three are COMMIT-side. They carry `IN_THIS_PR` because a rewrite plus a
|
|
3238
3900
|
// force-push does clear them without recreating the pull request — but the
|
|
3239
3901
|
// tag's wording invites a body edit, which cannot touch a commit message. The
|
|
@@ -3332,15 +3994,27 @@ function alreadyTraced(result) {
|
|
|
3332
3994
|
* `alreadyTraced` renders nothing here: it fires only for `protectedExempt`,
|
|
3333
3995
|
* and a back-merge onto a feature branch has none. So the zero went unexplained.
|
|
3334
3996
|
*
|
|
3335
|
-
* ##
|
|
3997
|
+
* ## The condition is the deferral's own, not a second opinion
|
|
3998
|
+
*
|
|
3999
|
+
* Both read {@link mergeOnlyRange}, deliberately: a message rendered on a
|
|
4000
|
+
* condition that merely AGREES with the deferral is a second implementation of
|
|
4001
|
+
* the same rule, free to drift from it, and a clause claiming a deferral that
|
|
4002
|
+
* did not happen is worse than no clause. This renders only on the push path,
|
|
4003
|
+
* where the range is ALWAYS a subset of the pull request's — `reportPushGroup`
|
|
4004
|
+
* passes `rangeIsPartial: true` unconditionally, and says why. `validate-pr`
|
|
4005
|
+
* reads the full `base..head` range and does not defer, which is why its
|
|
4006
|
+
* success line does not carry this.
|
|
4007
|
+
*
|
|
4008
|
+
* ## What it must say, and why the count alone did not
|
|
3336
4009
|
*
|
|
3337
|
-
*
|
|
3338
|
-
*
|
|
3339
|
-
*
|
|
3340
|
-
*
|
|
3341
|
-
*
|
|
4010
|
+
* A gate that stands down has to name what it stood down FROM and what still
|
|
4011
|
+
* ran, or the next reader cannot separate "deferred, correctly, with gates 4
|
|
4012
|
+
* and 5 still enforced" from "skipped" — and that distinction is the entire
|
|
4013
|
+
* subject of the surrounding work (CodySwannGT/lisa#3921). So the clause names
|
|
4014
|
+
* the count, names gate 3 as the one with no subject here, names where it is
|
|
4015
|
+
* asked instead, and names the gates that were enforced anyway.
|
|
3342
4016
|
*
|
|
3343
|
-
* ## This adds
|
|
4017
|
+
* ## This adds sentences and relaxes nothing
|
|
3344
4018
|
*
|
|
3345
4019
|
* The verdict, the exit status and every gate are untouched: a clause is
|
|
3346
4020
|
* appended to a line that already said OK. A diagnostic fix that also softened
|
|
@@ -3349,9 +4023,25 @@ function alreadyTraced(result) {
|
|
|
3349
4023
|
* @returns {string} A clause to append, or the empty string.
|
|
3350
4024
|
*/
|
|
3351
4025
|
function carriedByPullRequest(result) {
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
4026
|
+
if (!mergeOnlyRange(result)) return "";
|
|
4027
|
+
// Stated separately from the deferral itself, and this is the half #3921
|
|
4028
|
+
// asked for. "N merge commit(s)" alone leaves a reader unable to tell a
|
|
4029
|
+
// correct deferral from a skip: the two differ entirely in what ELSE ran, and
|
|
4030
|
+
// what else ran was not in the output. Gate 5 is named as inapplicable rather
|
|
4031
|
+
// than enforced under `trailer`, because a run that contacted no tracker has
|
|
4032
|
+
// not enforced a backlink — claiming it here would be the vacuous success
|
|
4033
|
+
// this line exists to prevent, one clause along.
|
|
4034
|
+
const stillEnforced =
|
|
4035
|
+
result.contract?.verify === "full"
|
|
4036
|
+
? "gates 4 and 5 were still enforced here, on this push, exactly as on any other"
|
|
4037
|
+
: 'gate 4 was still enforced here, on this push, exactly as on any other; gate 5 does not apply (workItem.verify is "trailer")';
|
|
4038
|
+
return (
|
|
4039
|
+
` (${result.mergeExempt} merge commit(s), and nothing else in this range; ` +
|
|
4040
|
+
`this push introduces no authored work, so gate 3 had no subject here and ` +
|
|
4041
|
+
`the pull request's own range carries the requirement — \`validate-pr\` ` +
|
|
4042
|
+
`asks it there from base..head, as a required check. Nothing else stood ` +
|
|
4043
|
+
`down: ${stillEnforced})`
|
|
4044
|
+
);
|
|
3355
4045
|
}
|
|
3356
4046
|
|
|
3357
4047
|
/**
|
|
@@ -3626,33 +4316,50 @@ function backlink(args) {
|
|
|
3626
4316
|
* somebody's pull request NAMED the item somewhere, which is a different and
|
|
3627
4317
|
* much weaker claim — see `sweepDeclarations` for what that difference cost.
|
|
3628
4318
|
*
|
|
3629
|
-
*
|
|
3630
|
-
* `%(trailers:key=Work-Item)`, and that is the load-bearing
|
|
3631
|
-
* parser only considers the LAST paragraph of a message,
|
|
3632
|
-
* commits put `Work-Item:` above the trailing co-author
|
|
3633
|
-
* does not see it as a trailer at all. Measured on
|
|
3634
|
-
* finds 602 distinct issues where a full-body scan
|
|
3635
|
-
* and the undercount is clean, plausible and
|
|
3636
|
-
* survived. Anything in this fleet that counts or
|
|
3637
|
-
* scan the body (CodySwannGT/lisa#3859).
|
|
4319
|
+
* Read through `workItemLines` — the whole body, prefix anchored at column zero
|
|
4320
|
+
* — rather than git's `%(trailers:key=Work-Item)`, and that is the load-bearing
|
|
4321
|
+
* part. Git's trailer parser only considers the LAST paragraph of a message,
|
|
4322
|
+
* and this project's commits put `Work-Item:` above the trailing co-author
|
|
4323
|
+
* block, so the parser does not see it as a trailer at all. Measured on
|
|
4324
|
+
* `origin/main`: the parser finds 602 distinct issues where a full-body scan
|
|
4325
|
+
* finds 835 — it misses 233, and the undercount is clean, plausible and
|
|
4326
|
+
* well-formed, which is why it survived. Anything in this fleet that counts or
|
|
4327
|
+
* verifies trailers has to scan the body (CodySwannGT/lisa#3859).
|
|
4328
|
+
*
|
|
4329
|
+
* It USED to carry its own regex, which is how the two readers drifted: that
|
|
4330
|
+
* one allowed leading whitespace where `workItemLines` anchors at column zero,
|
|
4331
|
+
* so it would have matched the context line of a verbose commit's own diff.
|
|
4332
|
+
* Measured over full `origin/main` history: 2,059 `Work-Item:` lines at column
|
|
4333
|
+
* zero and ZERO indented ones, so the looser form served no real input and
|
|
4334
|
+
* admitted one class of false positive. One definition now, not two (#3747).
|
|
4335
|
+
*
|
|
4336
|
+
* The `owner/name#number` filter stays HERE rather than moving down into the
|
|
4337
|
+
* reader. This function answers a narrower question — which issues in THIS
|
|
4338
|
+
* repository does the commit declare? — and a value the filter drops is one the
|
|
4339
|
+
* reader still saw. That ordering is what keeps a malformed trailer refusable
|
|
4340
|
+
* by the gate instead of invisible to it.
|
|
4341
|
+
*
|
|
4342
|
+
* CRLF needs nothing here, and a relayed review finding that said otherwise
|
|
4343
|
+
* was refuted rather than acted on. `workItemLines` splits on `/\r?\n/`, so a
|
|
4344
|
+
* CRLF line arrives without its carriage return; the value is trimmed besides.
|
|
4345
|
+
* The case is pinned in `work-item-cli-writes.test.ts` so the refutation is a
|
|
4346
|
+
* control rather than a paragraph.
|
|
3638
4347
|
* @param {string} body A commit message body.
|
|
3639
4348
|
* @param {string} repository `owner/name` the item must belong to.
|
|
3640
4349
|
* @returns {number[]} Declared issue numbers, de-duplicated, in first-seen order.
|
|
3641
4350
|
*/
|
|
3642
4351
|
export function declaredWorkItemNumbers(body, repository) {
|
|
3643
|
-
const
|
|
3644
|
-
|
|
3645
|
-
"\\$&"
|
|
3646
|
-
);
|
|
3647
|
-
if (!escaped) return [];
|
|
3648
|
-
const pattern = new RegExp(
|
|
3649
|
-
`^[ \\t]*Work-Item:[ \\t]*${escaped}#([1-9]\\d*)[ \\t]*$`,
|
|
3650
|
-
"gim"
|
|
3651
|
-
);
|
|
4352
|
+
const owner = String(repository ?? "").toLowerCase();
|
|
4353
|
+
if (owner === "") return [];
|
|
3652
4354
|
const numbers = [];
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
4355
|
+
for (const value of workItemLines(body)) {
|
|
4356
|
+
const hash = value.indexOf("#");
|
|
4357
|
+
if (hash === -1) continue;
|
|
4358
|
+
if (value.slice(0, hash).toLowerCase() !== owner) continue;
|
|
4359
|
+
const digits = value.slice(hash + 1);
|
|
4360
|
+
if (!isIssueNumber(digits)) continue;
|
|
4361
|
+
numbers.push(Number(digits));
|
|
4362
|
+
}
|
|
3656
4363
|
return [...new Set(numbers)];
|
|
3657
4364
|
}
|
|
3658
4365
|
|
|
@@ -4441,6 +5148,12 @@ function complete(args) {
|
|
|
4441
5148
|
*
|
|
4442
5149
|
* Reports by default and only acts under `--apply`, because a sweep that closes
|
|
4443
5150
|
* things as a side effect of being run is not something anyone will run twice.
|
|
5151
|
+
*
|
|
5152
|
+
* `--since <rev>` bounds the evidence scan to what a deploy branch gained after
|
|
5153
|
+
* `<rev>`. That is what makes an APPLYING run safe to trigger from a merge: the
|
|
5154
|
+
* unbounded question is "what has ever shipped and is still open", which is a
|
|
5155
|
+
* backlog nobody should complete unattended, while the bounded one is "what did
|
|
5156
|
+
* this push ship", which is exactly the item the merge earned. See `sweepBound`.
|
|
4444
5157
|
*/
|
|
4445
5158
|
/**
|
|
4446
5159
|
* Buffer bound for a deploy branch's commit log.
|
|
@@ -4518,6 +5231,92 @@ function resolvedBranchRev(branch) {
|
|
|
4518
5231
|
return undefined;
|
|
4519
5232
|
}
|
|
4520
5233
|
|
|
5234
|
+
/** Bounds a sweep to the commits a single push added to a deploy branch. */
|
|
5235
|
+
const SINCE_FLAG = "--since";
|
|
5236
|
+
|
|
5237
|
+
/**
|
|
5238
|
+
* The revision a bounded sweep starts AFTER, or undefined for the whole history.
|
|
5239
|
+
*
|
|
5240
|
+
* This is what gives directly-driven work a terminal path. The unbounded sweep
|
|
5241
|
+
* answers "what has ever shipped and is still open", which is a backlog
|
|
5242
|
+
* question: pointing `--apply` at it completes every item the history declares,
|
|
5243
|
+
* so it is only ever safe to run deliberately, by a person who has read the
|
|
5244
|
+
* report. A merge cannot run that. `--since <rev>` narrows the same
|
|
5245
|
+
* evidence to what THIS push added, which is a question a push-triggered run
|
|
5246
|
+
* can answer and act on — it completes the items its own merge shipped and
|
|
5247
|
+
* structurally cannot reach any other (CodySwannGT/lisa#3704).
|
|
5248
|
+
*
|
|
5249
|
+
* A valueless flag is REFUSED rather than read as absent. The shared `option`
|
|
5250
|
+
* helper falls back when a flag carries no value, which is right where the
|
|
5251
|
+
* fallback is another spelling of the same answer and catastrophic here: the
|
|
5252
|
+
* fallback is "no bound at all", so a typo would silently widen an applying run
|
|
5253
|
+
* from one merge to the entire backlog. That is the one direction this must
|
|
5254
|
+
* never fail in.
|
|
5255
|
+
* @param {string[]} args Command arguments.
|
|
5256
|
+
* @returns {string | undefined} The bound, or undefined when unbounded.
|
|
5257
|
+
*/
|
|
5258
|
+
function sweepBound(args) {
|
|
5259
|
+
const index = args.indexOf(SINCE_FLAG);
|
|
5260
|
+
if (index < 0) return undefined;
|
|
5261
|
+
const value = args[index + 1];
|
|
5262
|
+
if (value === undefined || value.startsWith("-") || value.trim() === "") {
|
|
5263
|
+
throw new TrackingError(
|
|
5264
|
+
`${SINCE_FLAG} was supplied without a revision, and it is not read as absent.\n` +
|
|
5265
|
+
`Pass ${SINCE_FLAG} <rev> — the commit the deploy branch was at before the push — ` +
|
|
5266
|
+
`or omit the flag entirely to sweep the whole history.\n` +
|
|
5267
|
+
`Treating it as absent would widen an --apply run from the one merge that triggered it ` +
|
|
5268
|
+
`to every item the history declares.`
|
|
5269
|
+
);
|
|
5270
|
+
}
|
|
5271
|
+
return value.trim();
|
|
5272
|
+
}
|
|
5273
|
+
|
|
5274
|
+
/**
|
|
5275
|
+
* Resolve a sweep bound to a commit, refusing when it names nothing here.
|
|
5276
|
+
*
|
|
5277
|
+
* An unresolvable bound is NOT DETERMINED, never an empty range. `git log
|
|
5278
|
+
* <branch> --not <unknown>` does not fail quietly — it fails — but a bound
|
|
5279
|
+
* that resolves in the caller's repository and not in this checkout (a shallow
|
|
5280
|
+
* clone, an unfetched ref, the all-zero SHA a branch-creation push carries) is
|
|
5281
|
+
* the case where a run could otherwise report a narrower answer than the one
|
|
5282
|
+
* its operator asked for. Refusing says which of the two happened.
|
|
5283
|
+
* @param {string | undefined} since The bound as supplied.
|
|
5284
|
+
* @returns {string | undefined} The resolved object ID, or undefined.
|
|
5285
|
+
*/
|
|
5286
|
+
function resolvedSinceRev(since) {
|
|
5287
|
+
if (since === undefined) return undefined;
|
|
5288
|
+
const result = run(
|
|
5289
|
+
"git",
|
|
5290
|
+
["rev-parse", "--verify", "--quiet", `${since}^{commit}`],
|
|
5291
|
+
{ allowFailure: true }
|
|
5292
|
+
);
|
|
5293
|
+
const sha = result.stdout.trim();
|
|
5294
|
+
if (result.status !== 0 || sha === "") {
|
|
5295
|
+
throw new TrackingError(
|
|
5296
|
+
`NOT DETERMINED: ${SINCE_FLAG} ${since} resolves to no commit in this checkout, ` +
|
|
5297
|
+
`so the range a bounded sweep would examine is unknown.\n` +
|
|
5298
|
+
`An unresolvable bound is not an empty one — reporting "no drift" from it would be ` +
|
|
5299
|
+
`an absence claim over evidence never read.\n` +
|
|
5300
|
+
`Fetch the history (\`git fetch --unshallow origin\`) and re-run, or drop ${SINCE_FLAG} ` +
|
|
5301
|
+
`to sweep the whole deploy history.`
|
|
5302
|
+
);
|
|
5303
|
+
}
|
|
5304
|
+
return sha;
|
|
5305
|
+
}
|
|
5306
|
+
|
|
5307
|
+
/**
|
|
5308
|
+
* The sentence a report owes its reader when the scan was bounded.
|
|
5309
|
+
* @param {string | undefined} since The bound as supplied.
|
|
5310
|
+
* @returns {string} A qualifying line, or the empty string when unbounded.
|
|
5311
|
+
*/
|
|
5312
|
+
function describeSinceBound(since) {
|
|
5313
|
+
if (since === undefined) return "";
|
|
5314
|
+
return (
|
|
5315
|
+
`\nBounded: only commits a deploy branch gained since ${since} were read, so anything ` +
|
|
5316
|
+
`declared before that is outside this result.`
|
|
5317
|
+
);
|
|
5318
|
+
}
|
|
5319
|
+
|
|
4521
5320
|
/**
|
|
4522
5321
|
* Work items DECLARED by commits reachable from a deploy branch.
|
|
4523
5322
|
*
|
|
@@ -4534,16 +5333,35 @@ function resolvedBranchRev(branch) {
|
|
|
4534
5333
|
*
|
|
4535
5334
|
* Reachability is also what confirms the work actually landed, rather than
|
|
4536
5335
|
* trusting a pull request's `baseRefName` to say where it went.
|
|
5336
|
+
*
|
|
5337
|
+
* UNRESOLVABLE branches are reported, never silently dropped. A configured
|
|
5338
|
+
* deploy branch that resolves to no commit is a branch the sweep did not look
|
|
5339
|
+
* at, and a `continue` past it turns part of an absence claim into evidence
|
|
5340
|
+
* never read — the same shape as the truncated log below, which this function
|
|
5341
|
+
* already refuses over. It is not fatal on its own, because a project may
|
|
5342
|
+
* configure `dev` or `staging` in a clone that has only ever fetched `main`
|
|
5343
|
+
* and the sweep is still useful there; what it may not do is let the omission
|
|
5344
|
+
* go unsaid. When NOTHING resolved there is no evidence at all, and the run
|
|
5345
|
+
* refuses rather than reporting a clean queue.
|
|
4537
5346
|
* @param {string} repository `owner/name` the items belong to.
|
|
4538
5347
|
* @param {object} contract Resolved tracker contract.
|
|
4539
|
-
* @
|
|
5348
|
+
* @param {string} [since] Revision to bound the scan after; see `sweepBound`.
|
|
5349
|
+
* @returns {{declarations: Map<number, string[]>, unresolved: string[]}} Issue
|
|
5350
|
+
* number to declaring commits, and the deploy branches that resolved to
|
|
5351
|
+
* nothing.
|
|
4540
5352
|
*/
|
|
4541
|
-
function deployedDeclarations(repository, contract) {
|
|
5353
|
+
function deployedDeclarations(repository, contract, since) {
|
|
5354
|
+
const sinceRev = resolvedSinceRev(since);
|
|
4542
5355
|
const declarations = new Map();
|
|
5356
|
+
const unresolved = [];
|
|
4543
5357
|
for (const branch of contract.deployBranches.keys()) {
|
|
4544
5358
|
const rev = resolvedBranchRev(branch);
|
|
4545
|
-
if (!rev)
|
|
4546
|
-
|
|
5359
|
+
if (!rev) {
|
|
5360
|
+
unresolved.push(branch);
|
|
5361
|
+
continue;
|
|
5362
|
+
}
|
|
5363
|
+
const bound = sinceRev ? ["--not", sinceRev] : [];
|
|
5364
|
+
const result = run("git", ["log", rev, ...bound, "-z", "--format=%H%n%B"], {
|
|
4547
5365
|
allowFailure: true,
|
|
4548
5366
|
maxBuffer: DECLARATION_LOG_MAX_BYTES,
|
|
4549
5367
|
});
|
|
@@ -4570,7 +5388,31 @@ function deployedDeclarations(repository, contract) {
|
|
|
4570
5388
|
]);
|
|
4571
5389
|
}
|
|
4572
5390
|
}
|
|
4573
|
-
|
|
5391
|
+
if (
|
|
5392
|
+
unresolved.length > 0 &&
|
|
5393
|
+
unresolved.length === contract.deployBranches.size
|
|
5394
|
+
) {
|
|
5395
|
+
throw new TrackingError(
|
|
5396
|
+
`no configured deploy branch resolves to a commit (${unresolved.join(", ")}), so no absence of drift can be reported.\n` +
|
|
5397
|
+
`Fetch them (\`git fetch origin\`) or correct \`deploy.branches\` in .lisa.config.json; ` +
|
|
5398
|
+
`a deploy branch the sweep could not read is not an empty one.`
|
|
5399
|
+
);
|
|
5400
|
+
}
|
|
5401
|
+
return { declarations, unresolved };
|
|
5402
|
+
}
|
|
5403
|
+
|
|
5404
|
+
/**
|
|
5405
|
+
* The sentence a report owes its reader when part of the evidence was missing.
|
|
5406
|
+
* @param {string[]} unresolved Deploy branches that resolved to no commit.
|
|
5407
|
+
* @returns {string} A qualifying line, or the empty string when nothing was.
|
|
5408
|
+
*/
|
|
5409
|
+
function describeUnresolvedBranches(unresolved) {
|
|
5410
|
+
if (unresolved.length === 0) return "";
|
|
5411
|
+
return (
|
|
5412
|
+
`\nNOT examined: ${unresolved.join(", ")} — configured as a deploy branch ` +
|
|
5413
|
+
`but resolving to no commit here, so anything shipped only there is ` +
|
|
5414
|
+
`outside this result.`
|
|
5415
|
+
);
|
|
4574
5416
|
}
|
|
4575
5417
|
|
|
4576
5418
|
/**
|
|
@@ -4603,7 +5445,12 @@ function sweep(args) {
|
|
|
4603
5445
|
else subjects.set(issue.number, { ...issue, roles: [role] });
|
|
4604
5446
|
}
|
|
4605
5447
|
}
|
|
4606
|
-
const
|
|
5448
|
+
const since = sweepBound(args);
|
|
5449
|
+
const { declarations, unresolved } = deployedDeclarations(
|
|
5450
|
+
repository,
|
|
5451
|
+
contract,
|
|
5452
|
+
since
|
|
5453
|
+
);
|
|
4607
5454
|
const apply = args.includes("--apply");
|
|
4608
5455
|
// Named in every report, clean or not. The old clean-result sentence spoke
|
|
4609
5456
|
// only of the claimed role while the subject list excluded the ready lane
|
|
@@ -4638,21 +5485,166 @@ function sweep(args) {
|
|
|
4638
5485
|
}
|
|
4639
5486
|
}
|
|
4640
5487
|
if (drifted === 0) {
|
|
5488
|
+
const examinedSummary = `Examined ${subjects.size} item(s) across ${roles.length} lifecycle role(s); no role outside ${examined} was queried.`;
|
|
4641
5489
|
console.log(
|
|
4642
|
-
`No drift: every open item carrying ${examined} is genuinely in flight.\n`
|
|
4643
|
-
`Examined ${subjects.size} item(s) across ${roles.length} lifecycle role(s); ` +
|
|
4644
|
-
`no role outside ${examined} was queried.`
|
|
5490
|
+
`No drift: every open item carrying ${examined} is genuinely in flight.\n${examinedSummary}${describeSinceBound(since)}${describeUnresolvedBranches(unresolved)}`
|
|
4645
5491
|
);
|
|
4646
5492
|
return;
|
|
4647
5493
|
}
|
|
4648
5494
|
if (!apply) {
|
|
5495
|
+
const driftHeadline = `${drifted} open item(s) carrying ${examined} are declared by a commit on a deploy branch.`;
|
|
4649
5496
|
console.log(
|
|
4650
|
-
`\n${
|
|
4651
|
-
`Re-run with --apply to complete them.`
|
|
5497
|
+
`\n${driftHeadline} Re-run with --apply to complete them.${describeSinceBound(since)}${describeUnresolvedBranches(unresolved)}`
|
|
4652
5498
|
);
|
|
4653
5499
|
}
|
|
4654
5500
|
}
|
|
4655
5501
|
|
|
5502
|
+
/**
|
|
5503
|
+
* Whether a line is `Key: value` — the shape git recognises as a trailer.
|
|
5504
|
+
*
|
|
5505
|
+
* Character-scanned rather than matched, so it is linear over a commit message
|
|
5506
|
+
* this script does not own.
|
|
5507
|
+
* @param {string} line One line of a commit message.
|
|
5508
|
+
* @returns {boolean} Whether git would read this line as a trailer.
|
|
5509
|
+
*/
|
|
5510
|
+
function isTrailerLine(line) {
|
|
5511
|
+
const colon = line.indexOf(": ");
|
|
5512
|
+
if (colon <= 0) return false;
|
|
5513
|
+
if (line.slice(colon + 2).trim() === "") return false;
|
|
5514
|
+
for (const character of line.slice(0, colon)) {
|
|
5515
|
+
const letter =
|
|
5516
|
+
(character >= "a" && character <= "z") ||
|
|
5517
|
+
(character >= "A" && character <= "Z");
|
|
5518
|
+
const digit = character >= "0" && character <= "9";
|
|
5519
|
+
if (!letter && !digit && character !== "-") return false;
|
|
5520
|
+
}
|
|
5521
|
+
return true;
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
/**
|
|
5525
|
+
* Where the lane line goes, so the final trailer paragraph is never split.
|
|
5526
|
+
*
|
|
5527
|
+
* `git interpret-trailers` starts a NEW paragraph whenever the last one holds
|
|
5528
|
+
* any line it does not recognise as a trailer — and the final block of an
|
|
5529
|
+
* agent-authored message routinely ends with a non-trailer signature line. The
|
|
5530
|
+
* split is silent and it costs something real: `Co-Authored-By` stops being in
|
|
5531
|
+
* the last paragraph, which is the only place GitHub reads co-authors from, so
|
|
5532
|
+
* a routing aid would quietly break attribution to fix attribution.
|
|
5533
|
+
*
|
|
5534
|
+
* Scanning back from the end and stopping at the first blank line keeps the
|
|
5535
|
+
* search inside the final paragraph. A message whose tail is prose has no
|
|
5536
|
+
* trailer to sit beside and is left to git, which correctly opens a paragraph
|
|
5537
|
+
* for it.
|
|
5538
|
+
* @param {string[]} lines Message lines, trailing blanks already removed.
|
|
5539
|
+
* @returns {number} Index to insert after, or -1 for "let git decide".
|
|
5540
|
+
*/
|
|
5541
|
+
function trailerInsertionPoint(lines) {
|
|
5542
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
5543
|
+
if (lines[index].trim() === "") return -1;
|
|
5544
|
+
if (isTrailerLine(lines[index])) return index;
|
|
5545
|
+
}
|
|
5546
|
+
return -1;
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
/**
|
|
5550
|
+
* Stamp the caller's lane onto a commit message being prepared.
|
|
5551
|
+
*
|
|
5552
|
+
* Never the reason a commit fails. Attribution is a routing aid; a git that
|
|
5553
|
+
* declines to rewrite trailers is a worse commit message, not a violation, and
|
|
5554
|
+
* refusing the commit over it would make every session's first encounter with
|
|
5555
|
+
* this feature an outage.
|
|
5556
|
+
*
|
|
5557
|
+
* An existing stamp is left alone. On a rebase, an amend or a cherry-pick the
|
|
5558
|
+
* message already carries the ORIGINATING lane's stamp, and the lane doing the
|
|
5559
|
+
* rewrite must not take credit for work it did not start — which is precisely
|
|
5560
|
+
* the guess a survey has to make when nothing is recorded.
|
|
5561
|
+
* @param {string} file Commit message file.
|
|
5562
|
+
* @returns {void}
|
|
5563
|
+
*/
|
|
5564
|
+
function stampLane(file) {
|
|
5565
|
+
const lane = resolveCallerLaneId();
|
|
5566
|
+
if (lane === undefined) return;
|
|
5567
|
+
const message = readFileSync(file, "utf8");
|
|
5568
|
+
if (message.split("\n").some(line => LANE_PREFIX_MATCH.test(line))) return;
|
|
5569
|
+
const lines = message.split("\n");
|
|
5570
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
5571
|
+
const at = trailerInsertionPoint(lines);
|
|
5572
|
+
if (at === -1) {
|
|
5573
|
+
run(
|
|
5574
|
+
"git",
|
|
5575
|
+
[
|
|
5576
|
+
"interpret-trailers",
|
|
5577
|
+
"--in-place",
|
|
5578
|
+
"--if-missing=add",
|
|
5579
|
+
"--trailer",
|
|
5580
|
+
renderLaneTrailer(lane),
|
|
5581
|
+
file,
|
|
5582
|
+
],
|
|
5583
|
+
{ allowFailure: true }
|
|
5584
|
+
);
|
|
5585
|
+
return;
|
|
5586
|
+
}
|
|
5587
|
+
lines.splice(at + 1, 0, renderLaneTrailer(lane));
|
|
5588
|
+
writeFileSync(file, `${lines.join("\n")}\n`);
|
|
5589
|
+
}
|
|
5590
|
+
|
|
5591
|
+
/**
|
|
5592
|
+
* Answer "which lane does this belong to, and is it mine?".
|
|
5593
|
+
*
|
|
5594
|
+
* With no argument it reports the caller's own lane, which is what a session
|
|
5595
|
+
* needs to recognise its own work later. With `--commit` it reads the message
|
|
5596
|
+
* of a commit — the form that survives branch deletion, because the commit
|
|
5597
|
+
* lives on in the base branch once the pull request has landed. With
|
|
5598
|
+
* `--text-file` it reads any text at all, which is how a pull-request body or
|
|
5599
|
+
* a tracker comment is classified with the same vocabulary.
|
|
5600
|
+
* @param {string[]} args Command arguments.
|
|
5601
|
+
* @returns {void}
|
|
5602
|
+
*/
|
|
5603
|
+
function laneCommand(args) {
|
|
5604
|
+
const caller = resolveCallerLaneId();
|
|
5605
|
+
const commit = flagValue(args, "--commit");
|
|
5606
|
+
const textFile = flagValue(args, "--text-file");
|
|
5607
|
+
if (commit === undefined && textFile === undefined) {
|
|
5608
|
+
console.log(caller ?? "unattributed");
|
|
5609
|
+
return;
|
|
5610
|
+
}
|
|
5611
|
+
const text =
|
|
5612
|
+
textFile === undefined
|
|
5613
|
+
? commitMessageOf(commit)
|
|
5614
|
+
: readFileSync(textFile, "utf8");
|
|
5615
|
+
const marker = parseLaneId(text);
|
|
5616
|
+
console.log(`LANE ${judgeLane(marker, caller)} ${marker ?? "none"}`);
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5619
|
+
/**
|
|
5620
|
+
* The value that follows a flag, refusing a flag left without one.
|
|
5621
|
+
* @param {string[]} args Command arguments.
|
|
5622
|
+
* @param {string} flag Flag to look for.
|
|
5623
|
+
* @returns {string | undefined} The value, or undefined when the flag is absent.
|
|
5624
|
+
*/
|
|
5625
|
+
function flagValue(args, flag) {
|
|
5626
|
+
const index = args.indexOf(flag);
|
|
5627
|
+
if (index === -1) return undefined;
|
|
5628
|
+
const value = args[index + 1];
|
|
5629
|
+
if (value === undefined || value.startsWith("--"))
|
|
5630
|
+
throw new TrackingError(`${flag} requires a value`);
|
|
5631
|
+
return value;
|
|
5632
|
+
}
|
|
5633
|
+
|
|
5634
|
+
/**
|
|
5635
|
+
* The full message of one commit.
|
|
5636
|
+
* @param {string} ref Commit-ish to read.
|
|
5637
|
+
* @returns {string} The commit message body.
|
|
5638
|
+
*/
|
|
5639
|
+
function commitMessageOf(ref) {
|
|
5640
|
+
const result = run("git", ["log", "-1", RAW_MESSAGE_FORMAT, ref], {
|
|
5641
|
+
allowFailure: true,
|
|
5642
|
+
});
|
|
5643
|
+
if (result.status !== 0)
|
|
5644
|
+
throw new TrackingError(`could not read the commit message of '${ref}'`);
|
|
5645
|
+
return result.stdout;
|
|
5646
|
+
}
|
|
5647
|
+
|
|
4656
5648
|
function prepareCommitMessage(args) {
|
|
4657
5649
|
const [file, source = ""] = args;
|
|
4658
5650
|
if (!file)
|
|
@@ -4664,6 +5656,11 @@ function prepareCommitMessage(args) {
|
|
|
4664
5656
|
RELEASE_SUBJECT.test(messageSubject(readFileSync(file, "utf8")))
|
|
4665
5657
|
)
|
|
4666
5658
|
return;
|
|
5659
|
+
// Before the binding check, deliberately. The lane stamp is what makes an
|
|
5660
|
+
// ORPHAN routable, and an orphan is disproportionately likely to be exactly
|
|
5661
|
+
// the commit that never got a work item bound — so gating attribution on the
|
|
5662
|
+
// binding would withhold it in the one case it exists for.
|
|
5663
|
+
stampLane(file);
|
|
4667
5664
|
const state = readState(true);
|
|
4668
5665
|
if (!state) return;
|
|
4669
5666
|
assertStateBranch(state);
|
|
@@ -4684,10 +5681,17 @@ function validateCommit(args) {
|
|
|
4684
5681
|
const file = args[0];
|
|
4685
5682
|
if (!file)
|
|
4686
5683
|
throw new TrackingError("validate-commit requires the commit message file");
|
|
5684
|
+
const message = readFileSync(file, "utf8");
|
|
5685
|
+
// Before the traceability gates and outside their exemptions, deliberately.
|
|
5686
|
+
// A merge or release message is exempt from naming a work item because
|
|
5687
|
+
// nothing bound one; neither fact says anything about whether the harness
|
|
5688
|
+
// appended an identifier to it, and those are exactly the messages no agent
|
|
5689
|
+
// proof-reads.
|
|
5690
|
+
assertNoSessionUrl(message);
|
|
4687
5691
|
let result;
|
|
4688
5692
|
try {
|
|
4689
|
-
result = validateMessage(
|
|
4690
|
-
|
|
5693
|
+
result = validateMessage(message, {
|
|
5694
|
+
allowMergeExemption: true,
|
|
4691
5695
|
});
|
|
4692
5696
|
} catch (error) {
|
|
4693
5697
|
// The commit-msg hook is the EARLIEST moment an operator meets any of this,
|
|
@@ -5232,6 +6236,7 @@ function main() {
|
|
|
5232
6236
|
// broken, which is when the answer matters most.
|
|
5233
6237
|
if (command === "contract-version")
|
|
5234
6238
|
return console.log(WORK_ITEM_CONTRACT_VERSION);
|
|
6239
|
+
if (command === "lane") return laneCommand(args);
|
|
5235
6240
|
if (command === "prepare-commit-msg") return prepareCommitMessage(args);
|
|
5236
6241
|
if (command === "validate-commit") return validateCommit(args);
|
|
5237
6242
|
if (command === "validate-push") return validatePush(args);
|
|
@@ -5240,7 +6245,7 @@ function main() {
|
|
|
5240
6245
|
if (command === "validate-pr") return validatePr(args);
|
|
5241
6246
|
if (command === DISCHARGE_COMMAND) return dischargePrGates(args);
|
|
5242
6247
|
throw new TrackingError(
|
|
5243
|
-
"Usage: lisa-work-item.mjs link|current|attach-branch|clear|verify-level|contract-version|backlink|complete|sweep|prepare-commit-msg|validate-commit|validate-push|validate-push-destination|validate-pr|discharge-pr-gates" +
|
|
6248
|
+
"Usage: lisa-work-item.mjs link|current|attach-branch|clear|verify-level|contract-version|backlink|complete|sweep|lane|prepare-commit-msg|validate-commit|validate-push|validate-push-destination|validate-pr|discharge-pr-gates" +
|
|
5244
6249
|
"\n(`bind` is accepted as an alias for `link`, but some agent harnesses refuse the token `bind` in a command line.)"
|
|
5245
6250
|
);
|
|
5246
6251
|
}
|