@codyswann/lisa 4.74.8 → 4.75.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/.claude-plugin/marketplace.json +60 -0
- package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +4 -2
- package/all/copy-overwrite/scripts/lib/npm-update-checkpoint-codec.mjs +141 -0
- package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +162 -116
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +1 -0
- package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +9 -60
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +18 -6
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +20 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/lisa-skill-sources.d.ts +3 -3
- package/dist/core/lisa-skill-sources.d.ts.map +1 -1
- package/dist/core/lisa-skill-sources.js +3 -2
- package/dist/core/lisa-skill-sources.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +739 -11
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- 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-cdk-devin/.claude-plugin/plugin.json +11 -0
- package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-cdk-devin/skills/setup-aws-accounts/SKILL.md +292 -0
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.claude-plugin/plugin.json +8 -0
- package/plugins/lisa-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-devin/.mcp.json +8 -0
- package/plugins/lisa-devin/agents/architecture-specialist.md +32 -0
- package/plugins/lisa-devin/agents/bug-fixer.md +43 -0
- package/plugins/lisa-devin/agents/builder.md +44 -0
- package/plugins/lisa-devin/agents/confluence-prd-intake.md +65 -0
- package/plugins/lisa-devin/agents/debug-specialist.md +34 -0
- package/plugins/lisa-devin/agents/eval-specialist.md +33 -0
- package/plugins/lisa-devin/agents/git-history-analyzer.md +183 -0
- package/plugins/lisa-devin/agents/github-agent.md +155 -0
- package/plugins/lisa-devin/agents/github-build-intake.md +64 -0
- package/plugins/lisa-devin/agents/github-prd-intake.md +66 -0
- package/plugins/lisa-devin/agents/jira-agent.md +135 -0
- package/plugins/lisa-devin/agents/jira-build-intake.md +64 -0
- package/plugins/lisa-devin/agents/learner.md +85 -0
- package/plugins/lisa-devin/agents/learning-judge.md +119 -0
- package/plugins/lisa-devin/agents/learnings-synthesizer.md +155 -0
- package/plugins/lisa-devin/agents/linear-agent.md +138 -0
- package/plugins/lisa-devin/agents/linear-build-intake.md +64 -0
- package/plugins/lisa-devin/agents/linear-prd-intake.md +66 -0
- package/plugins/lisa-devin/agents/notion-prd-intake.md +63 -0
- package/plugins/lisa-devin/agents/performance-specialist.md +26 -0
- package/plugins/lisa-devin/agents/pr-mining-specialist.md +85 -0
- package/plugins/lisa-devin/agents/product-specialist.md +30 -0
- package/plugins/lisa-devin/agents/quality-specialist.md +30 -0
- package/plugins/lisa-devin/agents/security-specialist.md +43 -0
- package/plugins/lisa-devin/agents/skill-evaluator.md +205 -0
- package/plugins/lisa-devin/agents/spec-conformance-specialist.md +56 -0
- package/plugins/lisa-devin/agents/test-specialist.md +32 -0
- package/plugins/lisa-devin/agents/tracker-mining-specialist.md +85 -0
- package/plugins/lisa-devin/agents/verification-specialist.md +145 -0
- package/plugins/lisa-devin/hooks/auto-update.mjs +824 -0
- package/plugins/lisa-devin/hooks/auto-update.sh +31 -0
- package/plugins/lisa-devin/hooks/block-blind-automerge.sh +1242 -0
- package/plugins/lisa-devin/hooks/block-direct-issue-create.sh +2603 -0
- package/plugins/lisa-devin/hooks/block-host-name-leak.mjs +412 -0
- package/plugins/lisa-devin/hooks/block-host-name-leak.sh +61 -0
- package/plugins/lisa-devin/hooks/block-instruction-file-edits.sh +207 -0
- package/plugins/lisa-devin/hooks/block-managed-file-edits.sh +1175 -0
- package/plugins/lisa-devin/hooks/block-no-verify.sh +2288 -0
- package/plugins/lisa-devin/hooks/block-shell-json-parsing.sh +263 -0
- package/plugins/lisa-devin/hooks/cleanup-stale-worktrees.sh +122 -0
- package/plugins/lisa-devin/hooks/devin-tool-name-shim.sh +30 -0
- package/plugins/lisa-devin/hooks/discharge-work-item-gates.sh +90 -0
- package/plugins/lisa-devin/hooks/enforce-verification-gate.sh +506 -0
- package/plugins/lisa-devin/hooks/enforcement-vintage-npm.mjs +320 -0
- package/plugins/lisa-devin/hooks/enforcement-vintage.mjs +472 -0
- package/plugins/lisa-devin/hooks/enforcement-vintage.sh +48 -0
- package/plugins/lisa-devin/hooks/failure-signature-index.mjs +717 -0
- package/plugins/lisa-devin/hooks/failure-signature-index.sh +26 -0
- package/plugins/lisa-devin/hooks/guard-dedupe.bash +374 -0
- package/plugins/lisa-devin/hooks/inject-resolved-config.mjs +903 -0
- package/plugins/lisa-devin/hooks/inject-resolved-config.sh +107 -0
- package/plugins/lisa-devin/hooks/inject-rules.sh +49 -0
- package/plugins/lisa-devin/hooks/install-pkgs.sh +84 -0
- package/plugins/lisa-devin/hooks/operational-hazards.mjs +736 -0
- package/plugins/lisa-devin/hooks/operational-hazards.sh +28 -0
- package/plugins/lisa-devin/hooks/parity-safety-net-heredoc.py +1004 -0
- package/plugins/lisa-devin/hooks/parity-safety-net.sh +3275 -0
- package/plugins/lisa-devin/hooks/secrets-preflight.sh +72 -0
- package/plugins/lisa-devin/hooks/setup-jira-cli.sh +205 -0
- package/plugins/lisa-devin/hooks/shell-write-nudge.sh +86 -0
- package/plugins/lisa-devin/hooks/sonar-secrets.sh +205 -0
- package/plugins/lisa-devin/hooks/threshold-ratchet-compare.mjs +518 -0
- package/plugins/lisa-devin/hooks/threshold-ratchet-families.mjs +543 -0
- package/plugins/lisa-devin/hooks/threshold-ratchet.mjs +568 -0
- package/plugins/lisa-devin/hooks/threshold-ratchet.sh +22 -0
- package/plugins/lisa-devin/hooks/withdrawn-rulings.mjs +600 -0
- package/plugins/lisa-devin/hooks/withdrawn-rulings.sh +24 -0
- package/plugins/lisa-devin/hooks/worktree-binding-guard.mjs +1383 -0
- package/plugins/lisa-devin/hooks/worktree-binding-guard.sh +79 -0
- package/plugins/lisa-devin/hooks.json +247 -0
- package/plugins/lisa-devin/rules/eager/00-rule-index.md +82 -0
- package/plugins/lisa-devin/rules/eager/base-rules.md +85 -0
- package/plugins/lisa-devin/rules/eager/coding-philosophy.md +27 -0
- package/plugins/lisa-devin/rules/eager/config-resolution.md +37 -0
- package/plugins/lisa-devin/rules/eager/do-it-now.md +11 -0
- package/plugins/lisa-devin/rules/eager/empirical-inquiry.md +24 -0
- package/plugins/lisa-devin/rules/eager/falsifiable-checks.md +48 -0
- package/plugins/lisa-devin/rules/eager/history-audit.md +27 -0
- package/plugins/lisa-devin/rules/eager/integration-access-layer.md +19 -0
- package/plugins/lisa-devin/rules/eager/intent-routing.md +18 -0
- package/plugins/lisa-devin/rules/eager/learnings-ladder.md +18 -0
- package/plugins/lisa-devin/rules/eager/local-ci-first.md +15 -0
- package/plugins/lisa-devin/rules/eager/measurement-integrity.md +18 -0
- package/plugins/lisa-devin/rules/eager/never-name-a-downstream-project.md +9 -0
- package/plugins/lisa-devin/rules/eager/not-blocked-just-waiting.md +13 -0
- package/plugins/lisa-devin/rules/eager/operational-hazards.md +50 -0
- package/plugins/lisa-devin/rules/eager/report-actionability.md +26 -0
- package/plugins/lisa-devin/rules/eager/security-audit-handling.md +40 -0
- package/plugins/lisa-devin/rules/eager/session-status-updates.md +20 -0
- package/plugins/lisa-devin/rules/eager/settled-decisions.md +75 -0
- package/plugins/lisa-devin/rules/eager/stale-state-claims.md +26 -0
- package/plugins/lisa-devin/rules/eager/tool-access-gate.md +23 -0
- package/plugins/lisa-devin/rules/eager/tracked-work.md +9 -0
- package/plugins/lisa-devin/rules/eager/upstream-to-lisa.md +14 -0
- package/plugins/lisa-devin/rules/eager/verification.md +33 -0
- package/plugins/lisa-devin/rules/eager/wiki-knowledge-source.md +13 -0
- package/plugins/lisa-devin/rules/eager/withdrawn-rulings.md +102 -0
- package/plugins/lisa-devin/rules/reference/automation-runbook-contract.md +355 -0
- package/plugins/lisa-devin/rules/reference/base-rules.md +136 -0
- package/plugins/lisa-devin/rules/reference/bdd-e2e-coverage.md +301 -0
- package/plugins/lisa-devin/rules/reference/blocker-containment.md +165 -0
- package/plugins/lisa-devin/rules/reference/claim-archaeology.md +184 -0
- package/plugins/lisa-devin/rules/reference/claim-evidence-mapping.md +362 -0
- package/plugins/lisa-devin/rules/reference/claim-time-guards.md +135 -0
- package/plugins/lisa-devin/rules/reference/coding-philosophy.md +428 -0
- package/plugins/lisa-devin/rules/reference/config-resolution.md +1225 -0
- package/plugins/lisa-devin/rules/reference/control-reachability.md +166 -0
- package/plugins/lisa-devin/rules/reference/convergent-review.md +145 -0
- package/plugins/lisa-devin/rules/reference/credential-substrate-precedence.md +249 -0
- package/plugins/lisa-devin/rules/reference/dependency-decision-records.md +177 -0
- package/plugins/lisa-devin/rules/reference/dependency-internalization-kit.md +244 -0
- package/plugins/lisa-devin/rules/reference/dependency-trust-classes.md +291 -0
- package/plugins/lisa-devin/rules/reference/deployed-state-readback.md +142 -0
- package/plugins/lisa-devin/rules/reference/derived-branch-plan.md +214 -0
- package/plugins/lisa-devin/rules/reference/design-source-of-truth.md +260 -0
- package/plugins/lisa-devin/rules/reference/design-value-binding.md +311 -0
- package/plugins/lisa-devin/rules/reference/do-it-now.md +19 -0
- package/plugins/lisa-devin/rules/reference/documentation-source-paths.md +31 -0
- package/plugins/lisa-devin/rules/reference/empirical-inquiry.md +28 -0
- package/plugins/lisa-devin/rules/reference/factory-model.md +111 -0
- package/plugins/lisa-devin/rules/reference/falsifiable-checks.md +275 -0
- package/plugins/lisa-devin/rules/reference/history-audit.md +42 -0
- package/plugins/lisa-devin/rules/reference/integration-access-layer.md +89 -0
- package/plugins/lisa-devin/rules/reference/intent-routing.md +428 -0
- package/plugins/lisa-devin/rules/reference/leaf-only-lifecycle.md +227 -0
- package/plugins/lisa-devin/rules/reference/learnings-ladder.md +41 -0
- package/plugins/lisa-devin/rules/reference/local-ci-first.md +34 -0
- package/plugins/lisa-devin/rules/reference/measurement-integrity.md +119 -0
- package/plugins/lisa-devin/rules/reference/never-name-a-downstream-project.md +37 -0
- package/plugins/lisa-devin/rules/reference/not-blocked-just-waiting.md +41 -0
- package/plugins/lisa-devin/rules/reference/observability-audit.md +146 -0
- package/plugins/lisa-devin/rules/reference/operational-hazards.md +115 -0
- package/plugins/lisa-devin/rules/reference/prd-definition-of-ready.md +118 -0
- package/plugins/lisa-devin/rules/reference/prd-lifecycle-rollup.md +192 -0
- package/plugins/lisa-devin/rules/reference/pre-flight-autofill.md +250 -0
- package/plugins/lisa-devin/rules/reference/project-learnings.md +159 -0
- package/plugins/lisa-devin/rules/reference/promotion-contract.md +148 -0
- package/plugins/lisa-devin/rules/reference/readiness-rubric.md +242 -0
- package/plugins/lisa-devin/rules/reference/ready-role-filing.md +212 -0
- package/plugins/lisa-devin/rules/reference/rejection-detection.md +308 -0
- package/plugins/lisa-devin/rules/reference/repo-scope-split.md +119 -0
- package/plugins/lisa-devin/rules/reference/report-actionability.md +61 -0
- package/plugins/lisa-devin/rules/reference/reset-seed-coverage.md +286 -0
- package/plugins/lisa-devin/rules/reference/security-audit-handling.md +55 -0
- package/plugins/lisa-devin/rules/reference/session-status-updates.md +111 -0
- package/plugins/lisa-devin/rules/reference/settled-decisions.md +39 -0
- package/plugins/lisa-devin/rules/reference/stale-state-claims.md +88 -0
- package/plugins/lisa-devin/rules/reference/tool-access-gate.md +114 -0
- package/plugins/lisa-devin/rules/reference/tracked-work.md +49 -0
- package/plugins/lisa-devin/rules/reference/upstream-to-lisa.md +26 -0
- package/plugins/lisa-devin/rules/reference/usage-accounting.md +261 -0
- package/plugins/lisa-devin/rules/reference/verification.md +278 -0
- package/plugins/lisa-devin/rules/reference/wiki-knowledge-source.md +37 -0
- package/plugins/lisa-devin/rules/reference/withdrawn-rulings.md +148 -0
- package/plugins/lisa-devin/rules/reference/work-item-definition-of-ready.md +177 -0
- package/plugins/lisa-devin/rules/reference/work-item-trailer-definition.md +129 -0
- package/plugins/lisa-devin/scripts/automation-run-record.mjs +678 -0
- package/plugins/lisa-devin/scripts/automation-status-claude-adapter.mjs +804 -0
- package/plugins/lisa-devin/scripts/automation-status-codex-adapter.mjs +1081 -0
- package/plugins/lisa-devin/scripts/automation-status-contract-drift.mjs +392 -0
- package/plugins/lisa-devin/scripts/automation-status-expected-fleet.mjs +508 -0
- package/plugins/lisa-devin/scripts/automation-status-report.mjs +200 -0
- package/plugins/lisa-devin/scripts/automation-status-run-history.mjs +216 -0
- package/plugins/lisa-devin/scripts/automation-status-unrecorded-runs.mjs +127 -0
- package/plugins/lisa-devin/scripts/blocker-edge-resolution.mjs +150 -0
- package/plugins/lisa-devin/scripts/cross-pollinate.mjs +763 -0
- package/plugins/lisa-devin/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/lisa-devin/scripts/design-intake-gate.mjs +470 -0
- package/plugins/lisa-devin/scripts/design-source-gate.mjs +610 -0
- package/plugins/lisa-devin/scripts/design-variable-ids.mjs +567 -0
- package/plugins/lisa-devin/scripts/doctor-report.mjs +635 -0
- package/plugins/lisa-devin/scripts/install-remote-agent-aws.mjs +443 -0
- package/plugins/lisa-devin/scripts/intake-blocker-reprobe.mjs +1065 -0
- package/plugins/lisa-devin/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/lisa-devin/scripts/lifecycle-label-trust.mjs +593 -0
- package/plugins/lisa-devin/scripts/linear-state-write-target.mjs +362 -0
- package/plugins/lisa-devin/scripts/plugin-sync-explain.mjs +654 -0
- package/plugins/lisa-devin/scripts/pr-arming-sweep.mjs +561 -0
- package/plugins/lisa-devin/scripts/project-ideation-idempotency-harness.mjs +338 -0
- package/plugins/lisa-devin/scripts/qa-signal-lifecycle.mjs +523 -0
- package/plugins/lisa-devin/scripts/queue-contract-resolution.mjs +583 -0
- package/plugins/lisa-devin/scripts/queue-health-classification.mjs +157 -0
- package/plugins/lisa-devin/scripts/queue-status-build-readers.mjs +561 -0
- package/plugins/lisa-devin/scripts/queue-status-prd-readers.mjs +452 -0
- package/plugins/lisa-devin/scripts/remote-agent-aws-setup.sh +677 -0
- package/plugins/lisa-devin/scripts/repo-scope-vocabulary-audit.mjs +458 -0
- package/plugins/lisa-devin/scripts/resolve-lifecycle-role.mjs +347 -0
- package/plugins/lisa-devin/scripts/rollup-blocker-classification.mjs +616 -0
- package/plugins/lisa-devin/scripts/scoping-label-audit.mjs +474 -0
- package/plugins/lisa-devin/skills/lisa-acceptance-criteria/SKILL.md +73 -0
- package/plugins/lisa-devin/skills/lisa-agent-design-best-practices/SKILL.md +242 -0
- package/plugins/lisa-devin/skills/lisa-agent-ready/SKILL.md +271 -0
- package/plugins/lisa-devin/skills/lisa-analyze-claude-remote/SKILL.md +434 -0
- package/plugins/lisa-devin/skills/lisa-atlassian-access/SKILL.md +469 -0
- package/plugins/lisa-devin/skills/lisa-atlassian-access/scripts/markdown-to-adf.mjs +236 -0
- package/plugins/lisa-devin/skills/lisa-attribute-failure/SKILL.md +142 -0
- package/plugins/lisa-devin/skills/lisa-automation-status/SKILL.md +149 -0
- package/plugins/lisa-devin/skills/lisa-bug-triage/SKILL.md +23 -0
- package/plugins/lisa-devin/skills/lisa-capability-drift/SKILL.md +71 -0
- package/plugins/lisa-devin/skills/lisa-codebase-research/SKILL.md +87 -0
- package/plugins/lisa-devin/skills/lisa-codify-verification/SKILL.md +213 -0
- package/plugins/lisa-devin/skills/lisa-confluence-prd-intake/SKILL.md +417 -0
- package/plugins/lisa-devin/skills/lisa-confluence-to-tracker/SKILL.md +469 -0
- package/plugins/lisa-devin/skills/lisa-confluence-write-prd/SKILL.md +111 -0
- package/plugins/lisa-devin/skills/lisa-cross-pollinate/SKILL.md +175 -0
- package/plugins/lisa-devin/skills/lisa-debrief/SKILL.md +96 -0
- package/plugins/lisa-devin/skills/lisa-debrief-apply/SKILL.md +106 -0
- package/plugins/lisa-devin/skills/lisa-delivery-effectiveness/SKILL.md +62 -0
- package/plugins/lisa-devin/skills/lisa-design-intake/SKILL.md +198 -0
- package/plugins/lisa-devin/skills/lisa-detect-tooling/SKILL.md +110 -0
- package/plugins/lisa-devin/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
- package/plugins/lisa-devin/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +658 -0
- package/plugins/lisa-devin/skills/lisa-doctor/SKILL.md +743 -0
- package/plugins/lisa-devin/skills/lisa-drive-pr-to-merge/SKILL.md +1587 -0
- package/plugins/lisa-devin/skills/lisa-epic-triage/SKILL.md +28 -0
- package/plugins/lisa-devin/skills/lisa-evaluation-suite/SKILL.md +63 -0
- package/plugins/lisa-devin/skills/lisa-exploratory-qa/SKILL.md +185 -0
- package/plugins/lisa-devin/skills/lisa-fix-linter-error/SKILL.md +45 -0
- package/plugins/lisa-devin/skills/lisa-generate-claude-remote-build-script/SKILL.md +235 -0
- package/plugins/lisa-devin/skills/lisa-git-commit/SKILL.md +53 -0
- package/plugins/lisa-devin/skills/lisa-git-prune/SKILL.md +35 -0
- package/plugins/lisa-devin/skills/lisa-git-submit-pr/SKILL.md +170 -0
- package/plugins/lisa-devin/skills/lisa-github-add-journey/SKILL.md +118 -0
- package/plugins/lisa-devin/skills/lisa-github-build-intake/SKILL.md +745 -0
- package/plugins/lisa-devin/skills/lisa-github-claim/SKILL.md +27 -0
- package/plugins/lisa-devin/skills/lisa-github-create/SKILL.md +105 -0
- package/plugins/lisa-devin/skills/lisa-github-evidence/SKILL.md +120 -0
- package/plugins/lisa-devin/skills/lisa-github-journey/SKILL.md +134 -0
- package/plugins/lisa-devin/skills/lisa-github-prd-intake/SKILL.md +424 -0
- package/plugins/lisa-devin/skills/lisa-github-project-v2/SKILL.md +227 -0
- package/plugins/lisa-devin/skills/lisa-github-read-issue/SKILL.md +305 -0
- package/plugins/lisa-devin/skills/lisa-github-sync/SKILL.md +201 -0
- package/plugins/lisa-devin/skills/lisa-github-to-tracker/SKILL.md +456 -0
- package/plugins/lisa-devin/skills/lisa-github-validate-issue/SKILL.md +598 -0
- package/plugins/lisa-devin/skills/lisa-github-verify/SKILL.md +60 -0
- package/plugins/lisa-devin/skills/lisa-github-write-issue/SKILL.md +531 -0
- package/plugins/lisa-devin/skills/lisa-github-write-prd/SKILL.md +156 -0
- package/plugins/lisa-devin/skills/lisa-health/SKILL.md +142 -0
- package/plugins/lisa-devin/skills/lisa-health-drift-cron/SKILL.md +80 -0
- package/plugins/lisa-devin/skills/lisa-implement/SKILL.md +372 -0
- package/plugins/lisa-devin/skills/lisa-improve-code-complexity/SKILL.md +44 -0
- package/plugins/lisa-devin/skills/lisa-improve-harness/SKILL.md +487 -0
- package/plugins/lisa-devin/skills/lisa-improve-max-lines/SKILL.md +45 -0
- package/plugins/lisa-devin/skills/lisa-improve-max-lines-per-function/SKILL.md +46 -0
- package/plugins/lisa-devin/skills/lisa-improve-test-coverage/SKILL.md +47 -0
- package/plugins/lisa-devin/skills/lisa-improve-tests/SKILL.md +50 -0
- package/plugins/lisa-devin/skills/lisa-intake/SKILL.md +208 -0
- package/plugins/lisa-devin/skills/lisa-intake-explain/SKILL.md +279 -0
- package/plugins/lisa-devin/skills/lisa-jam-access/SKILL.md +128 -0
- package/plugins/lisa-devin/skills/lisa-jira-add-journey/SKILL.md +139 -0
- package/plugins/lisa-devin/skills/lisa-jira-build-intake/SKILL.md +556 -0
- package/plugins/lisa-devin/skills/lisa-jira-claim/SKILL.md +27 -0
- package/plugins/lisa-devin/skills/lisa-jira-create/SKILL.md +157 -0
- package/plugins/lisa-devin/skills/lisa-jira-evidence/SKILL.md +106 -0
- package/plugins/lisa-devin/skills/lisa-jira-evidence/scripts/post-evidence.sh +262 -0
- package/plugins/lisa-devin/skills/lisa-jira-journey/SKILL.md +168 -0
- package/plugins/lisa-devin/skills/lisa-jira-journey/scripts/generate-templates.py +239 -0
- package/plugins/lisa-devin/skills/lisa-jira-journey/scripts/parse-plan.py +570 -0
- package/plugins/lisa-devin/skills/lisa-jira-read-ticket/SKILL.md +199 -0
- package/plugins/lisa-devin/skills/lisa-jira-read-ticket/scripts/download-attachment.sh +211 -0
- package/plugins/lisa-devin/skills/lisa-jira-sync/SKILL.md +117 -0
- package/plugins/lisa-devin/skills/lisa-jira-validate-ticket/SKILL.md +525 -0
- package/plugins/lisa-devin/skills/lisa-jira-verify/SKILL.md +61 -0
- package/plugins/lisa-devin/skills/lisa-jira-write-ticket/SKILL.md +440 -0
- package/plugins/lisa-devin/skills/lisa-jsdoc-best-practices/SKILL.md +491 -0
- package/plugins/lisa-devin/skills/lisa-kane-browser/SKILL.md +80 -0
- package/plugins/lisa-devin/skills/lisa-learnings-audit/SKILL.md +490 -0
- package/plugins/lisa-devin/skills/lisa-linear-access/SKILL.md +483 -0
- package/plugins/lisa-devin/skills/lisa-linear-add-journey/SKILL.md +109 -0
- package/plugins/lisa-devin/skills/lisa-linear-build-intake/SKILL.md +548 -0
- package/plugins/lisa-devin/skills/lisa-linear-claim/SKILL.md +28 -0
- package/plugins/lisa-devin/skills/lisa-linear-create/SKILL.md +150 -0
- package/plugins/lisa-devin/skills/lisa-linear-evidence/SKILL.md +124 -0
- package/plugins/lisa-devin/skills/lisa-linear-journey/SKILL.md +147 -0
- package/plugins/lisa-devin/skills/lisa-linear-prd-intake/SKILL.md +411 -0
- package/plugins/lisa-devin/skills/lisa-linear-read-issue/SKILL.md +201 -0
- package/plugins/lisa-devin/skills/lisa-linear-sync/SKILL.md +164 -0
- package/plugins/lisa-devin/skills/lisa-linear-to-tracker/SKILL.md +454 -0
- package/plugins/lisa-devin/skills/lisa-linear-validate-issue/SKILL.md +504 -0
- package/plugins/lisa-devin/skills/lisa-linear-verify/SKILL.md +65 -0
- package/plugins/lisa-devin/skills/lisa-linear-write-issue/SKILL.md +448 -0
- package/plugins/lisa-devin/skills/lisa-linear-write-prd/SKILL.md +98 -0
- package/plugins/lisa-devin/skills/lisa-monitor/SKILL.md +224 -0
- package/plugins/lisa-devin/skills/lisa-nightly-add-test-coverage/SKILL.md +40 -0
- package/plugins/lisa-devin/skills/lisa-nightly-improve-tests/SKILL.md +29 -0
- package/plugins/lisa-devin/skills/lisa-nightly-lower-code-complexity/SKILL.md +42 -0
- package/plugins/lisa-devin/skills/lisa-notion-access/SKILL.md +301 -0
- package/plugins/lisa-devin/skills/lisa-notion-prd-intake/SKILL.md +360 -0
- package/plugins/lisa-devin/skills/lisa-notion-to-tracker/SKILL.md +467 -0
- package/plugins/lisa-devin/skills/lisa-notion-write-prd/SKILL.md +111 -0
- package/plugins/lisa-devin/skills/lisa-parity-code-review/SKILL.md +83 -0
- package/plugins/lisa-devin/skills/lisa-parity-code-simplifier/SKILL.md +76 -0
- package/plugins/lisa-devin/skills/lisa-parity-coderabbit/SKILL.md +87 -0
- package/plugins/lisa-devin/skills/lisa-parity-sentry-sdk-setup/SKILL.md +375 -0
- package/plugins/lisa-devin/skills/lisa-parity-sentry-seer/SKILL.md +168 -0
- package/plugins/lisa-devin/skills/lisa-parity-skill-creator/SKILL.md +155 -0
- package/plugins/lisa-devin/skills/lisa-performance-review/SKILL.md +94 -0
- package/plugins/lisa-devin/skills/lisa-persist-learning/SKILL.md +247 -0
- package/plugins/lisa-devin/skills/lisa-plan/SKILL.md +64 -0
- package/plugins/lisa-devin/skills/lisa-plugin-sync-explain/SKILL.md +53 -0
- package/plugins/lisa-devin/skills/lisa-posthog-access/SKILL.md +140 -0
- package/plugins/lisa-devin/skills/lisa-prd-backlink/SKILL.md +274 -0
- package/plugins/lisa-devin/skills/lisa-prd-source-write/SKILL.md +104 -0
- package/plugins/lisa-devin/skills/lisa-prd-ticket-coverage/SKILL.md +187 -0
- package/plugins/lisa-devin/skills/lisa-product-walkthrough/SKILL.md +106 -0
- package/plugins/lisa-devin/skills/lisa-project-ideation/SKILL.md +417 -0
- package/plugins/lisa-devin/skills/lisa-project-ideation/examples/evidence-card-format.md +21 -0
- package/plugins/lisa-devin/skills/lisa-project-ideation/examples/host-project-only.md +22 -0
- package/plugins/lisa-devin/skills/lisa-project-ideation/examples/idempotency-verification-harness.md +57 -0
- package/plugins/lisa-devin/skills/lisa-project-ideation/examples/public-external-inspiration.md +22 -0
- package/plugins/lisa-devin/skills/lisa-project-ideation/examples/unavailable-data-rejection.md +22 -0
- package/plugins/lisa-devin/skills/lisa-pull-request-review/SKILL.md +132 -0
- package/plugins/lisa-devin/skills/lisa-quality-review/SKILL.md +63 -0
- package/plugins/lisa-devin/skills/lisa-queue-status/SKILL.md +215 -0
- package/plugins/lisa-devin/skills/lisa-remote-dispatch/SKILL.md +109 -0
- package/plugins/lisa-devin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +535 -0
- package/plugins/lisa-devin/skills/lisa-repair-intake/SKILL.md +1621 -0
- package/plugins/lisa-devin/skills/lisa-reproduce-bug/SKILL.md +75 -0
- package/plugins/lisa-devin/skills/lisa-research/SKILL.md +92 -0
- package/plugins/lisa-devin/skills/lisa-review-implementation/SKILL.md +209 -0
- package/plugins/lisa-devin/skills/lisa-review-local/SKILL.md +141 -0
- package/plugins/lisa-devin/skills/lisa-rework-triage/SKILL.md +206 -0
- package/plugins/lisa-devin/skills/lisa-root-cause-analysis/SKILL.md +135 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/SKILL.md +430 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +433 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +288 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +273 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/envfile.mjs +80 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/inspect-vault.mjs +142 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +1085 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/note-format.mjs +179 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/owner.mjs +96 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +342 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/prompt-secret.mjs +79 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/providers.mjs +805 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/publish-aws-bootstrap.mjs +680 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/read-secret-note.mjs +105 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/resolve-expo-token.mjs +90 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +285 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +297 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/surfaces.mjs +480 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +510 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +63 -0
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +550 -0
- package/plugins/lisa-devin/skills/lisa-security-review/SKILL.md +124 -0
- package/plugins/lisa-devin/skills/lisa-security-zap-scan/SKILL.md +53 -0
- package/plugins/lisa-devin/skills/lisa-sentry-access/SKILL.md +140 -0
- package/plugins/lisa-devin/skills/lisa-setup-atlassian/SKILL.md +407 -0
- package/plugins/lisa-devin/skills/lisa-setup-automations/SKILL.md +430 -0
- package/plugins/lisa-devin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +288 -0
- package/plugins/lisa-devin/skills/lisa-setup-confluence/SKILL.md +254 -0
- package/plugins/lisa-devin/skills/lisa-setup-github/SKILL.md +319 -0
- package/plugins/lisa-devin/skills/lisa-setup-github-repo/SKILL.md +128 -0
- package/plugins/lisa-devin/skills/lisa-setup-jira/SKILL.md +198 -0
- package/plugins/lisa-devin/skills/lisa-setup-kane/SKILL.md +96 -0
- package/plugins/lisa-devin/skills/lisa-setup-linear/SKILL.md +375 -0
- package/plugins/lisa-devin/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa-devin/skills/lisa-setup-local-env/scripts/local-env.mjs +196 -0
- package/plugins/lisa-devin/skills/lisa-setup-notion/SKILL.md +376 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-aws/SKILL.md +149 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/SKILL.md +382 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/assets/materialized-env-authority.mjs +262 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/assets/session-start.sh +87 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/assets/setup.sh +216 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/environment.mjs +227 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +378 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +1632 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +448 -0
- package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +250 -0
- package/plugins/lisa-devin/skills/lisa-setup-sonar/SKILL.md +153 -0
- package/plugins/lisa-devin/skills/lisa-setup-workstation/SKILL.md +143 -0
- package/plugins/lisa-devin/skills/lisa-setup-workstation/scripts/bounded-child.mjs +168 -0
- package/plugins/lisa-devin/skills/lisa-setup-workstation/scripts/catalogue.mjs +293 -0
- package/plugins/lisa-devin/skills/lisa-setup-workstation/scripts/cli.mjs +333 -0
- package/plugins/lisa-devin/skills/lisa-setup-workstation/scripts/workstation.mjs +511 -0
- package/plugins/lisa-devin/skills/lisa-sonarcloud-access/SKILL.md +202 -0
- package/plugins/lisa-devin/skills/lisa-spec-conformance/SKILL.md +205 -0
- package/plugins/lisa-devin/skills/lisa-starter-sync/SKILL.md +35 -0
- package/plugins/lisa-devin/skills/lisa-sync-down/SKILL.md +209 -0
- package/plugins/lisa-devin/skills/lisa-task-decomposition/SKILL.md +195 -0
- package/plugins/lisa-devin/skills/lisa-task-triage/SKILL.md +23 -0
- package/plugins/lisa-devin/skills/lisa-tdd-implementation/SKILL.md +91 -0
- package/plugins/lisa-devin/skills/lisa-tear-down-automations/SKILL.md +85 -0
- package/plugins/lisa-devin/skills/lisa-test-strategy/SKILL.md +66 -0
- package/plugins/lisa-devin/skills/lisa-ticket-triage/SKILL.md +214 -0
- package/plugins/lisa-devin/skills/lisa-track/SKILL.md +129 -0
- package/plugins/lisa-devin/skills/lisa-tracker-add-journey/SKILL.md +28 -0
- package/plugins/lisa-devin/skills/lisa-tracker-build-intake/SKILL.md +134 -0
- package/plugins/lisa-devin/skills/lisa-tracker-claim/SKILL.md +35 -0
- package/plugins/lisa-devin/skills/lisa-tracker-create/SKILL.md +28 -0
- package/plugins/lisa-devin/skills/lisa-tracker-evidence/SKILL.md +63 -0
- package/plugins/lisa-devin/skills/lisa-tracker-journey/SKILL.md +27 -0
- package/plugins/lisa-devin/skills/lisa-tracker-read/SKILL.md +28 -0
- package/plugins/lisa-devin/skills/lisa-tracker-source-artifacts/SKILL.md +111 -0
- package/plugins/lisa-devin/skills/lisa-tracker-sync/SKILL.md +150 -0
- package/plugins/lisa-devin/skills/lisa-tracker-validate/SKILL.md +37 -0
- package/plugins/lisa-devin/skills/lisa-tracker-verify/SKILL.md +29 -0
- package/plugins/lisa-devin/skills/lisa-tracker-write/SKILL.md +55 -0
- package/plugins/lisa-devin/skills/lisa-usage-accounting/SKILL.md +205 -0
- package/plugins/lisa-devin/skills/lisa-usage-accounting/references/effectiveness.md +63 -0
- package/plugins/lisa-devin/skills/lisa-use-the-product/SKILL.md +86 -0
- package/plugins/lisa-devin/skills/lisa-validate-tracker-mapping/SKILL.md +467 -0
- package/plugins/lisa-devin/skills/lisa-verification-lifecycle/SKILL.md +384 -0
- package/plugins/lisa-devin/skills/lisa-verify/SKILL.md +55 -0
- package/plugins/lisa-devin/skills/lisa-verify-prd/SKILL.md +394 -0
- package/plugins/lisa-devin/skills/lisa-verify-workflow-change/SKILL.md +154 -0
- package/plugins/lisa-devin/skills/lisa-wiki-install/SKILL.md +101 -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-expo-devin/.claude-plugin/plugin.json +11 -0
- package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-expo-devin/.mcp.json +8 -0
- package/plugins/lisa-expo-devin/THIRD-PARTY-NOTICES.md +84 -0
- package/plugins/lisa-expo-devin/agents/ops-specialist.md +124 -0
- package/plugins/lisa-expo-devin/hooks/devin-tool-name-shim.sh +30 -0
- package/plugins/lisa-expo-devin/hooks/register-maestro-mcp.sh +84 -0
- package/plugins/lisa-expo-devin/hooks.json +14 -0
- package/plugins/lisa-expo-devin/skills/add-app-clip/SKILL.md +280 -0
- package/plugins/lisa-expo-devin/skills/add-app-clip/references/native-module.md +96 -0
- package/plugins/lisa-expo-devin/skills/apollo-client/SKILL.md +238 -0
- package/plugins/lisa-expo-devin/skills/apollo-client/references/mutation-patterns.md +360 -0
- package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/SKILL.md +369 -0
- package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/references/atomic-levels.md +417 -0
- package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/references/folder-structure.md +257 -0
- package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/references/gluestack-mapping.md +233 -0
- package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
- package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +438 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/SKILL.md +321 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/animations.md +220 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/controls.md +272 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/form-sheet.md +253 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/gradients.md +106 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/icons.md +213 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/media.md +198 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/route-structure.md +229 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/search.md +248 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/storage.md +121 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/tabs.md +433 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/toolbar-and-headers.md +284 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/visual-effects.md +197 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/webgpu-three.md +605 -0
- package/plugins/lisa-expo-devin/skills/building-native-ui/references/zoom-transitions.md +158 -0
- package/plugins/lisa-expo-devin/skills/container-view-pattern/SKILL.md +355 -0
- package/plugins/lisa-expo-devin/skills/container-view-pattern/references/examples.md +749 -0
- package/plugins/lisa-expo-devin/skills/container-view-pattern/references/patterns.md +318 -0
- package/plugins/lisa-expo-devin/skills/container-view-pattern/scripts/create_component.py +200 -0
- package/plugins/lisa-expo-devin/skills/container-view-pattern/scripts/validate_component.py +332 -0
- package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/SKILL.md +268 -0
- package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/references/common-issues.md +619 -0
- package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/references/file-extensions.md +340 -0
- package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/references/platform-api.md +276 -0
- package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/scripts/validate_cross_platform.py +416 -0
- package/plugins/lisa-expo-devin/skills/directory-structure/SKILL.md +214 -0
- package/plugins/lisa-expo-devin/skills/directory-structure/scripts/validate_structure.py +454 -0
- package/plugins/lisa-expo-devin/skills/e2e-coverage-gaps/SKILL.md +117 -0
- package/plugins/lisa-expo-devin/skills/eas-update-insights/SKILL.md +228 -0
- package/plugins/lisa-expo-devin/skills/eas-update-insights/references/channel-insights-schema.md +47 -0
- package/plugins/lisa-expo-devin/skills/eas-update-insights/references/update-insights-schema.md +69 -0
- package/plugins/lisa-expo-devin/skills/expo-api-routes/SKILL.md +369 -0
- package/plugins/lisa-expo-devin/skills/expo-brownfield/SKILL.md +54 -0
- package/plugins/lisa-expo-devin/skills/expo-brownfield/references/brownfield-integrated.md +526 -0
- package/plugins/lisa-expo-devin/skills/expo-brownfield/references/brownfield-isolated.md +402 -0
- package/plugins/lisa-expo-devin/skills/expo-brownfield/references/comparison.md +63 -0
- package/plugins/lisa-expo-devin/skills/expo-brownfield/references/troubleshooting.md +88 -0
- package/plugins/lisa-expo-devin/skills/expo-cicd-workflows/SKILL.md +92 -0
- package/plugins/lisa-expo-devin/skills/expo-cicd-workflows/scripts/fetch.js +113 -0
- package/plugins/lisa-expo-devin/skills/expo-cicd-workflows/scripts/package.json +11 -0
- package/plugins/lisa-expo-devin/skills/expo-cicd-workflows/scripts/validate.js +85 -0
- package/plugins/lisa-expo-devin/skills/expo-deployment/SKILL.md +193 -0
- package/plugins/lisa-expo-devin/skills/expo-deployment/references/app-store-metadata.md +479 -0
- package/plugins/lisa-expo-devin/skills/expo-deployment/references/ios-app-store.md +355 -0
- package/plugins/lisa-expo-devin/skills/expo-deployment/references/play-store.md +264 -0
- package/plugins/lisa-expo-devin/skills/expo-deployment/references/testflight.md +58 -0
- package/plugins/lisa-expo-devin/skills/expo-deployment/references/workflows.md +200 -0
- package/plugins/lisa-expo-devin/skills/expo-dev-client/SKILL.md +164 -0
- package/plugins/lisa-expo-devin/skills/expo-env-config/SKILL.md +309 -0
- package/plugins/lisa-expo-devin/skills/expo-env-config/references/validation-patterns.md +417 -0
- package/plugins/lisa-expo-devin/skills/expo-module/SKILL.md +141 -0
- package/plugins/lisa-expo-devin/skills/expo-module/references/config-plugin.md +90 -0
- package/plugins/lisa-expo-devin/skills/expo-module/references/create-expo-module.md +206 -0
- package/plugins/lisa-expo-devin/skills/expo-module/references/lifecycle.md +127 -0
- package/plugins/lisa-expo-devin/skills/expo-module/references/module-config.md +48 -0
- package/plugins/lisa-expo-devin/skills/expo-module/references/native-module.md +286 -0
- package/plugins/lisa-expo-devin/skills/expo-module/references/native-view.md +171 -0
- package/plugins/lisa-expo-devin/skills/expo-router-best-practices/SKILL.md +432 -0
- package/plugins/lisa-expo-devin/skills/expo-router-best-practices/references/official-docs.md +290 -0
- package/plugins/lisa-expo-devin/skills/expo-router-best-practices/references/static-export-verification.md +54 -0
- package/plugins/lisa-expo-devin/skills/expo-router-best-practices/scripts/generate-route.py +171 -0
- package/plugins/lisa-expo-devin/skills/expo-tailwind-setup/SKILL.md +487 -0
- package/plugins/lisa-expo-devin/skills/expo-ui-jetpack-compose/SKILL.md +40 -0
- package/plugins/lisa-expo-devin/skills/expo-ui-swift-ui/SKILL.md +39 -0
- package/plugins/lisa-expo-devin/skills/gluestack-nativewind/SKILL.md +419 -0
- package/plugins/lisa-expo-devin/skills/gluestack-nativewind/references/color-tokens.md +343 -0
- package/plugins/lisa-expo-devin/skills/gluestack-nativewind/references/component-mapping.md +307 -0
- package/plugins/lisa-expo-devin/skills/gluestack-nativewind/references/spacing-scale.md +300 -0
- package/plugins/lisa-expo-devin/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
- package/plugins/lisa-expo-devin/skills/gluestack-nativewind/scripts/validate_styling.py +424 -0
- package/plugins/lisa-expo-devin/skills/jira-add-journey/SKILL.md +140 -0
- package/plugins/lisa-expo-devin/skills/jira-create/SKILL.md +161 -0
- package/plugins/lisa-expo-devin/skills/jira-evidence/SKILL.md +84 -0
- package/plugins/lisa-expo-devin/skills/jira-evidence/scripts/post-evidence.sh +262 -0
- package/plugins/lisa-expo-devin/skills/jira-journey/SKILL.md +196 -0
- package/plugins/lisa-expo-devin/skills/jira-journey/scripts/generate-templates.py +281 -0
- package/plugins/lisa-expo-devin/skills/jira-journey/scripts/parse-plan.py +400 -0
- package/plugins/lisa-expo-devin/skills/jira-verify/SKILL.md +27 -0
- package/plugins/lisa-expo-devin/skills/local-state/SKILL.md +362 -0
- package/plugins/lisa-expo-devin/skills/local-state/references/async-storage.md +505 -0
- package/plugins/lisa-expo-devin/skills/local-state/references/persistence-patterns.md +711 -0
- package/plugins/lisa-expo-devin/skills/local-state/references/reactive-variables.md +446 -0
- package/plugins/lisa-expo-devin/skills/maestro-mcp-setup/SKILL.md +144 -0
- package/plugins/lisa-expo-devin/skills/native-data-fetching/SKILL.md +507 -0
- package/plugins/lisa-expo-devin/skills/native-data-fetching/references/expo-router-loaders.md +344 -0
- package/plugins/lisa-expo-devin/skills/ops-browser-uat/SKILL.md +124 -0
- package/plugins/lisa-expo-devin/skills/ops-check-logs/SKILL.md +217 -0
- package/plugins/lisa-expo-devin/skills/ops-db-ops/SKILL.md +214 -0
- package/plugins/lisa-expo-devin/skills/ops-deploy/SKILL.md +123 -0
- package/plugins/lisa-expo-devin/skills/ops-monitor-errors/SKILL.md +99 -0
- package/plugins/lisa-expo-devin/skills/ops-performance/SKILL.md +165 -0
- package/plugins/lisa-expo-devin/skills/ops-run-local/SKILL.md +169 -0
- package/plugins/lisa-expo-devin/skills/ops-verify-health/SKILL.md +101 -0
- package/plugins/lisa-expo-devin/skills/owasp-zap/SKILL.md +61 -0
- package/plugins/lisa-expo-devin/skills/play-store-access/SKILL.md +226 -0
- package/plugins/lisa-expo-devin/skills/playwright-ci-debugging/SKILL.md +140 -0
- package/plugins/lisa-expo-devin/skills/playwright-selectors/SKILL.md +496 -0
- package/plugins/lisa-expo-devin/skills/reduce-complexity/SKILL.md +251 -0
- package/plugins/lisa-expo-devin/skills/reduce-complexity/references/extraction-strategies.md +456 -0
- package/plugins/lisa-expo-devin/skills/reduce-complexity/references/refactoring-patterns.md +557 -0
- package/plugins/lisa-expo-devin/skills/testing-library/SKILL.md +314 -0
- package/plugins/lisa-expo-devin/skills/testing-library/references/async-patterns.md +420 -0
- package/plugins/lisa-expo-devin/skills/testing-library/references/expo-router-testing.md +556 -0
- package/plugins/lisa-expo-devin/skills/testing-library/references/mocking-patterns.md +612 -0
- package/plugins/lisa-expo-devin/skills/testing-library/references/query-priority.md +291 -0
- package/plugins/lisa-expo-devin/skills/upgrading-expo/SKILL.md +134 -0
- package/plugins/lisa-expo-devin/skills/upgrading-expo/references/expo-av-to-audio.md +132 -0
- package/plugins/lisa-expo-devin/skills/upgrading-expo/references/expo-av-to-video.md +160 -0
- package/plugins/lisa-expo-devin/skills/upgrading-expo/references/native-tabs.md +124 -0
- package/plugins/lisa-expo-devin/skills/upgrading-expo/references/new-architecture.md +79 -0
- package/plugins/lisa-expo-devin/skills/upgrading-expo/references/react-19.md +79 -0
- package/plugins/lisa-expo-devin/skills/upgrading-expo/references/react-compiler.md +59 -0
- package/plugins/lisa-expo-devin/skills/upgrading-expo/references/react-navigation-to-expo-router.md +61 -0
- package/plugins/lisa-expo-devin/skills/use-dom/SKILL.md +417 -0
- 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-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +11 -0
- package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-harper-fabric-devin/generated-artifact-globs.txt +5 -0
- package/plugins/lisa-harper-fabric-devin/hooks/block-generated-artifact-edits.sh +141 -0
- package/plugins/lisa-harper-fabric-devin/hooks/devin-tool-name-shim.sh +30 -0
- package/plugins/lisa-harper-fabric-devin/hooks/enforce-config-extensions.mjs +196 -0
- package/plugins/lisa-harper-fabric-devin/hooks/enforce-config-extensions.sh +46 -0
- package/plugins/lisa-harper-fabric-devin/hooks/inject-rules.sh +27 -0
- package/plugins/lisa-harper-fabric-devin/hooks.json +36 -0
- package/plugins/lisa-harper-fabric-devin/rules/eager/harper-fabric.md +31 -0
- package/plugins/lisa-harper-fabric-devin/rules/reference/harper-fabric.md +52 -0
- package/plugins/lisa-harper-fabric-devin/skills/e2e-coverage-gaps/SKILL.md +117 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-auth/SKILL.md +368 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-build-and-deploy/SKILL.md +198 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-caching/SKILL.md +265 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-component-model/SKILL.md +87 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-config-yaml/SKILL.md +283 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-operations/SKILL.md +239 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-realtime/SKILL.md +187 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-resources/SKILL.md +142 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-rest-queries/SKILL.md +403 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-schema-graphql/SKILL.md +123 -0
- package/plugins/lisa-harper-fabric-devin/skills/harper-testing/SKILL.md +254 -0
- 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-nestjs-devin/.claude-plugin/plugin.json +11 -0
- package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-nestjs-devin/hooks/block-migration-edits.sh +125 -0
- package/plugins/lisa-nestjs-devin/hooks/devin-tool-name-shim.sh +30 -0
- package/plugins/lisa-nestjs-devin/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-nestjs-devin/hooks.json +15 -0
- package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/SKILL.md +176 -0
- package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/advanced-features.md +527 -0
- package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/project-patterns.md +531 -0
- package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/quick-start.md +257 -0
- package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/resolvers-mutations.md +413 -0
- package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/types-scalars.md +513 -0
- package/plugins/lisa-nestjs-devin/skills/nestjs-rules/SKILL.md +560 -0
- package/plugins/lisa-nestjs-devin/skills/typeorm-patterns/SKILL.md +275 -0
- package/plugins/lisa-nestjs-devin/skills/typeorm-patterns/references/configuration-patterns.md +487 -0
- package/plugins/lisa-nestjs-devin/skills/typeorm-patterns/references/entity-patterns.md +450 -0
- package/plugins/lisa-nestjs-devin/skills/typeorm-patterns/references/observability-patterns.md +536 -0
- 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-openclaw-devin/.claude-plugin/plugin.json +8 -0
- package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-repo-topic/SKILL.md +163 -0
- package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-repo-topic/references/repo-topic-config.md +185 -0
- package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-staff/SKILL.md +175 -0
- package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-staff/references/platform-routing.md +123 -0
- package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-staff/references/prompts.md +78 -0
- package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-setup/SKILL.md +141 -0
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +11 -0
- package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-phaser-devin/agents/accessibility-advocate.md +55 -0
- package/plugins/lisa-phaser-devin/agents/art-director.md +90 -0
- package/plugins/lisa-phaser-devin/agents/audio-director.md +65 -0
- package/plugins/lisa-phaser-devin/agents/combat-designer.md +61 -0
- package/plugins/lisa-phaser-devin/agents/economy-designer.md +59 -0
- package/plugins/lisa-phaser-devin/agents/game-designer.md +62 -0
- package/plugins/lisa-phaser-devin/agents/game-feel-specialist.md +56 -0
- package/plugins/lisa-phaser-devin/agents/level-designer.md +59 -0
- package/plugins/lisa-phaser-devin/agents/localization-manager.md +61 -0
- package/plugins/lisa-phaser-devin/agents/marketing-strategist.md +59 -0
- package/plugins/lisa-phaser-devin/agents/monetization-designer.md +59 -0
- package/plugins/lisa-phaser-devin/agents/narrative-designer.md +56 -0
- package/plugins/lisa-phaser-devin/agents/onboarding-advocate.md +59 -0
- package/plugins/lisa-phaser-devin/agents/player-advocate.md +57 -0
- package/plugins/lisa-phaser-devin/agents/producer.md +61 -0
- package/plugins/lisa-phaser-devin/agents/product-analyst.md +62 -0
- package/plugins/lisa-phaser-devin/agents/publisher.md +59 -0
- package/plugins/lisa-phaser-devin/agents/qa-playtester.md +57 -0
- package/plugins/lisa-phaser-devin/agents/target-player.md +52 -0
- package/plugins/lisa-phaser-devin/agents/ux-ui-designer.md +60 -0
- package/plugins/lisa-phaser-devin/hooks/devin-tool-name-shim.sh +30 -0
- package/plugins/lisa-phaser-devin/hooks/inject-rules.sh +27 -0
- package/plugins/lisa-phaser-devin/hooks.json +14 -0
- package/plugins/lisa-phaser-devin/rules/eager/phaser.md +44 -0
- package/plugins/lisa-phaser-devin/rules/reference/phaser.md +184 -0
- package/plugins/lisa-phaser-devin/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-devin/skills/phaser-asset-pipeline/SKILL.md +222 -0
- package/plugins/lisa-phaser-devin/skills/phaser-asset-sourcing/SKILL.md +151 -0
- package/plugins/lisa-phaser-devin/skills/phaser-build-deploy/SKILL.md +153 -0
- package/plugins/lisa-phaser-devin/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-devin/skills/phaser-project-structure/SKILL.md +89 -0
- package/plugins/lisa-phaser-devin/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-devin/skills/phaser-testing/SKILL.md +192 -0
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +11 -0
- package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-rails-devin/agents/ops-specialist.md +226 -0
- package/plugins/lisa-rails-devin/hooks/devin-tool-name-shim.sh +30 -0
- package/plugins/lisa-rails-devin/hooks/inject-rules.sh +40 -0
- package/plugins/lisa-rails-devin/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-rails-devin/hooks/rubocop-on-edit.sh +101 -0
- package/plugins/lisa-rails-devin/hooks/sg-scan-on-edit.sh +97 -0
- package/plugins/lisa-rails-devin/hooks.json +29 -0
- package/plugins/lisa-rails-devin/rules/eager/rails-conventions.md +23 -0
- package/plugins/lisa-rails-devin/rules/reference/rails-conventions.md +176 -0
- package/plugins/lisa-rails-devin/skills/action-controller-best-practices/SKILL.md +374 -0
- package/plugins/lisa-rails-devin/skills/action-view-best-practices/SKILL.md +335 -0
- package/plugins/lisa-rails-devin/skills/active-record-model-best-practices/SKILL.md +166 -0
- package/plugins/lisa-rails-devin/skills/e2e-coverage-gaps/SKILL.md +117 -0
- package/plugins/lisa-rails-devin/skills/fix-linter-error/SKILL.md +45 -0
- package/plugins/lisa-rails-devin/skills/improve-code-complexity/SKILL.md +51 -0
- package/plugins/lisa-rails-devin/skills/improve-max-lines/SKILL.md +46 -0
- package/plugins/lisa-rails-devin/skills/improve-max-lines-per-function/SKILL.md +46 -0
- package/plugins/lisa-rails-devin/skills/improve-test-coverage/SKILL.md +48 -0
- package/plugins/lisa-rails-devin/skills/jira-add-journey/SKILL.md +79 -0
- package/plugins/lisa-rails-devin/skills/jira-create/SKILL.md +100 -0
- package/plugins/lisa-rails-devin/skills/jira-evidence/SKILL.md +76 -0
- package/plugins/lisa-rails-devin/skills/jira-evidence/scripts/post-evidence.sh +262 -0
- package/plugins/lisa-rails-devin/skills/jira-journey/SKILL.md +65 -0
- package/plugins/lisa-rails-devin/skills/jira-verify/SKILL.md +26 -0
- package/plugins/lisa-rails-devin/skills/ops-check-logs/SKILL.md +191 -0
- package/plugins/lisa-rails-devin/skills/ops-deploy/SKILL.md +153 -0
- package/plugins/lisa-rails-devin/skills/ops-run-local/SKILL.md +169 -0
- package/plugins/lisa-rails-devin/skills/ops-verify-jobs/SKILL.md +157 -0
- package/plugins/lisa-rails-devin/skills/ops-verify-telemetry/SKILL.md +226 -0
- 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-typescript-devin/.claude-plugin/plugin.json +11 -0
- package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-typescript-devin/hooks/block-suppress-directives.sh +141 -0
- package/plugins/lisa-typescript-devin/hooks/devin-tool-name-shim.sh +30 -0
- package/plugins/lisa-typescript-devin/hooks/format-on-edit.sh +102 -0
- package/plugins/lisa-typescript-devin/hooks/lint-on-edit.sh +117 -0
- package/plugins/lisa-typescript-devin/hooks/lisa-edit-gate.sh +152 -0
- package/plugins/lisa-typescript-devin/hooks/sg-scan-on-edit.sh +94 -0
- package/plugins/lisa-typescript-devin/hooks.json +34 -0
- 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/lisa-wiki-devin/.claude-plugin/plugin.json +8 -0
- package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +8 -0
- package/plugins/lisa-wiki-devin/ci/lisa-wiki-validate.yml +33 -0
- package/plugins/lisa-wiki-devin/schema/lisa-wiki-config.schema.json +162 -0
- package/plugins/lisa-wiki-devin/schema/wiki-structure.schema.json +51 -0
- package/plugins/lisa-wiki-devin/scripts/_wiki-lib.mjs +204 -0
- package/plugins/lisa-wiki-devin/scripts/diff-guard.mjs +117 -0
- package/plugins/lisa-wiki-devin/scripts/ensure-gitignore.mjs +131 -0
- package/plugins/lisa-wiki-devin/scripts/ensure-wiki.mjs +268 -0
- package/plugins/lisa-wiki-devin/scripts/ingest-git.mjs +198 -0
- package/plugins/lisa-wiki-devin/scripts/ingest-memory.mjs +135 -0
- package/plugins/lisa-wiki-devin/scripts/ingest-roles.mjs +95 -0
- package/plugins/lisa-wiki-devin/scripts/ingest_slack_channel.py +361 -0
- package/plugins/lisa-wiki-devin/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/lisa-wiki-devin/scripts/lint-wiki.mjs +513 -0
- package/plugins/lisa-wiki-devin/scripts/mcp-doctor.mjs +72 -0
- package/plugins/lisa-wiki-devin/scripts/render-contract.mjs +107 -0
- package/plugins/lisa-wiki-devin/scripts/rewrite-refs.mjs +144 -0
- package/plugins/lisa-wiki-devin/scripts/slack_oauth_user.py +179 -0
- package/plugins/lisa-wiki-devin/scripts/validate-config.mjs +321 -0
- package/plugins/lisa-wiki-devin/scripts/verify-migration.mjs +203 -0
- package/plugins/lisa-wiki-devin/scripts/verify-wiki-safety.mjs +126 -0
- package/plugins/lisa-wiki-devin/scripts/wiki-safety.mjs +378 -0
- package/plugins/lisa-wiki-devin/scripts/wiki-status.mjs +345 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-add-ingest/SKILL.md +34 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-add-role/SKILL.md +35 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-confluence/SKILL.md +25 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-docs/SKILL.md +30 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-git/SKILL.md +25 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-jira/SKILL.md +28 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-memory/SKILL.md +28 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-notion/SKILL.md +25 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-roles/SKILL.md +22 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-slack/SKILL.md +30 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-web/SKILL.md +23 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-doctor/SKILL.md +47 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-ingest/SKILL.md +106 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-lint/SKILL.md +32 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-migrate/SKILL.md +43 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-onboard-me/SKILL.md +33 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-query/SKILL.md +40 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-setup/SKILL.md +75 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-setup-automations/SKILL.md +83 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-status/SKILL.md +47 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-tear-down-automations/SKILL.md +34 -0
- package/plugins/lisa-wiki-devin/skills/lisa-wiki-usage/SKILL.md +50 -0
- package/plugins/lisa-wiki-devin/templates/agents/role-agent.claude.md +16 -0
- package/plugins/lisa-wiki-devin/templates/agents/role-agent.codex.toml +15 -0
- package/plugins/lisa-wiki-devin/templates/index.md +17 -0
- package/plugins/lisa-wiki-devin/templates/llm-wiki-contract.md +72 -0
- package/plugins/lisa-wiki-devin/templates/log.md +8 -0
- package/plugins/lisa-wiki-devin/templates/page-types/architecture.md +18 -0
- package/plugins/lisa-wiki-devin/templates/page-types/concept.md +18 -0
- package/plugins/lisa-wiki-devin/templates/page-types/decision.md +18 -0
- package/plugins/lisa-wiki-devin/templates/page-types/entity.md +19 -0
- package/plugins/lisa-wiki-devin/templates/page-types/open-question.md +18 -0
- package/plugins/lisa-wiki-devin/templates/page-types/playbook.md +18 -0
- package/plugins/lisa-wiki-devin/templates/page-types/project.md +19 -0
- package/plugins/lisa-wiki-devin/templates/page-types/requirement.md +19 -0
- package/plugins/lisa-wiki-devin/templates/page-types/staff.md +26 -0
- package/plugins/lisa-wiki-devin/templates/start-here.md +24 -0
- package/plugins/lisa-wiki-devin/templates/state-readme.md +20 -0
- package/plugins/lisa-wiki-devin/templates/wrapper-gitignore.txt +19 -0
- package/scripts/build-plugins.sh +2 -2
- package/scripts/deployed-guard-advice.mjs +1 -0
- package/scripts/generate-devin-plugin-artifacts.mjs +263 -0
- package/scripts/lib/hook-registration-audit.mjs +3 -1
- package/scripts/lib/per-agent-hook-filter.mjs +174 -5
- package/scripts/lib/plugin-rule-citations.mjs +2 -2
- package/scripts/two-channel-couplings.json +1 -1
|
@@ -0,0 +1,1383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
3
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reconcile the worktree a session was TOLD it is in against the one it is
|
|
7
|
+
* ACTUALLY operating in, and refuse to act while the two disagree.
|
|
8
|
+
*
|
|
9
|
+
* WHAT WENT WRONG. `EnterWorktree` reported "the session is now working in the
|
|
10
|
+
* worktree" for a tree that Bash never moved to and that Edit then refused by
|
|
11
|
+
* name (CodySwannGT/lisa#3864). Three subsystems, three answers, and the one
|
|
12
|
+
* that answered affirmatively was the wrong one. A refusal teaches you
|
|
13
|
+
* something; a success message that is not true is a confirmation — it removes
|
|
14
|
+
* the reason to check and sends the agent forward confidently in the wrong
|
|
15
|
+
* place. Acting on that confirmation, one session ran an install that landed in
|
|
16
|
+
* a different agent's active worktree. The next such command is a `git commit`
|
|
17
|
+
* or a file write, and that tree held eighteen modified tracked files.
|
|
18
|
+
*
|
|
19
|
+
* The displacement is also mutual: whoever calls `EnterWorktree` last wins for
|
|
20
|
+
* every session whose own binding never took, so each agent repairing itself
|
|
21
|
+
* displaces the others. That is why "just re-enter your worktree" is not a
|
|
22
|
+
* remedy — it is the mechanism.
|
|
23
|
+
*
|
|
24
|
+
* WHAT THIS FIXES AND WHAT IT CANNOT. Lisa does not own `EnterWorktree`, so it
|
|
25
|
+
* cannot make that call itself return failure. What it can do is make the
|
|
26
|
+
* false confirmation cost nothing: the FIRST action taken on the strength of it
|
|
27
|
+
* is refused, and the refusal names the worktree the session is really bound
|
|
28
|
+
* to. The gap is stated rather than hidden — the acceptance criterion asking
|
|
29
|
+
* `EnterWorktree` to fail is met one tool call later, by the guard, not by the
|
|
30
|
+
* tool.
|
|
31
|
+
*
|
|
32
|
+
* WHY BLOCKING AND NOT A WARNING. The failure mode is that nothing feels wrong.
|
|
33
|
+
* There is no symptom to notice, so an advisory line scrolls past exactly when
|
|
34
|
+
* it matters. A refusal is the only form of this check the agent cannot read
|
|
35
|
+
* past.
|
|
36
|
+
*
|
|
37
|
+
* WHY AN EXPLICIT ACKNOWLEDGEMENT AND NOT "BLOCK ONCE". A guard that blocks the
|
|
38
|
+
* first attempt and lets the retry through is defeated by a blind retry, which
|
|
39
|
+
* is the single most likely next action. So the block stands until the session
|
|
40
|
+
* states, in the acknowledgement, the absolute path it now intends to work in.
|
|
41
|
+
* An acknowledgement can choose the observed root, return to the initial root,
|
|
42
|
+
* or accept a linked target this session was explicitly offered. A selected
|
|
43
|
+
* target does not prove a move: the next guarded call must actually be there.
|
|
44
|
+
*
|
|
45
|
+
* FAILING OPEN, LOUDLY. Missing input or unavailable I/O — no session id, no
|
|
46
|
+
* cwd, not a git repository, an unreadable state file — retains the measured
|
|
47
|
+
* fail-open behavior. A readable state file with missing, mismatched or damaged
|
|
48
|
+
* ownership is a different fact: it cannot authorize a baseline or be silently
|
|
49
|
+
* overwritten. Legacy ambiguity requires explicit intent; a conflicting reserved
|
|
50
|
+
* slot requires a new session identity.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { execFileSync } from "node:child_process";
|
|
54
|
+
import { createHash } from "node:crypto";
|
|
55
|
+
import {
|
|
56
|
+
mkdirSync,
|
|
57
|
+
readFileSync,
|
|
58
|
+
realpathSync,
|
|
59
|
+
statSync,
|
|
60
|
+
writeFileSync,
|
|
61
|
+
} from "node:fs";
|
|
62
|
+
import { homedir } from "node:os";
|
|
63
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
64
|
+
|
|
65
|
+
/** Tools whose effect on the wrong worktree is not undoable by re-reading. */
|
|
66
|
+
const GUARDED_TOOLS = new Set(["Bash", "Write", "Edit", "MultiEdit"]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The literal an agent echoes to rebind this session deliberately.
|
|
70
|
+
*
|
|
71
|
+
* A shell line rather than a flag on this script, because the only surface the
|
|
72
|
+
* agent has for saying so is a Bash command, and it must be one this guard sees
|
|
73
|
+
* on its own PreToolUse pass — which is exactly where a command string arrives.
|
|
74
|
+
*/
|
|
75
|
+
const ACCEPT_PREFIX = "lisa-worktree-binding: accept";
|
|
76
|
+
const ACK_AND_SEPARATOR = Symbol("acknowledgement AND separator");
|
|
77
|
+
const BINDING_VERSION = 4;
|
|
78
|
+
const CURRENT_BINDING = Symbol("current binding state");
|
|
79
|
+
const UNAVAILABLE_BINDING = Symbol("unavailable binding read");
|
|
80
|
+
const BINDING_WRITE_CONFLICT = Symbol("binding write conflict");
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a path the way git reports one.
|
|
84
|
+
*
|
|
85
|
+
* `git rev-parse --show-toplevel` answers with symlinks resolved, and on macOS
|
|
86
|
+
* every temporary directory is a symlink — `/var/…` is `/private/var/…`. A
|
|
87
|
+
* comparison between git's answer and a path handed in by a tool would then
|
|
88
|
+
* differ by that prefix alone and read as a displacement, which is a false
|
|
89
|
+
* accusation in exactly the state the guard is supposed to be trusted in. A
|
|
90
|
+
* path that does not exist yet cannot be resolved and is returned as given.
|
|
91
|
+
* @param value - Absolute path to normalize
|
|
92
|
+
* @returns The path with symlinks resolved where possible
|
|
93
|
+
*/
|
|
94
|
+
function realpath(value) {
|
|
95
|
+
try {
|
|
96
|
+
return realpathSync(value);
|
|
97
|
+
} catch {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function say(message) {
|
|
103
|
+
process.stderr.write(`worktree-binding-guard: ${message}\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Run git, returning trimmed stdout, or null when it exits non-zero. */
|
|
107
|
+
function git(args, cwd) {
|
|
108
|
+
try {
|
|
109
|
+
return execFileSync("git", args, {
|
|
110
|
+
cwd,
|
|
111
|
+
encoding: "utf8",
|
|
112
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
113
|
+
}).trim();
|
|
114
|
+
} catch {
|
|
115
|
+
// probe-direction: neutral — this helper REPORTS, it does not decide. Both
|
|
116
|
+
// callers below read null and each one says out loud what it could not do
|
|
117
|
+
// before returning, so no gate outcome follows from this null silently
|
|
118
|
+
// (#3848). Keep that true: a caller added here that spends null as an
|
|
119
|
+
// answer has to say so at its own line.
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The worktree root containing `cwd`, or null when `cwd` is not in a repo.
|
|
126
|
+
*
|
|
127
|
+
* `--show-toplevel` exits non-zero for two OPPOSITE reasons and hands back the
|
|
128
|
+
* same nothing for both: `cwd` is outside any repository — a real absence, and
|
|
129
|
+
* a call there is genuinely none of this guard's business — or git could not
|
|
130
|
+
* answer at all, where the binding is unproven rather than inapplicable. Only
|
|
131
|
+
* the second is a withdrawal of enforcement, and it is the one that must not
|
|
132
|
+
* pass for the first. `git --version` succeeds wherever git can run, so its
|
|
133
|
+
* failure is what separates them.
|
|
134
|
+
* @param {string} cwd Directory the tool call runs in.
|
|
135
|
+
* @returns {string|null} Absolute worktree root, or null.
|
|
136
|
+
*/
|
|
137
|
+
function worktreeRoot(cwd) {
|
|
138
|
+
const top = git(["rev-parse", "--show-toplevel"], cwd);
|
|
139
|
+
if (top) return resolve(top);
|
|
140
|
+
if (git(["--version"], cwd) === null) {
|
|
141
|
+
say("git could not be run here, so the worktree binding is NOT enforced");
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The MAIN checkout's root, which is where `.claude/worktrees/` lives.
|
|
148
|
+
*
|
|
149
|
+
* `--git-common-dir` is shared by every linked worktree, so its parent is the
|
|
150
|
+
* main checkout no matter which worktree asks.
|
|
151
|
+
*/
|
|
152
|
+
function mainCheckout(cwd) {
|
|
153
|
+
const common = git(
|
|
154
|
+
["rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
155
|
+
cwd
|
|
156
|
+
);
|
|
157
|
+
if (common) return dirname(resolve(common));
|
|
158
|
+
// Reached only for an `EnterWorktree` naming a worktree, which means a
|
|
159
|
+
// repository was already in hand — so failing here is "could not ask", and
|
|
160
|
+
// the consequence is that the claim goes unrecorded and the NEXT call's
|
|
161
|
+
// displacement check has nothing to compare against. Said out loud rather
|
|
162
|
+
// than returned as a quiet null (#3848).
|
|
163
|
+
say("git could not resolve the main checkout, so this claim is NOT recorded");
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Registered roots of the bound repository, with Git path quoting disabled. */
|
|
168
|
+
function linkedWorktreeRoots(bound) {
|
|
169
|
+
const listing = git(["worktree", "list", "--porcelain", "-z"], bound);
|
|
170
|
+
if (listing === null) {
|
|
171
|
+
say(
|
|
172
|
+
"git could not identify linked worktrees; repository identity is NOT enforced"
|
|
173
|
+
);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return new Set(
|
|
177
|
+
listing
|
|
178
|
+
.split("\0")
|
|
179
|
+
.filter(record => record.startsWith("worktree "))
|
|
180
|
+
.map(record => realpath(record.slice("worktree ".length)))
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Canonical common Git directory: stable identity across linked checkouts. */
|
|
185
|
+
function repositoryIdentity(root) {
|
|
186
|
+
if (typeof root !== "string") return null;
|
|
187
|
+
const common = git(["rev-parse", "--git-common-dir"], root);
|
|
188
|
+
if (common === null) {
|
|
189
|
+
say("git could not compare repository identity; identity is NOT enforced");
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return realpath(resolve(root, common));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A registration is only authoritative while its live repository agrees. */
|
|
196
|
+
function isLinkedWorktree(
|
|
197
|
+
bound,
|
|
198
|
+
candidate,
|
|
199
|
+
roots = linkedWorktreeRoots(bound)
|
|
200
|
+
) {
|
|
201
|
+
if (!roots?.has(candidate) || worktreeRoot(candidate) !== candidate)
|
|
202
|
+
return false;
|
|
203
|
+
const common = repositoryIdentity(bound);
|
|
204
|
+
return common !== null && common === repositoryIdentity(candidate);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* How much of an executed script this guard will read, in bytes.
|
|
209
|
+
*
|
|
210
|
+
* A generated fixture can be megabytes, and a guard that reads all of it on
|
|
211
|
+
* every Bash call is a guard someone switches off.
|
|
212
|
+
*/
|
|
213
|
+
const SCRIPT_READ_LIMIT = 256 * 1024;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Programs that take their operands as DATA and never execute them.
|
|
217
|
+
*
|
|
218
|
+
* The same split `block-direct-issue-create.sh` makes, and for the same reason
|
|
219
|
+
* it had to: a guard that opens every file a command names, then judges the
|
|
220
|
+
* COMMAND by that FILE's contents, refuses ordinary inspection. Reading a
|
|
221
|
+
* script that visits another worktree is not visiting it.
|
|
222
|
+
*
|
|
223
|
+
* The list is an exemption, not an allowlist. Anything unrecognised is
|
|
224
|
+
* FOLLOWED, because an interpreter roster fails open on the runner nobody
|
|
225
|
+
* enumerated.
|
|
226
|
+
*/
|
|
227
|
+
const READ_ONLY_PROGRAMS = new Set([
|
|
228
|
+
"awk",
|
|
229
|
+
"basename",
|
|
230
|
+
"cat",
|
|
231
|
+
"cksum",
|
|
232
|
+
"cmp",
|
|
233
|
+
"comm",
|
|
234
|
+
"cut",
|
|
235
|
+
"diff",
|
|
236
|
+
"dirname",
|
|
237
|
+
"du",
|
|
238
|
+
"file",
|
|
239
|
+
"find",
|
|
240
|
+
"grep",
|
|
241
|
+
"egrep",
|
|
242
|
+
"fgrep",
|
|
243
|
+
"head",
|
|
244
|
+
"less",
|
|
245
|
+
"ls",
|
|
246
|
+
"md5",
|
|
247
|
+
"md5sum",
|
|
248
|
+
"more",
|
|
249
|
+
"nl",
|
|
250
|
+
"od",
|
|
251
|
+
"realpath",
|
|
252
|
+
"rg",
|
|
253
|
+
"sed",
|
|
254
|
+
"sha1sum",
|
|
255
|
+
"sha256sum",
|
|
256
|
+
"sort",
|
|
257
|
+
"stat",
|
|
258
|
+
"strings",
|
|
259
|
+
"tail",
|
|
260
|
+
"tee",
|
|
261
|
+
"tr",
|
|
262
|
+
"uniq",
|
|
263
|
+
"wc",
|
|
264
|
+
"xxd",
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
/** Interpreters whose first non-option operand is the file they run. */
|
|
268
|
+
const EXECUTING_INTERPRETERS = new Set([
|
|
269
|
+
"bash",
|
|
270
|
+
"dash",
|
|
271
|
+
"ksh",
|
|
272
|
+
"node",
|
|
273
|
+
"perl",
|
|
274
|
+
"python",
|
|
275
|
+
"python3",
|
|
276
|
+
"ruby",
|
|
277
|
+
"sh",
|
|
278
|
+
"zsh",
|
|
279
|
+
"bun",
|
|
280
|
+
"deno",
|
|
281
|
+
"tsx",
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
/** Shell builtins that run a file in the CURRENT shell. */
|
|
285
|
+
const SOURCE_BUILTINS = new Set([".", "source"]);
|
|
286
|
+
|
|
287
|
+
/** Where one command ends and the next begins. */
|
|
288
|
+
const COMMAND_SEPARATORS = /\|\||&&|[;|&\n]/u;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Split a command into tokens, honouring simple quoting.
|
|
292
|
+
*
|
|
293
|
+
* Deliberately simple, and the guard fails OPEN when it is not enough — see
|
|
294
|
+
* {@link scriptReachingForeignTree}. A tokeniser that guessed would be worse
|
|
295
|
+
* than one that says it does not know.
|
|
296
|
+
*/
|
|
297
|
+
function tokenise(segment) {
|
|
298
|
+
const tokens = segment.match(/"[^"]*"|'[^']*'|[^\s]+/gu) ?? [];
|
|
299
|
+
return tokens.map(token => token.replace(/^["']|["']$/gu, ""));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The path this command EXECUTES, or null.
|
|
304
|
+
*
|
|
305
|
+
* The command-position question. A path anywhere else is an argument, and an
|
|
306
|
+
* argument is data.
|
|
307
|
+
*/
|
|
308
|
+
function executedPath(segment) {
|
|
309
|
+
const tokens = tokenise(segment);
|
|
310
|
+
if (tokens.length === 0) return null;
|
|
311
|
+
const [head, ...rest] = tokens;
|
|
312
|
+
const program = head.split("/").pop() ?? "";
|
|
313
|
+
if (READ_ONLY_PROGRAMS.has(program)) return null;
|
|
314
|
+
if (SOURCE_BUILTINS.has(program)) return rest[0] ?? null;
|
|
315
|
+
if (!EXECUTING_INTERPRETERS.has(program)) {
|
|
316
|
+
// A command word that is itself a file runs by its shebang.
|
|
317
|
+
return head.includes("/") ? head : null;
|
|
318
|
+
}
|
|
319
|
+
// A known interpreter: the first operand that is not an option.
|
|
320
|
+
const operand = rest.find(token => !token.startsWith("-"));
|
|
321
|
+
return operand ?? null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* A worktree root other than `bound` that this text reaches into, or null.
|
|
326
|
+
*
|
|
327
|
+
* ## Why this does not enumerate `cd`
|
|
328
|
+
*
|
|
329
|
+
* The redirect spellings — `cd`, `git -C`, `--work-tree`, `--git-dir`, a
|
|
330
|
+
* subshell, `env -C`, a variable holding the path — are an open set, and
|
|
331
|
+
* enumerating them is the failure this arm exists to end
|
|
332
|
+
* (CodySwannGT/lisa#3927 states it for the sibling guard). So the question is
|
|
333
|
+
* inverted the same way the filing guard inverted its own: not *which syntax
|
|
334
|
+
* moves*, which is unbounded, but *which tokens name a directory in another
|
|
335
|
+
* worktree*, which is bounded by the file.
|
|
336
|
+
*
|
|
337
|
+
* A path only has to RESOLVE. How it arrived does not matter, which is what
|
|
338
|
+
* makes an unenumerated spelling reach the same verdict as `cd`.
|
|
339
|
+
*
|
|
340
|
+
* ## Why comments are stripped first
|
|
341
|
+
*
|
|
342
|
+
* A script that merely MENTIONS a sibling worktree — a usage line, a comment,
|
|
343
|
+
* a log message — would otherwise be refused for describing the thing rather
|
|
344
|
+
* than doing it. Prose about a subject is the likeliest text to contain the
|
|
345
|
+
* subject's shapes, and the population that writes it is the people
|
|
346
|
+
* documenting the guard. A parity classifier elsewhere in this repository was
|
|
347
|
+
* fooled by exactly that and was caught only by a known-answer control.
|
|
348
|
+
*/
|
|
349
|
+
function foreignTreeIn(text, bound, cwd) {
|
|
350
|
+
let linkedRoots;
|
|
351
|
+
const code = text
|
|
352
|
+
.split("\n")
|
|
353
|
+
.map(line => line.replace(/^\s*#.*$/u, ""))
|
|
354
|
+
.join("\n");
|
|
355
|
+
for (const raw of code.match(/[^\s'";|&()<>]+/gu) ?? []) {
|
|
356
|
+
const token = raw.replace(/^["']|["']$/gu, "");
|
|
357
|
+
if (!token.includes("/")) continue;
|
|
358
|
+
// A null base means an unliteral `cd` left the directory this script runs
|
|
359
|
+
// from unknown. An ABSOLUTE token is unambiguous either way; a relative one
|
|
360
|
+
// is exactly the thing that cannot be placed, so it is skipped rather than
|
|
361
|
+
// resolved against a directory picked for having been handy.
|
|
362
|
+
if (!isAbsolute(token) && cwd === null) continue;
|
|
363
|
+
const candidate = isAbsolute(token) ? token : join(cwd, token);
|
|
364
|
+
let root;
|
|
365
|
+
try {
|
|
366
|
+
if (!statSync(candidate).isDirectory()) continue;
|
|
367
|
+
root = worktreeRoot(realpathSync(candidate));
|
|
368
|
+
} catch {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (root && root !== bound) {
|
|
372
|
+
if (linkedRoots === undefined) linkedRoots = linkedWorktreeRoots(bound);
|
|
373
|
+
if (isLinkedWorktree(bound, root, linkedRoots)) return root;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* A `cd` target this guard cannot turn into a literal directory.
|
|
381
|
+
*
|
|
382
|
+
* Mirrors `parity-safety-net.sh`'s `follow_cd` test exactly, and the mirror is
|
|
383
|
+
* the point: two spellings of one rule are how the guards drift.
|
|
384
|
+
*/
|
|
385
|
+
const UNLITERAL_CD_TARGET = /[$`*?[{]/u;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The literal target of a leading `cd`, `""` for a bare `cd`, or null when the
|
|
389
|
+
* segment does not change directory.
|
|
390
|
+
*
|
|
391
|
+
* A bare `cd` is `cd "$HOME"`. Leaving the previous directory in place for it
|
|
392
|
+
* would resolve later tokens from a directory the shell has already left.
|
|
393
|
+
* @param segment - One statement from the command
|
|
394
|
+
* @returns The target text, or null when this segment is not a `cd`
|
|
395
|
+
*/
|
|
396
|
+
function cdTarget(segment) {
|
|
397
|
+
const tokens = tokenise(segment);
|
|
398
|
+
if (tokens.length === 0 || tokens[0] !== "cd") return null;
|
|
399
|
+
return tokens[1] ?? "";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Apply one `cd` to the directory currently in force.
|
|
404
|
+
*
|
|
405
|
+
* Contract points 1-4 of `cwd-resolution-corpus.json`: a literal target
|
|
406
|
+
* REPLACES the directory rather than joining a list of candidates — the
|
|
407
|
+
* ordering was #3933's entire defect; a relative target composes against the
|
|
408
|
+
* directory in force; a target that is not literal text yields unknown; and
|
|
409
|
+
* unknown is sticky only until the next resolvable `cd`, because this returns
|
|
410
|
+
* a fresh answer for each one.
|
|
411
|
+
* @param current - Directory in force, or null when it is already unknown
|
|
412
|
+
* @param target - Literal `cd` target, `""` for a bare `cd`
|
|
413
|
+
* @returns The new directory, or null when it cannot be known
|
|
414
|
+
*/
|
|
415
|
+
function applyCd(current, target) {
|
|
416
|
+
const home = homedir();
|
|
417
|
+
let value = target === "" ? home : target;
|
|
418
|
+
if (value === "~") value = home;
|
|
419
|
+
else if (value.startsWith("~/")) value = join(home, value.slice(2));
|
|
420
|
+
if (
|
|
421
|
+
value === "" ||
|
|
422
|
+
value.startsWith("-") ||
|
|
423
|
+
UNLITERAL_CD_TARGET.test(value)
|
|
424
|
+
) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
if (!isAbsolute(value) && current === null) return null;
|
|
428
|
+
const next = isAbsolute(value) ? resolve(value) : resolve(current, value);
|
|
429
|
+
try {
|
|
430
|
+
return statSync(next).isDirectory() ? next : null;
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The foreign worktree an executed script reaches, or null.
|
|
438
|
+
*
|
|
439
|
+
* ## Why this arm exists at all
|
|
440
|
+
*
|
|
441
|
+
* The check above compares `payload.cwd` against the bound root, and that is
|
|
442
|
+
* the right comparison — but **`payload.cwd` is sampled before the child
|
|
443
|
+
* process runs**. A script that changes directory internally moves AFTER the
|
|
444
|
+
* guard has measured. The guard is not wrong about the target; it is right
|
|
445
|
+
* about a target that stops being the target one instruction later.
|
|
446
|
+
*
|
|
447
|
+
* A measurement taken before execution cannot bind what execution does
|
|
448
|
+
* (CodySwannGT/lisa#3924). So the only thing left is to read what is about to
|
|
449
|
+
* run.
|
|
450
|
+
*
|
|
451
|
+
* ## What this arm does NOT cover, stated rather than implied
|
|
452
|
+
*
|
|
453
|
+
* An INLINE redirect — `cd B && git status`, `git -C B status` — is not
|
|
454
|
+
* refused here. Those are already refused by the runtime's own worktree
|
|
455
|
+
* isolation, and duplicating a control is how two controls drift into
|
|
456
|
+
* disagreeing. The scripted form is the one neither covers, and it is the only
|
|
457
|
+
* cell this arm fills.
|
|
458
|
+
*
|
|
459
|
+
* That reason is a dependency on a runtime Lisa does not ship, and it can
|
|
460
|
+
* expire without anything here failing. It is pinned at
|
|
461
|
+
* {@link VERIFIED_RUNTIME_VERSION} and reported when the runtime moves off it
|
|
462
|
+
* — read that constant before widening this arm.
|
|
463
|
+
*
|
|
464
|
+
* ## A relative script path is resolved against the cwd the command will have
|
|
465
|
+
*
|
|
466
|
+
* A leading `cd` establishes the directory this guard resolves relative tokens
|
|
467
|
+
* from, because that is the directory the shell will be in once the command
|
|
468
|
+
* runs. Resolving against `payload.cwd` instead was CodySwannGT/lisa#3933's
|
|
469
|
+
* defect, inherited here knowingly and fixed in CodySwannGT/lisa#3952: on a
|
|
470
|
+
* machine running many worktrees of one repository, every tree holds its own
|
|
471
|
+
* copy of every script at the same relative path, and they differ by whatever
|
|
472
|
+
* each lane is doing.
|
|
473
|
+
*
|
|
474
|
+
* ## One contract, two implementations, and why that is the honest answer
|
|
475
|
+
*
|
|
476
|
+
* `parity-safety-net.sh` answers this same question — "which file will this
|
|
477
|
+
* command actually execute?" — in POSIX shell, where `follow_cd` mutates
|
|
478
|
+
* file-scope state that `follow_locate` reads. This guard is Node. **There is
|
|
479
|
+
* no call that reaches from here into a shell function's file-scope state**, so
|
|
480
|
+
* the duplication is not a factoring somebody skipped; it is one idea that
|
|
481
|
+
* cannot cross a language boundary as a function call.
|
|
482
|
+
*
|
|
483
|
+
* What binds the two is a written contract and a corpus of rows that BOTH
|
|
484
|
+
* guards are driven over in CI, so a divergence fails a test on the commit that
|
|
485
|
+
* causes it rather than surfacing later as a wrong verdict. Contract and corpus
|
|
486
|
+
* live in `tests/unit/hooks/support/cwd-resolution-corpus.json`.
|
|
487
|
+
*
|
|
488
|
+
* This is explicitly NOT unification, and CodySwannGT/lisa#3952 records it as
|
|
489
|
+
* such: two implementations remain. **A behaviour with no row in the corpus is
|
|
490
|
+
* free to diverge — add a row when you add a branch.**
|
|
491
|
+
*
|
|
492
|
+
* ## The two guards agree on the RESOLUTION, not on the verdict
|
|
493
|
+
*
|
|
494
|
+
* When the effective directory cannot be known, both guards decline to resolve
|
|
495
|
+
* the token. What they then do differs, and that difference is deliberate:
|
|
496
|
+
* `parity-safety-net.sh` refuses, because a destructive command it cannot see
|
|
497
|
+
* is the thing it exists to stop. This guard fails open, which is the doctrine
|
|
498
|
+
* every other undecidable case in this file follows. The corpus asserts the
|
|
499
|
+
* resolved path, not the exit code, precisely so that one contract can bind two
|
|
500
|
+
* guards whose fail directions are correctly different.
|
|
501
|
+
*
|
|
502
|
+
* Fails OPEN and says so, like every other undecidable case in this file.
|
|
503
|
+
*/
|
|
504
|
+
function scriptReachingForeignTree(payload, bound) {
|
|
505
|
+
const command = payload.tool_input?.command;
|
|
506
|
+
if (typeof command !== "string" || !command) return null;
|
|
507
|
+
let cwd = payload.cwd;
|
|
508
|
+
for (const segment of command.split(COMMAND_SEPARATORS)) {
|
|
509
|
+
const target = cdTarget(segment);
|
|
510
|
+
if (target !== null) {
|
|
511
|
+
cwd = applyCd(cwd, target);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const named = executedPath(segment);
|
|
515
|
+
if (!named) continue;
|
|
516
|
+
if (!isAbsolute(named) && cwd === null) {
|
|
517
|
+
// Contract point 3: the effective directory is unknown, so WHICH copy of
|
|
518
|
+
// a same-named script would run is unknown too. Declining to resolve is
|
|
519
|
+
// the half shared with parity-safety-net.sh; failing open rather than
|
|
520
|
+
// refusing is this guard's own doctrine, and the corpus binds the two
|
|
521
|
+
// guards on the resolution precisely so it does not have to bind them
|
|
522
|
+
// on a verdict one of them would be wrong to reach.
|
|
523
|
+
say(
|
|
524
|
+
`cannot tell which directory ${named} would run from; NOT enforcing on it`
|
|
525
|
+
);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const path = isAbsolute(named) ? resolve(named) : resolve(cwd, named);
|
|
529
|
+
let text;
|
|
530
|
+
try {
|
|
531
|
+
if (statSync(path).size > SCRIPT_READ_LIMIT) {
|
|
532
|
+
say(`${path} is too large to inspect; NOT enforcing on it`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
text = readFileSync(path, "utf8");
|
|
536
|
+
} catch {
|
|
537
|
+
// Not a readable file: nothing was executed that this can read.
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const foreign = foreignTreeIn(text, bound, cwd);
|
|
541
|
+
if (foreign) return { script: path, foreign };
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function reachesOut(script, foreign, bound) {
|
|
547
|
+
return refuse([
|
|
548
|
+
"worktree-binding-guard: this command runs a script that reaches into",
|
|
549
|
+
"another worktree.",
|
|
550
|
+
` script: ${script}`,
|
|
551
|
+
` reaches: ${foreign}`,
|
|
552
|
+
` bound to: ${bound}`,
|
|
553
|
+
"The access lives inside the file, after this guard measures the session.",
|
|
554
|
+
"Run the work from the tree it belongs to, or acknowledge that target:",
|
|
555
|
+
acceptanceLine(foreign),
|
|
556
|
+
"Then move to the acknowledged tree before further guarded calls.",
|
|
557
|
+
]);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function stateFile(bindingKey) {
|
|
561
|
+
const home = process.env.LISA_STATE_HOME || join(homedir(), ".lisa");
|
|
562
|
+
return join(home, "worktree-binding", `${bindingKey}.json`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Percent-escape every byte that is not filename-safe, so the output cannot
|
|
567
|
+
* contain `-` at all and two escaped parts can be joined on `--` without the
|
|
568
|
+
* joiner ever being smuggled in from either side.
|
|
569
|
+
*
|
|
570
|
+
* `:` and friends are folded too — the key lands in a filename and `:` is
|
|
571
|
+
* illegal on Windows/NTFS (CodySwannGT/lisa#4277). `encodeURIComponent`
|
|
572
|
+
* leaves `! ' ( ) *` alone, and `*` is equally illegal on Windows, so they
|
|
573
|
+
* are escaped by hand.
|
|
574
|
+
* @param {string} value - One component of the binding key
|
|
575
|
+
* @returns {string} A component containing only `[A-Za-z0-9._~]` and `%XX`
|
|
576
|
+
*/
|
|
577
|
+
function keyPart(value) {
|
|
578
|
+
return encodeURIComponent(value)
|
|
579
|
+
.replaceAll(
|
|
580
|
+
/[!'()*]/g,
|
|
581
|
+
c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
582
|
+
)
|
|
583
|
+
.replaceAll("-", "%2D");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The identity a binding belongs to.
|
|
588
|
+
*
|
|
589
|
+
* Parallel subagents share the session's `session_id`, so keying the binding
|
|
590
|
+
* on it alone made each subagent's guarded call read the binding a sibling
|
|
591
|
+
* recorded — the sibling's worktree then read as "changed underneath it" and
|
|
592
|
+
* its writes were refused, or an acknowledgement rebound the file out from
|
|
593
|
+
* under the sibling (CodySwannGT/lisa#4277). The payload's `agent_id` is what
|
|
594
|
+
* distinguishes a subagent from the main agent, so a caller carrying one gets
|
|
595
|
+
* its own state file. The main agent carries no `agent_id` and uses a null
|
|
596
|
+
* agent component, also the fallback for runtimes that never send the field.
|
|
597
|
+
*
|
|
598
|
+
* v4 hashes the JSON tuple into a lowercase, bounded filename in its own
|
|
599
|
+
* directory. The digest fixes filename size and casing; the persisted exact
|
|
600
|
+
* tuple remains the authority. A conflicting owner in a reserved slot is
|
|
601
|
+
* refused, never overwritten. Earlier flat filenames were ambiguous across
|
|
602
|
+
* formats (CodySwannGT/lisa#4311).
|
|
603
|
+
* @param {object} payload - Native hook identity
|
|
604
|
+
* @returns {[string, string|null]} Session and normalized optional agent
|
|
605
|
+
*/
|
|
606
|
+
function bindingIdentity(payload) {
|
|
607
|
+
const agent = payload?.agent_id;
|
|
608
|
+
return [
|
|
609
|
+
payload.session_id,
|
|
610
|
+
typeof agent === "string" && agent ? agent : null,
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Resolve the reserved slot without exposing filename-unsafe identity bytes.
|
|
616
|
+
* @param {object} payload - Native hook identity
|
|
617
|
+
* @returns {string} Versioned, bounded relative state-file stem
|
|
618
|
+
*/
|
|
619
|
+
function bindingKey(payload) {
|
|
620
|
+
const digest = createHash("sha256")
|
|
621
|
+
.update(JSON.stringify(bindingIdentity(payload)))
|
|
622
|
+
.digest("hex");
|
|
623
|
+
return join(`v${BINDING_VERSION}`, digest);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* The keys earlier versions wrote for this payload, newest first.
|
|
628
|
+
*
|
|
629
|
+
* The v1 key was the raw `session_id` (single shared file per session). v2
|
|
630
|
+
* joined sanitized session and agent on `--`; v3 escaped both components.
|
|
631
|
+
* All are inspected on upgrade, but only a matching v4 owner record grants
|
|
632
|
+
* authority. An unstamped file cannot distinguish its possible owners, so the
|
|
633
|
+
* guard requires explicit intent rather than guessing or silently re-baselining.
|
|
634
|
+
* Legacy files stay untouched and only safe filename stems are inspected.
|
|
635
|
+
* @param {object} payload - The hook payload
|
|
636
|
+
* @returns {string[]} Zero or more legacy keys this payload may have written
|
|
637
|
+
*/
|
|
638
|
+
function legacyKeys(payload) {
|
|
639
|
+
const agent = payload?.agent_id;
|
|
640
|
+
const session = payload.session_id;
|
|
641
|
+
const perAgent = typeof agent === "string" && agent;
|
|
642
|
+
const san = value => value.replaceAll(/[^A-Za-z0-9._-]/g, "_");
|
|
643
|
+
const older = perAgent ? `${san(session)}--${san(agent)}` : session;
|
|
644
|
+
let escaped = null;
|
|
645
|
+
try {
|
|
646
|
+
escaped = perAgent
|
|
647
|
+
? `${keyPart(session)}--${keyPart(agent)}`
|
|
648
|
+
: keyPart(session);
|
|
649
|
+
} catch {
|
|
650
|
+
// A lone surrogate could not have produced an escaped v3 filename.
|
|
651
|
+
}
|
|
652
|
+
return [...new Set([escaped, older])].filter(
|
|
653
|
+
key => typeof key === "string" && key && !/[\\/]/u.test(key)
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Require the producer's version, exact tuple and an absolute binding root.
|
|
659
|
+
* A matching identity with a damaged root cannot authorize re-baselining.
|
|
660
|
+
* @param {object|null} state - Parsed local state
|
|
661
|
+
* @param {object} payload - Caller claiming the slot
|
|
662
|
+
* @returns {boolean} Whether the record identifies this caller
|
|
663
|
+
*/
|
|
664
|
+
function ownsState(state, payload) {
|
|
665
|
+
const expected = bindingIdentity(payload);
|
|
666
|
+
return (
|
|
667
|
+
typeof state?.boundRoot === "string" &&
|
|
668
|
+
isAbsolute(state.boundRoot) &&
|
|
669
|
+
!state.boundRoot.includes("\0") &&
|
|
670
|
+
state?.bindingVersion === BINDING_VERSION &&
|
|
671
|
+
Array.isArray(state.bindingIdentity) &&
|
|
672
|
+
state.bindingIdentity.length === expected.length &&
|
|
673
|
+
state.bindingIdentity.every((part, index) => part === expected[index])
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function readState(bindingKeyValue, payload) {
|
|
678
|
+
try {
|
|
679
|
+
const state = JSON.parse(readFileSync(stateFile(bindingKeyValue), "utf8"));
|
|
680
|
+
return ownsState(state, payload)
|
|
681
|
+
? { ...state, [CURRENT_BINDING]: true }
|
|
682
|
+
: { unverifiedOwner: true, canonicalConflict: true };
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (error instanceof SyntaxError)
|
|
685
|
+
return { unverifiedOwner: true, canonicalConflict: true };
|
|
686
|
+
// Unavailable I/O proves neither absence nor conflicting ownership. Keep
|
|
687
|
+
// it separate so an exclusive write cannot mistake EACCES for a race.
|
|
688
|
+
if (error.code !== "ENOENT")
|
|
689
|
+
return { [UNAVAILABLE_BINDING]: error.message };
|
|
690
|
+
}
|
|
691
|
+
let ambiguous = false;
|
|
692
|
+
for (const key of legacyKeys(payload)) {
|
|
693
|
+
try {
|
|
694
|
+
const state = JSON.parse(readFileSync(stateFile(key), "utf8"));
|
|
695
|
+
if (ownsState(state, payload)) return state;
|
|
696
|
+
ambiguous = true;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (error instanceof SyntaxError) ambiguous = true;
|
|
699
|
+
// Not written under this key — try the next older one.
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return ambiguous ? { unverifiedOwner: true } : null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Write the session's binding state, MERGING over whatever is already there.
|
|
707
|
+
*
|
|
708
|
+
* Explicit `claimedRoot: null` still clears a claim. Merging means that a
|
|
709
|
+
* field one writer owns is not silently dropped by
|
|
710
|
+
* another writer that had no opinion about it: `runtimeNoticed` is written by
|
|
711
|
+
* {@link noticeRuntimeDrift} and read by nobody else, and a clobbering write
|
|
712
|
+
* would turn its "reported once" into "reported again after the next
|
|
713
|
+
* acknowledgement", which is the noise the notice is designed not to be.
|
|
714
|
+
* @param bindingKeyValue - The key whose state file this is
|
|
715
|
+
* @param state - Fields to write over the recorded state
|
|
716
|
+
* @param payload - The hook payload, for legacy-key reads while a binding is
|
|
717
|
+
* migrating to the current key format
|
|
718
|
+
* @param options - Validated acknowledgement or create-only baseline intent
|
|
719
|
+
* @returns True on success, false for unavailable I/O, or a conflict sentinel
|
|
720
|
+
*/
|
|
721
|
+
function writeState(bindingKeyValue, state, payload, options = {}) {
|
|
722
|
+
const file = stateFile(bindingKeyValue);
|
|
723
|
+
const previous = readState(bindingKeyValue, payload);
|
|
724
|
+
if (previous?.[UNAVAILABLE_BINDING]) {
|
|
725
|
+
say(
|
|
726
|
+
`could not read the binding (${previous[UNAVAILABLE_BINDING]}); NOT enforcing`
|
|
727
|
+
);
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
if (
|
|
731
|
+
(options.createOnly && previous) ||
|
|
732
|
+
previous?.canonicalConflict ||
|
|
733
|
+
(previous?.unverifiedOwner && !options.acceptUnowned)
|
|
734
|
+
)
|
|
735
|
+
return BINDING_WRITE_CONFLICT;
|
|
736
|
+
const proven = previous?.unverifiedOwner ? null : previous;
|
|
737
|
+
// A runtime notice alone cannot establish a binding after a failed baseline.
|
|
738
|
+
// Every record we produce must satisfy the same authority check as a read.
|
|
739
|
+
const next = {
|
|
740
|
+
...proven,
|
|
741
|
+
...state,
|
|
742
|
+
bindingVersion: BINDING_VERSION,
|
|
743
|
+
bindingIdentity: bindingIdentity(payload),
|
|
744
|
+
};
|
|
745
|
+
if (!ownsState(next, payload)) return false;
|
|
746
|
+
const originalRoot =
|
|
747
|
+
proven?.originalRoot ?? proven?.boundRoot ?? state.boundRoot;
|
|
748
|
+
const originalCommonDir =
|
|
749
|
+
proven?.originalCommonDir ?? repositoryIdentity(originalRoot);
|
|
750
|
+
try {
|
|
751
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
752
|
+
writeFileSync(
|
|
753
|
+
file,
|
|
754
|
+
`${JSON.stringify(
|
|
755
|
+
{
|
|
756
|
+
...next,
|
|
757
|
+
// Preserve the initial root after intentional rebinds. A legacy
|
|
758
|
+
// state's established root is its only proven recovery anchor.
|
|
759
|
+
originalRoot,
|
|
760
|
+
originalCommonDir,
|
|
761
|
+
},
|
|
762
|
+
null,
|
|
763
|
+
2
|
|
764
|
+
)}\n`,
|
|
765
|
+
{ flag: previous?.[CURRENT_BINDING] ? "w" : "wx" }
|
|
766
|
+
);
|
|
767
|
+
return true;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (error.code === "EEXIST") return BINDING_WRITE_CONFLICT;
|
|
770
|
+
say(`could not record the binding (${error.message}); NOT enforcing`);
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* The worktree `EnterWorktree` claims the session moved to.
|
|
777
|
+
*
|
|
778
|
+
* `path` is given verbatim; `name` resolves under the main checkout's
|
|
779
|
+
* `.claude/worktrees/`, which is where the tool creates one. A call carrying
|
|
780
|
+
* neither is a generated name this guard cannot predict, so it returns null and
|
|
781
|
+
* the claim is simply not recorded — an unrecorded claim degrades to the plain
|
|
782
|
+
* displacement check rather than to a false accusation.
|
|
783
|
+
*/
|
|
784
|
+
function claimedRoot(toolInput, cwd) {
|
|
785
|
+
const path = toolInput?.path;
|
|
786
|
+
if (typeof path === "string" && path.trim()) {
|
|
787
|
+
return realpath(resolve(isAbsolute(path) ? path : join(cwd, path)));
|
|
788
|
+
}
|
|
789
|
+
const name = toolInput?.name;
|
|
790
|
+
if (typeof name === "string" && name.trim()) {
|
|
791
|
+
const main = mainCheckout(cwd);
|
|
792
|
+
return main
|
|
793
|
+
? realpath(join(main, ".claude", "worktrees", name.trim()))
|
|
794
|
+
: null;
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Record where the session STARTED, so its first guarded call is checked
|
|
801
|
+
* against a baseline rather than establishing one.
|
|
802
|
+
*
|
|
803
|
+
* ## Why the first call could not be checked before
|
|
804
|
+
*
|
|
805
|
+
* It established the baseline instead of testing one, which is
|
|
806
|
+
* trust-on-first-use — and TOFU is only as safe as its first use. Here the
|
|
807
|
+
* first use is assigned by the harness, unasked (CodySwannGT/lisa#3864), so the
|
|
808
|
+
* one moment the guard trusted was the moment the session had least control
|
|
809
|
+
* over. A session already sitting in a foreign worktree when it first acted
|
|
810
|
+
* bound to that tree and was never refused, and every later call was then
|
|
811
|
+
* checked faithfully against the wrong baseline — defending the session INTO
|
|
812
|
+
* the wrong worktree rather than out of it (CodySwannGT/lisa#3955).
|
|
813
|
+
*
|
|
814
|
+
* ## Why an earlier observation separates what a cleverer check cannot
|
|
815
|
+
*
|
|
816
|
+
* At the first guarded call, "I was displaced before I acted" and "I
|
|
817
|
+
* legitimately started here" present identically: one session id, one observed
|
|
818
|
+
* root, no prior state. No comparison can tell them apart, because the
|
|
819
|
+
* information is not in the hook's inputs at that instant.
|
|
820
|
+
*
|
|
821
|
+
* At session start it is. The displacement mechanism actually observed is a
|
|
822
|
+
* PEER's `EnterWorktree` moving this session (CodySwannGT/lisa#3712), which
|
|
823
|
+
* happens during the session — after it has started. A baseline taken at start
|
|
824
|
+
* is therefore recorded BEFORE the event that moves it, and the first guarded
|
|
825
|
+
* call becomes an ordinary comparison against a baseline that already exists.
|
|
826
|
+
* It reaches the same refusal a later displacement produces, because by then it
|
|
827
|
+
* is one.
|
|
828
|
+
*
|
|
829
|
+
* ## What this does NOT achieve
|
|
830
|
+
*
|
|
831
|
+
* #3955 asks for a baseline from the ASSIGNER rather than from observation.
|
|
832
|
+
* That is not what this is and Lisa cannot build it: Lisa does not own
|
|
833
|
+
* `EnterWorktree`, and nothing the harness writes names a session's assigned
|
|
834
|
+
* worktree in a form a hook can read. This is still an observation, just an
|
|
835
|
+
* earlier one. It closes the window between session start and the first guarded
|
|
836
|
+
* call; it leaves open a session LAUNCHED already displaced, where start
|
|
837
|
+
* observes the foreign tree and trust-on-first-use still applies.
|
|
838
|
+
*
|
|
839
|
+
* Returns nothing on purpose, like its sibling below: SessionStart has no call
|
|
840
|
+
* to refuse, so every path through it allows.
|
|
841
|
+
* @param bindingKeyValue - The key this baseline belongs to
|
|
842
|
+
* @param observed - The worktree root the session started in
|
|
843
|
+
* @param payload - The hook payload, for legacy-key reads
|
|
844
|
+
*/
|
|
845
|
+
function recordBaseline(bindingKeyValue, observed, payload) {
|
|
846
|
+
// Never overwrite. A resumed session, a reconnect, or anything else that
|
|
847
|
+
// fires the event twice must not be able to launder a displaced binding into
|
|
848
|
+
// a fresh baseline — which is the whole failure this exists to close.
|
|
849
|
+
const previous = readState(bindingKeyValue, payload);
|
|
850
|
+
if (previous?.boundRoot || previous?.unverifiedOwner) return;
|
|
851
|
+
writeState(
|
|
852
|
+
bindingKeyValue,
|
|
853
|
+
{
|
|
854
|
+
boundRoot: observed,
|
|
855
|
+
claimedRoot: null,
|
|
856
|
+
updatedAt: new Date().toISOString(),
|
|
857
|
+
},
|
|
858
|
+
payload,
|
|
859
|
+
{ createOnly: true }
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Record what `EnterWorktree` just claimed, for the next tool call to check.
|
|
865
|
+
*
|
|
866
|
+
* Returns nothing on purpose. This runs on PostToolUse, where the tool has
|
|
867
|
+
* already happened and there is no call left to refuse, so every path through
|
|
868
|
+
* it is an allow — and a function whose only answer is "allow" should not be
|
|
869
|
+
* spelled as one that computes an exit code.
|
|
870
|
+
*/
|
|
871
|
+
function recordClaim(payload, observed) {
|
|
872
|
+
const claimed = claimedRoot(payload.tool_input, payload.cwd);
|
|
873
|
+
if (!claimed || claimed === observed) return;
|
|
874
|
+
const previous = readState(bindingKey(payload), payload);
|
|
875
|
+
if (previous?.unverifiedOwner) return;
|
|
876
|
+
writeState(
|
|
877
|
+
bindingKey(payload),
|
|
878
|
+
{
|
|
879
|
+
boundRoot: previous?.boundRoot ?? observed,
|
|
880
|
+
claimedRoot: claimed,
|
|
881
|
+
updatedAt: new Date().toISOString(),
|
|
882
|
+
},
|
|
883
|
+
payload,
|
|
884
|
+
{ createOnly: !previous }
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function refuse(lines) {
|
|
889
|
+
process.stderr.write(`${lines.join("\n")}\n`);
|
|
890
|
+
return 2;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Refuse an action whose binding changed while its creation was in flight. */
|
|
894
|
+
function bindingWriteConflict() {
|
|
895
|
+
return refuse([
|
|
896
|
+
"worktree-binding-guard: another hook recorded this session's binding during this call.",
|
|
897
|
+
"The existing file has been preserved. Retry the call to check its established binding.",
|
|
898
|
+
]);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function acceptanceLine(observed) {
|
|
902
|
+
const literal = `${ACCEPT_PREFIX} ${observed}`.replaceAll("'", "'\\''");
|
|
903
|
+
return ` echo '${literal}'`;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Explain a real ownership ambiguity without treating a stranger's root as fact.
|
|
908
|
+
* @param {object} state - Unverified-state sentinel, containing no imported fields
|
|
909
|
+
* @param {string|null} observed - Measured checkout
|
|
910
|
+
* @returns {number} Blocking native-hook status
|
|
911
|
+
*/
|
|
912
|
+
function unownedBinding(state, observed) {
|
|
913
|
+
return refuse([
|
|
914
|
+
"worktree-binding-guard: cannot prove this binding belongs to this session.",
|
|
915
|
+
"The existing file has no matching owner record. It has not been imported or changed.",
|
|
916
|
+
...(state.canonicalConflict
|
|
917
|
+
? [
|
|
918
|
+
"This session's reserved state file identifies another owner or an unknown format.",
|
|
919
|
+
"Start a new session with its own identity; acknowledging cannot overwrite that file.",
|
|
920
|
+
]
|
|
921
|
+
: [
|
|
922
|
+
"Choose the worktree you intend to use by acknowledging its measured path:",
|
|
923
|
+
acceptanceLine(observed),
|
|
924
|
+
]),
|
|
925
|
+
]);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** Read literal shell words and AND separators without evaluating expansions. */
|
|
929
|
+
function acknowledgementWords(text) {
|
|
930
|
+
const words = [];
|
|
931
|
+
let word = "",
|
|
932
|
+
active = false,
|
|
933
|
+
quote = null;
|
|
934
|
+
const emit = () => {
|
|
935
|
+
if (active) words.push(word);
|
|
936
|
+
word = "";
|
|
937
|
+
active = false;
|
|
938
|
+
};
|
|
939
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
940
|
+
const char = text[index];
|
|
941
|
+
if (quote === "'") {
|
|
942
|
+
if (char === "'") quote = null;
|
|
943
|
+
else word += char;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (char === "\\") {
|
|
947
|
+
const next = text[index + 1];
|
|
948
|
+
if (next === undefined) return null;
|
|
949
|
+
if (quote === '"' && !['"', "\\", "$", "`", "\n"].includes(next)) {
|
|
950
|
+
word += char;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
index += 1;
|
|
954
|
+
if (next !== "\n") {
|
|
955
|
+
word += next;
|
|
956
|
+
active = true;
|
|
957
|
+
}
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (quote === '"') {
|
|
961
|
+
if (char === '"') quote = null;
|
|
962
|
+
else if (char === "$" || char === "`") return null;
|
|
963
|
+
else word += char;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (char === "'" || char === '"') {
|
|
967
|
+
quote = char;
|
|
968
|
+
active = true;
|
|
969
|
+
} else if (char === " " || char === "\t") emit();
|
|
970
|
+
else if (char === "\n") return null;
|
|
971
|
+
else if (char === "&" && text[index + 1] === "&") {
|
|
972
|
+
emit();
|
|
973
|
+
words.push(ACK_AND_SEPARATOR);
|
|
974
|
+
index += 1;
|
|
975
|
+
} else if (/[&;|<>$`*?[\]{}()]/u.test(char)) return null;
|
|
976
|
+
else {
|
|
977
|
+
word += char;
|
|
978
|
+
active = true;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (quote !== null) return null;
|
|
982
|
+
emit();
|
|
983
|
+
return words;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** Strip only actual shell edge whitespace in linear time. */
|
|
987
|
+
function acknowledgementText(command) {
|
|
988
|
+
let start = 0,
|
|
989
|
+
end = command.length;
|
|
990
|
+
while (start < end && " \t\n".includes(command[start])) start += 1;
|
|
991
|
+
while (end > start && " \t\n".includes(command[end - 1])) end -= 1;
|
|
992
|
+
return command.slice(start, end);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** A literal echo, optionally preceded by a literal cd to the stated root. */
|
|
996
|
+
function acceptanceRequest(command) {
|
|
997
|
+
const text = acknowledgementText(command);
|
|
998
|
+
const words = acknowledgementWords(text.replace(/;$/u, ""));
|
|
999
|
+
if (!words) return null;
|
|
1000
|
+
const direct = words.length === 2 && words[0] === "echo";
|
|
1001
|
+
const moving =
|
|
1002
|
+
words.length === 5 &&
|
|
1003
|
+
words[0] === "cd" &&
|
|
1004
|
+
typeof words[1] === "string" &&
|
|
1005
|
+
words[2] === ACK_AND_SEPARATOR &&
|
|
1006
|
+
words[3] === "echo";
|
|
1007
|
+
if (!direct && !moving) return null;
|
|
1008
|
+
const message = words.at(-1);
|
|
1009
|
+
if (typeof message !== "string" || !message.startsWith(`${ACCEPT_PREFIX} `))
|
|
1010
|
+
return null;
|
|
1011
|
+
const stated = message.slice(ACCEPT_PREFIX.length + 1);
|
|
1012
|
+
// The printed protocol names absolute roots. Empty/relative operands and
|
|
1013
|
+
// cd's special ~, -, or -- forms cannot authorize a move.
|
|
1014
|
+
if (!isAbsolute(stated)) return null;
|
|
1015
|
+
const path = realpath(stated);
|
|
1016
|
+
const destination =
|
|
1017
|
+
moving && isAbsolute(words[1]) ? realpath(words[1]) : null;
|
|
1018
|
+
return {
|
|
1019
|
+
path,
|
|
1020
|
+
validMove: !moving || (destination !== null && destination === path),
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Validate the initial recovery root against the identity captured at baseline.
|
|
1026
|
+
* The current checkout may have been removed or deliberately rebound elsewhere.
|
|
1027
|
+
*/
|
|
1028
|
+
function isOriginalRoot(state, path) {
|
|
1029
|
+
const original = state?.originalRoot ?? state?.boundRoot;
|
|
1030
|
+
if (path !== original || worktreeRoot(path) !== path) return false;
|
|
1031
|
+
// Older state can only prove the repository of its established binding.
|
|
1032
|
+
const common = state.originalCommonDir ?? repositoryIdentity(state.boundRoot);
|
|
1033
|
+
return common !== null && common === repositoryIdentity(path);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** Acknowledgements select intent; the next call must prove actual movement. */
|
|
1037
|
+
function handleAcceptance(payload, observed) {
|
|
1038
|
+
const command = payload.tool_input?.command;
|
|
1039
|
+
if (typeof command !== "string" || !command.includes(ACCEPT_PREFIX))
|
|
1040
|
+
return null;
|
|
1041
|
+
const request = acceptanceRequest(command);
|
|
1042
|
+
if (!request) return null;
|
|
1043
|
+
const { path, validMove } = request;
|
|
1044
|
+
const previous = readState(bindingKey(payload), payload);
|
|
1045
|
+
if (previous?.canonicalConflict) return unownedBinding(previous, observed);
|
|
1046
|
+
const original = previous?.originalRoot ?? previous?.boundRoot;
|
|
1047
|
+
const recovering = isOriginalRoot(previous, path);
|
|
1048
|
+
const offered =
|
|
1049
|
+
path === previous?.offeredRoot &&
|
|
1050
|
+
isLinkedWorktree(previous.boundRoot, path);
|
|
1051
|
+
if (!validMove || (path !== observed && !recovering && !offered)) {
|
|
1052
|
+
const suggestion = observed ?? original;
|
|
1053
|
+
return refuse([
|
|
1054
|
+
`worktree-binding-guard: this acknowledgement names ${path ?? "no path"},`,
|
|
1055
|
+
"but it is not a valid move to the observed, original, or offered worktree. Not rebinding.",
|
|
1056
|
+
...(suggestion
|
|
1057
|
+
? [
|
|
1058
|
+
"Acknowledge the measured or original worktree:",
|
|
1059
|
+
acceptanceLine(suggestion),
|
|
1060
|
+
]
|
|
1061
|
+
: []),
|
|
1062
|
+
]);
|
|
1063
|
+
}
|
|
1064
|
+
const written = writeState(
|
|
1065
|
+
bindingKey(payload),
|
|
1066
|
+
{
|
|
1067
|
+
boundRoot: path,
|
|
1068
|
+
claimedRoot: null,
|
|
1069
|
+
offeredRoot: null,
|
|
1070
|
+
updatedAt: new Date().toISOString(),
|
|
1071
|
+
},
|
|
1072
|
+
payload,
|
|
1073
|
+
{ acceptUnowned: true }
|
|
1074
|
+
);
|
|
1075
|
+
if (written === BINDING_WRITE_CONFLICT) return bindingWriteConflict();
|
|
1076
|
+
if (written)
|
|
1077
|
+
say(
|
|
1078
|
+
path === observed
|
|
1079
|
+
? `bound to ${path}`
|
|
1080
|
+
: `binding set to ${path}; move there before further guarded calls`
|
|
1081
|
+
);
|
|
1082
|
+
return 0;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** Refusal text for a switch that reported success without taking effect. */
|
|
1086
|
+
function unconfirmedSwitch(claimed, observed) {
|
|
1087
|
+
return refuse([
|
|
1088
|
+
`worktree-binding-guard: EnterWorktree reported success for`,
|
|
1089
|
+
` ${claimed}`,
|
|
1090
|
+
`but this session is still operating in`,
|
|
1091
|
+
` ${observed}`,
|
|
1092
|
+
"The switch did not take effect. Anything run now lands in the second",
|
|
1093
|
+
"path, not the first — including installs, commits, and file writes, and",
|
|
1094
|
+
"that tree may hold another agent's uncommitted work.",
|
|
1095
|
+
"",
|
|
1096
|
+
"Use absolute paths under the tree you actually want, or acknowledge that",
|
|
1097
|
+
"you intend to keep working where you are:",
|
|
1098
|
+
acceptanceLine(observed),
|
|
1099
|
+
]);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** Refusal text for a binding that moved without this session asking. */
|
|
1103
|
+
function displaced(bound, observed) {
|
|
1104
|
+
return refuse([
|
|
1105
|
+
"worktree-binding-guard: this session's worktree changed underneath it.",
|
|
1106
|
+
` bound to: ${bound}`,
|
|
1107
|
+
` now in: ${observed}`,
|
|
1108
|
+
"Concurrent sessions share one worktree binding, so another agent's",
|
|
1109
|
+
"EnterWorktree can move this one. Commands run now land in the second",
|
|
1110
|
+
"path. If you moved deliberately, say so:",
|
|
1111
|
+
acceptanceLine(observed),
|
|
1112
|
+
]);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* The runtime version the inline-redirect gap was last verified at.
|
|
1117
|
+
*
|
|
1118
|
+
* ## What this constant is a pin ON
|
|
1119
|
+
*
|
|
1120
|
+
* This guard deliberately does NOT refuse an INLINE redirect into another
|
|
1121
|
+
* worktree — `cd <other> && git …`, `git -C <other> …`. The reason is good and
|
|
1122
|
+
* is written out at {@link scriptReachingForeignTree}: the runtime's own
|
|
1123
|
+
* worktree isolation already refuses them, and duplicating a control is how two
|
|
1124
|
+
* controls drift into disagreeing about the same command.
|
|
1125
|
+
*
|
|
1126
|
+
* That reason is a DEPENDENCY on behaviour Lisa does not ship. It was verified
|
|
1127
|
+
* refusing at this version, with the refusal wording "this command names git in
|
|
1128
|
+
* a form too complex to verify that it stays inside the worktree" and
|
|
1129
|
+
* "redirects git to the shared checkout via -C".
|
|
1130
|
+
*
|
|
1131
|
+
* ## Why a pin rather than a test
|
|
1132
|
+
*
|
|
1133
|
+
* The runtime's isolation is not reachable from a unit test: it adjudicates
|
|
1134
|
+
* inside a live worktree-isolated session, and there is no entry point that
|
|
1135
|
+
* takes one hook envelope and returns a verdict the way this guard does. So the
|
|
1136
|
+
* dependency cannot be turned red by writing a test, and a claim that cannot go
|
|
1137
|
+
* red goes stale silently instead (CodySwannGT/lisa#3944).
|
|
1138
|
+
*
|
|
1139
|
+
* What CAN be measured is whether the runtime is still the one the claim was
|
|
1140
|
+
* checked against. That does not prove the behaviour changed — only that nobody
|
|
1141
|
+
* has checked since, which is the honest thing to report and the whole of what
|
|
1142
|
+
* this pin does.
|
|
1143
|
+
*
|
|
1144
|
+
* ## Moving it
|
|
1145
|
+
*
|
|
1146
|
+
* Re-run both inline forms in a worktree-isolated session. Still refused: move
|
|
1147
|
+
* this pin, and say where. Permitted: those two cells are UNCOVERED, and #3944
|
|
1148
|
+
* owns the decision about which of them Lisa takes on.
|
|
1149
|
+
*/
|
|
1150
|
+
const VERIFIED_RUNTIME_VERSION = "2.1.261";
|
|
1151
|
+
|
|
1152
|
+
/** The ticket that owns the inline-redirect dependency and its expiry. */
|
|
1153
|
+
const RUNTIME_ASSUMPTION_TICKET = "CodySwannGT/lisa#3944";
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* This session's runtime version, or null when this is not that runtime.
|
|
1157
|
+
*
|
|
1158
|
+
* Three answers, and the difference between the last two is the point:
|
|
1159
|
+
* - `null` — not Claude Code. Codex, Cursor and Copilot ship this guard too,
|
|
1160
|
+
* and the assumption above is about a control none of them has. A notice that
|
|
1161
|
+
* fires on every session of every harness is noise, and noise gets switched
|
|
1162
|
+
* off.
|
|
1163
|
+
* - `""` — Claude Code, which will not say which version. Read as a DIVERGENCE
|
|
1164
|
+
* rather than as a match: the assumption is unconfirmed either way, and
|
|
1165
|
+
* treating an unreadable version as "still 2.1.261" is exactly the silent
|
|
1166
|
+
* expiry #3944 was filed about.
|
|
1167
|
+
* - a version — take it at its word.
|
|
1168
|
+
*
|
|
1169
|
+
* Both env vars are read because either can be the one a future release keeps.
|
|
1170
|
+
* `AI_AGENT` carries `claude-code_2-1-261_agent`; `CLAUDE_CODE_EXECPATH` ends
|
|
1171
|
+
* in the version directory the running build was launched from.
|
|
1172
|
+
* @param env - Environment to read
|
|
1173
|
+
* @returns The version, `""` when unreadable, or null for another runtime
|
|
1174
|
+
*/
|
|
1175
|
+
const isDigit = ch => ch >= "0" && ch <= "9";
|
|
1176
|
+
|
|
1177
|
+
/** @param value - Segment to test @returns Whether it is all digits */
|
|
1178
|
+
const allDigits = value => value.length > 0 && [...value].every(isDigit);
|
|
1179
|
+
|
|
1180
|
+
/** @param value - Segment to read @returns Its trailing digit run, else `""` */
|
|
1181
|
+
const trailingDigits = value => {
|
|
1182
|
+
let start = value.length;
|
|
1183
|
+
while (start > 0 && isDigit(value[start - 1])) start -= 1;
|
|
1184
|
+
return value.slice(start);
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
function runtimeVersion(env) {
|
|
1188
|
+
if (!env.CLAUDECODE) return null;
|
|
1189
|
+
const tagged = /claude-code[_-](\d+)[-.](\d+)[-.](\d+)/u.exec(
|
|
1190
|
+
env.AI_AGENT ?? ""
|
|
1191
|
+
);
|
|
1192
|
+
if (tagged) return `${tagged[1]}.${tagged[2]}.${tagged[3]}`;
|
|
1193
|
+
// Read the version off the final path segment by scanning characters rather
|
|
1194
|
+
// than by regex: an unanchored `\d+\.\d+\.\d+` before `$` backtracks
|
|
1195
|
+
// super-linearly on a long execpath, and the version Claude Code launches
|
|
1196
|
+
// from is always the trailing directory name.
|
|
1197
|
+
const segments = (env.CLAUDE_CODE_EXECPATH ?? "").split("/").filter(Boolean);
|
|
1198
|
+
const trailing = segments.length ? segments[segments.length - 1] : "";
|
|
1199
|
+
const parts = trailing.split(".");
|
|
1200
|
+
if (parts.length < 3) return "";
|
|
1201
|
+
const [major, minor, patch] = parts.slice(-3);
|
|
1202
|
+
const majorDigits = trailingDigits(major);
|
|
1203
|
+
if (!majorDigits || !allDigits(minor) || !allDigits(patch)) return "";
|
|
1204
|
+
return `${majorDigits}.${minor}.${patch}`;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* The notice text for a runtime the inline-redirect claim was not checked at.
|
|
1209
|
+
* @param version - The observed runtime version, `""` when unreadable
|
|
1210
|
+
* @returns The lines to report
|
|
1211
|
+
*/
|
|
1212
|
+
function driftNotice(version) {
|
|
1213
|
+
return [
|
|
1214
|
+
"worktree-binding-guard: the inline-redirect gap is UNVERIFIED here.",
|
|
1215
|
+
"",
|
|
1216
|
+
"This guard does not refuse `cd <other-worktree> && git …` or",
|
|
1217
|
+
"`git -C <other-worktree> …`. Those two cells are covered by the runtime's",
|
|
1218
|
+
`own worktree isolation, verified refusing at Claude Code ${VERIFIED_RUNTIME_VERSION}.`,
|
|
1219
|
+
version
|
|
1220
|
+
? `This session is running ${version}, so that check does not cover it.`
|
|
1221
|
+
: "This session's runtime version could not be read, so nothing covers it.",
|
|
1222
|
+
"",
|
|
1223
|
+
"This does NOT say the runtime stopped refusing them — only that nobody has",
|
|
1224
|
+
"checked since. Run both forms in a worktree-isolated session: still",
|
|
1225
|
+
"refused, move VERIFIED_RUNTIME_VERSION in worktree-binding-guard.mjs;",
|
|
1226
|
+
`permitted, the two cells are uncovered and ${RUNTIME_ASSUMPTION_TICKET}`,
|
|
1227
|
+
"owns what Lisa does about it.",
|
|
1228
|
+
];
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Report, once per session, that the runtime moved off the verified version.
|
|
1233
|
+
*
|
|
1234
|
+
* ## Why SessionStart and not the guarded call
|
|
1235
|
+
*
|
|
1236
|
+
* SessionStart is the guard's first evaluation of a session, it fires exactly
|
|
1237
|
+
* once per start, and its `additionalContext` is the one channel here that the
|
|
1238
|
+
* agent actually reads — the `say()` lines elsewhere in this file are for a
|
|
1239
|
+
* human reading a transcript. A notice attached to a guarded tool call would
|
|
1240
|
+
* either repeat on every Bash command or need a suppression rule of its own.
|
|
1241
|
+
*
|
|
1242
|
+
* ## Why it is recorded rather than counted
|
|
1243
|
+
*
|
|
1244
|
+
* A resume fires SessionStart again for the same session id. The recorded
|
|
1245
|
+
* version is what makes the second one silent, and recording the VERSION rather
|
|
1246
|
+
* than a flag means a runtime that moves twice inside one resumed session is
|
|
1247
|
+
* still reported the second time.
|
|
1248
|
+
*
|
|
1249
|
+
* Never refuses. The dependency is a reporting gap, not a live displacement,
|
|
1250
|
+
* and blocking a session over a version number would be a wall where an
|
|
1251
|
+
* observation belongs.
|
|
1252
|
+
* @param bindingKeyValue - The key being started
|
|
1253
|
+
* @param env - Environment to read the runtime version from
|
|
1254
|
+
* @param payload - The hook payload, for legacy-key reads
|
|
1255
|
+
* @returns Nothing; every path through this allows
|
|
1256
|
+
*/
|
|
1257
|
+
function noticeRuntimeDrift(bindingKeyValue, env, payload) {
|
|
1258
|
+
const version = runtimeVersion(env);
|
|
1259
|
+
if (version === null || version === VERIFIED_RUNTIME_VERSION) return;
|
|
1260
|
+
if (readState(bindingKeyValue, payload)?.runtimeNoticed === version) return;
|
|
1261
|
+
writeState(bindingKeyValue, { runtimeNoticed: version }, payload);
|
|
1262
|
+
process.stdout.write(
|
|
1263
|
+
`${JSON.stringify({
|
|
1264
|
+
hookSpecificOutput: {
|
|
1265
|
+
hookEventName: "SessionStart",
|
|
1266
|
+
additionalContext: driftNotice(version).join("\n"),
|
|
1267
|
+
},
|
|
1268
|
+
})}\n`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function evaluate(payload) {
|
|
1273
|
+
const sessionId = payload.session_id;
|
|
1274
|
+
const cwd = payload.cwd;
|
|
1275
|
+
if (typeof sessionId !== "string" || !sessionId || typeof cwd !== "string") {
|
|
1276
|
+
say("payload carries no session id or cwd; binding is NOT enforced");
|
|
1277
|
+
return 0;
|
|
1278
|
+
}
|
|
1279
|
+
const binding = bindingKey(payload);
|
|
1280
|
+
const observed = worktreeRoot(cwd);
|
|
1281
|
+
// The initial-root recovery acknowledgement also works outside any repo.
|
|
1282
|
+
if (GUARDED_TOOLS.has(payload.tool_name)) {
|
|
1283
|
+
const accepted = handleAcceptance(payload, observed);
|
|
1284
|
+
if (accepted !== null) return accepted;
|
|
1285
|
+
}
|
|
1286
|
+
if (!observed) return 0;
|
|
1287
|
+
|
|
1288
|
+
if (payload.hook_event_name === "SessionStart") {
|
|
1289
|
+
recordBaseline(binding, observed, payload);
|
|
1290
|
+
// After the baseline, never before: `recordBaseline` writes a fresh state
|
|
1291
|
+
// for a session it has not seen, and the notice's record has to go on top
|
|
1292
|
+
// of that write rather than under it.
|
|
1293
|
+
noticeRuntimeDrift(binding, process.env, payload);
|
|
1294
|
+
return 0;
|
|
1295
|
+
}
|
|
1296
|
+
if (payload.tool_name === "EnterWorktree") {
|
|
1297
|
+
const previous = readState(binding, payload);
|
|
1298
|
+
if (
|
|
1299
|
+
previous?.boundRoot &&
|
|
1300
|
+
previous.boundRoot !== observed &&
|
|
1301
|
+
!isLinkedWorktree(previous.boundRoot, observed)
|
|
1302
|
+
)
|
|
1303
|
+
return 0;
|
|
1304
|
+
recordClaim(payload, observed);
|
|
1305
|
+
return 0;
|
|
1306
|
+
}
|
|
1307
|
+
if (!GUARDED_TOOLS.has(payload.tool_name)) return 0;
|
|
1308
|
+
|
|
1309
|
+
const state = readState(binding, payload);
|
|
1310
|
+
if (state?.unverifiedOwner) return unownedBinding(state, observed);
|
|
1311
|
+
if (!state?.boundRoot) {
|
|
1312
|
+
// No baseline means SessionStart did not run for this session — an older
|
|
1313
|
+
// install, a runtime that fires no such event, a harness that skips it.
|
|
1314
|
+
// Absence of a baseline is absence of evidence, not evidence of
|
|
1315
|
+
// displacement, so this records one exactly as it always did rather than
|
|
1316
|
+
// refusing. Treating it as a refusal would turn a floor into a wall on
|
|
1317
|
+
// every surface where the event does not fire, and a wall gets switched
|
|
1318
|
+
// off, which costs the whole guard.
|
|
1319
|
+
const written = writeState(
|
|
1320
|
+
binding,
|
|
1321
|
+
{
|
|
1322
|
+
boundRoot: observed,
|
|
1323
|
+
claimedRoot: null,
|
|
1324
|
+
updatedAt: new Date().toISOString(),
|
|
1325
|
+
},
|
|
1326
|
+
payload,
|
|
1327
|
+
{ createOnly: true }
|
|
1328
|
+
);
|
|
1329
|
+
if (written === BINDING_WRITE_CONFLICT) return bindingWriteConflict();
|
|
1330
|
+
return 0;
|
|
1331
|
+
}
|
|
1332
|
+
if (
|
|
1333
|
+
state.boundRoot !== observed &&
|
|
1334
|
+
!isLinkedWorktree(state.boundRoot, observed)
|
|
1335
|
+
)
|
|
1336
|
+
return 0;
|
|
1337
|
+
if (state.claimedRoot && state.claimedRoot !== observed) {
|
|
1338
|
+
return unconfirmedSwitch(state.claimedRoot, observed);
|
|
1339
|
+
}
|
|
1340
|
+
if (state.claimedRoot === observed) {
|
|
1341
|
+
const written = writeState(
|
|
1342
|
+
binding,
|
|
1343
|
+
{
|
|
1344
|
+
boundRoot: observed,
|
|
1345
|
+
claimedRoot: null,
|
|
1346
|
+
offeredRoot: null,
|
|
1347
|
+
updatedAt: new Date().toISOString(),
|
|
1348
|
+
},
|
|
1349
|
+
payload
|
|
1350
|
+
);
|
|
1351
|
+
if (written === BINDING_WRITE_CONFLICT) return bindingWriteConflict();
|
|
1352
|
+
return 0;
|
|
1353
|
+
}
|
|
1354
|
+
if (state.boundRoot !== observed) return displaced(state.boundRoot, observed);
|
|
1355
|
+
// Last, and only once the session is provably where it should be: a script
|
|
1356
|
+
// that moves somewhere else after this check has already run.
|
|
1357
|
+
const reach = scriptReachingForeignTree(payload, observed);
|
|
1358
|
+
if (reach) {
|
|
1359
|
+
writeState(binding, { offeredRoot: reach.foreign }, payload);
|
|
1360
|
+
return reachesOut(reach.script, reach.foreign, observed);
|
|
1361
|
+
}
|
|
1362
|
+
return 0;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function main() {
|
|
1366
|
+
let raw = "";
|
|
1367
|
+
try {
|
|
1368
|
+
raw = readFileSync(0, "utf8");
|
|
1369
|
+
} catch {
|
|
1370
|
+
say("could not read the hook payload; binding is NOT enforced");
|
|
1371
|
+
return 0;
|
|
1372
|
+
}
|
|
1373
|
+
let payload;
|
|
1374
|
+
try {
|
|
1375
|
+
payload = JSON.parse(raw);
|
|
1376
|
+
} catch {
|
|
1377
|
+
say("hook payload was not JSON; binding is NOT enforced");
|
|
1378
|
+
return 0;
|
|
1379
|
+
}
|
|
1380
|
+
return evaluate(payload);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
process.exit(main());
|