@codyswann/lisa 4.74.9 → 4.75.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (805) hide show
  1. package/.claude-plugin/marketplace.json +60 -0
  2. package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +73 -40
  3. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +54 -0
  4. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +3 -2
  5. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  6. package/dist/core/lisa-owned-hash-ledger.js +6 -0
  7. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  8. package/dist/core/lisa-skill-sources.d.ts +3 -3
  9. package/dist/core/lisa-skill-sources.d.ts.map +1 -1
  10. package/dist/core/lisa-skill-sources.js +3 -2
  11. package/dist/core/lisa-skill-sources.js.map +1 -1
  12. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  13. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  14. package/dist/core/upstream-evidence-manifest.js +735 -8
  15. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  16. package/package.json +4 -4
  17. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +11 -0
  26. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +8 -0
  27. package/plugins/lisa-cdk-devin/skills/setup-aws-accounts/SKILL.md +292 -0
  28. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-devin/.claude-plugin/plugin.json +8 -0
  31. package/plugins/lisa-devin/.devin-plugin/plugin.json +8 -0
  32. package/plugins/lisa-devin/.mcp.json +8 -0
  33. package/plugins/lisa-devin/agents/architecture-specialist.md +32 -0
  34. package/plugins/lisa-devin/agents/bug-fixer.md +43 -0
  35. package/plugins/lisa-devin/agents/builder.md +44 -0
  36. package/plugins/lisa-devin/agents/confluence-prd-intake.md +65 -0
  37. package/plugins/lisa-devin/agents/debug-specialist.md +34 -0
  38. package/plugins/lisa-devin/agents/eval-specialist.md +33 -0
  39. package/plugins/lisa-devin/agents/git-history-analyzer.md +183 -0
  40. package/plugins/lisa-devin/agents/github-agent.md +155 -0
  41. package/plugins/lisa-devin/agents/github-build-intake.md +64 -0
  42. package/plugins/lisa-devin/agents/github-prd-intake.md +66 -0
  43. package/plugins/lisa-devin/agents/jira-agent.md +135 -0
  44. package/plugins/lisa-devin/agents/jira-build-intake.md +64 -0
  45. package/plugins/lisa-devin/agents/learner.md +85 -0
  46. package/plugins/lisa-devin/agents/learning-judge.md +119 -0
  47. package/plugins/lisa-devin/agents/learnings-synthesizer.md +155 -0
  48. package/plugins/lisa-devin/agents/linear-agent.md +138 -0
  49. package/plugins/lisa-devin/agents/linear-build-intake.md +64 -0
  50. package/plugins/lisa-devin/agents/linear-prd-intake.md +66 -0
  51. package/plugins/lisa-devin/agents/notion-prd-intake.md +63 -0
  52. package/plugins/lisa-devin/agents/performance-specialist.md +26 -0
  53. package/plugins/lisa-devin/agents/pr-mining-specialist.md +85 -0
  54. package/plugins/lisa-devin/agents/product-specialist.md +30 -0
  55. package/plugins/lisa-devin/agents/quality-specialist.md +30 -0
  56. package/plugins/lisa-devin/agents/security-specialist.md +43 -0
  57. package/plugins/lisa-devin/agents/skill-evaluator.md +205 -0
  58. package/plugins/lisa-devin/agents/spec-conformance-specialist.md +56 -0
  59. package/plugins/lisa-devin/agents/test-specialist.md +32 -0
  60. package/plugins/lisa-devin/agents/tracker-mining-specialist.md +85 -0
  61. package/plugins/lisa-devin/agents/verification-specialist.md +145 -0
  62. package/plugins/lisa-devin/hooks/auto-update.mjs +824 -0
  63. package/plugins/lisa-devin/hooks/auto-update.sh +31 -0
  64. package/plugins/lisa-devin/hooks/block-blind-automerge.sh +1242 -0
  65. package/plugins/lisa-devin/hooks/block-direct-issue-create.sh +2603 -0
  66. package/plugins/lisa-devin/hooks/block-host-name-leak.mjs +412 -0
  67. package/plugins/lisa-devin/hooks/block-host-name-leak.sh +61 -0
  68. package/plugins/lisa-devin/hooks/block-instruction-file-edits.sh +207 -0
  69. package/plugins/lisa-devin/hooks/block-managed-file-edits.sh +1175 -0
  70. package/plugins/lisa-devin/hooks/block-no-verify.sh +2288 -0
  71. package/plugins/lisa-devin/hooks/block-shell-json-parsing.sh +263 -0
  72. package/plugins/lisa-devin/hooks/cleanup-stale-worktrees.sh +122 -0
  73. package/plugins/lisa-devin/hooks/devin-tool-name-shim.sh +30 -0
  74. package/plugins/lisa-devin/hooks/discharge-work-item-gates.sh +90 -0
  75. package/plugins/lisa-devin/hooks/enforce-verification-gate.sh +506 -0
  76. package/plugins/lisa-devin/hooks/enforcement-vintage-npm.mjs +320 -0
  77. package/plugins/lisa-devin/hooks/enforcement-vintage.mjs +472 -0
  78. package/plugins/lisa-devin/hooks/enforcement-vintage.sh +48 -0
  79. package/plugins/lisa-devin/hooks/failure-signature-index.mjs +717 -0
  80. package/plugins/lisa-devin/hooks/failure-signature-index.sh +26 -0
  81. package/plugins/lisa-devin/hooks/guard-dedupe.bash +374 -0
  82. package/plugins/lisa-devin/hooks/inject-resolved-config.mjs +903 -0
  83. package/plugins/lisa-devin/hooks/inject-resolved-config.sh +107 -0
  84. package/plugins/lisa-devin/hooks/inject-rules.sh +49 -0
  85. package/plugins/lisa-devin/hooks/install-pkgs.sh +84 -0
  86. package/plugins/lisa-devin/hooks/operational-hazards.mjs +736 -0
  87. package/plugins/lisa-devin/hooks/operational-hazards.sh +28 -0
  88. package/plugins/lisa-devin/hooks/parity-safety-net-heredoc.py +1004 -0
  89. package/plugins/lisa-devin/hooks/parity-safety-net.sh +3275 -0
  90. package/plugins/lisa-devin/hooks/secrets-preflight.sh +72 -0
  91. package/plugins/lisa-devin/hooks/setup-jira-cli.sh +205 -0
  92. package/plugins/lisa-devin/hooks/shell-write-nudge.sh +86 -0
  93. package/plugins/lisa-devin/hooks/sonar-secrets.sh +205 -0
  94. package/plugins/lisa-devin/hooks/threshold-ratchet-compare.mjs +518 -0
  95. package/plugins/lisa-devin/hooks/threshold-ratchet-families.mjs +543 -0
  96. package/plugins/lisa-devin/hooks/threshold-ratchet.mjs +568 -0
  97. package/plugins/lisa-devin/hooks/threshold-ratchet.sh +22 -0
  98. package/plugins/lisa-devin/hooks/withdrawn-rulings.mjs +600 -0
  99. package/plugins/lisa-devin/hooks/withdrawn-rulings.sh +24 -0
  100. package/plugins/lisa-devin/hooks/worktree-binding-guard.mjs +1383 -0
  101. package/plugins/lisa-devin/hooks/worktree-binding-guard.sh +79 -0
  102. package/plugins/lisa-devin/hooks.json +247 -0
  103. package/plugins/lisa-devin/rules/eager/00-rule-index.md +82 -0
  104. package/plugins/lisa-devin/rules/eager/base-rules.md +85 -0
  105. package/plugins/lisa-devin/rules/eager/coding-philosophy.md +27 -0
  106. package/plugins/lisa-devin/rules/eager/config-resolution.md +37 -0
  107. package/plugins/lisa-devin/rules/eager/do-it-now.md +11 -0
  108. package/plugins/lisa-devin/rules/eager/empirical-inquiry.md +24 -0
  109. package/plugins/lisa-devin/rules/eager/falsifiable-checks.md +48 -0
  110. package/plugins/lisa-devin/rules/eager/history-audit.md +27 -0
  111. package/plugins/lisa-devin/rules/eager/integration-access-layer.md +19 -0
  112. package/plugins/lisa-devin/rules/eager/intent-routing.md +18 -0
  113. package/plugins/lisa-devin/rules/eager/learnings-ladder.md +18 -0
  114. package/plugins/lisa-devin/rules/eager/local-ci-first.md +15 -0
  115. package/plugins/lisa-devin/rules/eager/measurement-integrity.md +18 -0
  116. package/plugins/lisa-devin/rules/eager/never-name-a-downstream-project.md +9 -0
  117. package/plugins/lisa-devin/rules/eager/not-blocked-just-waiting.md +13 -0
  118. package/plugins/lisa-devin/rules/eager/operational-hazards.md +50 -0
  119. package/plugins/lisa-devin/rules/eager/report-actionability.md +26 -0
  120. package/plugins/lisa-devin/rules/eager/security-audit-handling.md +40 -0
  121. package/plugins/lisa-devin/rules/eager/session-status-updates.md +20 -0
  122. package/plugins/lisa-devin/rules/eager/settled-decisions.md +75 -0
  123. package/plugins/lisa-devin/rules/eager/stale-state-claims.md +26 -0
  124. package/plugins/lisa-devin/rules/eager/tool-access-gate.md +23 -0
  125. package/plugins/lisa-devin/rules/eager/tracked-work.md +9 -0
  126. package/plugins/lisa-devin/rules/eager/upstream-to-lisa.md +14 -0
  127. package/plugins/lisa-devin/rules/eager/verification.md +33 -0
  128. package/plugins/lisa-devin/rules/eager/wiki-knowledge-source.md +13 -0
  129. package/plugins/lisa-devin/rules/eager/withdrawn-rulings.md +102 -0
  130. package/plugins/lisa-devin/rules/reference/automation-runbook-contract.md +355 -0
  131. package/plugins/lisa-devin/rules/reference/base-rules.md +136 -0
  132. package/plugins/lisa-devin/rules/reference/bdd-e2e-coverage.md +301 -0
  133. package/plugins/lisa-devin/rules/reference/blocker-containment.md +165 -0
  134. package/plugins/lisa-devin/rules/reference/claim-archaeology.md +184 -0
  135. package/plugins/lisa-devin/rules/reference/claim-evidence-mapping.md +362 -0
  136. package/plugins/lisa-devin/rules/reference/claim-time-guards.md +135 -0
  137. package/plugins/lisa-devin/rules/reference/coding-philosophy.md +428 -0
  138. package/plugins/lisa-devin/rules/reference/config-resolution.md +1225 -0
  139. package/plugins/lisa-devin/rules/reference/control-reachability.md +166 -0
  140. package/plugins/lisa-devin/rules/reference/convergent-review.md +145 -0
  141. package/plugins/lisa-devin/rules/reference/credential-substrate-precedence.md +249 -0
  142. package/plugins/lisa-devin/rules/reference/dependency-decision-records.md +177 -0
  143. package/plugins/lisa-devin/rules/reference/dependency-internalization-kit.md +244 -0
  144. package/plugins/lisa-devin/rules/reference/dependency-trust-classes.md +291 -0
  145. package/plugins/lisa-devin/rules/reference/deployed-state-readback.md +142 -0
  146. package/plugins/lisa-devin/rules/reference/derived-branch-plan.md +214 -0
  147. package/plugins/lisa-devin/rules/reference/design-source-of-truth.md +260 -0
  148. package/plugins/lisa-devin/rules/reference/design-value-binding.md +311 -0
  149. package/plugins/lisa-devin/rules/reference/do-it-now.md +19 -0
  150. package/plugins/lisa-devin/rules/reference/documentation-source-paths.md +31 -0
  151. package/plugins/lisa-devin/rules/reference/empirical-inquiry.md +28 -0
  152. package/plugins/lisa-devin/rules/reference/factory-model.md +111 -0
  153. package/plugins/lisa-devin/rules/reference/falsifiable-checks.md +275 -0
  154. package/plugins/lisa-devin/rules/reference/history-audit.md +42 -0
  155. package/plugins/lisa-devin/rules/reference/integration-access-layer.md +89 -0
  156. package/plugins/lisa-devin/rules/reference/intent-routing.md +428 -0
  157. package/plugins/lisa-devin/rules/reference/leaf-only-lifecycle.md +227 -0
  158. package/plugins/lisa-devin/rules/reference/learnings-ladder.md +41 -0
  159. package/plugins/lisa-devin/rules/reference/local-ci-first.md +34 -0
  160. package/plugins/lisa-devin/rules/reference/measurement-integrity.md +119 -0
  161. package/plugins/lisa-devin/rules/reference/never-name-a-downstream-project.md +37 -0
  162. package/plugins/lisa-devin/rules/reference/not-blocked-just-waiting.md +41 -0
  163. package/plugins/lisa-devin/rules/reference/observability-audit.md +146 -0
  164. package/plugins/lisa-devin/rules/reference/operational-hazards.md +115 -0
  165. package/plugins/lisa-devin/rules/reference/prd-definition-of-ready.md +118 -0
  166. package/plugins/lisa-devin/rules/reference/prd-lifecycle-rollup.md +192 -0
  167. package/plugins/lisa-devin/rules/reference/pre-flight-autofill.md +250 -0
  168. package/plugins/lisa-devin/rules/reference/project-learnings.md +159 -0
  169. package/plugins/lisa-devin/rules/reference/promotion-contract.md +148 -0
  170. package/plugins/lisa-devin/rules/reference/readiness-rubric.md +242 -0
  171. package/plugins/lisa-devin/rules/reference/ready-role-filing.md +212 -0
  172. package/plugins/lisa-devin/rules/reference/rejection-detection.md +308 -0
  173. package/plugins/lisa-devin/rules/reference/repo-scope-split.md +119 -0
  174. package/plugins/lisa-devin/rules/reference/report-actionability.md +61 -0
  175. package/plugins/lisa-devin/rules/reference/reset-seed-coverage.md +286 -0
  176. package/plugins/lisa-devin/rules/reference/security-audit-handling.md +55 -0
  177. package/plugins/lisa-devin/rules/reference/session-status-updates.md +111 -0
  178. package/plugins/lisa-devin/rules/reference/settled-decisions.md +39 -0
  179. package/plugins/lisa-devin/rules/reference/stale-state-claims.md +88 -0
  180. package/plugins/lisa-devin/rules/reference/tool-access-gate.md +114 -0
  181. package/plugins/lisa-devin/rules/reference/tracked-work.md +49 -0
  182. package/plugins/lisa-devin/rules/reference/upstream-to-lisa.md +26 -0
  183. package/plugins/lisa-devin/rules/reference/usage-accounting.md +261 -0
  184. package/plugins/lisa-devin/rules/reference/verification.md +278 -0
  185. package/plugins/lisa-devin/rules/reference/wiki-knowledge-source.md +37 -0
  186. package/plugins/lisa-devin/rules/reference/withdrawn-rulings.md +148 -0
  187. package/plugins/lisa-devin/rules/reference/work-item-definition-of-ready.md +177 -0
  188. package/plugins/lisa-devin/rules/reference/work-item-trailer-definition.md +129 -0
  189. package/plugins/lisa-devin/scripts/automation-run-record.mjs +678 -0
  190. package/plugins/lisa-devin/scripts/automation-status-claude-adapter.mjs +804 -0
  191. package/plugins/lisa-devin/scripts/automation-status-codex-adapter.mjs +1081 -0
  192. package/plugins/lisa-devin/scripts/automation-status-contract-drift.mjs +392 -0
  193. package/plugins/lisa-devin/scripts/automation-status-expected-fleet.mjs +508 -0
  194. package/plugins/lisa-devin/scripts/automation-status-report.mjs +200 -0
  195. package/plugins/lisa-devin/scripts/automation-status-run-history.mjs +216 -0
  196. package/plugins/lisa-devin/scripts/automation-status-unrecorded-runs.mjs +127 -0
  197. package/plugins/lisa-devin/scripts/blocker-edge-resolution.mjs +150 -0
  198. package/plugins/lisa-devin/scripts/cross-pollinate.mjs +763 -0
  199. package/plugins/lisa-devin/scripts/design-bindings-probe.mjs +713 -0
  200. package/plugins/lisa-devin/scripts/design-intake-gate.mjs +470 -0
  201. package/plugins/lisa-devin/scripts/design-source-gate.mjs +610 -0
  202. package/plugins/lisa-devin/scripts/design-variable-ids.mjs +567 -0
  203. package/plugins/lisa-devin/scripts/doctor-report.mjs +635 -0
  204. package/plugins/lisa-devin/scripts/install-remote-agent-aws.mjs +443 -0
  205. package/plugins/lisa-devin/scripts/intake-blocker-reprobe.mjs +1065 -0
  206. package/plugins/lisa-devin/scripts/intake-prework-denominator.mjs +249 -0
  207. package/plugins/lisa-devin/scripts/lifecycle-label-trust.mjs +593 -0
  208. package/plugins/lisa-devin/scripts/linear-state-write-target.mjs +362 -0
  209. package/plugins/lisa-devin/scripts/plugin-sync-explain.mjs +654 -0
  210. package/plugins/lisa-devin/scripts/pr-arming-sweep.mjs +561 -0
  211. package/plugins/lisa-devin/scripts/project-ideation-idempotency-harness.mjs +338 -0
  212. package/plugins/lisa-devin/scripts/qa-signal-lifecycle.mjs +523 -0
  213. package/plugins/lisa-devin/scripts/queue-contract-resolution.mjs +583 -0
  214. package/plugins/lisa-devin/scripts/queue-health-classification.mjs +157 -0
  215. package/plugins/lisa-devin/scripts/queue-status-build-readers.mjs +561 -0
  216. package/plugins/lisa-devin/scripts/queue-status-prd-readers.mjs +452 -0
  217. package/plugins/lisa-devin/scripts/remote-agent-aws-setup.sh +677 -0
  218. package/plugins/lisa-devin/scripts/repo-scope-vocabulary-audit.mjs +458 -0
  219. package/plugins/lisa-devin/scripts/resolve-lifecycle-role.mjs +347 -0
  220. package/plugins/lisa-devin/scripts/rollup-blocker-classification.mjs +616 -0
  221. package/plugins/lisa-devin/scripts/scoping-label-audit.mjs +474 -0
  222. package/plugins/lisa-devin/skills/lisa-acceptance-criteria/SKILL.md +73 -0
  223. package/plugins/lisa-devin/skills/lisa-agent-design-best-practices/SKILL.md +242 -0
  224. package/plugins/lisa-devin/skills/lisa-agent-ready/SKILL.md +271 -0
  225. package/plugins/lisa-devin/skills/lisa-analyze-claude-remote/SKILL.md +434 -0
  226. package/plugins/lisa-devin/skills/lisa-atlassian-access/SKILL.md +469 -0
  227. package/plugins/lisa-devin/skills/lisa-atlassian-access/scripts/markdown-to-adf.mjs +236 -0
  228. package/plugins/lisa-devin/skills/lisa-attribute-failure/SKILL.md +142 -0
  229. package/plugins/lisa-devin/skills/lisa-automation-status/SKILL.md +149 -0
  230. package/plugins/lisa-devin/skills/lisa-bug-triage/SKILL.md +23 -0
  231. package/plugins/lisa-devin/skills/lisa-capability-drift/SKILL.md +71 -0
  232. package/plugins/lisa-devin/skills/lisa-codebase-research/SKILL.md +87 -0
  233. package/plugins/lisa-devin/skills/lisa-codify-verification/SKILL.md +213 -0
  234. package/plugins/lisa-devin/skills/lisa-confluence-prd-intake/SKILL.md +417 -0
  235. package/plugins/lisa-devin/skills/lisa-confluence-to-tracker/SKILL.md +469 -0
  236. package/plugins/lisa-devin/skills/lisa-confluence-write-prd/SKILL.md +111 -0
  237. package/plugins/lisa-devin/skills/lisa-cross-pollinate/SKILL.md +175 -0
  238. package/plugins/lisa-devin/skills/lisa-debrief/SKILL.md +96 -0
  239. package/plugins/lisa-devin/skills/lisa-debrief-apply/SKILL.md +106 -0
  240. package/plugins/lisa-devin/skills/lisa-delivery-effectiveness/SKILL.md +62 -0
  241. package/plugins/lisa-devin/skills/lisa-design-intake/SKILL.md +198 -0
  242. package/plugins/lisa-devin/skills/lisa-detect-tooling/SKILL.md +110 -0
  243. package/plugins/lisa-devin/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
  244. package/plugins/lisa-devin/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +658 -0
  245. package/plugins/lisa-devin/skills/lisa-doctor/SKILL.md +743 -0
  246. package/plugins/lisa-devin/skills/lisa-drive-pr-to-merge/SKILL.md +1587 -0
  247. package/plugins/lisa-devin/skills/lisa-epic-triage/SKILL.md +28 -0
  248. package/plugins/lisa-devin/skills/lisa-evaluation-suite/SKILL.md +63 -0
  249. package/plugins/lisa-devin/skills/lisa-exploratory-qa/SKILL.md +185 -0
  250. package/plugins/lisa-devin/skills/lisa-fix-linter-error/SKILL.md +45 -0
  251. package/plugins/lisa-devin/skills/lisa-generate-claude-remote-build-script/SKILL.md +235 -0
  252. package/plugins/lisa-devin/skills/lisa-git-commit/SKILL.md +53 -0
  253. package/plugins/lisa-devin/skills/lisa-git-prune/SKILL.md +35 -0
  254. package/plugins/lisa-devin/skills/lisa-git-submit-pr/SKILL.md +170 -0
  255. package/plugins/lisa-devin/skills/lisa-github-add-journey/SKILL.md +118 -0
  256. package/plugins/lisa-devin/skills/lisa-github-build-intake/SKILL.md +745 -0
  257. package/plugins/lisa-devin/skills/lisa-github-claim/SKILL.md +27 -0
  258. package/plugins/lisa-devin/skills/lisa-github-create/SKILL.md +105 -0
  259. package/plugins/lisa-devin/skills/lisa-github-evidence/SKILL.md +120 -0
  260. package/plugins/lisa-devin/skills/lisa-github-journey/SKILL.md +134 -0
  261. package/plugins/lisa-devin/skills/lisa-github-prd-intake/SKILL.md +424 -0
  262. package/plugins/lisa-devin/skills/lisa-github-project-v2/SKILL.md +227 -0
  263. package/plugins/lisa-devin/skills/lisa-github-read-issue/SKILL.md +305 -0
  264. package/plugins/lisa-devin/skills/lisa-github-sync/SKILL.md +201 -0
  265. package/plugins/lisa-devin/skills/lisa-github-to-tracker/SKILL.md +456 -0
  266. package/plugins/lisa-devin/skills/lisa-github-validate-issue/SKILL.md +598 -0
  267. package/plugins/lisa-devin/skills/lisa-github-verify/SKILL.md +60 -0
  268. package/plugins/lisa-devin/skills/lisa-github-write-issue/SKILL.md +531 -0
  269. package/plugins/lisa-devin/skills/lisa-github-write-prd/SKILL.md +156 -0
  270. package/plugins/lisa-devin/skills/lisa-health/SKILL.md +142 -0
  271. package/plugins/lisa-devin/skills/lisa-health-drift-cron/SKILL.md +80 -0
  272. package/plugins/lisa-devin/skills/lisa-implement/SKILL.md +372 -0
  273. package/plugins/lisa-devin/skills/lisa-improve-code-complexity/SKILL.md +44 -0
  274. package/plugins/lisa-devin/skills/lisa-improve-harness/SKILL.md +487 -0
  275. package/plugins/lisa-devin/skills/lisa-improve-max-lines/SKILL.md +45 -0
  276. package/plugins/lisa-devin/skills/lisa-improve-max-lines-per-function/SKILL.md +46 -0
  277. package/plugins/lisa-devin/skills/lisa-improve-test-coverage/SKILL.md +47 -0
  278. package/plugins/lisa-devin/skills/lisa-improve-tests/SKILL.md +50 -0
  279. package/plugins/lisa-devin/skills/lisa-intake/SKILL.md +208 -0
  280. package/plugins/lisa-devin/skills/lisa-intake-explain/SKILL.md +279 -0
  281. package/plugins/lisa-devin/skills/lisa-jam-access/SKILL.md +128 -0
  282. package/plugins/lisa-devin/skills/lisa-jira-add-journey/SKILL.md +139 -0
  283. package/plugins/lisa-devin/skills/lisa-jira-build-intake/SKILL.md +556 -0
  284. package/plugins/lisa-devin/skills/lisa-jira-claim/SKILL.md +27 -0
  285. package/plugins/lisa-devin/skills/lisa-jira-create/SKILL.md +157 -0
  286. package/plugins/lisa-devin/skills/lisa-jira-evidence/SKILL.md +106 -0
  287. package/plugins/lisa-devin/skills/lisa-jira-evidence/scripts/post-evidence.sh +262 -0
  288. package/plugins/lisa-devin/skills/lisa-jira-journey/SKILL.md +168 -0
  289. package/plugins/lisa-devin/skills/lisa-jira-journey/scripts/generate-templates.py +239 -0
  290. package/plugins/lisa-devin/skills/lisa-jira-journey/scripts/parse-plan.py +570 -0
  291. package/plugins/lisa-devin/skills/lisa-jira-read-ticket/SKILL.md +199 -0
  292. package/plugins/lisa-devin/skills/lisa-jira-read-ticket/scripts/download-attachment.sh +211 -0
  293. package/plugins/lisa-devin/skills/lisa-jira-sync/SKILL.md +117 -0
  294. package/plugins/lisa-devin/skills/lisa-jira-validate-ticket/SKILL.md +525 -0
  295. package/plugins/lisa-devin/skills/lisa-jira-verify/SKILL.md +61 -0
  296. package/plugins/lisa-devin/skills/lisa-jira-write-ticket/SKILL.md +440 -0
  297. package/plugins/lisa-devin/skills/lisa-jsdoc-best-practices/SKILL.md +491 -0
  298. package/plugins/lisa-devin/skills/lisa-kane-browser/SKILL.md +80 -0
  299. package/plugins/lisa-devin/skills/lisa-learnings-audit/SKILL.md +490 -0
  300. package/plugins/lisa-devin/skills/lisa-linear-access/SKILL.md +483 -0
  301. package/plugins/lisa-devin/skills/lisa-linear-add-journey/SKILL.md +109 -0
  302. package/plugins/lisa-devin/skills/lisa-linear-build-intake/SKILL.md +548 -0
  303. package/plugins/lisa-devin/skills/lisa-linear-claim/SKILL.md +28 -0
  304. package/plugins/lisa-devin/skills/lisa-linear-create/SKILL.md +150 -0
  305. package/plugins/lisa-devin/skills/lisa-linear-evidence/SKILL.md +124 -0
  306. package/plugins/lisa-devin/skills/lisa-linear-journey/SKILL.md +147 -0
  307. package/plugins/lisa-devin/skills/lisa-linear-prd-intake/SKILL.md +411 -0
  308. package/plugins/lisa-devin/skills/lisa-linear-read-issue/SKILL.md +201 -0
  309. package/plugins/lisa-devin/skills/lisa-linear-sync/SKILL.md +164 -0
  310. package/plugins/lisa-devin/skills/lisa-linear-to-tracker/SKILL.md +454 -0
  311. package/plugins/lisa-devin/skills/lisa-linear-validate-issue/SKILL.md +504 -0
  312. package/plugins/lisa-devin/skills/lisa-linear-verify/SKILL.md +65 -0
  313. package/plugins/lisa-devin/skills/lisa-linear-write-issue/SKILL.md +448 -0
  314. package/plugins/lisa-devin/skills/lisa-linear-write-prd/SKILL.md +98 -0
  315. package/plugins/lisa-devin/skills/lisa-monitor/SKILL.md +224 -0
  316. package/plugins/lisa-devin/skills/lisa-nightly-add-test-coverage/SKILL.md +40 -0
  317. package/plugins/lisa-devin/skills/lisa-nightly-improve-tests/SKILL.md +29 -0
  318. package/plugins/lisa-devin/skills/lisa-nightly-lower-code-complexity/SKILL.md +42 -0
  319. package/plugins/lisa-devin/skills/lisa-notion-access/SKILL.md +301 -0
  320. package/plugins/lisa-devin/skills/lisa-notion-prd-intake/SKILL.md +360 -0
  321. package/plugins/lisa-devin/skills/lisa-notion-to-tracker/SKILL.md +467 -0
  322. package/plugins/lisa-devin/skills/lisa-notion-write-prd/SKILL.md +111 -0
  323. package/plugins/lisa-devin/skills/lisa-parity-code-review/SKILL.md +83 -0
  324. package/plugins/lisa-devin/skills/lisa-parity-code-simplifier/SKILL.md +76 -0
  325. package/plugins/lisa-devin/skills/lisa-parity-coderabbit/SKILL.md +87 -0
  326. package/plugins/lisa-devin/skills/lisa-parity-sentry-sdk-setup/SKILL.md +375 -0
  327. package/plugins/lisa-devin/skills/lisa-parity-sentry-seer/SKILL.md +168 -0
  328. package/plugins/lisa-devin/skills/lisa-parity-skill-creator/SKILL.md +155 -0
  329. package/plugins/lisa-devin/skills/lisa-performance-review/SKILL.md +94 -0
  330. package/plugins/lisa-devin/skills/lisa-persist-learning/SKILL.md +247 -0
  331. package/plugins/lisa-devin/skills/lisa-plan/SKILL.md +64 -0
  332. package/plugins/lisa-devin/skills/lisa-plugin-sync-explain/SKILL.md +53 -0
  333. package/plugins/lisa-devin/skills/lisa-posthog-access/SKILL.md +140 -0
  334. package/plugins/lisa-devin/skills/lisa-prd-backlink/SKILL.md +274 -0
  335. package/plugins/lisa-devin/skills/lisa-prd-source-write/SKILL.md +104 -0
  336. package/plugins/lisa-devin/skills/lisa-prd-ticket-coverage/SKILL.md +187 -0
  337. package/plugins/lisa-devin/skills/lisa-product-walkthrough/SKILL.md +106 -0
  338. package/plugins/lisa-devin/skills/lisa-project-ideation/SKILL.md +417 -0
  339. package/plugins/lisa-devin/skills/lisa-project-ideation/examples/evidence-card-format.md +21 -0
  340. package/plugins/lisa-devin/skills/lisa-project-ideation/examples/host-project-only.md +22 -0
  341. package/plugins/lisa-devin/skills/lisa-project-ideation/examples/idempotency-verification-harness.md +57 -0
  342. package/plugins/lisa-devin/skills/lisa-project-ideation/examples/public-external-inspiration.md +22 -0
  343. package/plugins/lisa-devin/skills/lisa-project-ideation/examples/unavailable-data-rejection.md +22 -0
  344. package/plugins/lisa-devin/skills/lisa-pull-request-review/SKILL.md +132 -0
  345. package/plugins/lisa-devin/skills/lisa-quality-review/SKILL.md +63 -0
  346. package/plugins/lisa-devin/skills/lisa-queue-status/SKILL.md +215 -0
  347. package/plugins/lisa-devin/skills/lisa-remote-dispatch/SKILL.md +109 -0
  348. package/plugins/lisa-devin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +535 -0
  349. package/plugins/lisa-devin/skills/lisa-repair-intake/SKILL.md +1621 -0
  350. package/plugins/lisa-devin/skills/lisa-reproduce-bug/SKILL.md +75 -0
  351. package/plugins/lisa-devin/skills/lisa-research/SKILL.md +92 -0
  352. package/plugins/lisa-devin/skills/lisa-review-implementation/SKILL.md +209 -0
  353. package/plugins/lisa-devin/skills/lisa-review-local/SKILL.md +141 -0
  354. package/plugins/lisa-devin/skills/lisa-rework-triage/SKILL.md +206 -0
  355. package/plugins/lisa-devin/skills/lisa-root-cause-analysis/SKILL.md +135 -0
  356. package/plugins/lisa-devin/skills/lisa-secrets-access/SKILL.md +430 -0
  357. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +433 -0
  358. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +288 -0
  359. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +273 -0
  360. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/envfile.mjs +80 -0
  361. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/inspect-vault.mjs +142 -0
  362. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +1085 -0
  363. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/note-format.mjs +179 -0
  364. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/owner.mjs +96 -0
  365. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +342 -0
  366. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/prompt-secret.mjs +79 -0
  367. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/providers.mjs +805 -0
  368. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/publish-aws-bootstrap.mjs +680 -0
  369. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/read-secret-note.mjs +105 -0
  370. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/resolve-expo-token.mjs +90 -0
  371. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +285 -0
  372. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +297 -0
  373. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  374. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/surfaces.mjs +480 -0
  375. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +510 -0
  376. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +63 -0
  377. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +550 -0
  378. package/plugins/lisa-devin/skills/lisa-security-review/SKILL.md +124 -0
  379. package/plugins/lisa-devin/skills/lisa-security-zap-scan/SKILL.md +53 -0
  380. package/plugins/lisa-devin/skills/lisa-sentry-access/SKILL.md +140 -0
  381. package/plugins/lisa-devin/skills/lisa-setup-atlassian/SKILL.md +407 -0
  382. package/plugins/lisa-devin/skills/lisa-setup-automations/SKILL.md +430 -0
  383. package/plugins/lisa-devin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +288 -0
  384. package/plugins/lisa-devin/skills/lisa-setup-confluence/SKILL.md +254 -0
  385. package/plugins/lisa-devin/skills/lisa-setup-github/SKILL.md +319 -0
  386. package/plugins/lisa-devin/skills/lisa-setup-github-repo/SKILL.md +128 -0
  387. package/plugins/lisa-devin/skills/lisa-setup-jira/SKILL.md +198 -0
  388. package/plugins/lisa-devin/skills/lisa-setup-kane/SKILL.md +96 -0
  389. package/plugins/lisa-devin/skills/lisa-setup-linear/SKILL.md +375 -0
  390. package/plugins/lisa-devin/skills/lisa-setup-local-env/SKILL.md +95 -0
  391. package/plugins/lisa-devin/skills/lisa-setup-local-env/scripts/local-env.mjs +196 -0
  392. package/plugins/lisa-devin/skills/lisa-setup-notion/SKILL.md +376 -0
  393. package/plugins/lisa-devin/skills/lisa-setup-remote-aws/SKILL.md +149 -0
  394. package/plugins/lisa-devin/skills/lisa-setup-remote-env/SKILL.md +382 -0
  395. package/plugins/lisa-devin/skills/lisa-setup-remote-env/assets/materialized-env-authority.mjs +262 -0
  396. package/plugins/lisa-devin/skills/lisa-setup-remote-env/assets/session-start.sh +87 -0
  397. package/plugins/lisa-devin/skills/lisa-setup-remote-env/assets/setup.sh +216 -0
  398. package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/environment.mjs +227 -0
  399. package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +378 -0
  400. package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +1632 -0
  401. package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  402. package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +448 -0
  403. package/plugins/lisa-devin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +250 -0
  404. package/plugins/lisa-devin/skills/lisa-setup-sonar/SKILL.md +153 -0
  405. package/plugins/lisa-devin/skills/lisa-setup-workstation/SKILL.md +143 -0
  406. package/plugins/lisa-devin/skills/lisa-setup-workstation/scripts/bounded-child.mjs +168 -0
  407. package/plugins/lisa-devin/skills/lisa-setup-workstation/scripts/catalogue.mjs +293 -0
  408. package/plugins/lisa-devin/skills/lisa-setup-workstation/scripts/cli.mjs +333 -0
  409. package/plugins/lisa-devin/skills/lisa-setup-workstation/scripts/workstation.mjs +511 -0
  410. package/plugins/lisa-devin/skills/lisa-sonarcloud-access/SKILL.md +202 -0
  411. package/plugins/lisa-devin/skills/lisa-spec-conformance/SKILL.md +205 -0
  412. package/plugins/lisa-devin/skills/lisa-starter-sync/SKILL.md +35 -0
  413. package/plugins/lisa-devin/skills/lisa-sync-down/SKILL.md +209 -0
  414. package/plugins/lisa-devin/skills/lisa-task-decomposition/SKILL.md +195 -0
  415. package/plugins/lisa-devin/skills/lisa-task-triage/SKILL.md +23 -0
  416. package/plugins/lisa-devin/skills/lisa-tdd-implementation/SKILL.md +91 -0
  417. package/plugins/lisa-devin/skills/lisa-tear-down-automations/SKILL.md +85 -0
  418. package/plugins/lisa-devin/skills/lisa-test-strategy/SKILL.md +66 -0
  419. package/plugins/lisa-devin/skills/lisa-ticket-triage/SKILL.md +214 -0
  420. package/plugins/lisa-devin/skills/lisa-track/SKILL.md +129 -0
  421. package/plugins/lisa-devin/skills/lisa-tracker-add-journey/SKILL.md +28 -0
  422. package/plugins/lisa-devin/skills/lisa-tracker-build-intake/SKILL.md +134 -0
  423. package/plugins/lisa-devin/skills/lisa-tracker-claim/SKILL.md +35 -0
  424. package/plugins/lisa-devin/skills/lisa-tracker-create/SKILL.md +28 -0
  425. package/plugins/lisa-devin/skills/lisa-tracker-evidence/SKILL.md +63 -0
  426. package/plugins/lisa-devin/skills/lisa-tracker-journey/SKILL.md +27 -0
  427. package/plugins/lisa-devin/skills/lisa-tracker-read/SKILL.md +28 -0
  428. package/plugins/lisa-devin/skills/lisa-tracker-source-artifacts/SKILL.md +111 -0
  429. package/plugins/lisa-devin/skills/lisa-tracker-sync/SKILL.md +150 -0
  430. package/plugins/lisa-devin/skills/lisa-tracker-validate/SKILL.md +37 -0
  431. package/plugins/lisa-devin/skills/lisa-tracker-verify/SKILL.md +29 -0
  432. package/plugins/lisa-devin/skills/lisa-tracker-write/SKILL.md +55 -0
  433. package/plugins/lisa-devin/skills/lisa-usage-accounting/SKILL.md +205 -0
  434. package/plugins/lisa-devin/skills/lisa-usage-accounting/references/effectiveness.md +63 -0
  435. package/plugins/lisa-devin/skills/lisa-use-the-product/SKILL.md +86 -0
  436. package/plugins/lisa-devin/skills/lisa-validate-tracker-mapping/SKILL.md +467 -0
  437. package/plugins/lisa-devin/skills/lisa-verification-lifecycle/SKILL.md +384 -0
  438. package/plugins/lisa-devin/skills/lisa-verify/SKILL.md +55 -0
  439. package/plugins/lisa-devin/skills/lisa-verify-prd/SKILL.md +394 -0
  440. package/plugins/lisa-devin/skills/lisa-verify-workflow-change/SKILL.md +154 -0
  441. package/plugins/lisa-devin/skills/lisa-wiki-install/SKILL.md +101 -0
  442. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  443. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  444. package/plugins/lisa-expo-agy/plugin.json +1 -1
  445. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  446. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  447. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +11 -0
  448. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +8 -0
  449. package/plugins/lisa-expo-devin/.mcp.json +8 -0
  450. package/plugins/lisa-expo-devin/THIRD-PARTY-NOTICES.md +84 -0
  451. package/plugins/lisa-expo-devin/agents/ops-specialist.md +124 -0
  452. package/plugins/lisa-expo-devin/hooks/devin-tool-name-shim.sh +30 -0
  453. package/plugins/lisa-expo-devin/hooks/register-maestro-mcp.sh +84 -0
  454. package/plugins/lisa-expo-devin/hooks.json +14 -0
  455. package/plugins/lisa-expo-devin/skills/add-app-clip/SKILL.md +280 -0
  456. package/plugins/lisa-expo-devin/skills/add-app-clip/references/native-module.md +96 -0
  457. package/plugins/lisa-expo-devin/skills/apollo-client/SKILL.md +238 -0
  458. package/plugins/lisa-expo-devin/skills/apollo-client/references/mutation-patterns.md +360 -0
  459. package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/SKILL.md +369 -0
  460. package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/references/atomic-levels.md +417 -0
  461. package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/references/folder-structure.md +257 -0
  462. package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/references/gluestack-mapping.md +233 -0
  463. package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  464. package/plugins/lisa-expo-devin/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +438 -0
  465. package/plugins/lisa-expo-devin/skills/building-native-ui/SKILL.md +321 -0
  466. package/plugins/lisa-expo-devin/skills/building-native-ui/references/animations.md +220 -0
  467. package/plugins/lisa-expo-devin/skills/building-native-ui/references/controls.md +272 -0
  468. package/plugins/lisa-expo-devin/skills/building-native-ui/references/form-sheet.md +253 -0
  469. package/plugins/lisa-expo-devin/skills/building-native-ui/references/gradients.md +106 -0
  470. package/plugins/lisa-expo-devin/skills/building-native-ui/references/icons.md +213 -0
  471. package/plugins/lisa-expo-devin/skills/building-native-ui/references/media.md +198 -0
  472. package/plugins/lisa-expo-devin/skills/building-native-ui/references/route-structure.md +229 -0
  473. package/plugins/lisa-expo-devin/skills/building-native-ui/references/search.md +248 -0
  474. package/plugins/lisa-expo-devin/skills/building-native-ui/references/storage.md +121 -0
  475. package/plugins/lisa-expo-devin/skills/building-native-ui/references/tabs.md +433 -0
  476. package/plugins/lisa-expo-devin/skills/building-native-ui/references/toolbar-and-headers.md +284 -0
  477. package/plugins/lisa-expo-devin/skills/building-native-ui/references/visual-effects.md +197 -0
  478. package/plugins/lisa-expo-devin/skills/building-native-ui/references/webgpu-three.md +605 -0
  479. package/plugins/lisa-expo-devin/skills/building-native-ui/references/zoom-transitions.md +158 -0
  480. package/plugins/lisa-expo-devin/skills/container-view-pattern/SKILL.md +355 -0
  481. package/plugins/lisa-expo-devin/skills/container-view-pattern/references/examples.md +749 -0
  482. package/plugins/lisa-expo-devin/skills/container-view-pattern/references/patterns.md +318 -0
  483. package/plugins/lisa-expo-devin/skills/container-view-pattern/scripts/create_component.py +200 -0
  484. package/plugins/lisa-expo-devin/skills/container-view-pattern/scripts/validate_component.py +332 -0
  485. package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/SKILL.md +268 -0
  486. package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/references/common-issues.md +619 -0
  487. package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/references/file-extensions.md +340 -0
  488. package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/references/platform-api.md +276 -0
  489. package/plugins/lisa-expo-devin/skills/cross-platform-compatibility/scripts/validate_cross_platform.py +416 -0
  490. package/plugins/lisa-expo-devin/skills/directory-structure/SKILL.md +214 -0
  491. package/plugins/lisa-expo-devin/skills/directory-structure/scripts/validate_structure.py +454 -0
  492. package/plugins/lisa-expo-devin/skills/e2e-coverage-gaps/SKILL.md +117 -0
  493. package/plugins/lisa-expo-devin/skills/eas-update-insights/SKILL.md +228 -0
  494. package/plugins/lisa-expo-devin/skills/eas-update-insights/references/channel-insights-schema.md +47 -0
  495. package/plugins/lisa-expo-devin/skills/eas-update-insights/references/update-insights-schema.md +69 -0
  496. package/plugins/lisa-expo-devin/skills/expo-api-routes/SKILL.md +369 -0
  497. package/plugins/lisa-expo-devin/skills/expo-brownfield/SKILL.md +54 -0
  498. package/plugins/lisa-expo-devin/skills/expo-brownfield/references/brownfield-integrated.md +526 -0
  499. package/plugins/lisa-expo-devin/skills/expo-brownfield/references/brownfield-isolated.md +402 -0
  500. package/plugins/lisa-expo-devin/skills/expo-brownfield/references/comparison.md +63 -0
  501. package/plugins/lisa-expo-devin/skills/expo-brownfield/references/troubleshooting.md +88 -0
  502. package/plugins/lisa-expo-devin/skills/expo-cicd-workflows/SKILL.md +92 -0
  503. package/plugins/lisa-expo-devin/skills/expo-cicd-workflows/scripts/fetch.js +113 -0
  504. package/plugins/lisa-expo-devin/skills/expo-cicd-workflows/scripts/package.json +11 -0
  505. package/plugins/lisa-expo-devin/skills/expo-cicd-workflows/scripts/validate.js +85 -0
  506. package/plugins/lisa-expo-devin/skills/expo-deployment/SKILL.md +193 -0
  507. package/plugins/lisa-expo-devin/skills/expo-deployment/references/app-store-metadata.md +479 -0
  508. package/plugins/lisa-expo-devin/skills/expo-deployment/references/ios-app-store.md +355 -0
  509. package/plugins/lisa-expo-devin/skills/expo-deployment/references/play-store.md +264 -0
  510. package/plugins/lisa-expo-devin/skills/expo-deployment/references/testflight.md +58 -0
  511. package/plugins/lisa-expo-devin/skills/expo-deployment/references/workflows.md +200 -0
  512. package/plugins/lisa-expo-devin/skills/expo-dev-client/SKILL.md +164 -0
  513. package/plugins/lisa-expo-devin/skills/expo-env-config/SKILL.md +309 -0
  514. package/plugins/lisa-expo-devin/skills/expo-env-config/references/validation-patterns.md +417 -0
  515. package/plugins/lisa-expo-devin/skills/expo-module/SKILL.md +141 -0
  516. package/plugins/lisa-expo-devin/skills/expo-module/references/config-plugin.md +90 -0
  517. package/plugins/lisa-expo-devin/skills/expo-module/references/create-expo-module.md +206 -0
  518. package/plugins/lisa-expo-devin/skills/expo-module/references/lifecycle.md +127 -0
  519. package/plugins/lisa-expo-devin/skills/expo-module/references/module-config.md +48 -0
  520. package/plugins/lisa-expo-devin/skills/expo-module/references/native-module.md +286 -0
  521. package/plugins/lisa-expo-devin/skills/expo-module/references/native-view.md +171 -0
  522. package/plugins/lisa-expo-devin/skills/expo-router-best-practices/SKILL.md +432 -0
  523. package/plugins/lisa-expo-devin/skills/expo-router-best-practices/references/official-docs.md +290 -0
  524. package/plugins/lisa-expo-devin/skills/expo-router-best-practices/references/static-export-verification.md +54 -0
  525. package/plugins/lisa-expo-devin/skills/expo-router-best-practices/scripts/generate-route.py +171 -0
  526. package/plugins/lisa-expo-devin/skills/expo-tailwind-setup/SKILL.md +487 -0
  527. package/plugins/lisa-expo-devin/skills/expo-ui-jetpack-compose/SKILL.md +40 -0
  528. package/plugins/lisa-expo-devin/skills/expo-ui-swift-ui/SKILL.md +39 -0
  529. package/plugins/lisa-expo-devin/skills/gluestack-nativewind/SKILL.md +419 -0
  530. package/plugins/lisa-expo-devin/skills/gluestack-nativewind/references/color-tokens.md +343 -0
  531. package/plugins/lisa-expo-devin/skills/gluestack-nativewind/references/component-mapping.md +307 -0
  532. package/plugins/lisa-expo-devin/skills/gluestack-nativewind/references/spacing-scale.md +300 -0
  533. package/plugins/lisa-expo-devin/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  534. package/plugins/lisa-expo-devin/skills/gluestack-nativewind/scripts/validate_styling.py +424 -0
  535. package/plugins/lisa-expo-devin/skills/jira-add-journey/SKILL.md +140 -0
  536. package/plugins/lisa-expo-devin/skills/jira-create/SKILL.md +161 -0
  537. package/plugins/lisa-expo-devin/skills/jira-evidence/SKILL.md +84 -0
  538. package/plugins/lisa-expo-devin/skills/jira-evidence/scripts/post-evidence.sh +262 -0
  539. package/plugins/lisa-expo-devin/skills/jira-journey/SKILL.md +196 -0
  540. package/plugins/lisa-expo-devin/skills/jira-journey/scripts/generate-templates.py +281 -0
  541. package/plugins/lisa-expo-devin/skills/jira-journey/scripts/parse-plan.py +400 -0
  542. package/plugins/lisa-expo-devin/skills/jira-verify/SKILL.md +27 -0
  543. package/plugins/lisa-expo-devin/skills/local-state/SKILL.md +362 -0
  544. package/plugins/lisa-expo-devin/skills/local-state/references/async-storage.md +505 -0
  545. package/plugins/lisa-expo-devin/skills/local-state/references/persistence-patterns.md +711 -0
  546. package/plugins/lisa-expo-devin/skills/local-state/references/reactive-variables.md +446 -0
  547. package/plugins/lisa-expo-devin/skills/maestro-mcp-setup/SKILL.md +144 -0
  548. package/plugins/lisa-expo-devin/skills/native-data-fetching/SKILL.md +507 -0
  549. package/plugins/lisa-expo-devin/skills/native-data-fetching/references/expo-router-loaders.md +344 -0
  550. package/plugins/lisa-expo-devin/skills/ops-browser-uat/SKILL.md +124 -0
  551. package/plugins/lisa-expo-devin/skills/ops-check-logs/SKILL.md +217 -0
  552. package/plugins/lisa-expo-devin/skills/ops-db-ops/SKILL.md +214 -0
  553. package/plugins/lisa-expo-devin/skills/ops-deploy/SKILL.md +123 -0
  554. package/plugins/lisa-expo-devin/skills/ops-monitor-errors/SKILL.md +99 -0
  555. package/plugins/lisa-expo-devin/skills/ops-performance/SKILL.md +165 -0
  556. package/plugins/lisa-expo-devin/skills/ops-run-local/SKILL.md +169 -0
  557. package/plugins/lisa-expo-devin/skills/ops-verify-health/SKILL.md +101 -0
  558. package/plugins/lisa-expo-devin/skills/owasp-zap/SKILL.md +61 -0
  559. package/plugins/lisa-expo-devin/skills/play-store-access/SKILL.md +226 -0
  560. package/plugins/lisa-expo-devin/skills/playwright-ci-debugging/SKILL.md +140 -0
  561. package/plugins/lisa-expo-devin/skills/playwright-selectors/SKILL.md +496 -0
  562. package/plugins/lisa-expo-devin/skills/reduce-complexity/SKILL.md +251 -0
  563. package/plugins/lisa-expo-devin/skills/reduce-complexity/references/extraction-strategies.md +456 -0
  564. package/plugins/lisa-expo-devin/skills/reduce-complexity/references/refactoring-patterns.md +557 -0
  565. package/plugins/lisa-expo-devin/skills/testing-library/SKILL.md +314 -0
  566. package/plugins/lisa-expo-devin/skills/testing-library/references/async-patterns.md +420 -0
  567. package/plugins/lisa-expo-devin/skills/testing-library/references/expo-router-testing.md +556 -0
  568. package/plugins/lisa-expo-devin/skills/testing-library/references/mocking-patterns.md +612 -0
  569. package/plugins/lisa-expo-devin/skills/testing-library/references/query-priority.md +291 -0
  570. package/plugins/lisa-expo-devin/skills/upgrading-expo/SKILL.md +134 -0
  571. package/plugins/lisa-expo-devin/skills/upgrading-expo/references/expo-av-to-audio.md +132 -0
  572. package/plugins/lisa-expo-devin/skills/upgrading-expo/references/expo-av-to-video.md +160 -0
  573. package/plugins/lisa-expo-devin/skills/upgrading-expo/references/native-tabs.md +124 -0
  574. package/plugins/lisa-expo-devin/skills/upgrading-expo/references/new-architecture.md +79 -0
  575. package/plugins/lisa-expo-devin/skills/upgrading-expo/references/react-19.md +79 -0
  576. package/plugins/lisa-expo-devin/skills/upgrading-expo/references/react-compiler.md +59 -0
  577. package/plugins/lisa-expo-devin/skills/upgrading-expo/references/react-navigation-to-expo-router.md +61 -0
  578. package/plugins/lisa-expo-devin/skills/use-dom/SKILL.md +417 -0
  579. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  580. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  581. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  582. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  583. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  584. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +11 -0
  585. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +8 -0
  586. package/plugins/lisa-harper-fabric-devin/generated-artifact-globs.txt +5 -0
  587. package/plugins/lisa-harper-fabric-devin/hooks/block-generated-artifact-edits.sh +141 -0
  588. package/plugins/lisa-harper-fabric-devin/hooks/devin-tool-name-shim.sh +30 -0
  589. package/plugins/lisa-harper-fabric-devin/hooks/enforce-config-extensions.mjs +196 -0
  590. package/plugins/lisa-harper-fabric-devin/hooks/enforce-config-extensions.sh +46 -0
  591. package/plugins/lisa-harper-fabric-devin/hooks/inject-rules.sh +27 -0
  592. package/plugins/lisa-harper-fabric-devin/hooks.json +36 -0
  593. package/plugins/lisa-harper-fabric-devin/rules/eager/harper-fabric.md +31 -0
  594. package/plugins/lisa-harper-fabric-devin/rules/reference/harper-fabric.md +52 -0
  595. package/plugins/lisa-harper-fabric-devin/skills/e2e-coverage-gaps/SKILL.md +117 -0
  596. package/plugins/lisa-harper-fabric-devin/skills/harper-auth/SKILL.md +368 -0
  597. package/plugins/lisa-harper-fabric-devin/skills/harper-build-and-deploy/SKILL.md +198 -0
  598. package/plugins/lisa-harper-fabric-devin/skills/harper-caching/SKILL.md +265 -0
  599. package/plugins/lisa-harper-fabric-devin/skills/harper-component-model/SKILL.md +87 -0
  600. package/plugins/lisa-harper-fabric-devin/skills/harper-config-yaml/SKILL.md +283 -0
  601. package/plugins/lisa-harper-fabric-devin/skills/harper-operations/SKILL.md +239 -0
  602. package/plugins/lisa-harper-fabric-devin/skills/harper-realtime/SKILL.md +187 -0
  603. package/plugins/lisa-harper-fabric-devin/skills/harper-resources/SKILL.md +142 -0
  604. package/plugins/lisa-harper-fabric-devin/skills/harper-rest-queries/SKILL.md +403 -0
  605. package/plugins/lisa-harper-fabric-devin/skills/harper-schema-graphql/SKILL.md +123 -0
  606. package/plugins/lisa-harper-fabric-devin/skills/harper-testing/SKILL.md +254 -0
  607. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  608. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  609. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  610. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  611. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  612. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +11 -0
  613. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +8 -0
  614. package/plugins/lisa-nestjs-devin/hooks/block-migration-edits.sh +125 -0
  615. package/plugins/lisa-nestjs-devin/hooks/devin-tool-name-shim.sh +30 -0
  616. package/plugins/lisa-nestjs-devin/hooks/lisa-edit-gate.sh +152 -0
  617. package/plugins/lisa-nestjs-devin/hooks.json +15 -0
  618. package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/SKILL.md +176 -0
  619. package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/advanced-features.md +527 -0
  620. package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/project-patterns.md +531 -0
  621. package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/quick-start.md +257 -0
  622. package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/resolvers-mutations.md +413 -0
  623. package/plugins/lisa-nestjs-devin/skills/nestjs-graphql/references/types-scalars.md +513 -0
  624. package/plugins/lisa-nestjs-devin/skills/nestjs-rules/SKILL.md +560 -0
  625. package/plugins/lisa-nestjs-devin/skills/typeorm-patterns/SKILL.md +275 -0
  626. package/plugins/lisa-nestjs-devin/skills/typeorm-patterns/references/configuration-patterns.md +487 -0
  627. package/plugins/lisa-nestjs-devin/skills/typeorm-patterns/references/entity-patterns.md +450 -0
  628. package/plugins/lisa-nestjs-devin/skills/typeorm-patterns/references/observability-patterns.md +536 -0
  629. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  630. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  631. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  632. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  633. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  634. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +8 -0
  635. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +8 -0
  636. package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-repo-topic/SKILL.md +163 -0
  637. package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-repo-topic/references/repo-topic-config.md +185 -0
  638. package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-staff/SKILL.md +175 -0
  639. package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-staff/references/platform-routing.md +123 -0
  640. package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-connect-staff/references/prompts.md +78 -0
  641. package/plugins/lisa-openclaw-devin/skills/lisa-openclaw-setup/SKILL.md +141 -0
  642. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  643. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  644. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  645. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  646. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  647. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +11 -0
  648. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +8 -0
  649. package/plugins/lisa-phaser-devin/agents/accessibility-advocate.md +55 -0
  650. package/plugins/lisa-phaser-devin/agents/art-director.md +90 -0
  651. package/plugins/lisa-phaser-devin/agents/audio-director.md +65 -0
  652. package/plugins/lisa-phaser-devin/agents/combat-designer.md +61 -0
  653. package/plugins/lisa-phaser-devin/agents/economy-designer.md +59 -0
  654. package/plugins/lisa-phaser-devin/agents/game-designer.md +62 -0
  655. package/plugins/lisa-phaser-devin/agents/game-feel-specialist.md +56 -0
  656. package/plugins/lisa-phaser-devin/agents/level-designer.md +59 -0
  657. package/plugins/lisa-phaser-devin/agents/localization-manager.md +61 -0
  658. package/plugins/lisa-phaser-devin/agents/marketing-strategist.md +59 -0
  659. package/plugins/lisa-phaser-devin/agents/monetization-designer.md +59 -0
  660. package/plugins/lisa-phaser-devin/agents/narrative-designer.md +56 -0
  661. package/plugins/lisa-phaser-devin/agents/onboarding-advocate.md +59 -0
  662. package/plugins/lisa-phaser-devin/agents/player-advocate.md +57 -0
  663. package/plugins/lisa-phaser-devin/agents/producer.md +61 -0
  664. package/plugins/lisa-phaser-devin/agents/product-analyst.md +62 -0
  665. package/plugins/lisa-phaser-devin/agents/publisher.md +59 -0
  666. package/plugins/lisa-phaser-devin/agents/qa-playtester.md +57 -0
  667. package/plugins/lisa-phaser-devin/agents/target-player.md +52 -0
  668. package/plugins/lisa-phaser-devin/agents/ux-ui-designer.md +60 -0
  669. package/plugins/lisa-phaser-devin/hooks/devin-tool-name-shim.sh +30 -0
  670. package/plugins/lisa-phaser-devin/hooks/inject-rules.sh +27 -0
  671. package/plugins/lisa-phaser-devin/hooks.json +14 -0
  672. package/plugins/lisa-phaser-devin/rules/eager/phaser.md +44 -0
  673. package/plugins/lisa-phaser-devin/rules/reference/phaser.md +184 -0
  674. package/plugins/lisa-phaser-devin/skills/phaser-accessibility/SKILL.md +135 -0
  675. package/plugins/lisa-phaser-devin/skills/phaser-asset-pipeline/SKILL.md +222 -0
  676. package/plugins/lisa-phaser-devin/skills/phaser-asset-sourcing/SKILL.md +151 -0
  677. package/plugins/lisa-phaser-devin/skills/phaser-build-deploy/SKILL.md +153 -0
  678. package/plugins/lisa-phaser-devin/skills/phaser-i18n/SKILL.md +122 -0
  679. package/plugins/lisa-phaser-devin/skills/phaser-project-structure/SKILL.md +89 -0
  680. package/plugins/lisa-phaser-devin/skills/phaser-services/SKILL.md +208 -0
  681. package/plugins/lisa-phaser-devin/skills/phaser-testing/SKILL.md +192 -0
  682. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  683. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  684. package/plugins/lisa-rails-agy/plugin.json +1 -1
  685. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  686. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  687. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +11 -0
  688. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +8 -0
  689. package/plugins/lisa-rails-devin/agents/ops-specialist.md +226 -0
  690. package/plugins/lisa-rails-devin/hooks/devin-tool-name-shim.sh +30 -0
  691. package/plugins/lisa-rails-devin/hooks/inject-rules.sh +40 -0
  692. package/plugins/lisa-rails-devin/hooks/lisa-edit-gate.sh +152 -0
  693. package/plugins/lisa-rails-devin/hooks/rubocop-on-edit.sh +101 -0
  694. package/plugins/lisa-rails-devin/hooks/sg-scan-on-edit.sh +97 -0
  695. package/plugins/lisa-rails-devin/hooks.json +29 -0
  696. package/plugins/lisa-rails-devin/rules/eager/rails-conventions.md +23 -0
  697. package/plugins/lisa-rails-devin/rules/reference/rails-conventions.md +176 -0
  698. package/plugins/lisa-rails-devin/skills/action-controller-best-practices/SKILL.md +374 -0
  699. package/plugins/lisa-rails-devin/skills/action-view-best-practices/SKILL.md +335 -0
  700. package/plugins/lisa-rails-devin/skills/active-record-model-best-practices/SKILL.md +166 -0
  701. package/plugins/lisa-rails-devin/skills/e2e-coverage-gaps/SKILL.md +117 -0
  702. package/plugins/lisa-rails-devin/skills/fix-linter-error/SKILL.md +45 -0
  703. package/plugins/lisa-rails-devin/skills/improve-code-complexity/SKILL.md +51 -0
  704. package/plugins/lisa-rails-devin/skills/improve-max-lines/SKILL.md +46 -0
  705. package/plugins/lisa-rails-devin/skills/improve-max-lines-per-function/SKILL.md +46 -0
  706. package/plugins/lisa-rails-devin/skills/improve-test-coverage/SKILL.md +48 -0
  707. package/plugins/lisa-rails-devin/skills/jira-add-journey/SKILL.md +79 -0
  708. package/plugins/lisa-rails-devin/skills/jira-create/SKILL.md +100 -0
  709. package/plugins/lisa-rails-devin/skills/jira-evidence/SKILL.md +76 -0
  710. package/plugins/lisa-rails-devin/skills/jira-evidence/scripts/post-evidence.sh +262 -0
  711. package/plugins/lisa-rails-devin/skills/jira-journey/SKILL.md +65 -0
  712. package/plugins/lisa-rails-devin/skills/jira-verify/SKILL.md +26 -0
  713. package/plugins/lisa-rails-devin/skills/ops-check-logs/SKILL.md +191 -0
  714. package/plugins/lisa-rails-devin/skills/ops-deploy/SKILL.md +153 -0
  715. package/plugins/lisa-rails-devin/skills/ops-run-local/SKILL.md +169 -0
  716. package/plugins/lisa-rails-devin/skills/ops-verify-jobs/SKILL.md +157 -0
  717. package/plugins/lisa-rails-devin/skills/ops-verify-telemetry/SKILL.md +226 -0
  718. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  719. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  720. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  721. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  722. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  723. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +11 -0
  724. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +8 -0
  725. package/plugins/lisa-typescript-devin/hooks/block-suppress-directives.sh +141 -0
  726. package/plugins/lisa-typescript-devin/hooks/devin-tool-name-shim.sh +30 -0
  727. package/plugins/lisa-typescript-devin/hooks/format-on-edit.sh +102 -0
  728. package/plugins/lisa-typescript-devin/hooks/lint-on-edit.sh +117 -0
  729. package/plugins/lisa-typescript-devin/hooks/lisa-edit-gate.sh +152 -0
  730. package/plugins/lisa-typescript-devin/hooks/sg-scan-on-edit.sh +94 -0
  731. package/plugins/lisa-typescript-devin/hooks.json +34 -0
  732. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  733. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  734. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  735. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  736. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  737. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +8 -0
  738. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +8 -0
  739. package/plugins/lisa-wiki-devin/ci/lisa-wiki-validate.yml +33 -0
  740. package/plugins/lisa-wiki-devin/schema/lisa-wiki-config.schema.json +162 -0
  741. package/plugins/lisa-wiki-devin/schema/wiki-structure.schema.json +51 -0
  742. package/plugins/lisa-wiki-devin/scripts/_wiki-lib.mjs +204 -0
  743. package/plugins/lisa-wiki-devin/scripts/diff-guard.mjs +117 -0
  744. package/plugins/lisa-wiki-devin/scripts/ensure-gitignore.mjs +131 -0
  745. package/plugins/lisa-wiki-devin/scripts/ensure-wiki.mjs +268 -0
  746. package/plugins/lisa-wiki-devin/scripts/ingest-git.mjs +198 -0
  747. package/plugins/lisa-wiki-devin/scripts/ingest-memory.mjs +135 -0
  748. package/plugins/lisa-wiki-devin/scripts/ingest-roles.mjs +95 -0
  749. package/plugins/lisa-wiki-devin/scripts/ingest_slack_channel.py +361 -0
  750. package/plugins/lisa-wiki-devin/scripts/lib/bounded-child.mjs +159 -0
  751. package/plugins/lisa-wiki-devin/scripts/lint-wiki.mjs +513 -0
  752. package/plugins/lisa-wiki-devin/scripts/mcp-doctor.mjs +72 -0
  753. package/plugins/lisa-wiki-devin/scripts/render-contract.mjs +107 -0
  754. package/plugins/lisa-wiki-devin/scripts/rewrite-refs.mjs +144 -0
  755. package/plugins/lisa-wiki-devin/scripts/slack_oauth_user.py +179 -0
  756. package/plugins/lisa-wiki-devin/scripts/validate-config.mjs +321 -0
  757. package/plugins/lisa-wiki-devin/scripts/verify-migration.mjs +203 -0
  758. package/plugins/lisa-wiki-devin/scripts/verify-wiki-safety.mjs +126 -0
  759. package/plugins/lisa-wiki-devin/scripts/wiki-safety.mjs +378 -0
  760. package/plugins/lisa-wiki-devin/scripts/wiki-status.mjs +345 -0
  761. package/plugins/lisa-wiki-devin/skills/lisa-wiki-add-ingest/SKILL.md +34 -0
  762. package/plugins/lisa-wiki-devin/skills/lisa-wiki-add-role/SKILL.md +35 -0
  763. package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-confluence/SKILL.md +25 -0
  764. package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-docs/SKILL.md +30 -0
  765. package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-git/SKILL.md +25 -0
  766. package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-jira/SKILL.md +28 -0
  767. package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-memory/SKILL.md +28 -0
  768. package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-notion/SKILL.md +25 -0
  769. package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-roles/SKILL.md +22 -0
  770. package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-slack/SKILL.md +30 -0
  771. package/plugins/lisa-wiki-devin/skills/lisa-wiki-connector-web/SKILL.md +23 -0
  772. package/plugins/lisa-wiki-devin/skills/lisa-wiki-doctor/SKILL.md +47 -0
  773. package/plugins/lisa-wiki-devin/skills/lisa-wiki-ingest/SKILL.md +106 -0
  774. package/plugins/lisa-wiki-devin/skills/lisa-wiki-lint/SKILL.md +32 -0
  775. package/plugins/lisa-wiki-devin/skills/lisa-wiki-migrate/SKILL.md +43 -0
  776. package/plugins/lisa-wiki-devin/skills/lisa-wiki-onboard-me/SKILL.md +33 -0
  777. package/plugins/lisa-wiki-devin/skills/lisa-wiki-query/SKILL.md +40 -0
  778. package/plugins/lisa-wiki-devin/skills/lisa-wiki-setup/SKILL.md +75 -0
  779. package/plugins/lisa-wiki-devin/skills/lisa-wiki-setup-automations/SKILL.md +83 -0
  780. package/plugins/lisa-wiki-devin/skills/lisa-wiki-status/SKILL.md +47 -0
  781. package/plugins/lisa-wiki-devin/skills/lisa-wiki-tear-down-automations/SKILL.md +34 -0
  782. package/plugins/lisa-wiki-devin/skills/lisa-wiki-usage/SKILL.md +50 -0
  783. package/plugins/lisa-wiki-devin/templates/agents/role-agent.claude.md +16 -0
  784. package/plugins/lisa-wiki-devin/templates/agents/role-agent.codex.toml +15 -0
  785. package/plugins/lisa-wiki-devin/templates/index.md +17 -0
  786. package/plugins/lisa-wiki-devin/templates/llm-wiki-contract.md +72 -0
  787. package/plugins/lisa-wiki-devin/templates/log.md +8 -0
  788. package/plugins/lisa-wiki-devin/templates/page-types/architecture.md +18 -0
  789. package/plugins/lisa-wiki-devin/templates/page-types/concept.md +18 -0
  790. package/plugins/lisa-wiki-devin/templates/page-types/decision.md +18 -0
  791. package/plugins/lisa-wiki-devin/templates/page-types/entity.md +19 -0
  792. package/plugins/lisa-wiki-devin/templates/page-types/open-question.md +18 -0
  793. package/plugins/lisa-wiki-devin/templates/page-types/playbook.md +18 -0
  794. package/plugins/lisa-wiki-devin/templates/page-types/project.md +19 -0
  795. package/plugins/lisa-wiki-devin/templates/page-types/requirement.md +19 -0
  796. package/plugins/lisa-wiki-devin/templates/page-types/staff.md +26 -0
  797. package/plugins/lisa-wiki-devin/templates/start-here.md +24 -0
  798. package/plugins/lisa-wiki-devin/templates/state-readme.md +20 -0
  799. package/plugins/lisa-wiki-devin/templates/wrapper-gitignore.txt +19 -0
  800. package/scripts/build-plugins.sh +2 -2
  801. package/scripts/deployed-guard-advice.mjs +1 -0
  802. package/scripts/generate-devin-plugin-artifacts.mjs +263 -0
  803. package/scripts/lib/hook-registration-audit.mjs +3 -1
  804. package/scripts/lib/per-agent-hook-filter.mjs +174 -5
  805. package/scripts/lib/plugin-rule-citations.mjs +2 -2
@@ -0,0 +1,2603 @@
1
+ #!/usr/bin/env bash
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
+ # PreToolUse hook for Bash: refuse a direct tracker-creation command that
6
+ # declares no readiness.
7
+ #
8
+ # WHY THIS IS A HOOK AND NOT A RULE
9
+ #
10
+ # The `ready-role-filing` rule says every filing declares either
11
+ # `build_ready: true` or a `human_gate:` reason, and that filings go through
12
+ # `lisa-track` / `lisa-tracker-write`. A conformance audit of the ~13 issues
13
+ # filed during one working session found 13/13 bypassed it, with zero
14
+ # `lisa-track` invocations — eight of them filed AFTER the rule merged, several
15
+ # by the agent that wrote the rule. Over the same window `Co-Authored-By`
16
+ # compliance was 50/50, because a husky `commit-msg` hook enforces it.
17
+ #
18
+ # Prose at the EAGER-RULE rung did not bind even its own author; the executable
19
+ # control was never once violated. Lisa's `learnings-ladder` rule says
20
+ # machine-checkable knowledge belongs at EXECUTABLE-CONTROL. This is that
21
+ # promotion.
22
+ #
23
+ # WHAT IT CHECKS, AND WHY THAT AND NOT "DID YOU USE THE SKILL"
24
+ #
25
+ # A Bash-level hook cannot observe call provenance. Any provenance signal an
26
+ # agent could carry — a flag, a marker, an env var — is settable by the very
27
+ # agent being governed, so a guard built on one is theatre. What the hook CAN
28
+ # observe is the artifact: whether the command about to run produces a
29
+ # correctly declared work item. So it enforces the checkable half.
30
+ #
31
+ # A creation command is refused unless it carries a readiness declaration:
32
+ # - the project's configured build-ready role (GitHub label, JIRA/Linear
33
+ # workflow state) resolved from `.lisa.config.json`, never hard-coded;
34
+ # - on a tracker whose ready role is a STATE rather than a label, the
35
+ # `lifecycle_role: ready` declaration the access layer resolves that state
36
+ # from — because no flag on the mandated client can carry a state, and a
37
+ # guard with no satisfiable declaration is a guard that teaches lying; or
38
+ # - an explicit `[lisa-human-gate]` marker, inline or in the `--body-file`
39
+ # the create is about to submit.
40
+ #
41
+ # WHERE IT LOOKS
42
+ #
43
+ # argv, the request payload (inline, in a `--data-binary @file`, or piped in
44
+ # over stdin from the same pipeline), and the contents of any file the command
45
+ # names. The last of those is what CodySwannGT/lisa#3484 was: the guard
46
+ # inspected argv and nothing else, so `bash /path/create.sh` showed it two
47
+ # tokens and the creation was one file away. Lisa's own `parity-safety-net.sh`
48
+ # tells agents to write payloads to a file and execute the file, so complying
49
+ # with Lisa's guidance produced the bypass.
50
+ #
51
+ # That is exactly the machine-checkable content of `ready-role-filing`, and it
52
+ # lets `lisa-github-write-issue` / `lisa-jira-write-ticket` /
53
+ # `lisa-linear-write-issue` through by construction, because those writers
54
+ # always stamp one. A blanket refusal would have blocked Lisa's own writers and
55
+ # left the factory unable to file anything.
56
+ #
57
+ # Creation signatures, per tracker CLI and the two ways around each:
58
+ # gh issue create · gh api POST to .../issues · gh api graphql createIssue
59
+ # linear issue create · jira issue create · acli … workitem/issue create
60
+ # curl/http POST to api.github.com/…/issues, api.linear.app/graphql with
61
+ # issueCreate, or …atlassian.net/rest/api/…/issue
62
+ # Reads never fire: `gh issue list`, `gh issue view`, `gh issue edit`,
63
+ # `gh pr create`, `gh label create`, a bare `gh api …/issues` GET, and a prose
64
+ # mention inside a quoted string are all allowed.
65
+ #
66
+ # STANDING DOWN
67
+ #
68
+ # - No tracker configured (`.lisa.config.json` absent, or carrying no
69
+ # `tracker`). There is no `lisa-tracker-write` to route through, so the
70
+ # guard has nothing to redirect to. This is the bootstrapping case, and it
71
+ # is DETECTED rather than asserted — the operator does not have to remember
72
+ # an env var to bring up a new repo.
73
+ # - `LISA_ALLOW_DIRECT_ISSUE_CREATE` non-empty in the hook's inherited
74
+ # environment. This is the human operator's override, mirroring
75
+ # `LISA_ALLOW_INSTRUCTION_FILE_WRITE`.
76
+ #
77
+ # The override is honored ONLY from the ambient environment, and is refused
78
+ # outright when it appears as an inline assignment in the intercepted command.
79
+ # That distinction is the whole point: a tool-call shell is fresh every time
80
+ # and its exports do not reach this hook's environment, so the ambient variable
81
+ # can only have been set by a human before the session started (shell profile,
82
+ # settings env block, CI config). An escape the governed agent reaches by
83
+ # typing one more token in front of the command it was just refused is not an
84
+ # escape hatch — it is the prose problem with extra steps.
85
+ #
86
+ # ## What this hook costs, and the number that would change the decision
87
+ #
88
+ # Registered `matcher: ""` since CodySwannGT/lisa#3753 — it runs on EVERY tool
89
+ # call, not only Bash. Measured on a developer machine at load ~8-26, 50
90
+ # invocations per row:
91
+ #
92
+ # 16.7 ms enforce-team-first.sh already `matcher: ""` today
93
+ # 17.5 ms enforce-verification-gate.sh already `matcher: ""` today
94
+ # 11.5 ms this guard, non-Bash payload what the broad matcher adds
95
+ # 112.3 ms this guard, Bash payload what Bash calls already paid
96
+ #
97
+ # So the broad registration was not novel — two hooks were already there, this
98
+ # one is cheaper than either, and it raises an existing ~34 ms per-call baseline
99
+ # by about a third.
100
+ #
101
+ # THIS IS A THRESHOLD, NOT A PRECEDENT. The argument is about the TOTAL cost of
102
+ # the broad set, so it does not transfer to the next guard: four more hooks
103
+ # moved to `matcher: ""` would put ~90 ms on every tool call and the answer
104
+ # flips. **Before widening any other hook's matcher, re-run the measurement and
105
+ # reconsider if the broad set totals more than ~100 ms per tool call, or if any
106
+ # single broad hook exceeds ~50 ms.** Do not cite these numbers for a fifth
107
+ # hook; measure again, because the number that matters is the sum.
108
+ #
109
+ # The cost is also irreducible rather than sloppy. A 9-line script containing
110
+ # only this file's preamble measured 11.7 ms, and padding it to this file's
111
+ # length changed nothing (11.5 ms) — it is process spawn plus one `jq`, not
112
+ # parsing. Moving the early exit further up the file buys nothing; the exit is
113
+ # already ahead of config resolution, which is what keeps the figure at 11.5
114
+ # rather than at the 112 ms the Bash path pays.
115
+ set -euo pipefail
116
+
117
+ input="$(cat)"
118
+
119
+ # Evaluate once per tool call when this guard is registered on both channels.
120
+ #
121
+ # Lisa reaches an agent through the repository dispatcher AND the plugin
122
+ # manifest, and where both are live the harness runs this guard twice for one
123
+ # tool call. `guard-dedupe.bash` short-circuits the second run ONLY when a
124
+ # byte-identical copy already ALLOWED this exact payload on this exact tool
125
+ # call; a differing vintage, a refusal, and a host with one channel all
126
+ # evaluate exactly as before. Nothing is de-registered by it
127
+ # (CodySwannGT/lisa#3814).
128
+ #
129
+ # Absent library means no dedupe, which is the pre-existing behaviour, so an
130
+ # older channel copy that predates it is unaffected.
131
+ lisa_guard_hook_dir="${BASH_SOURCE[0]%/*}"
132
+ lisa_guard_dedupe_lib="$lisa_guard_hook_dir/guard-dedupe.bash"
133
+ if [ -r "$lisa_guard_dedupe_lib" ]; then
134
+ # shellcheck source=guard-dedupe.bash
135
+ . "$lisa_guard_dedupe_lib"
136
+ trap 'lisa_guard_dedupe_record $?' EXIT
137
+ lisa_guard_dedupe block-direct-issue-create "$input"
138
+ fi
139
+
140
+ # Probe both interpreters before use and announce a missing one rather than
141
+ # swallowing it. Under `set -e` an absent jq aborts with 127, and Claude Code
142
+ # treats any non-2 exit as a NON-BLOCKING hook error — so the guard would
143
+ # silently permit exactly what it exists to stop. Degrading to "allow" is
144
+ # right (a hook that cannot parse its input cannot tell a filing from a read),
145
+ # but doing it quietly is not: a guard that is silently absent reads exactly
146
+ # like a guard that is passing. Same reasoning as block-no-verify.sh.
147
+ for required in jq python3; do
148
+ if ! command -v "$required" >/dev/null 2>&1; then
149
+ printf 'block-direct-issue-create: %s not found; ready-role filing enforcement is NOT active\n' \
150
+ "$required" >&2
151
+ exit 0
152
+ fi
153
+ done
154
+
155
+ tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
156
+
157
+ # ── The two substrates ────────────────────────────────────────────────────
158
+ #
159
+ # A creation reaches a tracker two ways, and until CodySwannGT/lisa#3753 this
160
+ # guard saw only one of them. It was registered `matcher: "Bash"` AND gated
161
+ # here on `tool_name != "Bash"`, so an MCP tool call was refused entry twice
162
+ # over. MEASURED before the change, driving the guard with synthetic payloads:
163
+ #
164
+ # ALLOW mcp__linear-server__create_issue, undeclared <- the defect
165
+ # ALLOW mcp__github__create_issue, undeclared <- the defect
166
+ # REFUSE the same creation as `gh issue create` shell text <- control
167
+ # ALLOW an ordinary Read tool call <- control
168
+ #
169
+ # The two controls are what make those ALLOWs mean anything: the refusal proves
170
+ # the guard is live, the Read proves it is not simply refusing everything.
171
+ #
172
+ # **Widening the matcher alone would have changed nothing** — the call would
173
+ # arrive and this gate would still exit 0. Anyone who "fixed" #3753 by editing
174
+ # `plugin.json` would have seen no test fail and shipped a guard that still
175
+ # allows every MCP filing. The registration and this gate had to move together.
176
+ #
177
+ # The SHELL path below is untouched. A structured payload has no command line
178
+ # to tokenise, so it gets its own small classifier rather than being forced
179
+ # through a parser built for shell text.
180
+ structured_call=""
181
+ if [ "$tool_name" != "Bash" ]; then
182
+ # The cheap shape gate, deliberately BEFORE config resolution.
183
+ #
184
+ # This hook now runs on EVERY tool call, so the cost of the path that does
185
+ # nothing is the cost of the whole change. Config resolution spawns two jq
186
+ # processes per lookup and is what makes the Bash path ~112 ms; putting it
187
+ # ahead of this gate would charge that to every Read and Grep. So the
188
+ # not-a-creation exit happens here, on a shell `case` with no subprocess.
189
+ #
190
+ # Matched on SHAPE, not on an enumerated list of server tool names. Server
191
+ # names are supplied by whoever wrote the server and change when one is
192
+ # added or renamed, so a list would pass every row anyone thought of and
193
+ # miss the first one nobody did — the failure mode this repository's guard
194
+ # suites are written to defeat.
195
+ #
196
+ # RESIDUAL, stated rather than hidden: a server whose creation tool is named
197
+ # without a create-verb or without a tracker noun (`mcp__x__file_work`) is
198
+ # not recognised and is allowed. That is a fail-open, and it is the honest
199
+ # cost of refusing to enumerate. Add shapes here as they are measured.
200
+ case "$tool_name" in
201
+ Bash | Read | Write | Edit | MultiEdit | Glob | Grep | Task | TodoWrite | \
202
+ TaskCreate | TaskGet | TaskList | TaskOutput | TaskStop | TaskUpdate)
203
+ # The `Task*` family is Claude Code's in-session task list — LOCAL
204
+ # scratch, not a tracker write. `TaskCreate` carries `"type": "bug"`
205
+ # metadata the lisa-implement skill itself prescribes, so its name
206
+ # satisfies both shape gates below (create-verb + task noun) and was
207
+ # refused as "a tracker creation through TaskCreate"
208
+ # (CodySwannGT/lisa#4274). MCP creations are namespaced `mcp__*__*` and
209
+ # can never collide with a bare built-in name, so enumerating the
210
+ # built-ins here does not reopen the shape matcher for real filings.
211
+ exit 0
212
+ ;;
213
+ esac
214
+ # These terminal actions edit an existing item, not create the item.
215
+ # Strip only the measured action shape, then still inspect any preceding
216
+ # compound operation: create_issue_and_add_issue_comment remains a filing.
217
+ creation_name="$tool_name"
218
+ case "$creation_name" in
219
+ *[Aa]dd_[Ii]ssue_[Cc]omment)
220
+ creation_name="${creation_name%[Aa]dd_[Ii]ssue_[Cc]omment}" ;;
221
+ *[Aa]dd[Cc]omment[Tt]o[Jj]ira[Ii]ssue)
222
+ creation_name="${creation_name%[Aa]dd[Cc]omment[Tt]o[Jj]ira[Ii]ssue}" ;;
223
+ *[Aa]dd_[Cc]omment_[Tt]o_[Ii]ssue)
224
+ creation_name="${creation_name%[Aa]dd_[Cc]omment_[Tt]o_[Ii]ssue}" ;;
225
+ *[Aa]dd_[Ii]ssue_[Ll]abels)
226
+ creation_name="${creation_name%[Aa]dd_[Ii]ssue_[Ll]abels}" ;;
227
+ *[Aa]dd_[Ll]abels_[Tt]o_[Ii]ssue)
228
+ creation_name="${creation_name%[Aa]dd_[Ll]abels_[Tt]o_[Ii]ssue}" ;;
229
+ *[Aa]dd_[Rr]eaction_[Tt]o_[Ii]ssue_[Cc]omment)
230
+ creation_name="${creation_name%[Aa]dd_[Rr]eaction_[Tt]o_[Ii]ssue_[Cc]omment}" ;;
231
+ esac
232
+ case "$creation_name" in
233
+ *[Cc]reate* | *[Nn]ew* | *[Aa]dd* | *[Ff]ile*) ;;
234
+ *) exit 0 ;;
235
+ esac
236
+ case "$creation_name" in
237
+ *[Ii]ssue* | *[Tt]icket* | *[Tt]ask* | *[Ss]tory* | *[Bb]ug* | *[Ee]pic* | *[Ww]ork*) ;;
238
+ *) exit 0 ;;
239
+ esac
240
+ structured_call="1"
241
+ fi
242
+
243
+ command_str=""
244
+ if [ -z "$structured_call" ]; then
245
+ command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
246
+ if [ -z "$command_str" ]; then
247
+ exit 0
248
+ fi
249
+ fi
250
+
251
+ project_dir="${CLAUDE_PROJECT_DIR:-}"
252
+ if [ -z "$project_dir" ]; then
253
+ project_dir="$PWD"
254
+ fi
255
+
256
+ # Merged config, local overlay over base — the same precedence
257
+ # `lisa-tracker-read` / `lisa-tracker-write` resolve with, so the guard can
258
+ # never disagree with the writer about which tracker a project has.
259
+ read_config_value() {
260
+ local filter="$1"
261
+ local value=""
262
+ local file
263
+ for file in "$project_dir/.lisa.config.json" "$project_dir/.lisa.config.local.json"; do
264
+ [ -f "$file" ] || continue
265
+ local candidate
266
+ candidate="$(jq -r "$filter // empty" "$file" 2>/dev/null || true)"
267
+ [ -n "$candidate" ] && value="$candidate"
268
+ done
269
+ printf '%s' "$value"
270
+ }
271
+
272
+ tracker="$(read_config_value '.tracker')"
273
+ if [ -z "$tracker" ]; then
274
+ exit 0
275
+ fi
276
+
277
+ # The build-ready role is read from config, never hard-coded: a project that
278
+ # renamed its ready lane must still be able to satisfy the guard, and the
279
+ # refusal has to name the token that project actually uses.
280
+ default_ready_role="status:ready"
281
+ case "$tracker" in
282
+ github) ready_role="$(read_config_value '.github.labels.build.ready')" ;;
283
+ jira) ready_role="$(read_config_value '.jira.workflow.ready')" ;;
284
+ linear) ready_role="$(read_config_value '.linear.workflow.ready')" ;;
285
+ *) ready_role="" ;;
286
+ esac
287
+ if [ -z "$ready_role" ]; then
288
+ ready_role="$default_ready_role"
289
+ fi
290
+
291
+ # WHICH REPOSITORY'S VOCABULARY ANSWERS FOR THIS FILING
292
+ #
293
+ # The role above is the CALLING project's. For a same-repo filing that is the
294
+ # right question. For a filing addressed at a DIFFERENT repository — which
295
+ # Lisa ships a first-class, cron-driven path for in `lisa-persist-learning` —
296
+ # it is the wrong repository's vocabulary, and the guard demanded a token the
297
+ # target does not carry:
298
+ #
299
+ # - a JIRA or Linear caller's ready role is a workflow STATE. Demanded as a
300
+ # `gh --label` on another repo it is unsatisfiable, because that label does
301
+ # not exist there and `gh` rejects an unknown one. Obeying the guard made
302
+ # the command fail, which is not the same thing as being refused.
303
+ # - a GitHub caller that renamed its ready lane demanded its own token of a
304
+ # repository that never had it.
305
+ # - a GitHub caller on the stock lane worked only because both repositories
306
+ # happened to choose the same string. That is a coincidence, not routing.
307
+ #
308
+ # The one escape that IS satisfiable cross-repo, `[lisa-human-gate]`, is a lie
309
+ # about the item: it stamps a build-ready defect report as held for a human
310
+ # product call, and the target's build queue scans the ready role and nothing
311
+ # else. The report is filed and never picked up — precisely the incomplete
312
+ # handoff this guard exists to prevent, committed one repository over.
313
+ #
314
+ # So the target's own role answers, resolved from CONFIG rather than the
315
+ # network. A live `gh api repos/<o>/<r>/labels` lookup would be more general
316
+ # and is the wrong trade for a PreToolUse hook: a network round-trip on every
317
+ # intercepted command, and a new fail-open surface when it errors.
318
+ own_org="$(read_config_value '.github.org')"
319
+ own_name="$(read_config_value '.github.repo')"
320
+ own_repo=""
321
+ if [ -n "$own_org" ] && [ -n "$own_name" ]; then
322
+ own_repo="$own_org/$own_name"
323
+ fi
324
+
325
+ # `hardening.upstreamRepo` already names the upstream repository across Lisa's
326
+ # filing skills; `hardening.upstreamReadyRole` is its sibling, so the guard
327
+ # keeps its existing discipline — read the role from config, never hard-code it
328
+ # — while reading it from the RIGHT repository's config.
329
+ upstream_repo="$(read_config_value '.hardening.upstreamRepo')"
330
+ if [ -z "$upstream_repo" ]; then
331
+ upstream_repo="CodySwannGT/lisa"
332
+ fi
333
+ upstream_ready_role="$(read_config_value '.hardening.upstreamReadyRole')"
334
+ if [ -z "$upstream_ready_role" ]; then
335
+ upstream_ready_role="$default_ready_role"
336
+ fi
337
+
338
+ # A non-GitHub caller's ready role is a workflow STATE, so it is never the right
339
+ # vocabulary for a GitHub target no matter what the target turns out to be. The
340
+ # classifier needs to know that even when this project declares no repo of its
341
+ # own to compare against.
342
+ caller_is_github="0"
343
+ if [ "$tracker" = "github" ]; then
344
+ caller_is_github="1"
345
+ fi
346
+
347
+ # Ambient-only override. Deliberately read here, from the hook process's own
348
+ # environment, and never from the command being inspected.
349
+ ambient_override="${LISA_ALLOW_DIRECT_ISSUE_CREATE:-}"
350
+
351
+ # How to write the declaration down on THIS project's tracker. The answer is
352
+ # not the same everywhere, and printing the GitHub answer at a Linear operator
353
+ # is what made this guard unsatisfiable: it named a `--label` flag that the
354
+ # mandated client does not have, on a role that is not a label.
355
+ declaration_hint() {
356
+ case "$tracker" in
357
+ jira | linear)
358
+ cat <<EOF
359
+ Your build-ready role \`$ready_role\` is a workflow STATE, not a label, and
360
+ the mandated client is \`curl\` — which has no flag that carries a state. So
361
+ declare the LIFECYCLE ROLE the access layer resolves the state from, and let
362
+ it do the resolving:
363
+
364
+ LIFECYCLE_ROLE=ready curl -sS -X POST <the tracker endpoint> …
365
+
366
+ or \`lifecycle_role:ready\` in the request payload, or a \`--state\` /
367
+ \`--status\` flag where the CLI has one. The \`$tracker\` access layer takes
368
+ that role, resolves it against the tracker's own state catalog, and fails
369
+ CLOSED if it cannot — so the token is the input that decides the lane, not a
370
+ decoration.
371
+ EOF
372
+ ;;
373
+ *)
374
+ cat <<EOF
375
+ The command has to carry the configured build-ready role \`$ready_role\` as
376
+ the value of a \`--label\` / \`--status\` / \`--state\` flag — not in the
377
+ title or body, because a role named in prose is not a role applied.
378
+ EOF
379
+ ;;
380
+ esac
381
+ }
382
+
383
+ # The branch-specific half of the remediation, printed only for the branch that
384
+ # fired. Every refusal this guard issues has to name an inverse the author can
385
+ # actually perform — a refusal with none is a state change with no inverse, and
386
+ # on a file-contents refusal it made a path permanently unnameable to any
387
+ # command the guard did not recognise as a reader (CodySwannGT/lisa#3683).
388
+ #
389
+ # NEITHER PARAGRAPH IS AN ESCAPE HATCH, and that is deliberate. `3594` records
390
+ # that a PreToolUse guard whose escape the guarded agent can set is not a
391
+ # control, so nothing here is settable: the only inverses named are edits to
392
+ # the refused command itself, and performing either one leaves a genuine
393
+ # undeclared creation refused by the checks above. Re-quoting an unlexable
394
+ # creation hands it to the parsed path, which refuses it; naming a document to
395
+ # a reader files nothing, which is the whole point. The one true escape stays
396
+ # what it was — an ambient variable a human exports before the session, which a
397
+ # tool-call shell cannot reach because its exports do not survive into this
398
+ # hook's environment.
399
+ remedy_hint() {
400
+ case "${1:-}" in
401
+ unparseable)
402
+ cat <<'EOF'
403
+
404
+ THIS COMMAND DID NOT LEX, so it was judged by pattern rather than by parse. An
405
+ unbalanced quote is the cause, and an apostrophe in ordinary prose is the
406
+ measured one. The pattern cannot separate a sentence about filing from a real
407
+ filing hidden behind a trailing `#` comment, so it refuses both.
408
+
409
+ THE INVERSE IS EXECUTABLE: re-quote the command so it lexes, and run it again.
410
+ It is not a bypass — a re-quoted creation is handed to the full parser, which
411
+ refuses it unless it declares readiness the ordinary way.
412
+
413
+ A command whose every segment runs a known READER is not refused on this branch
414
+ at all, so `cat`, `grep`, `ls`, `cp`, `mv`, `git` and `echo` never reach it.
415
+ EOF
416
+ ;;
417
+ file)
418
+ cat <<'EOF'
419
+
420
+ THIS REFUSAL CAME FROM A FILE THIS COMMAND RUNS. A path is opened only when the
421
+ command puts it in a COMMAND position; a program that takes it as data — `cat`,
422
+ `grep`, `ls`, `cp`, `mv`, `git`, a test runner — never reaches this branch. Two
423
+ inverses, both executable:
424
+
425
+ - The file WRITES a payload locally and cannot transmit it (no HTTP client,
426
+ no CLI, no process spawn): it is read as data, not as a filing. Drop the
427
+ transmitting primitive, or slice the payload out of an existing file
428
+ instead of restating it here.
429
+ - The file produces no local artifact either — it is a DOCUMENT that quotes a
430
+ creation rather than a program that performs one. Name it to a reader
431
+ rather than to an interpreter; rewording its contents is not the remedy and
432
+ does not converge.
433
+ EOF
434
+ ;;
435
+ esac
436
+ }
437
+
438
+ refuse() {
439
+ local signature="$1"
440
+ local roles="$2"
441
+ local target="$3"
442
+ local remedy="${4:-}"
443
+ if [ -n "$target" ]; then
444
+ refuse_cross_repo "$signature" "$roles" "$target"
445
+ fi
446
+ cat >&2 <<EOF
447
+ BLOCKED: refusing \`$signature\` — this filing declares no readiness.
448
+
449
+ WHY: a work item filed without the build-ready role is an incomplete handoff.
450
+ Build-intake scans the ready lane and nothing else, so nothing will ever pick
451
+ it up: the write succeeds and the work still dies. An audit of one working
452
+ session found 13 of 13 issues filed this way, none of them through Lisa's
453
+ filing path. The rule saying not to do this already existed — it did not bind,
454
+ so it is now enforced here.
455
+
456
+ FILE IT THE SANCTIONED WAY — one of these three, always explicit:
457
+
458
+ 1. The item is complete enough to build. Use the filing flow, not the CLI:
459
+
460
+ /lisa:track "<what needs building>"
461
+
462
+ which resolves or creates exactly one live leaf through
463
+ \`lisa-tracker-write\` with \`build_ready: true\`, validates it before the
464
+ write, and claims it. Complete means the \`work-item-definition-of-ready\`
465
+ bar — reproduction, observed-versus-expected, Gherkin acceptance criteria.
466
+
467
+ 2. A human product call is genuinely pending. Route the same way but pass
468
+ \`human_gate: "<why a human must judge this first>"\` in place of
469
+ \`build_ready: true\` — the two are mutually exclusive. The leaf is filed
470
+ into the default backlog role, outside the lane build-intake claims from,
471
+ and is deliberately NOT claimed, so it keeps attracting the human attention
472
+ it was filed for. The hold is stamped so it is auditable rather than
473
+ indistinguishable from an accident:
474
+
475
+ Held for a human product call: <reason>.
476
+ <!-- [lisa-human-gate] reason=<short-slug> -->
477
+
478
+ 3. The item is a CONTAINER — an Epic, or any item whose state rolls up from
479
+ its children. It is neither of the two above and must not pretend to be:
480
+ \`leaf-only-lifecycle\` FORBIDS the build-ready role on a container, and a
481
+ human gate it does not have is a durable false hold. It declares the third
482
+ thing instead — the canonical container line, in place of a Target Backend
483
+ Environment:
484
+
485
+ ## Target Backend Environment
486
+
487
+ None — container: state rolls up from children
488
+
489
+ That declaration is refused alongside a by-design leaf type
490
+ (\`type:Bug\` / \`type:Task\` / \`type:Sub-task\` / \`type:Improvement\`):
491
+ an item cannot be a container and a leaf at once.
492
+
493
+ Filed, not ready, and no \`human_gate\` is the incomplete-handoff case, and
494
+ \`build_ready: false\` with no reason is the same omission with a value
495
+ attached. See the \`ready-role-filing\` rule for the full contract.
496
+
497
+ If you must run the CLI directly, the command has to carry one of the three
498
+ declarations itself:
499
+
500
+ $(declaration_hint)
501
+
502
+ Or a \`[lisa-human-gate]\` marker in the body it submits, when a human
503
+ product call really is pending.
504
+
505
+ Or, for a container, \`None — container: state rolls up from children\` in
506
+ that body, and no by-design leaf type on the command.
507
+
508
+ WHERE THE DECLARATION IS READ FROM: argv, the request payload — inline, in a
509
+ \`--data-binary @file\`, or piped in over stdin — and the contents of a script
510
+ this command runs. Moving the create into a file no longer moves it out of
511
+ sight, so the declaration can live wherever the create does.
512
+
513
+ $(remedy_hint "$remedy")
514
+
515
+ OPERATOR ESCAPE: a human can export \`LISA_ALLOW_DIRECT_ISSUE_CREATE=1\` in the
516
+ environment before starting the session. It is deliberately not reachable by
517
+ setting it inline on this command — an inline assignment is refused.
518
+ EOF
519
+ exit 2
520
+ }
521
+
522
+ # The cross-repo refusal is a separate message, not a variable swapped into the
523
+ # one above, because its REMEDIATION is different. The local filing flow writes
524
+ # to this project's own tracker and structurally cannot reach another
525
+ # repository, so naming it here would send the agent to a path that cannot do
526
+ # the thing it was just refused for. Name the route that reaches the target,
527
+ # and name the target's role rather than this project's.
528
+ refuse_cross_repo() {
529
+ local signature="$1"
530
+ local roles="$2"
531
+ local target="$3"
532
+ cat >&2 <<EOF
533
+ BLOCKED: refusing \`$signature\` — this filing declares no readiness.
534
+
535
+ WHY: a work item filed without the build-ready role is an incomplete handoff.
536
+ Build-intake scans the ready lane and nothing else, so nothing will ever pick
537
+ it up: the write succeeds and the work still dies.
538
+
539
+ THIS FILING IS ADDRESSED AT ANOTHER REPOSITORY: \`$target\`.
540
+ That repository runs its own build queue off its own ready role, so this
541
+ project's role does not answer for it — and this project's filing flow writes
542
+ to this project's tracker, so it cannot reach the target at all.
543
+
544
+ FILE IT THE SANCTIONED WAY:
545
+
546
+ 1. An upstream defect or hardening report — the highest-signal report there is,
547
+ because it is reproduced and attributed rather than guessed at. Use the
548
+ upstream filing path, which composes a redacted, public-safe body through an
549
+ allowlist projection instead of free-form prose:
550
+
551
+ bunx @codyswann/lisa file-upstream --input <filing-event>.json
552
+
553
+ \`lisa-persist-learning\` step 6 runs exactly this, headless, on a cron, and
554
+ files the result with explicit \`build_ready: true\` so the target's queue
555
+ picks it up.
556
+
557
+ 2. If you must run the CLI directly, the command has to carry the TARGET
558
+ repository's build-ready role — \`$roles\` — as the value of a \`--label\`
559
+ flag. Configure it as \`hardening.upstreamReadyRole\` when the target renamed
560
+ its lane.
561
+
562
+ DO NOT reach for \`[lisa-human-gate]\` to get past this one. It still satisfies
563
+ the guard — it is a real declaration — but on an upstream defect report it is a
564
+ false one: it stamps the item as held for a human product call, and the
565
+ target's build queue scans the ready role and nothing else. The report is filed
566
+ and never picked up, which is the incomplete handoff this guard exists to
567
+ prevent, committed one repository over. Use it only when a human product call
568
+ is genuinely pending on the target.
569
+
570
+ OPERATOR ESCAPE: a human can export \`LISA_ALLOW_DIRECT_ISSUE_CREATE=1\` in the
571
+ environment before starting the session. It is deliberately not reachable by
572
+ setting it inline on this command — an inline assignment is refused.
573
+ EOF
574
+ exit 2
575
+ }
576
+
577
+ # The classifier is read into a variable with a top-level here-document rather
578
+ # than piped straight in from inside `$( … )`. bash 3.2 — which is what macOS
579
+ # still ships as /bin/bash, and therefore what this fleet's hooks run under —
580
+ # mis-parses a here-document nested in a command substitution, scanning the
581
+ # document body for quotes and parentheses it should be treating as literal.
582
+ # The whole script then fails to parse, which is the worst possible failure for
583
+ # a guard: a syntax error exits non-zero, and Claude Code reads every non-2
584
+ # exit as a non-blocking hook error, so the command runs unchecked.
585
+ classifier=""
586
+ read -r -d '' classifier <<'PY' || true
587
+ import os
588
+ import re
589
+ import shlex
590
+ import sys
591
+
592
+ command = os.environ.get("LISA_GUARD_COMMAND", "")
593
+ ready_role = os.environ.get("LISA_GUARD_READY_ROLE", "")
594
+ ambient_override = os.environ.get("LISA_GUARD_AMBIENT_OVERRIDE", "")
595
+ default_ready_role = os.environ.get("LISA_GUARD_DEFAULT_READY_ROLE", "")
596
+ own_repo = os.environ.get("LISA_GUARD_OWN_REPO", "").strip().lower()
597
+ upstream_repo = os.environ.get("LISA_GUARD_UPSTREAM_REPO", "").strip().lower()
598
+ upstream_ready_role = os.environ.get("LISA_GUARD_UPSTREAM_READY_ROLE", "")
599
+ caller_is_github = os.environ.get("LISA_GUARD_CALLER_IS_GITHUB", "") == "1"
600
+ tracker = os.environ.get("LISA_GUARD_TRACKER", "").strip().lower()
601
+ project_dir = os.environ.get("LISA_GUARD_PROJECT_DIR", "")
602
+
603
+ OVERRIDE_NAME = "LISA_ALLOW_DIRECT_ISSUE_CREATE"
604
+ HUMAN_GATE_MARKER = "[lisa-human-gate]"
605
+
606
+ # Decoration a hold declaration may sit behind, and nothing more: whitespace,
607
+ # blockquote arrows, list bullets or numbers, emphasis, an opening HTML
608
+ # comment. One character class plus two optional groups, deliberately not a
609
+ # nested quantifier — this runs over untrusted issue bodies.
610
+ HUMAN_GATE_DECORATION = re.compile(r"^[ \t>*_+#-]*(?:\d+[.)][ \t]*)?(?:<!--[ \t]*)?[ \t]*")
611
+ HUMAN_GATE_FENCE = re.compile(r"```.*?```", re.DOTALL)
612
+ HUMAN_GATE_CODE_SPAN = re.compile(r"`[^`\n]*`")
613
+
614
+
615
+ def declares_human_gate(text):
616
+ """Whether a text DECLARES a hold rather than mentioning one.
617
+
618
+ The comment this replaces said the marker is matched anywhere because it
619
+ "has no other meaning, so its presence in the title or body IS the
620
+ declaration". That was true when written and is false now: the marker
621
+ acquired a second meaning -- being discussed -- the moment the feature
622
+ became something people file tickets about, and a ticket about the marker
623
+ then declares a hold on itself (CodySwannGT/lisa#3815).
624
+
625
+ A declaration is POSITIONAL: the marker leads its line, behind at most the
626
+ decoration above, with fenced blocks and inline code spans removed first
627
+ because that is how the marker is written ABOUT. The same rule the intake
628
+ matcher applies, so the two surfaces cannot disagree about one body.
629
+
630
+ NOTE the direction here differs from the intake matcher's. There a match
631
+ HOLDS an item, so a false positive parks work. Here a match ADMITS a
632
+ filing, so a false positive is a hole -- which makes this the tightening
633
+ direction on both sides at once.
634
+
635
+ Args:
636
+ text: Any candidate text -- one argv value, a body file, a payload.
637
+
638
+ Returns:
639
+ True when some line of the text declares a hold.
640
+ """
641
+ if not text:
642
+ return False
643
+ body = HUMAN_GATE_CODE_SPAN.sub("", HUMAN_GATE_FENCE.sub("", str(text)))
644
+ for line in body.split("\n"):
645
+ if HUMAN_GATE_DECORATION.sub("", line, count=1).startswith(
646
+ HUMAN_GATE_MARKER
647
+ ):
648
+ return True
649
+ if _comment_carries_marker(line):
650
+ return True
651
+ return False
652
+
653
+
654
+ def _comment_carries_marker(line):
655
+ """Whether an HTML comment on this line carries the marker.
656
+
657
+ The second accepted form, and the one this guard needs most: its inputs
658
+ are not markdown documents. A `--body` value is a single line, and a shell
659
+ script writes its declaration as `# <!-- ... -->`, so a purely
660
+ line-leading rule would refuse two declarations people have already
661
+ written -- both of which are in this repository's own test corpus.
662
+
663
+ It is not a loosening. An HTML comment is invisible when rendered, so
664
+ nobody wraps one around the marker in order to TALK about it; a quotation
665
+ has to be visible to be a quotation. Measured over this repository's 42
666
+ matching issue bodies, adding this branch changes not one verdict.
667
+
668
+ Args:
669
+ line: One line of text.
670
+
671
+ Returns:
672
+ True when some comment on the line contains the marker.
673
+ """
674
+ segments = line.split("<!--")
675
+ for segment in segments[1:]:
676
+ close = segment.find("-->")
677
+ inside = segment if close == -1 else segment[:close]
678
+ if HUMAN_GATE_MARKER in inside:
679
+ return True
680
+ return False
681
+
682
+ # ---------------------------------------------------------------------------
683
+ # WHY THIS IS A TOKEN SCAN AND NOT A PROGRAM RESOLVER
684
+ #
685
+ # The first version of this classifier asked "what program is being invoked?"
686
+ # and answered it by stripping a fixed allowlist of wrapper programs. That
687
+ # question is unbounded, so the allowlist could never be complete, and every
688
+ # gap failed OPEN: an unrecognised prefix became the resolved program,
689
+ # classification returned "not a creation", and the guard allowed. A
690
+ # branch-enumerating probe found 36 fail-opens, including `nice -n 10 gh issue
691
+ # create` — and `nice` is POSIX, present on every host, so one prepended token
692
+ # defeated the whole control end to end.
693
+ #
694
+ # Enumerating command SHAPES had scored 21/21 against that same code. The
695
+ # lesson is the method, not the list: probe the parser's decision points
696
+ # (tokenise, split, locate, classify, declare), not the attacks you can think
697
+ # of.
698
+ #
699
+ # So the question is inverted. Instead of "what program is this?" — unbounded —
700
+ # the classifier asks "does this command line invoke a tracker CLI in a
701
+ # creation mode?", which is bounded by a tracker list we already maintain.
702
+ # Every token is examined; anything before the CLI token is irrelevant by
703
+ # construction. `nice`, `stdbuf`, `caffeinate`, `xcrun`, `arch`, `script`,
704
+ # `timeout`, `env -i`, `sudo -u`, `xargs -I{}`, absolute paths, and every
705
+ # wrapper nobody has thought of yet all collapse into one code path.
706
+ #
707
+ # THE DEFAULT ANSWER TO "I DO NOT UNDERSTAND THIS" IS NOW REFUSE.
708
+ # Tokenisation failure, a nesting bound, an unparsed operand: each refuses with
709
+ # an explanation rather than passing. An operator who hits a false refusal says
710
+ # so and the recogniser widens; an agent who hits a false allow says nothing,
711
+ # which is the entire problem.
712
+ # ---------------------------------------------------------------------------
713
+
714
+ # Tracker CLIs. Bounded and reviewable, unlike the wrapper space.
715
+ TRACKER_CLIS = {"gh", "jira", "linear", "acli"}
716
+ HTTP_CLIS = {"curl", "http", "httpie", "wget"}
717
+
718
+ # Text that looks like a tracker creation even when it cannot be tokenised.
719
+ # Used ONLY on the tokenise-failure path, where the choice is between refusing
720
+ # and permitting something we demonstrably cannot read. Deliberately narrower
721
+ # than "mentions a tracker CLI" so an ordinary unparseable command
722
+ # (`echo 'it's fine`) still passes.
723
+ UNPARSEABLE_CREATION = re.compile(
724
+ r"\b(?:gh|jira|linear)\b[^\n]*?\bissue\b[^\n]*?\bcreate\b"
725
+ r"|\bacli\b[^\n]*?\b(?:workitem|issue)s?\b[^\n]*?\bcreate\b"
726
+ r"|\bcreateIssue\b|\bissueCreate\b"
727
+ r"|repos/[^/\s]+/[^/\s]+/issues"
728
+ r"|atlassian\.net/rest/api/[^/\s]+/issue",
729
+ re.IGNORECASE,
730
+ )
731
+
732
+ BODY_FILE_FLAGS = {"--body-file", "-F", "--input", "--data-binary"}
733
+ LABEL_FLAGS = {"--label", "--labels", "--add-label", "--status", "--state"}
734
+
735
+ # ---------------------------------------------------------------------------
736
+ # THE THIRD DECLARATION: THIS ITEM IS A CONTAINER
737
+ #
738
+ # The two declarations above are both illegitimate for a container, and the
739
+ # rules say so in as many words. `leaf-only-lifecycle` FORBIDS the build-ready
740
+ # role on an Epic — its state rolls up from its children, and a hand-applied
741
+ # role on a parent is the exact input that rule's claim-time arm exists to
742
+ # reject. `lisa-github-write-issue` says a container needs no human gate.
743
+ # So a guard with only those two arms could be satisfied for a container only
744
+ # by writing something untrue, and BOTH untruths corrupt data another control
745
+ # reads: a stamped role puts a container in the build lane, and a fabricated
746
+ # hold marker is a state change with no inverse that reads to every later
747
+ # observer as a real product hold. A guard that can only be obeyed by lying is
748
+ # worse than one that simply refuses, because the lie is indistinguishable from
749
+ # a correct filing afterwards.
750
+ #
751
+ # WHY NOT `--label type:Epic`
752
+ #
753
+ # Because a declared type is a CLAIM, and one that costs a leaf nothing: the
754
+ # item it produces still looks buildable, so the exemption would be a free
755
+ # bypass of the readiness requirement for anything willing to mistype itself.
756
+ # That is the shape of this repository's measured case where an allowlist added
757
+ # to harden a guard became the way around it.
758
+ #
759
+ # WHAT IS READ INSTEAD
760
+ #
761
+ # The canonical container declaration the `derived-branch-plan` rule already
762
+ # defines and `lisa-github-write-issue` already stamps, in place of a Target
763
+ # Backend Environment:
764
+ #
765
+ # None — container: state rolls up from children
766
+ #
767
+ # It is a marker with no other meaning, so it is matched wherever the payload
768
+ # reaches — the asymmetry `HUMAN_GATE_MARKER` gets, and for the related
769
+ # reason. Reading the declaration the writer already emits is also what keeps
770
+ # the guard and the skill from drifting apart a second time: there is one
771
+ # string, defined in one rule, and both read it.
772
+ #
773
+ # It is self-limiting in a way a type label is not. Writing it costs the item
774
+ # its Target Backend Environment and its Branch Plan — the two fields a leaf
775
+ # needs to be built at all — so the declaration is only useful to something
776
+ # that actually is a container. A leaf that writes it does not smuggle a
777
+ # buildable item past the gate; it files an unbuildable one, in the lane
778
+ # `leaf-only-lifecycle` and `lisa-repair-intake` already sweep.
779
+ #
780
+ # The WHOLE canonical value, `None` included, not just the distinctive tail.
781
+ # Dropping the leading half would match ordinary prose about how a container's
782
+ # state behaves — including a bug report ABOUT this guard — and a declaration
783
+ # that ordinary prose satisfies is not a declaration. Whitespace-tolerant
784
+ # because a body file may wrap the line; dash-tolerant because an author
785
+ # retyping it by hand will not always reach for the em dash.
786
+ CONTAINER_DECLARATION = re.compile(
787
+ r"None\s*[\u2014\u2013-]\s*container:\s*state\s+rolls\s+up\s+from\s+children",
788
+ re.IGNORECASE,
789
+ )
790
+ # The checkable half of "is it really a container". Provenance is unobservable
791
+ # here, as the header notes, but a CONTRADICTION is not: an item cannot be a
792
+ # container and a by-design leaf at once. Quoted from `leaf-only-lifecycle` —
793
+ # "the by-design leaf types (Bug, Task, Sub-task, Improvement)". Story and
794
+ # Spike are deliberately ABSENT: the same rule's childless-parent exception
795
+ # makes them leaf-or-container depending on child work, and a decomposition
796
+ # legitimately files a parent Story before the children that make it one.
797
+ BY_DESIGN_LEAF_TYPES = {"bug", "task", "sub-task", "subtask", "improvement"}
798
+ # Where a declared type can land on the created item. The label spellings are
799
+ # GitHub's `type:<value>` convention; `--type` and its aliases are the JIRA and
800
+ # Linear clients' spelling. Read with the same positional discipline as the
801
+ # build-ready role — a type named in a title or body is not a type applied.
802
+ TYPE_FLAGS = {
803
+ "--label", "--labels", "--add-label",
804
+ "--type", "--issue-type", "--issuetype",
805
+ }
806
+ POST_METHOD_FLAGS = {"-X", "--request", "--method"}
807
+ POST_PAYLOAD_FLAGS = {
808
+ "-d", "--data", "--data-raw", "--data-binary",
809
+ "-f", "-F", "--raw-field", "--field", "--input",
810
+ }
811
+ # Flags whose VALUE is a payload field. `-f path=repos/o/r/issues` must not be
812
+ # read as an endpoint: it is data being sent, not the address being posted to.
813
+ PAYLOAD_VALUE_FLAGS = {"-f", "-F", "--raw-field", "--field"}
814
+
815
+ # Matched against a token's PATH COMPONENT, never the raw argument — see
816
+ # `endpoint_path`. The `$` anchor is load-bearing and stays: without it
817
+ # `repos/o/r/issues/123/comments` reads as a creation, which is a different
818
+ # operation this guard must not refuse. The segment classes exclude `?` and
819
+ # `#` so the anchor cannot be reached past a decoration even if some future
820
+ # caller forgets to parse first.
821
+ GITHUB_ISSUES_PATH = re.compile(r"repos/[^/\s?#]+/[^/\s?#]+/issues/?$")
822
+ GITHUB_ISSUES_URL = re.compile(r"api\.github\.com/repos/[^/\s]+/[^/\s]+/issues")
823
+ # The repository a creation is ADDRESSED at, which decides whose ready role
824
+ # answers for it. `gh` accepts the flag before or after the subcommand and in
825
+ # either spelling, and the REST paths carry the same pair positionally.
826
+ REPO_FLAGS = {"--repo", "-R"}
827
+ GITHUB_ISSUES_PATH_REPO = re.compile(
828
+ r"repos/([^/\s?#]+)/([^/\s?#]+)/issues/?$"
829
+ )
830
+ GITHUB_ISSUES_URL_REPO = re.compile(
831
+ r"api\.github\.com/repos/([^/\s]+)/([^/\s]+)/issues"
832
+ )
833
+ JIRA_ISSUE_URL = re.compile(r"atlassian\.net/rest/api/[^/\s]+/issue")
834
+ GRAPHQL_CREATE = re.compile(r"createIssue|issueCreate")
835
+
836
+ # ---------------------------------------------------------------------------
837
+ # REACH: THE COMMAND IS NOT THE ONLY TEXT THAT RUNS
838
+ #
839
+ # The classifier above asks the right question of the wrong text. It examines
840
+ # argv, and argv is not where a creation has to live:
841
+ #
842
+ # bash /tmp/create.sh # URL and `issueCreate` both inside the script
843
+ # node wrapper.mjs --state x # same, in JavaScript
844
+ # curl … --data-binary @p.json # URL in argv, mutation in the file
845
+ #
846
+ # All three ALLOWED, verified by driving the guard with synthetic payloads
847
+ # (CodySwannGT/lisa#3484). The detection is a CONJUNCTION — an endpoint token
848
+ # AND a creation verb in the SAME inspected command — so moving either half one
849
+ # file away means the conjunction never forms. `bash <path>` shows the
850
+ # classifier two tokens, neither of which is a tracker CLI.
851
+ #
852
+ # It is not an oversight that an operator had to be clever to find. Lisa's OWN
853
+ # guards instruct agents into this shape: `parity-safety-net.sh` refuses
854
+ # heredocs and says "write the payload to a file with the Write tool, then
855
+ # execute that file directly", and `block-shell-json-parsing.sh` pushes JSON
856
+ # construction into `jq` scripts. An agent complying perfectly with Lisa's
857
+ # guidance lands in the uninspected path BY DEFAULT. The guards were
858
+ # individually reasonable and jointly self-defeating.
859
+ #
860
+ # The fix follows this file's own inverted method. Enumerating interpreters —
861
+ # bash, sh, zsh, ksh, dash, python3, node, bun, deno, ruby, perl, `./script`,
862
+ # `source`, `env bash` — is the unbounded question all over again, and every
863
+ # gap in it fails OPEN. So the question is inverted the same way the wrapper
864
+ # question was: instead of "which programs execute their operands?", the
865
+ # classifier asks "does this command name a readable file whose CONTENTS read
866
+ # as an undeclared tracker creation?" — bounded by the tokens actually present.
867
+ #
868
+ # THE COST IS STATED, NOT HIDDEN. `cat create.sh` and `git add create.sh` are
869
+ # refused too, because proving that a program does NOT execute its operand
870
+ # requires exactly the allowlist this file already refuses to keep. That is the
871
+ # documented direction of failure: an operator who hits a false refusal says
872
+ # so, an agent who hits a false allow says nothing.
873
+ FILE_OPERAND_MAX_BYTES = 262144
874
+ FILE_OPERANDS_PER_SEGMENT = 8
875
+ # Payload sources. A creation's mutation text may arrive inline, from a file,
876
+ # or over stdin from an earlier stage of the same pipeline.
877
+ PAYLOAD_SOURCE_FLAGS = {
878
+ "-d", "--data", "--data-raw", "--data-binary", "--data-ascii",
879
+ "--data-urlencode", "--input", "-F", "--form", "--upload-file", "-T",
880
+ }
881
+ STDIN_PAYLOAD_TOKENS = {"-", "@-"}
882
+ # A tracker endpoint appearing anywhere in a file's text. Paired with
883
+ # GRAPHQL_CREATE as a CONJUNCTION, so an ordinary file that merely mentions
884
+ # `issueCreate` in prose — a changelog, a test name, a code comment — does not
885
+ # read as a creation on its own.
886
+ TRACKER_ENDPOINT = re.compile(
887
+ r"api\.linear\.app/graphql"
888
+ r"|api\.github\.com"
889
+ r"|atlassian\.net/rest/api"
890
+ r"|repos/[^/\s?#'\"]+/[^/\s?#'\"]+/issues",
891
+ re.IGNORECASE,
892
+ )
893
+
894
+ # ---------------------------------------------------------------------------
895
+ # A PAYLOAD HELD AS DATA IS NOT A SUBMISSION
896
+ #
897
+ # The conjunction above — an endpoint AND a creation verb in one file — cannot
898
+ # tell a payload being WRITTEN AS TEST DATA from one about to be SUBMITTED.
899
+ # Measured (CodySwannGT/lisa#3943): a helper that assembles test fixtures, and
900
+ # holds a mutation body as a string constant it writes to a fixture file, was
901
+ # refused as "a tracker creation inside <path>". It submits nothing. The same
902
+ # helper rewritten to SLICE the payload out of an existing file was allowed —
903
+ # so the discriminator was the literal, not the behaviour.
904
+ #
905
+ # The behaviour that separates them is EGRESS. A file can only file an issue if
906
+ # it can hand its bytes to another host or another process. A file with no such
907
+ # primitive anywhere in it cannot submit whatever its constants spell, so the
908
+ # conjunction is reading data.
909
+ #
910
+ # BOTH HALVES ARE REQUIRED, and the second one is why this is not a hole. The
911
+ # exemption needs POSITIVE evidence that the file produces a local artifact
912
+ # (`LOCAL_WRITE`) as well as the ABSENCE of egress, so it is bounded to the
913
+ # measured population — a fixture writer — rather than granted to any file that
914
+ # happens to use a transport nobody enumerated. Egress is over-matched on
915
+ # purpose: every miss in `PAYLOAD_EGRESS` widens the exemption, so the list
916
+ # reaches for `.post(`-shaped calls and process spawning as well as named HTTP
917
+ # clients, and a file doing both is refused. That is the guard's usual
918
+ # direction of failure — a false refusal is reported, a false allow is silent.
919
+ #
920
+ # RESIDUAL, stated rather than hidden: a submitting file that ALSO writes a
921
+ # local artifact and reaches the network through a primitive no pattern here
922
+ # names is allowed. Nothing about `text_declares_readiness`, the nested shell
923
+ # scan, the unparseable arm, or the argv path is weakened — this reads only the
924
+ # coarse conjunction, which is the only arm that ever inferred a submission
925
+ # from contents alone.
926
+ PAYLOAD_EGRESS = re.compile(
927
+ # Clients and trackers, by name.
928
+ r"\bcurl\b|\bwget\b|\bhttpie\b|\bgh\b|\bjira\b|\bacli\b"
929
+ r"|requests\.|httpx|aiohttp|pycurl|urllib|urlopen|http\.client|\bsocket\b"
930
+ r"|https?connection|httpurlconnection|httpclient|httprequest|webclient"
931
+ r"|okhttp|net::http|restclient|faraday|httparty|open-uri|\blwp\b|http::tiny"
932
+ r"|curl_init|curl_exec|guzzle|fsockopen|file_get_contents"
933
+ r"|\bfetch\s*\(|axios|xmlhttprequest|node-fetch|undici|superagent"
934
+ r"|http\.post|http\.newrequest"
935
+ # Handing the payload to another process, which can carry it anywhere.
936
+ r"|subprocess|os\.system|popen|child_process|execsync|spawnsync"
937
+ r"|\bspawn\s*\(|\bexec\s*\(|\bsystem\s*\(|bun\.spawn|deno\.command"
938
+ # The shape a send takes in almost any language, whatever the client is
939
+ # called. Over-matching here costs an exemption, never a refusal.
940
+ r"|\.post\s*\(|\.put\s*\(|\.patch\s*\(|\.request\s*\(|\.send\s*\("
941
+ r"|\.execute\s*\(|\.mutate\s*\(|\.do\s*\(",
942
+ re.IGNORECASE,
943
+ )
944
+ # Producing a local artifact: the positive half of the exemption.
945
+ LOCAL_WRITE = re.compile(
946
+ r"\.write\s*\(|\.writelines\s*\(|write_text\s*\(|write_bytes\s*\("
947
+ r"|writefilesync|writefile\s*\(|createwritestream|outputstream"
948
+ r"|ioutil\.writefile|\btee\b"
949
+ # A shell redirect. Anchored on a separator so `=>` and `->` are not read
950
+ # as one, and required to be followed by something path-shaped.
951
+ r"|(?:^|[\s;&|(])>>?\s*[\"']?[\w./$~-]",
952
+ re.IGNORECASE | re.MULTILINE,
953
+ )
954
+
955
+
956
+ def payload_is_inert(text):
957
+ """Whether this file HOLDS a creation payload rather than submitting one.
958
+
959
+ Args:
960
+ text: The file's contents.
961
+
962
+ Returns:
963
+ True when the file writes a local artifact and carries no primitive
964
+ capable of transmitting anything.
965
+ """
966
+ return (
967
+ LOCAL_WRITE.search(text) is not None
968
+ and PAYLOAD_EGRESS.search(text) is None
969
+ )
970
+
971
+
972
+ # ---------------------------------------------------------------------------
973
+ # DECLARING BUILD-READY WHEN THE ROLE IS A STATE AND NOT A LABEL
974
+ #
975
+ # `declares_readiness` accepted the build-ready role only as the value of a
976
+ # LABEL_FLAGS flag in argv. That is well-formed for a LABEL-based tracker and
977
+ # structurally impossible for a STATE-based one:
978
+ #
979
+ # - GitHub's ready role is a label, labels are argv-native (`--label`), so
980
+ # `flag_values` finds it and an honest command exists.
981
+ # - JIRA's and Linear's ready roles are workflow STATES. The mandated access
982
+ # path is raw `curl` to a GraphQL/REST endpoint, `curl` has no `--state`
983
+ # flag, and the state lives in the request payload as an ID the guard
984
+ # cannot resolve without a network round-trip it refuses to make.
985
+ #
986
+ # So on a Linear-tracked project the only declaration left that passed was
987
+ # `[lisa-human-gate]`, which this file's own comments correctly forbid for a
988
+ # build-ready item: it stamps the item as held for a human product call, and
989
+ # build-intake scans the ready role and nothing else. An honest operator had NO
990
+ # compliant command and exactly one dishonest one — a guard failing in the
991
+ # harmful direction, where complying is worse than not.
992
+ #
993
+ # The declaration accepted here for state-based trackers is the LIFECYCLE ROLE
994
+ # the access layer already consumes. `lisa-linear-access` refuses to accept a
995
+ # caller-supplied `stateId`; it takes `lifecycle_role:<ROLE>`, resolves it
996
+ # against config and the team's own state catalog through
997
+ # `linear-state-write-target.mjs`, and fails CLOSED when it cannot. So the role
998
+ # token is not decoration: it is the input that decides which lane the item
999
+ # lands in, and a command carrying it either places the item in the ready lane
1000
+ # or refuses. That is the same epistemic standing as `--label status:ready`,
1001
+ # whose effect also happens one layer down inside `gh`.
1002
+ #
1003
+ # Scoped deliberately: accepted ONLY when the configured tracker's ready role
1004
+ # is a state, and ONLY for a filing addressed at this project's own tracker. A
1005
+ # GitHub filing — same-repo or cross-repo — still has to carry the label,
1006
+ # because for GitHub the label IS expressible and a second, weaker spelling
1007
+ # would be a hole rather than a remedy.
1008
+ STATE_ROLE_TRACKERS = {"jira", "linear"}
1009
+ #
1010
+ # The optional quote AFTER the key name is load-bearing, not decoration. Both
1011
+ # refusal messages tell the operator to put the declaration in the request
1012
+ # payload, and a JSON payload quotes its keys — so a pattern demanding `[:=]`
1013
+ # immediately after a bare key name refuses `"lifecycle_role": "ready"`, which
1014
+ # is the exact spelling it just asked for. Caught by review before it shipped.
1015
+ LIFECYCLE_ROLE_READY = re.compile(
1016
+ r"(?:^|[^\w.-])(?:lifecycle_role|LIFECYCLE_ROLE)[\"']?\s*[:=]\s*[\"']?ready\b"
1017
+ r"|(?:^|[^\w.-])--role[=\s]+[\"']?ready\b",
1018
+ )
1019
+ # Built per role at the point of use, because the role is project data. Kept
1020
+ # as a template rather than a compiled pattern so the role is always escaped.
1021
+ LABEL_FLAG_TEXT = r"--(?:label|labels|add-label|status|state)[=\s]+[\"']?%s(?![\w:.-])"
1022
+
1023
+ MAX_NESTING_DEPTH = 3
1024
+ # Operators that can be GLUED to an adjacent word (`true&&gh issue create`),
1025
+ # so they must be split out of a token. Braces and parentheses are deliberately
1026
+ # absent: shlex collapses a quoted argument into one token, so a GraphQL
1027
+ # payload arrives as `query=mutation{issueCreate(input:{})…}` and splitting on
1028
+ # braces tore it into fragments — silently un-refusing every GraphQL creation.
1029
+ # Standalone grouping punctuation is handled at segment and basename level
1030
+ # instead, where it cannot reach into a payload's contents.
1031
+ GLUED_OPERATORS = ("&&", "||", ";;", ";", "|", "&")
1032
+ SEGMENT_BOUNDARIES = set(GLUED_OPERATORS) | {"(", ")", "{", "}"}
1033
+
1034
+
1035
+ def strip_full_line_comments(text):
1036
+ """Blank lines beginning with `#` after optional whitespace.
1037
+
1038
+ This bounded fallback prevents ordinary prose comments from matching a
1039
+ creation token after an apostrophe defeats shlex (CodySwannGT/lisa#3551).
1040
+ It does not determine quote context: a leading `#` inside a multiline
1041
+ string is also removed. This is a textual guard, not shell interpretation.
1042
+
1043
+ Args:
1044
+ text: The raw command or file text.
1045
+
1046
+ Returns:
1047
+ The text with whole-line comments blanked, line count preserved.
1048
+ """
1049
+ return "\n".join(
1050
+ "" if line.lstrip().startswith("#") else line for line in text.split("\n")
1051
+ )
1052
+
1053
+
1054
+ def strip_heredocs(text):
1055
+ """Drop heredoc bodies so quoted prose cannot be read as argv.
1056
+
1057
+ Args:
1058
+ text: The raw command string.
1059
+
1060
+ Returns:
1061
+ The command with heredoc bodies removed.
1062
+ """
1063
+ lines = text.splitlines()
1064
+ output = []
1065
+ pending = []
1066
+ marker_pattern = re.compile(
1067
+ r"<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
1068
+ )
1069
+ index = 0
1070
+ while index < len(lines):
1071
+ line = lines[index]
1072
+ output.append(line)
1073
+ pending.extend(
1074
+ next(group for group in match.groups() if group)
1075
+ for match in marker_pattern.finditer(line)
1076
+ )
1077
+ index += 1
1078
+ while pending and index < len(lines):
1079
+ if lines[index].strip() == pending[0]:
1080
+ output.append(lines[index])
1081
+ pending.pop(0)
1082
+ index += 1
1083
+ break
1084
+ index += 1
1085
+ return "\n".join(output)
1086
+
1087
+
1088
+ class LiteralToken(str):
1089
+ """A source word containing quoted data, never a standalone separator."""
1090
+
1091
+
1092
+ def explode_operators(tokens, text=""):
1093
+ """Separate unquoted operators without losing mixed-word quote boundaries.
1094
+
1095
+ Hide each quoted or escaped source fragment before the ordinary shlex pass.
1096
+ Split operators while those fragments are opaque, then restore their values.
1097
+ Per-occurrence placeholders cannot exempt a different, unquoted separator.
1098
+ """
1099
+ operators = re.compile("(" + "|".join(re.escape(op) for op in GLUED_OPERATORS) + ")")
1100
+ if not text:
1101
+ return [piece for token in tokens for piece in operators.split(token) if piece]
1102
+
1103
+ prefix = "__lisa_literal_"
1104
+ while prefix in text:
1105
+ prefix += "_"
1106
+ values = []
1107
+ fragments = re.compile(r"""'[^']*'|"(?:\\[\s\S]|[^"\\])*"|\\[\s\S]""")
1108
+
1109
+ def hide(match):
1110
+ """Decode one protected fragment through the same POSIX lexer."""
1111
+ value = shlex.split(match.group(0), posix=True)
1112
+ values.append(value[0] if value else "")
1113
+ return prefix + str(len(values) - 1) + "__"
1114
+
1115
+ masked = fragments.sub(hide, text)
1116
+ marker = re.compile(re.escape(prefix) + r"(\d+)__")
1117
+
1118
+ def restore(value):
1119
+ """Restore once, so decoded text cannot become another placeholder."""
1120
+ return marker.sub(lambda match: values[int(match.group(1))], value)
1121
+
1122
+ shadow = shlex.split(masked, posix=True)
1123
+ if [restore(token) for token in shadow] != tokens:
1124
+ # If alignment cannot be established, grant no quoting exemptions.
1125
+ return [piece for token in tokens for piece in operators.split(token) if piece]
1126
+
1127
+ exploded = []
1128
+ for token in shadow:
1129
+ for piece in operators.split(token):
1130
+ if piece:
1131
+ exploded.append(LiteralToken(restore(piece)) if marker.search(piece) else piece)
1132
+ return exploded
1133
+
1134
+
1135
+ def segment(tokens):
1136
+ """Split a token stream into individual commands at shell operators.
1137
+
1138
+ Args:
1139
+ tokens: Exploded tokens.
1140
+
1141
+ Returns:
1142
+ A list of argv lists.
1143
+ """
1144
+ segments = []
1145
+ current = []
1146
+ for token in tokens:
1147
+ if token in SEGMENT_BOUNDARIES and not isinstance(token, LiteralToken):
1148
+ segments.append(current)
1149
+ current = []
1150
+ continue
1151
+ current.append(token)
1152
+ segments.append(current)
1153
+ return [item for item in segments if item]
1154
+
1155
+
1156
+ def basename(token):
1157
+ """The final path component of a token, quotes stripped.
1158
+
1159
+ Args:
1160
+ token: A shell token.
1161
+
1162
+ Returns:
1163
+ The basename.
1164
+ """
1165
+ return token.strip("'\"").strip("(){}").rsplit("/", 1)[-1]
1166
+
1167
+
1168
+ def is_flag_value(args, index):
1169
+ """Whether the token at `index` is the value of the preceding flag.
1170
+
1171
+ This is the single position question the classifier keeps having to ask,
1172
+ and getting it wrong is what produced three separate bypasses: the role
1173
+ read from a `--title`, the role read past `--`, and a subcommand read as a
1174
+ flag's value. It is answered in exactly one place now.
1175
+
1176
+ Args:
1177
+ args: A command's arguments.
1178
+ index: The position to test.
1179
+
1180
+ Returns:
1181
+ True when the previous token is a flag that carries no `=`.
1182
+ """
1183
+ if index == 0:
1184
+ return False
1185
+ previous = args[index - 1]
1186
+ return previous.startswith("-") and previous != "-" and "=" not in previous
1187
+
1188
+
1189
+ def bare_index(args, word, start=0):
1190
+ """Index of `word` appearing as itself rather than as a flag's value.
1191
+
1192
+ Args:
1193
+ args: A command's arguments.
1194
+ word: The word to locate.
1195
+ start: Index to search from.
1196
+
1197
+ Returns:
1198
+ The index, or -1.
1199
+ """
1200
+ for index in range(start, len(args)):
1201
+ if args[index] == word and not is_flag_value(args, index):
1202
+ return index
1203
+ return -1
1204
+
1205
+
1206
+ def invokes_verb(args, groups, verb):
1207
+ """Whether a group word is followed later by a bare verb.
1208
+
1209
+ Deliberately tolerant of anything between them, because a flag may sit
1210
+ between the group and the verb — `gh issue --repo o/r create` is accepted
1211
+ by cobra, which strips persistent flags before resolving the subcommand.
1212
+ Tolerance is safe here only because the verb itself must be bare: that is
1213
+ what keeps `gh issue list --search create` from reading as a creation.
1214
+
1215
+ Args:
1216
+ args: A command's arguments.
1217
+ groups: Acceptable group words, e.g. {"issue", "workitem"}.
1218
+ verb: The verb, e.g. "create".
1219
+
1220
+ Returns:
1221
+ True when the invocation names the verb.
1222
+ """
1223
+ # The bare-token filter is applied to the VERB only, never to the group
1224
+ # word, and the asymmetry is the point. `--verbose` is boolean, so treating
1225
+ # the token after any flag as that flag's value swallowed `api` in
1226
+ # `gh --verbose api …` and `issue` in `gh --verbose issue create`. Being
1227
+ # permissive about the group costs nothing, because the verb still has to
1228
+ # match; being permissive about the VERB is what would read
1229
+ # `gh issue list --search create` as a creation. Over-include where the
1230
+ # consequence is another check, filter where the consequence is a refusal.
1231
+ for group in groups:
1232
+ if group not in args:
1233
+ continue
1234
+ group_at = args.index(group)
1235
+ if bare_index(args, verb, group_at + 1) >= 0:
1236
+ return True
1237
+ return False
1238
+
1239
+
1240
+ def is_write_request(args):
1241
+ """Whether the arguments describe an HTTP write rather than a read.
1242
+
1243
+ Args:
1244
+ args: A command's arguments.
1245
+
1246
+ Returns:
1247
+ True if a POST method or a payload-bearing flag is present.
1248
+ """
1249
+ for index, token in enumerate(args):
1250
+ if token in POST_METHOD_FLAGS:
1251
+ if index + 1 < len(args) and args[index + 1].upper() == "POST":
1252
+ return True
1253
+ if "=" in token:
1254
+ head, value = token.split("=", 1)
1255
+ if head in POST_METHOD_FLAGS and value.upper() == "POST":
1256
+ return True
1257
+ if token.upper() == "-XPOST":
1258
+ return True
1259
+ if token in POST_PAYLOAD_FLAGS:
1260
+ return True
1261
+ return False
1262
+
1263
+
1264
+ def endpoint_path(token):
1265
+ """The path component of an endpoint-shaped argument.
1266
+
1267
+ An endpoint is a URL, and a URL is not its path: `?query` and `#fragment`
1268
+ are separate components that address the SAME resource. Comparing the raw
1269
+ argument against a path pattern therefore recognised a URL SHAPE rather
1270
+ than an endpoint, and `repos/o/r/issues?foo=1` — the identical request —
1271
+ was classified as a non-creation and allowed (#2939).
1272
+
1273
+ Trailing whitespace is stripped for the same reason: `"repos/o/r/issues "`
1274
+ survives shlex as one token and addresses the same endpoint, but defeats an
1275
+ end-anchored comparison just as a query string does.
1276
+
1277
+ Args:
1278
+ token: One argument, possibly a decorated endpoint.
1279
+
1280
+ Returns:
1281
+ The token with any fragment, query, and surrounding whitespace removed.
1282
+ """
1283
+ # Fragment first: it is the LAST component of a URL, so `path?q#f` yields
1284
+ # `path?q` here and `path` after the query split, while a malformed
1285
+ # `path#a?b` still collapses to `path` rather than keeping `a?b`.
1286
+ return token.split("#", 1)[0].split("?", 1)[0].strip()
1287
+
1288
+
1289
+ def endpoint_paths(args, pattern):
1290
+ """Endpoint path components matching a pattern, excluding payload values.
1291
+
1292
+ Scans every token rather than a filtered positional list, because a BOOLEAN
1293
+ flag has no value to skip and filtering swallowed the endpoint behind one:
1294
+ `gh api -X POST --silent repos/o/r/issues -f title=x` hid the endpoint
1295
+ behind `--silent`. Payload values are excluded the other way, so
1296
+ `-f path=repos/o/r/issues` is not mistaken for the address being posted to.
1297
+
1298
+ That payload exclusion is tested on the PATH, not the raw token, and the
1299
+ order matters: a query string carries `=`, so testing the raw token skipped
1300
+ `repos/o/r/issues?foo=1` before the pattern ever ran. Two independent
1301
+ mechanisms — this filter and the pattern's end anchor — hid the same
1302
+ creation, which is why fixing only the anchor would not have closed it.
1303
+
1304
+ Args:
1305
+ args: A command's arguments.
1306
+ pattern: The endpoint regex, written against a path component.
1307
+
1308
+ Returns:
1309
+ The matching path components, decoration removed.
1310
+ """
1311
+ found = []
1312
+ for index, token in enumerate(args):
1313
+ path = endpoint_path(token)
1314
+ if "=" in path:
1315
+ continue
1316
+ if index > 0 and args[index - 1] in PAYLOAD_VALUE_FLAGS:
1317
+ continue
1318
+ if pattern.search(path):
1319
+ found.append(path)
1320
+ return found
1321
+
1322
+
1323
+ def resolve_operand(token):
1324
+ """A readable regular file named by one token, or None.
1325
+
1326
+ Bounded on purpose. The size cap keeps a PreToolUse hook off a multi-
1327
+ megabyte read on every intercepted command, and a file too large to
1328
+ inspect is skipped rather than half-read: a truncated scan reports a
1329
+ confident ALLOW about text it never saw.
1330
+
1331
+ Args:
1332
+ token: One argument, possibly quoted or `@`-prefixed for curl.
1333
+
1334
+ Returns:
1335
+ An existing path, or None.
1336
+ """
1337
+ text = token.strip().strip("'\"")
1338
+ # `-d@payload.json` names a file just as plainly as `@payload.json` does.
1339
+ attached = attached_value(text)
1340
+ if attached is not None:
1341
+ text = attached
1342
+ if text.startswith("@"):
1343
+ text = text[1:]
1344
+ if not text or text in STDIN_PAYLOAD_TOKENS:
1345
+ return None
1346
+ candidates = [text]
1347
+ if project_dir and not os.path.isabs(text):
1348
+ candidates.append(os.path.join(project_dir, text))
1349
+ for candidate in candidates:
1350
+ try:
1351
+ if not os.path.isfile(candidate):
1352
+ continue
1353
+ if os.path.getsize(candidate) > FILE_OPERAND_MAX_BYTES:
1354
+ continue
1355
+ except OSError:
1356
+ continue
1357
+ return candidate
1358
+ return None
1359
+
1360
+
1361
+ def read_operand(path):
1362
+ """The text of a file the command names.
1363
+
1364
+ Args:
1365
+ path: A path from `resolve_operand`.
1366
+
1367
+ Returns:
1368
+ The contents, or an empty string when unreadable.
1369
+ """
1370
+ try:
1371
+ with open(path, encoding="utf-8", errors="replace") as handle:
1372
+ return handle.read(FILE_OPERAND_MAX_BYTES)
1373
+ except OSError:
1374
+ return ""
1375
+
1376
+
1377
+ def attached_value(token):
1378
+ """A payload flag's value when it is GLUED to the flag.
1379
+
1380
+ `curl -d@payload.json` and `curl -d'{"query":…}'` are one token each, so a
1381
+ parser that only looks at `args[i + 1]` and at `flag=value` sees neither —
1382
+ and `-d@file` is the ordinary spelling, not an exotic one. Caught by review
1383
+ before it shipped, and it was a real bypass: the mutation stayed in the
1384
+ file, the conjunction never formed, and an undeclared creation passed.
1385
+
1386
+ Longest flag first, so `--data-binary@f` is not read as `--data` with a
1387
+ `-binary@f` value. A remainder starting with `-` is rejected for the same
1388
+ reason: it is another option, not this one's value.
1389
+
1390
+ Args:
1391
+ token: One argument.
1392
+
1393
+ Returns:
1394
+ The attached value, or None.
1395
+ """
1396
+ for flag in sorted(PAYLOAD_SOURCE_FLAGS, key=len, reverse=True):
1397
+ if not token.startswith(flag) or len(token) == len(flag):
1398
+ continue
1399
+ rest = token[len(flag) :]
1400
+ if rest.startswith("="):
1401
+ rest = rest[1:]
1402
+ if not rest or rest.startswith("-"):
1403
+ continue
1404
+ return rest
1405
+ return None
1406
+
1407
+
1408
+ def payload_text(args, whole_command):
1409
+ """The body this command will submit, wherever it is coming from.
1410
+
1411
+ Three sources, because a request body has three places to live and the
1412
+ guard was reading only the first: inline after `-d`, in a file after
1413
+ `--data-binary @path` / `--input path`, or on stdin from an earlier stage
1414
+ of the same pipeline.
1415
+
1416
+ The stdin case takes the WHOLE command text rather than the segment,
1417
+ because `jq -n … | curl … --data-binary @-` is one logical command that
1418
+ `segment` has already split in two — the payload literally is the other
1419
+ half. That is the shape `lisa-linear-access` mandates, so it has to be
1420
+ readable rather than invisible.
1421
+
1422
+ Args:
1423
+ args: A command's arguments.
1424
+ whole_command: The full intercepted command string.
1425
+
1426
+ Returns:
1427
+ The payload text, possibly empty.
1428
+ """
1429
+ parts = []
1430
+ for index, token in enumerate(args):
1431
+ value = None
1432
+ if token in PAYLOAD_SOURCE_FLAGS and index + 1 < len(args):
1433
+ value = args[index + 1]
1434
+ elif "=" in token:
1435
+ head, rhs = token.split("=", 1)
1436
+ if head in PAYLOAD_SOURCE_FLAGS:
1437
+ value = rhs
1438
+ if value is None:
1439
+ value = attached_value(token)
1440
+ if value is None:
1441
+ continue
1442
+ stripped = value.strip().strip("'\"")
1443
+ if stripped in STDIN_PAYLOAD_TOKENS:
1444
+ parts.append(whole_command)
1445
+ continue
1446
+ parts.append(stripped)
1447
+ path = resolve_operand(stripped)
1448
+ if path is not None:
1449
+ parts.append(read_operand(path))
1450
+ return "\n".join(parts)
1451
+
1452
+
1453
+ def text_declares_readiness(text):
1454
+ """Whether a file's or payload's own text carries a declaration.
1455
+
1456
+ The token-position machinery above cannot be reused here: a file is not
1457
+ argv, and a payload is JSON. What is checked is deliberately the SAME
1458
+ three declarations, matched textually and never loosened into "the role
1459
+ string appears somewhere" — a state-based ready role is an ordinary word
1460
+ like `Ready`, and accepting a bare occurrence of it would let prose in a
1461
+ description declare readiness the item does not have.
1462
+
1463
+ Args:
1464
+ text: File or payload contents.
1465
+
1466
+ Returns:
1467
+ True when the text declares build-ready or a human gate.
1468
+ """
1469
+ if declares_human_gate(text):
1470
+ return True
1471
+ for role in (ready_role, upstream_ready_role, default_ready_role):
1472
+ if not role:
1473
+ continue
1474
+ if re.search(LABEL_FLAG_TEXT % re.escape(role), text):
1475
+ return True
1476
+ if tracker in STATE_ROLE_TRACKERS:
1477
+ if LIFECYCLE_ROLE_READY.search(text):
1478
+ return True
1479
+ if ready_role and re.search(
1480
+ r"\"(?:state|status|stateName|statusName|state_name|name|transition)\""
1481
+ r"\s*:\s*\"%s\"" % re.escape(ready_role),
1482
+ text,
1483
+ re.IGNORECASE,
1484
+ ):
1485
+ return True
1486
+ return False
1487
+
1488
+
1489
+ def scope_declaration(text, state_role_ok):
1490
+ """Whether a whole text declares readiness for everything inside it.
1491
+
1492
+ Only the two MARKERS qualify, never the `--label` flag, and that asymmetry
1493
+ is the same one the argv check already makes. A marker has no other
1494
+ meaning, so its presence anywhere in a script IS the declaration for that
1495
+ script. A label flag is positional and belongs to one create; letting it
1496
+ vouch for a second, unlabelled create further down the same file would be a
1497
+ hole rather than a convenience.
1498
+
1499
+ `state_role_ok` carries the SAME scoping the argv path applies, and it has
1500
+ to be threaded here rather than recomputed from the tracker alone: a Linear
1501
+ project whose script files `gh issue create --repo <other>` is a cross-repo
1502
+ GitHub filing, and this project's workflow role does not answer for another
1503
+ repository's queue. Checking only `tracker in STATE_ROLE_TRACKERS` waved
1504
+ exactly that through. Caught by review before it shipped.
1505
+
1506
+ Args:
1507
+ text: A script's contents, or one command segment.
1508
+ state_role_ok: Whether a lifecycle-role declaration answers here.
1509
+
1510
+ Returns:
1511
+ True when the text carries a whole-scope declaration.
1512
+ """
1513
+ if declares_human_gate(text):
1514
+ return True
1515
+ return bool(state_role_ok and LIFECYCLE_ROLE_READY.search(text))
1516
+
1517
+
1518
+ def creation_signature(name, args, extra=""):
1519
+ """Classify a tracker CLI invocation as a creation.
1520
+
1521
+ Args:
1522
+ name: The CLI basename.
1523
+ args: Every token after it in this segment.
1524
+ extra: Payload text this command submits, from a file or stdin.
1525
+
1526
+ Returns:
1527
+ A short human-readable signature, or None.
1528
+ """
1529
+ if "--help" in args or "-h" in args:
1530
+ return None
1531
+ joined = " ".join(args)
1532
+ if extra:
1533
+ joined = joined + "\n" + extra
1534
+
1535
+ if name == "gh":
1536
+ if invokes_verb(args, {"issue"}, "create"):
1537
+ return "gh issue create"
1538
+ # Same reasoning: `api` is located without the flag-value filter, since
1539
+ # an endpoint match and a write method must both also hold.
1540
+ if "api" in args:
1541
+ if GRAPHQL_CREATE.search(joined):
1542
+ return "gh api graphql issue creation"
1543
+ if endpoint_paths(args, GITHUB_ISSUES_PATH) and is_write_request(args):
1544
+ return "gh api POST .../issues"
1545
+ return None
1546
+
1547
+ if name in {"linear", "jira"}:
1548
+ if invokes_verb(args, {"issue", "issues"}, "create"):
1549
+ return "%s issue create" % name
1550
+ return None
1551
+
1552
+ if name == "acli":
1553
+ if invokes_verb(args, {"workitem", "workitems", "issue", "issues"}, "create"):
1554
+ return "acli … create"
1555
+ return None
1556
+
1557
+ if name in HTTP_CLIS:
1558
+ if not is_write_request(args):
1559
+ return None
1560
+ for token in args:
1561
+ if GITHUB_ISSUES_URL.search(token):
1562
+ return "%s POST api.github.com/…/issues" % name
1563
+ if JIRA_ISSUE_URL.search(token):
1564
+ return "%s POST …/rest/api/…/issue" % name
1565
+ if "api.linear.app/graphql" in token and GRAPHQL_CREATE.search(joined):
1566
+ return "%s POST api.linear.app/graphql issueCreate" % name
1567
+ return None
1568
+
1569
+ return None
1570
+
1571
+
1572
+ def before_end_of_options(args):
1573
+ """The arguments up to a bare `--`.
1574
+
1575
+ Everything after `--` is an operand, not a flag, so it cannot reach the
1576
+ created item — crediting a declaration from there is the same mistake as
1577
+ reading the role out of a title, one position over.
1578
+
1579
+ The two CLIs available for testing disagree about it, which is why the
1580
+ guard cannot lean on any of them being strict: gh 2.96.0 rejects a
1581
+ post-`--` flag outright, while `acli` parses straight past it and proceeds
1582
+ to create the work item with the trailing `--status` silently unapplied.
1583
+ That made it a live bypass on the JIRA path, verified by running it.
1584
+
1585
+ Args:
1586
+ args: A command's arguments.
1587
+
1588
+ Returns:
1589
+ The arguments preceding the first bare `--`.
1590
+ """
1591
+ return args[: args.index("--")] if "--" in args else args
1592
+
1593
+
1594
+ def body_file_paths(args):
1595
+ """Paths the command will submit as the item body.
1596
+
1597
+ Args:
1598
+ args: A command's arguments.
1599
+
1600
+ Returns:
1601
+ Candidate file paths, unverified.
1602
+ """
1603
+ paths = []
1604
+ for index, token in enumerate(args):
1605
+ if token in BODY_FILE_FLAGS and index + 1 < len(args):
1606
+ paths.append(args[index + 1])
1607
+ if "=" in token:
1608
+ head, value = token.split("=", 1)
1609
+ if head in BODY_FILE_FLAGS:
1610
+ paths.append(value)
1611
+ if token.startswith("@") and len(token) > 1:
1612
+ paths.append(token[1:])
1613
+ return paths
1614
+
1615
+
1616
+ def normalise_repo(value):
1617
+ """A `--repo` value reduced to a comparable `owner/name`.
1618
+
1619
+ `gh` accepts `OWNER/REPO`, `HOST/OWNER/REPO`, and a full browser URL, and
1620
+ GitHub itself is case-insensitive about both halves — so comparing the raw
1621
+ token would call the same repository two different places depending on how
1622
+ it was typed.
1623
+
1624
+ Casing is PRESERVED here and folded only at the point of comparison. The
1625
+ refusal names this string back to an operator, and echoing
1626
+ `codyswanngt/lisa` at someone who typed `CodySwannGT/lisa` reads as a
1627
+ different repository — a message that has to be squinted at is the thing
1628
+ this change is repairing.
1629
+
1630
+ Args:
1631
+ value: The raw token.
1632
+
1633
+ Returns:
1634
+ An as-typed `owner/name`, or None when the token names no repository.
1635
+ """
1636
+ text = value.strip().strip("'\"")
1637
+ if text.endswith(".git"):
1638
+ text = text[: -len(".git")]
1639
+ parts = [part for part in text.split("/") if part and not part.endswith(":")]
1640
+ if len(parts) < 2:
1641
+ return None
1642
+ return "%s/%s" % (parts[-2], parts[-1])
1643
+
1644
+
1645
+ def target_repository(args):
1646
+ """The repository this creation is addressed at, when it names one.
1647
+
1648
+ Read only from positions that actually reach the created item: a flag
1649
+ before the end-of-options marker, or the endpoint the write is posted to.
1650
+ A `-f repo=o/r` payload field is data being SENT, not the address being
1651
+ posted to, and `endpoint_paths` already excludes it.
1652
+
1653
+ Args:
1654
+ args: A creating command's arguments.
1655
+
1656
+ Returns:
1657
+ A lowercased `owner/name`, or None when the calling project is the
1658
+ target — which is the overwhelmingly common case and today's behaviour.
1659
+ """
1660
+ scoped = before_end_of_options(args)
1661
+ for index, token in enumerate(scoped):
1662
+ if token in REPO_FLAGS and index + 1 < len(scoped):
1663
+ return normalise_repo(scoped[index + 1])
1664
+ if "=" in token:
1665
+ head, value = token.split("=", 1)
1666
+ if head in REPO_FLAGS:
1667
+ return normalise_repo(value)
1668
+ for path in endpoint_paths(scoped, GITHUB_ISSUES_PATH):
1669
+ match = GITHUB_ISSUES_PATH_REPO.search(path)
1670
+ if match:
1671
+ return normalise_repo("%s/%s" % (match.group(1), match.group(2)))
1672
+ for token in scoped:
1673
+ match = GITHUB_ISSUES_URL_REPO.search(token)
1674
+ if match:
1675
+ return normalise_repo("%s/%s" % (match.group(1), match.group(2)))
1676
+ return None
1677
+
1678
+
1679
+ def roles_for(target):
1680
+ """Which ready-role tokens satisfy a creation addressed at `target`.
1681
+
1682
+ The guard demands a declaration either way; this decides only WHOSE
1683
+ vocabulary the declaration is written in.
1684
+
1685
+ The indeterminate case is the last branch: a GitHub-tracked project that
1686
+ declares no `github.org`/`github.repo` cannot be compared against a target,
1687
+ so both roles are accepted rather than inventing a refusal. That is
1688
+ permissive about which token, never about whether one is required.
1689
+
1690
+ Args:
1691
+ target: The addressed repository as typed, or None.
1692
+
1693
+ Returns:
1694
+ A (roles, cross_repo_target) pair. The target is None when the calling
1695
+ project is the one being written to. When set it is the as-typed
1696
+ spelling, because the refusal names it back to an operator.
1697
+ """
1698
+ # GitHub is case-insensitive about owner and name, so the comparison folds
1699
+ # case while the reported string keeps the operator's own spelling.
1700
+ folded = target.lower() if target is not None else None
1701
+ if folded is None or (own_repo and folded == own_repo):
1702
+ return [ready_role], None
1703
+ if upstream_repo and folded == upstream_repo:
1704
+ role = upstream_ready_role
1705
+ else:
1706
+ # Another repository Lisa has no configuration for. Its lane is
1707
+ # whatever GitHub's stock one is; the caller's token is categorically
1708
+ # not it.
1709
+ role = default_ready_role
1710
+ if own_repo or not caller_is_github:
1711
+ return [role], target
1712
+ # Indeterminate, and the target is deliberately NOT reported. The refusal
1713
+ # would otherwise say "this filing is addressed at another repository" and
1714
+ # "this project's role does not answer for it" — the first unproven and the
1715
+ # second flatly false, since this branch accepts the project's role. A
1716
+ # message naming a token that does not work is the remediation pointing
1717
+ # away from the fix, which is the defect being repaired here.
1718
+ return [ready_role, role], None
1719
+
1720
+
1721
+ def declares_readiness(raw_args, roles, extra="", state_role_ok=False):
1722
+ """Whether the create carries one of the required declarations.
1723
+
1724
+ Args:
1725
+ raw_args: The creating command's arguments.
1726
+ roles: The build-ready role tokens that satisfy this filing.
1727
+ extra: Payload text this command submits, from a file or stdin.
1728
+ state_role_ok: Whether a lifecycle-role declaration answers here. True
1729
+ only for a state-based tracker filing into its own tracker, where
1730
+ no argv flag on the mandated client can carry the state.
1731
+
1732
+ Returns:
1733
+ True when a build-ready role or a human-gate marker is present.
1734
+ """
1735
+ args = before_end_of_options(raw_args)
1736
+ for role in roles:
1737
+ if not role:
1738
+ continue
1739
+ for raw in flag_values(args, LABEL_FLAGS):
1740
+ candidates = [part.strip().strip("'\"") for part in raw.split(",")]
1741
+ if role in candidates:
1742
+ return True
1743
+ # Checked per ARGV VALUE rather than against `" ".join(args)`, and that is
1744
+ # what makes the positional rule mean anything here. Joining first destroys
1745
+ # position: after a join, no value's line start survives except the first
1746
+ # argument's, so "the marker leads its line" would be decided by argument
1747
+ # order. A `--title` value is its own single line, which is exactly the
1748
+ # unit the rule is about -- and titles must keep being read, since this
1749
+ # guard reads them and the intake matcher does not.
1750
+ if any(declares_human_gate(value) for value in args):
1751
+ return True
1752
+ for path in body_file_paths(args):
1753
+ try:
1754
+ with open(path, encoding="utf-8", errors="replace") as handle:
1755
+ if declares_human_gate(handle.read()):
1756
+ return True
1757
+ except OSError:
1758
+ continue
1759
+ if declares_human_gate(extra):
1760
+ return True
1761
+ # The state-based path. Scoped by `state_role_ok` rather than checked
1762
+ # unconditionally, so this adds a compliant command where none existed and
1763
+ # takes none away where one already did.
1764
+ if state_role_ok:
1765
+ if LIFECYCLE_ROLE_READY.search(" ".join(args)):
1766
+ return True
1767
+ if extra and text_declares_readiness(extra):
1768
+ return True
1769
+ return False
1770
+
1771
+
1772
+ def declares_leaf_type(args):
1773
+ """Whether the filing declares a by-design leaf type as a flag value.
1774
+
1775
+ Read only from positions that reach the created item, and never from a
1776
+ title or body — the same discipline the build-ready role is held to, for
1777
+ the same reason: a type named in prose is not a type applied.
1778
+
1779
+ Args:
1780
+ args: The creating command's arguments, already scoped to the flags.
1781
+
1782
+ Returns:
1783
+ True when a Bug / Task / Sub-task / Improvement is declared.
1784
+ """
1785
+ for raw in flag_values(args, TYPE_FLAGS):
1786
+ for part in raw.split(","):
1787
+ value = part.strip().strip("'\"").lower()
1788
+ if value.startswith("type:"):
1789
+ value = value[len("type:") :]
1790
+ if value in BY_DESIGN_LEAF_TYPES:
1791
+ return True
1792
+ return False
1793
+
1794
+
1795
+ def declares_container(raw_args, texts=()):
1796
+ """Whether the create declares the item a container.
1797
+
1798
+ A container is neither build-ready nor human-gated by construction, so this
1799
+ is the third accepted declaration rather than a third way to satisfy the
1800
+ first two. See the CONTAINER_DECLARATION block for why it reads the
1801
+ canonical declaration and not a `type:Epic` claim.
1802
+
1803
+ Args:
1804
+ raw_args: The creating command's arguments.
1805
+ texts: Payload or script text this command submits or runs.
1806
+
1807
+ Returns:
1808
+ True when the container declaration is present and uncontradicted.
1809
+ """
1810
+ args = before_end_of_options(raw_args)
1811
+ # The contradiction check comes FIRST, so a filing that declares itself
1812
+ # both a container and a by-design leaf is refused rather than allowed on
1813
+ # the strength of the half that suits it.
1814
+ if declares_leaf_type(args):
1815
+ return False
1816
+ if CONTAINER_DECLARATION.search(" ".join(args)):
1817
+ return True
1818
+ for path in body_file_paths(args):
1819
+ try:
1820
+ with open(path, encoding="utf-8", errors="replace") as handle:
1821
+ if CONTAINER_DECLARATION.search(handle.read()):
1822
+ return True
1823
+ except OSError:
1824
+ continue
1825
+ for text in texts:
1826
+ if text and CONTAINER_DECLARATION.search(text):
1827
+ return True
1828
+ return False
1829
+
1830
+
1831
+ def flag_values(args, names):
1832
+ """Every value assigned to one of the named flags.
1833
+
1834
+ Args:
1835
+ args: A command's arguments.
1836
+ names: The flag spellings to collect.
1837
+
1838
+ Returns:
1839
+ The raw values, unsplit and unquoted.
1840
+ """
1841
+ values = []
1842
+ for index, token in enumerate(args):
1843
+ if token in names and index + 1 < len(args):
1844
+ values.append(args[index + 1])
1845
+ if "=" in token:
1846
+ head, value = token.split("=", 1)
1847
+ if head in names:
1848
+ values.append(value)
1849
+ return values
1850
+
1851
+
1852
+ def nested_operands(argv):
1853
+ """Command strings this argv hands to another interpreter.
1854
+
1855
+ Position-scoped rather than shell-allowlisted: the operand after `-c` (or
1856
+ after `eval`) is a command by the calling convention itself, whoever the
1857
+ program is. That covers `bash -c`, `sh -c`, `zsh -c`, `python -c`, and the
1858
+ POSIX builtin `eval`, without an allowlist to keep complete.
1859
+
1860
+ Recursing into arbitrary trailing quoted operands was considered and
1861
+ rejected: it re-refuses `git commit -m "the gh issue create guard"`, which
1862
+ is an ordinary and correct command. `ssh host '…'` is therefore NOT
1863
+ intercepted — a documented limit, since that runs against another host's
1864
+ tracker config and needs that host's own guard.
1865
+
1866
+ Args:
1867
+ argv: One command's tokens.
1868
+
1869
+ Returns:
1870
+ Nested command strings.
1871
+ """
1872
+ operands = []
1873
+ for index, token in enumerate(argv):
1874
+ if index + 1 >= len(argv):
1875
+ continue
1876
+ if token == "-c" or token.endswith("-c") and token.startswith("-"):
1877
+ operands.append(argv[index + 1])
1878
+ elif basename(token) == "eval":
1879
+ operands.append(argv[index + 1])
1880
+ return operands
1881
+
1882
+
1883
+ inspected_files = set()
1884
+
1885
+
1886
+ # Interpreters whose OPERAND is a program they run. Broader than a shell list
1887
+ # because `file_creation` recognises a creation in any language — the
1888
+ # `node wrapper.mjs` that speaks HTTP directly is exactly the shape #3484
1889
+ # measured as a fail-open, so narrowing this to shells would reopen it.
1890
+ EXECUTING_INTERPRETERS = {
1891
+ "bash", "dash", "ksh", "sh", "zsh",
1892
+ "bun", "deno", "node", "nodejs", "perl", "php",
1893
+ "python", "python3", "ruby", "ts-node", "tsx",
1894
+ }
1895
+
1896
+ # `.` and its `source` alias run the named file in the CURRENT shell.
1897
+ SOURCE_BUILTINS = {"source", "."}
1898
+
1899
+ # Wrappers that run what FOLLOWS them without changing what it is, mapped to
1900
+ # (options whose value is a SEPARATE token, positional operands consumed).
1901
+ # The positional count is why this is a table: `timeout 5 bash create.sh` puts
1902
+ # an operand between the wrapper and the interpreter, so a walk that steps over
1903
+ # `-flags` only stops at `5` and never reaches the interpreter.
1904
+ EXEC_WRAPPERS = {
1905
+ "builtin": (frozenset(), 0),
1906
+ "command": (frozenset(), 0),
1907
+ "exec": (frozenset({"-a"}), 0),
1908
+ "env": (frozenset({"-u", "--unset", "-C", "--chdir", "--argv0"}), 0),
1909
+ "nice": (frozenset({"-n", "--adjustment"}), 0),
1910
+ "nohup": (frozenset(), 0),
1911
+ "setsid": (frozenset(), 0),
1912
+ "stdbuf": (frozenset({"-i", "-o", "-e"}), 0),
1913
+ "sudo": (frozenset({
1914
+ "-u", "--user", "-g", "--group", "-C", "--close-from",
1915
+ "-p", "--prompt", "-h", "--host", "-r", "--role",
1916
+ "-t", "--type", "-U", "--other-user",
1917
+ }), 0),
1918
+ "time": (frozenset(), 0),
1919
+ "timeout": (frozenset({"-k", "--kill-after", "-s", "--signal"}), 1),
1920
+ }
1921
+
1922
+ # Interpreter options that mean "no script file follows": a command string or a
1923
+ # module name, both of which the guard reads by other means or not at all. These
1924
+ # are interpreter-specific on purpose. `bash -e create.sh` and `sh -m create.sh`
1925
+ # still execute the following file; treating Python and Node flags as universal
1926
+ # silently stopped the scan before those shell operands.
1927
+ NO_SCRIPT_OPTIONS = {
1928
+ "bun": {"-e", "--eval"},
1929
+ "node": {"-e", "--eval"},
1930
+ "nodejs": {"-e", "--eval"},
1931
+ "perl": {"-e"},
1932
+ "php": {"-r"},
1933
+ "python": {"-c", "-m", "--command", "--module"},
1934
+ "python3": {"-c", "-m", "--command", "--module"},
1935
+ "ruby": {"-e"},
1936
+ }
1937
+
1938
+ # Interpreter switches whose value is a separate token, followed by the real
1939
+ # script operand. Treating the value as the script makes the guard inspect a
1940
+ # preload or option value and never reach what the interpreter executes.
1941
+ INTERPRETER_VALUE_OPTIONS = {
1942
+ "bash": {"-o", "--option"},
1943
+ "dash": {"-o"},
1944
+ "deno": {"-c", "--config"},
1945
+ "ksh": {"-o"},
1946
+ "node": {"-r", "--require"},
1947
+ "nodejs": {"-r", "--require"},
1948
+ "sh": {"-o"},
1949
+ "zsh": {"-o"},
1950
+ }
1951
+
1952
+ # Runtime subcommands that introduce the script path rather than name it.
1953
+ INTERPRETER_SUBCOMMANDS = {
1954
+ "bun": {"run"},
1955
+ "deno": {"run"},
1956
+ }
1957
+
1958
+
1959
+ def is_assignment_word(token):
1960
+ """Whether token is a POSIX shell assignment in command-prefix position."""
1961
+ return re.match(r"^[A-Za-z_][A-Za-z0-9_]*\+?=", token) is not None
1962
+
1963
+ # Programs that READ their operands and never execute them as programs.
1964
+ #
1965
+ # This is an allowlist of readers rather than the inverse, and the asymmetry is
1966
+ # deliberate. An UNKNOWN program with a file operand is still followed, because
1967
+ # whether it executes that operand is genuinely unknowable from the command —
1968
+ # and a probe in this guard's own suite drives it with a runner nobody
1969
+ # enumerated precisely so a fix keyed on a list of interpreters fails. Failing
1970
+ # closed on the unknown keeps that coverage.
1971
+ #
1972
+ # What the list buys is the measured population of #3705, every member of which
1973
+ # is a well-known inspector: `git diff`, `grep -n`, `wc -l`, a `git grep`
1974
+ # PATHSPEC, `sed -n '1,50p'`, and a test run. `git` is here wholesale because no
1975
+ # git subcommand executes a named file as a program — `git commit -F <file>` and
1976
+ # `git grep -- <pathspec>` both take the path as data.
1977
+ #
1978
+ # Test runners are here on purpose and it is the one entry worth arguing about.
1979
+ # `vitest <file>` does run that file, so the executes-rule would follow it — but
1980
+ # running a test is not filing an issue, and refusing a test run is the sharpest
1981
+ # harm #3705 records, because it can block the verification of a fix while
1982
+ # saying nothing about why.
1983
+ #
1984
+ # RELOCATION AND ECHOING ARE READS TOO, and leaving them off is what turned a
1985
+ # false positive into a property of the repository (CodySwannGT/lisa#3683). A
1986
+ # file that QUOTES a creation was already reachable by `grep`, `cat` and `ls`
1987
+ # once this list existed — but not by `cp` or `mv`, so the file could be read
1988
+ # and never moved, by any agent and by CI, for as long as the literal stayed in
1989
+ # it. The measured case shows why "reword it" is not the inverse: the refusal
1990
+ # recurred AFTER the content had been reworded, on a different file quoting the
1991
+ # same command. None of these four executes an operand: `cp` and `mv` copy
1992
+ # bytes, `echo` and `printf` write them to stdout. They are also the vocabulary
1993
+ # `unparseable_reads_only` answers with when the text could not be lexed at all.
1994
+ #
1995
+ # RESIDUAL, stated rather than hidden: a reader NOT on this list is still
1996
+ # followed and can still over-refuse. The set of read-only tools is unbounded,
1997
+ # so this closes the measured population and not the class. Add names here as
1998
+ # they are measured; do not invert the default to close it by fiat, or the
1999
+ # executed-script reach that #3484 bought is lost.
2000
+ #
2001
+ # The copy/move/link family is measured, not inferred: `cp <a file that files>`
2002
+ # was refused as "an unparseable command that reads as a tracker creation
2003
+ # inside <path>" (CodySwannGT/lisa#3683, trip 5, reproduced in this guard's own
2004
+ # suite). These four relocate bytes and never execute an operand, so following
2005
+ # them can only ever over-refuse — and the harm is the propagating kind this
2006
+ # ticket is about, because a file that quotes a creation then cannot be copied
2007
+ # by any agent or by CI.
2008
+ #
2009
+ # `mv`, `ln` and `install` join `cp` as the same operation rather than as a
2010
+ # guess: the list already carries whole families (`git` wholesale, `bat`/`xxd`
2011
+ # beside `cat`) on exactly that reasoning. Path arithmetic and metadata tools
2012
+ # are deliberately NOT added — unmeasured, and the residual note above is the
2013
+ # standing instruction to add on measurement rather than by sweep.
2014
+ READ_ONLY_PROGRAMS = {
2015
+ "awk", "bat", "cat", "cksum", "cmp", "column", "comm", "cp", "cut",
2016
+ "diff", "du", "echo", "file", "fold", "git", "grep", "head",
2017
+ "hexdump", "install", "jest", "jq", "less", "ln", "ls", "md5",
2018
+ "md5sum", "more", "mv", "nl", "od", "printf", "pytest", "rg",
2019
+ "sed", "sha1sum", "sha256sum", "shellcheck", "shfmt", "sort",
2020
+ "stat", "strings", "tail", "tee", "uniq", "vitest", "wc", "xxd",
2021
+ "yamllint",
2022
+ }
2023
+
2024
+
2025
+ def executing_command(argv):
2026
+ """The program this command runs, and the tokens after it.
2027
+
2028
+ Steps over leading variable assignments and over wrappers that do not
2029
+ change what runs. Reading a wrapper as the command is how a payload hides
2030
+ from a classifier that inspects only a segment's first word.
2031
+
2032
+ Args:
2033
+ argv: One command's tokens.
2034
+
2035
+ Returns:
2036
+ A triple (token, program, args); (None, None, []) when none is named.
2037
+ The raw token is carried alongside the basename because a script run by
2038
+ bare path — `./create.sh`, relying on its shebang — has no interpreter
2039
+ to recognise, and the command word IS the thing being executed.
2040
+ """
2041
+ index = 0
2042
+ while index < len(argv):
2043
+ token = argv[index]
2044
+ if is_assignment_word(token):
2045
+ index += 1
2046
+ continue
2047
+ program = basename(token)
2048
+ if program not in EXEC_WRAPPERS:
2049
+ return (token, program, argv[index + 1 :])
2050
+ separate, positional = EXEC_WRAPPERS[program]
2051
+ index += 1
2052
+ while index < len(argv) and argv[index].startswith("-"):
2053
+ option = argv[index]
2054
+ if option == "--":
2055
+ index += 1
2056
+ break
2057
+ index += 2 if option in separate else 1
2058
+ index += positional
2059
+ return (None, None, [])
2060
+
2061
+
2062
+ # Operators that end a command in raw shell text. Used ONLY where `shlex` has
2063
+ # already refused the text, so a quote-aware split is not available — and
2064
+ # OVER-splitting is the safe direction here, because an extra segment can only
2065
+ # add a command word to check, never remove one.
2066
+ RAW_SEGMENT_SPLIT = re.compile(r"&&|\|\||[;|&\n]")
2067
+
2068
+
2069
+ def raw_command_words(text):
2070
+ """The program each segment of unlexable text puts in command position.
2071
+
2072
+ Answered on a whitespace split because the reason this path exists is that
2073
+ `shlex` refused the text. It still routes through `executing_command`, so a
2074
+ leading assignment and a wrapper — `nice`, `env`, `sudo`, `timeout` — are
2075
+ stepped over here exactly as they are on the parsed path. Reading the
2076
+ wrapper as the command is the bypass this guard's own comments record.
2077
+
2078
+ Args:
2079
+ text: The raw command string.
2080
+
2081
+ Returns:
2082
+ One entry per non-empty segment: the resolved program name, or None
2083
+ when the segment names none.
2084
+ """
2085
+ words = []
2086
+ for chunk in RAW_SEGMENT_SPLIT.split(text):
2087
+ tokens = chunk.split()
2088
+ if not tokens:
2089
+ continue
2090
+ words.append(executing_command(tokens)[1])
2091
+ return words
2092
+
2093
+
2094
+ def unparseable_reads_only(text):
2095
+ """Whether unlexable text only READS, and therefore files nothing.
2096
+
2097
+ `UNPARSEABLE_CREATION` matches raw text, so it cannot tell a filing from a
2098
+ sentence about one. `echo the guard's <creation> behaviour` fails to lex
2099
+ for the apostrophe alone, and was refused as a tracker creation — a command
2100
+ that files nothing, told that "this filing declares no readiness", with no
2101
+ printed remedy that applied to any part of it (CodySwannGT/lisa#3683).
2102
+
2103
+ The fallback is NOT removable and is not narrowed here. `<creation> #'` is
2104
+ a real filing that bash runs and `shlex` rejects, so "I could not parse it"
2105
+ must keep meaning refuse. What this asks instead is the COMMAND POSITION
2106
+ question the parsed path already asks and this arm skipped: only a command
2107
+ position can run anything, and a reader takes its operands as data.
2108
+
2109
+ Fails closed on everything else, which is the same asymmetry
2110
+ `READ_ONLY_PROGRAMS` documents: an unresolvable command word, an
2111
+ unrecognised program, or a text with no segments at all is refused.
2112
+
2113
+ Args:
2114
+ text: The raw command string that would not lex.
2115
+
2116
+ Returns:
2117
+ True when every segment's command word is a known reader.
2118
+ """
2119
+ # Unlexable operands cannot be certified as data when shell substitutions
2120
+ # may execute inside them. Preserve the plain apostrophe reader exemption.
2121
+ if any(operator in text for operator in ("$(", "`", "<(", ">(")):
2122
+ return False
2123
+ words = raw_command_words(text)
2124
+ return bool(words) and all(word in READ_ONLY_PROGRAMS for word in words)
2125
+
2126
+
2127
+ def executed_operand(argv):
2128
+ """The path this command EXECUTES, or None.
2129
+
2130
+ THIS IS THE COMMAND-POSITION QUESTION, and declining to ask it is what
2131
+ #3705 records: the guard opened any readable file any token named, then
2132
+ justified a refusal from that FILE's contents rather than from the
2133
+ COMMAND's behaviour. Measured refusals included `git diff`, `grep -n`,
2134
+ `wc -l`, a `git grep` PATHSPEC and a test run — every one of them read-only,
2135
+ and one of them blocked running the tests that would have proved a fix.
2136
+
2137
+ `parity-safety-net.sh` already states the rule this restores, and names the
2138
+ opposite as the known-wrong fix (`origin/main`, lines 337-346 and 704-705):
2139
+ "Only a COMMAND POSITION can execute something. A path anywhere else is an
2140
+ argument, and an argument is data." `block-no-verify.sh:495-513`
2141
+ (`strip_command_prefix`) implements the same split with a fail-closed third
2142
+ state. Both live in this directory; this guard was the one that declined.
2143
+
2144
+ NOT narrowed to shells, and NOT narrowed to an enumerated interpreter list.
2145
+ `file_creation` recognises a creation in any language, so `node wrapper.mjs`
2146
+ must still be followed — a measured fail-open in #3484 — and this guard's
2147
+ own suite drives it with a runner nobody enumerated, precisely so a fix
2148
+ keyed on such a list fails. So the default for an unrecognised program is to
2149
+ FOLLOW, and only a known reader is exempt. A script run by bare path with no
2150
+ interpreter at all (`./create.sh`, via its shebang) is followed too: there
2151
+ the command word is itself the thing being executed.
2152
+
2153
+ Reading a REQUEST PAYLOAD is a separate question with a separate answer:
2154
+ `payload_text` resolves `--data-binary @file` and friends by flag name, so
2155
+ narrowing this walk does not stop a creation whose body lives in a file
2156
+ from being seen.
2157
+
2158
+ Args:
2159
+ argv: One command's tokens.
2160
+
2161
+ Returns:
2162
+ The operand token naming a program that will run, or None.
2163
+ """
2164
+ token, program, args = executing_command(argv)
2165
+ if program is None:
2166
+ return None
2167
+ # A reader takes its operands as data. This is the #3705 population.
2168
+ if program in READ_ONLY_PROGRAMS:
2169
+ return None
2170
+ if program in SOURCE_BUILTINS:
2171
+ return args[0] if args else None
2172
+ # A command word that is ITSELF a readable file is being executed by its
2173
+ # shebang — `./create.sh` names no interpreter and runs all the same.
2174
+ if program not in EXECUTING_INTERPRETERS and resolve_operand(token) is not None:
2175
+ return token
2176
+ # Reached by a known interpreter AND by a program this parser cannot name.
2177
+ # The unknown case is followed on purpose — see READ_ONLY_PROGRAMS.
2178
+ redirected = None
2179
+ index = 0
2180
+ while index < len(args):
2181
+ argument = args[index]
2182
+ if argument == "<":
2183
+ if index + 1 < len(args):
2184
+ redirected = args[index + 1]
2185
+ index += 2
2186
+ continue
2187
+ if argument == "--":
2188
+ index += 1
2189
+ return args[index] if index < len(args) else redirected
2190
+ if argument.startswith("-"):
2191
+ # A command string or a module name; no script file follows.
2192
+ head = argument.split("=", 1)[0]
2193
+ if program in {"bash", "dash", "ksh", "sh", "zsh"}:
2194
+ # Shell `-c` may be clustered (`-ec`), while `-e` and `-m` are
2195
+ # ordinary shell options and do not consume the script path.
2196
+ if not head.startswith("--") and "c" in head[1:]:
2197
+ return None
2198
+ # `-n` is noexec: the shell READS and parses the file and then
2199
+ # exits without running a line of it. `bash -n <file>` is the
2200
+ # one shape that puts a path at a command position while
2201
+ # provably executing nothing, so it is the exact seam between
2202
+ # reading and running — and this walk got it wrong in BOTH
2203
+ # directions (CodySwannGT/lisa#3781). A creation-shaped file
2204
+ # earned a false refusal; a file carrying a human-gate marker
2205
+ # earned a false ALLOW, adjudicating as "declared" a command
2206
+ # that filed nothing. The silent direction is the worse one.
2207
+ #
2208
+ # Not a read-only-command exemption: `bash <file>` still runs
2209
+ # the file and is still refused. The distinction is noexec, not
2210
+ # the program.
2211
+ if head == "--noexec" or (
2212
+ not head.startswith("--") and "n" in head[1:]
2213
+ ):
2214
+ return None
2215
+ elif head in NO_SCRIPT_OPTIONS.get(program, set()):
2216
+ return None
2217
+ takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
2218
+ index += 2 if takes_value and "=" not in argument else 1
2219
+ continue
2220
+ if argument in INTERPRETER_SUBCOMMANDS.get(program, set()):
2221
+ index += 1
2222
+ continue
2223
+ return argument
2224
+ # `bash < create.sh` runs stdin only when no explicit script was named.
2225
+ return redirected
2226
+
2227
+
2228
+ def file_operands(argv):
2229
+ """The file this command EXECUTES, if it executes one.
2230
+
2231
+ Deciding WHICH programs execute their operands is the question this file
2232
+ used to refuse to ask, and refusing it is what made the guard read a path
2233
+ quoted as prose inside a `--body-file` markdown and attribute its contents
2234
+ to the command. It is answered in `executed_operand` now.
2235
+
2236
+ Args:
2237
+ argv: One command's tokens.
2238
+
2239
+ Returns:
2240
+ Existing paths, deduplicated across the whole scan and capped.
2241
+ """
2242
+ paths = []
2243
+ operand = executed_operand(argv)
2244
+ for token in [operand] if operand is not None else []:
2245
+ if len(paths) >= FILE_OPERANDS_PER_SEGMENT:
2246
+ break
2247
+ path = resolve_operand(token)
2248
+ if path is None:
2249
+ continue
2250
+ try:
2251
+ key = os.path.realpath(path)
2252
+ except OSError:
2253
+ key = path
2254
+ if key in inspected_files:
2255
+ continue
2256
+ inspected_files.add(key)
2257
+ paths.append(path)
2258
+ return paths
2259
+
2260
+
2261
+ def file_creation(text, depth):
2262
+ """An undeclared tracker creation inside a file's contents, or None.
2263
+
2264
+ Two recognisers, because a creation inside a file is not always shell.
2265
+ The shell path handles `bash create.sh`; the CONJUNCTION path handles
2266
+ `node wrapper.mjs`, a Python client, or anything else that speaks HTTP
2267
+ directly — it needs a tracker endpoint AND a creation verb in the same
2268
+ file, which is what keeps a changelog that merely mentions `issueCreate`
2269
+ from reading as a creation. It also needs the file to be capable of
2270
+ SENDING what it spells: a helper that writes the same payload into a test
2271
+ fixture submits nothing. See `payload_is_inert`.
2272
+
2273
+ Args:
2274
+ text: The file's contents.
2275
+ depth: Current nesting depth.
2276
+
2277
+ Returns:
2278
+ A (signature, roles, cross_repo_target) triple, or None.
2279
+ """
2280
+ nested = scan(text, depth + 1, from_file=True)
2281
+ if nested is not None:
2282
+ return nested
2283
+ # The coarse path answers to a coarse declaration check, and the precise
2284
+ # path above answers to the precise one. Reversing that — screening the
2285
+ # whole file first — would let a declaration on one create in a script
2286
+ # vouch for a different, undeclared one further down.
2287
+ if (
2288
+ GRAPHQL_CREATE.search(text)
2289
+ and TRACKER_ENDPOINT.search(text)
2290
+ and not text_declares_readiness(text)
2291
+ # A payload the file WRITES rather than SENDS is data. See
2292
+ # `payload_is_inert` for why the absence of egress alone is not enough.
2293
+ and not payload_is_inert(text)
2294
+ ):
2295
+ return "a tracker creation", [ready_role], None
2296
+ return None
2297
+
2298
+
2299
+ def scan(text, depth, from_file=False):
2300
+ """Find the first undeclared tracker creation in a command string.
2301
+
2302
+ Args:
2303
+ text: A shell command.
2304
+ depth: Current nesting depth.
2305
+ from_file: Whether `text` is a file's contents rather than a typed
2306
+ command.
2307
+
2308
+ Returns:
2309
+ A (signature, roles, cross_repo_target) triple, or None when nothing
2310
+ creation-shaped was found.
2311
+ """
2312
+ try:
2313
+ stripped = strip_heredocs(text)
2314
+ tokens = explode_operators(shlex.split(stripped, posix=True), stripped)
2315
+ except ValueError:
2316
+ # Bash's grammar is not shlex's. `gh issue create --title x #'` is a
2317
+ # comment to bash, which strips it and RUNS the create, while shlex
2318
+ # raises on the unbalanced quote. Two appended characters, no binary
2319
+ # required. "I could not parse it" must never mean "it is fine".
2320
+ #
2321
+ # A FILE that does not lex is judged by the same recogniser rather than
2322
+ # waved through: an unbalanced quote inside a script is the identical
2323
+ # two-character trick moved one file away, and skipping it would hand
2324
+ # the bypass straight back.
2325
+ #
2326
+ # The declaration check is NOT gated on `from_file`, and that is the
2327
+ # #3727 fix. Tokenisation is what feeds every argv-based check below,
2328
+ # so when it throws, a TYPED command was previously judged with no
2329
+ # declaration consulted at all — not its `--body-file`, not even a
2330
+ # plain `--label <ready role>` sitting in argv. A correctly declared
2331
+ # filing was refused for the unrelated reason that its command happened
2332
+ # not to lex, and told "this filing declares no readiness", which was
2333
+ # false. Reading the text here is the only check available once `shlex`
2334
+ # has failed.
2335
+ #
2336
+ # It cannot reopen the bypass the paragraph above defends. That bypass
2337
+ # is an UNDECLARED create hiding behind a quote, and an undeclared
2338
+ # create has no marker and no `--label <ready role>` for
2339
+ # `text_declares_readiness` to match — which is asserted, not assumed,
2340
+ # by the rejection controls covering a bare create, the role mentioned
2341
+ # in prose, and the role appearing inside a URL.
2342
+ #
2343
+ # A marker in a SEPARATE `--body-file` stays refused, because finding
2344
+ # that operand needs the tokenisation that just failed. That case is
2345
+ # reachable by declaring inline instead, which this change makes work.
2346
+ if text_declares_readiness(text):
2347
+ return None
2348
+ # The command-position arm of the same reasoning `executed_operand`
2349
+ # applies. A text every one of whose segments runs a known READER
2350
+ # cannot file anything, whatever its prose says — see
2351
+ # `unparseable_reads_only` for why this does not narrow the fallback.
2352
+ if unparseable_reads_only(text):
2353
+ return None
2354
+ # Ignore whole-line prose comments when applying the fallback matcher.
2355
+ if UNPARSEABLE_CREATION.search(strip_full_line_comments(text)):
2356
+ return (
2357
+ "an unparseable command that reads as a tracker creation",
2358
+ [ready_role],
2359
+ None,
2360
+ )
2361
+ return None
2362
+
2363
+ for argv in segment(tokens):
2364
+ for index, token in enumerate(argv):
2365
+ name = basename(token)
2366
+ if name not in TRACKER_CLIS and name not in HTTP_CLIS:
2367
+ continue
2368
+ args = argv[index + 1 :]
2369
+ submitted = payload_text(args, text)
2370
+ signature = creation_signature(name, args, submitted)
2371
+ if signature is None:
2372
+ continue
2373
+ # The ambient override is the human operator's. An inline
2374
+ # assignment is the agent granting itself the exemption, so it
2375
+ # disqualifies the override rather than supplying it.
2376
+ if ambient_override and not inline_override:
2377
+ continue
2378
+ roles, target = roles_for(target_repository(args))
2379
+ state_role_ok = tracker in STATE_ROLE_TRACKERS and target is None
2380
+ if declares_readiness(args, roles, submitted, state_role_ok):
2381
+ continue
2382
+ # The container arm. Checked after readiness, so it adds a
2383
+ # compliant filing where none existed and takes none away where one
2384
+ # already did. `text` joins the scan only for a creation found
2385
+ # INSIDE a file, where the declaration lives in the same file as
2386
+ # the create it answers for.
2387
+ if declares_container(
2388
+ args, (submitted, text) if from_file else (submitted,)
2389
+ ):
2390
+ continue
2391
+ # `LIFECYCLE_ROLE=ready curl …` puts the declaration BEFORE the
2392
+ # client, which is where an inline assignment has to go, so the
2393
+ # role is read from the whole segment rather than from the client's
2394
+ # own arguments. Scoped to the segment and not the command, so a
2395
+ # declaration cannot be shouted from an unrelated pipeline stage.
2396
+ if state_role_ok and LIFECYCLE_ROLE_READY.search(" ".join(argv)):
2397
+ continue
2398
+ # A script declares once, for itself. See `scope_declaration`.
2399
+ if from_file and scope_declaration(text, state_role_ok):
2400
+ continue
2401
+ return signature, roles, target
2402
+
2403
+ for operand in nested_operands(argv):
2404
+ if depth >= MAX_NESTING_DEPTH:
2405
+ # Refuse at the bound rather than skipping past it. Skipping
2406
+ # made a creation inside a 4th `bash -c` layer pass, which is
2407
+ # the depth cap being used as the bypass.
2408
+ if UNPARSEABLE_CREATION.search(operand):
2409
+ return (
2410
+ "a tracker creation nested past the inspection depth",
2411
+ [ready_role],
2412
+ None,
2413
+ )
2414
+ continue
2415
+ nested = scan(operand, depth + 1)
2416
+ if nested is not None:
2417
+ return nested
2418
+
2419
+ # The locate step, which used to stop at `bash` and never reach the
2420
+ # operand. Deliberately last: an inline creation is the cheaper and
2421
+ # more precise finding, so it is reported before a file is opened.
2422
+ # The operator's ambient override is checked BEFORE the depth bound,
2423
+ # not after it. The other order meant a creation reached past the cap
2424
+ # was refused even with `LISA_ALLOW_DIRECT_ISSUE_CREATE=1` exported —
2425
+ # while the refusal text advertised that escape. An escape hatch the
2426
+ # refusal names and the code ignores is worse than none. Caught by
2427
+ # review before it shipped.
2428
+ if ambient_override and not inline_override:
2429
+ continue
2430
+ for path in file_operands(argv):
2431
+ contents = read_operand(path)
2432
+ if not contents:
2433
+ continue
2434
+ if depth >= MAX_NESTING_DEPTH:
2435
+ if GRAPHQL_CREATE.search(contents) or UNPARSEABLE_CREATION.search(
2436
+ contents
2437
+ ):
2438
+ return (
2439
+ "a tracker creation inside %s, nested past the "
2440
+ "inspection depth" % path,
2441
+ [ready_role],
2442
+ None,
2443
+ )
2444
+ continue
2445
+ found = file_creation(contents, depth)
2446
+ if found is not None:
2447
+ return ("%s inside %s" % (found[0], path), found[1], found[2])
2448
+ return None
2449
+
2450
+
2451
+ # The inline-override check runs over the WHOLE raw command text, not over
2452
+ # parsed tokens, so it catches `X=1 gh …`, `env X=1 gh …`, `export X=1 && gh …`,
2453
+ # and the same forms buried inside a nested `bash -c '…'` string alike. Any
2454
+ # appearance of the assignment disqualifies the ambient override: this is the
2455
+ # one place the guard deliberately over-matches, because a false positive costs
2456
+ # a human one retry and a false negative costs the entire control.
2457
+ inline_override = (OVERRIDE_NAME + "=") in command
2458
+
2459
+
2460
+ def remedy_for(signature):
2461
+ """Which extra remediation paragraph this refusal earns.
2462
+
2463
+ A guard that prints every remedy it knows prints one that does not apply,
2464
+ and a remedy that cannot be performed is the failure this ticket is about
2465
+ (CodySwannGT/lisa#3683): the file-payload paragraph told an author to drop
2466
+ a transmitting primitive that a DOCUMENT does not have, and the
2467
+ unparseable arm printed declaration advice for a command that files
2468
+ nothing. Both are now keyed off which branch actually fired.
2469
+
2470
+ Args:
2471
+ signature: The refusal signature.
2472
+
2473
+ Returns:
2474
+ A key the shell half maps to a paragraph, or an empty string.
2475
+ """
2476
+ if signature.startswith("an unparseable command"):
2477
+ return "unparseable"
2478
+ if " inside " in signature:
2479
+ return "file"
2480
+ return ""
2481
+
2482
+
2483
+ found = scan(command, 0)
2484
+ if found is not None:
2485
+ signature, roles, target = found
2486
+ # Key=value lines rather than one delimited string: a signature contains
2487
+ # spaces and slashes, and a role may contain a colon, so anything the shell
2488
+ # would have to split on appears inside a value already.
2489
+ print("REFUSE")
2490
+ print("signature=%s" % signature)
2491
+ print("roles=%s" % ", ".join(role for role in roles if role))
2492
+ print("target=%s" % (target or ""))
2493
+ print("remedy=%s" % remedy_for(signature))
2494
+ sys.exit(0)
2495
+
2496
+ print("ALLOW")
2497
+
2498
+ PY
2499
+
2500
+ set +e
2501
+ # ── The structured substrate ──────────────────────────────────────────────
2502
+ #
2503
+ # An MCP call carries named fields, not a command line, so `scan()` — which
2504
+ # tokenises shell text — has nothing to parse. The declaration is read from the
2505
+ # payload instead, and the recogniser is deliberately small: this path decides
2506
+ # ONE question (is a build-ready role or a human-gate marker present anywhere
2507
+ # in the submitted fields), where the shell path has to answer "which of these
2508
+ # tokens is a client, an endpoint, a flag value, or a file it executes".
2509
+ #
2510
+ # Every string in the payload is searched, at any depth, because a role lands
2511
+ # in a different field on every tracker — `labels[]` on GitHub, `stateId` or a
2512
+ # workflow state name on Linear, a transition `id` on JIRA — and enumerating
2513
+ # field names per vendor is the same brittleness as enumerating tool names.
2514
+ # Over-collecting is safe here: the values are compared against ONE configured
2515
+ # role string, so an unrelated field cannot accidentally satisfy it.
2516
+ if [ -n "$structured_call" ]; then
2517
+ # The operator's ambient escape works on both substrates. There is no inline
2518
+ # form to disqualify it here — a structured call has no shell in which to
2519
+ # assign one — so the override is simply honoured.
2520
+ if [ -n "$ambient_override" ]; then
2521
+ exit 0
2522
+ fi
2523
+
2524
+ # A PACKED label string counts. Exact equality against the flattened value
2525
+ # list reads `labels: ["status:ready"]` and nothing else, but the same
2526
+ # compliant filing spelled `labels: "status:ready,type:Bug"` — the shape
2527
+ # `gh issue create --label` takes, and the shape several MCP servers pass
2528
+ # through verbatim — carries no value equal to the role, so the guard
2529
+ # refused a filing that had declared exactly what it demanded. A false
2530
+ # positive in a guard costs more than a miss: it teaches the operator that
2531
+ # the guard is wrong, and the next refusal is argued with rather than
2532
+ # obeyed.
2533
+ #
2534
+ # Split on the DELIMITERS a packed list uses (comma, semicolon, newline) and
2535
+ # trimmed, never on a `contains` match. `contains` would accept a body that
2536
+ # merely mentions the role in prose — "do not mark this status:ready" — and
2537
+ # that is a fail-open on the one question this path exists to answer.
2538
+ structured_declaration="$(
2539
+ printf '%s' "$input" |
2540
+ jq -r --arg role "$ready_role" '
2541
+ [(.tool_input // {}) | .. | strings] as $values
2542
+ | ($values + ($values
2543
+ | map(splits("[,;\n]"))
2544
+ | map(sub("^\\s+"; "") | sub("\\s+$"; "")))) as $atoms
2545
+ | if ($atoms | index($role)) then "role"
2546
+ elif ($values | map(select(contains("[lisa-human-gate]"))) | length) > 0 then "gate"
2547
+ else "" end
2548
+ ' 2>/dev/null || printf 'UNREADABLE'
2549
+ )"
2550
+
2551
+ # Fail closed on a creation-shaped call whose payload cannot be read. Silence
2552
+ # about a filing the guard could not inspect is the defect this whole ticket
2553
+ # is about, one substrate over.
2554
+ if [ "$structured_declaration" = "UNREADABLE" ]; then
2555
+ refuse "an unreadable $tool_name payload that reads as a tracker creation" \
2556
+ "$ready_role" ""
2557
+ fi
2558
+
2559
+ if [ -z "$structured_declaration" ]; then
2560
+ refuse "a tracker creation through $tool_name" "$ready_role" ""
2561
+ fi
2562
+ exit 0
2563
+ fi
2564
+
2565
+ verdict="$(
2566
+ printf '%s' "$classifier" |
2567
+ LISA_GUARD_COMMAND="$command_str" \
2568
+ LISA_GUARD_READY_ROLE="$ready_role" \
2569
+ LISA_GUARD_AMBIENT_OVERRIDE="$ambient_override" \
2570
+ LISA_GUARD_DEFAULT_READY_ROLE="$default_ready_role" \
2571
+ LISA_GUARD_OWN_REPO="$own_repo" \
2572
+ LISA_GUARD_UPSTREAM_REPO="$upstream_repo" \
2573
+ LISA_GUARD_UPSTREAM_READY_ROLE="$upstream_ready_role" \
2574
+ LISA_GUARD_CALLER_IS_GITHUB="$caller_is_github" \
2575
+ LISA_GUARD_TRACKER="$tracker" \
2576
+ LISA_GUARD_PROJECT_DIR="$project_dir" \
2577
+ python3 -
2578
+ )"
2579
+ python_status=$?
2580
+ set -e
2581
+
2582
+ # A crashed classifier must not be read as "allow" without saying so.
2583
+ if [ "$python_status" -ne 0 ]; then
2584
+ printf 'block-direct-issue-create: classifier failed (exit %s); enforcement is NOT active for this call\n' \
2585
+ "$python_status" >&2
2586
+ exit 0
2587
+ fi
2588
+
2589
+ verdict_field() {
2590
+ printf '%s\n' "$verdict" | sed -n "s/^$1=//p" | head -1
2591
+ }
2592
+
2593
+ case "$verdict" in
2594
+ REFUSE*)
2595
+ refuse \
2596
+ "$(verdict_field signature)" \
2597
+ "$(verdict_field roles)" \
2598
+ "$(verdict_field target)" \
2599
+ "$(verdict_field remedy)"
2600
+ ;;
2601
+ esac
2602
+
2603
+ exit 0