@codyswann/lisa 4.72.0 → 4.72.2

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 (379) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +769 -37
  2. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.js +4 -0
  4. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  5. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  6. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.js +14 -5
  8. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  9. package/dist/opencode/plugin-templates/parity-safety-net.sh +769 -37
  10. package/package.json +4 -4
  11. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa/.codex-plugin/skills/lisa-acceptance-criteria/SKILL.md +2 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-agent-design-best-practices/SKILL.md +2 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-agent-ready/SKILL.md +2 -0
  16. package/plugins/lisa/.codex-plugin/skills/lisa-analyze-claude-remote/SKILL.md +2 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +2 -0
  18. package/plugins/lisa/.codex-plugin/skills/lisa-attribute-failure/SKILL.md +2 -0
  19. package/plugins/lisa/.codex-plugin/skills/lisa-automation-status/SKILL.md +2 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-bug-triage/SKILL.md +2 -0
  21. package/plugins/lisa/.codex-plugin/skills/lisa-capability-drift/SKILL.md +2 -0
  22. package/plugins/lisa/.codex-plugin/skills/lisa-codebase-research/SKILL.md +2 -0
  23. package/plugins/lisa/.codex-plugin/skills/lisa-codify-verification/SKILL.md +2 -0
  24. package/plugins/lisa/.codex-plugin/skills/lisa-confluence-prd-intake/SKILL.md +2 -0
  25. package/plugins/lisa/.codex-plugin/skills/lisa-confluence-to-tracker/SKILL.md +2 -0
  26. package/plugins/lisa/.codex-plugin/skills/lisa-confluence-write-prd/SKILL.md +2 -0
  27. package/plugins/lisa/.codex-plugin/skills/lisa-cross-pollinate/SKILL.md +2 -0
  28. package/plugins/lisa/.codex-plugin/skills/lisa-debrief/SKILL.md +2 -0
  29. package/plugins/lisa/.codex-plugin/skills/lisa-debrief-apply/SKILL.md +2 -0
  30. package/plugins/lisa/.codex-plugin/skills/lisa-delivery-effectiveness/SKILL.md +2 -0
  31. package/plugins/lisa/.codex-plugin/skills/lisa-design-intake/SKILL.md +2 -0
  32. package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/SKILL.md +2 -0
  33. package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +2 -0
  34. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +2 -0
  35. package/plugins/lisa/.codex-plugin/skills/lisa-epic-triage/SKILL.md +2 -0
  36. package/plugins/lisa/.codex-plugin/skills/lisa-evaluation-suite/SKILL.md +2 -0
  37. package/plugins/lisa/.codex-plugin/skills/lisa-exploratory-qa/SKILL.md +2 -0
  38. package/plugins/lisa/.codex-plugin/skills/lisa-fix-linter-error/SKILL.md +2 -0
  39. package/plugins/lisa/.codex-plugin/skills/lisa-generate-claude-remote-build-script/SKILL.md +2 -0
  40. package/plugins/lisa/.codex-plugin/skills/lisa-git-commit/SKILL.md +2 -0
  41. package/plugins/lisa/.codex-plugin/skills/lisa-git-prune/SKILL.md +2 -0
  42. package/plugins/lisa/.codex-plugin/skills/lisa-git-submit-pr/SKILL.md +2 -0
  43. package/plugins/lisa/.codex-plugin/skills/lisa-github-add-journey/SKILL.md +2 -0
  44. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +2 -0
  45. package/plugins/lisa/.codex-plugin/skills/lisa-github-claim/SKILL.md +2 -0
  46. package/plugins/lisa/.codex-plugin/skills/lisa-github-create/SKILL.md +2 -0
  47. package/plugins/lisa/.codex-plugin/skills/lisa-github-evidence/SKILL.md +2 -0
  48. package/plugins/lisa/.codex-plugin/skills/lisa-github-journey/SKILL.md +2 -0
  49. package/plugins/lisa/.codex-plugin/skills/lisa-github-prd-intake/SKILL.md +2 -0
  50. package/plugins/lisa/.codex-plugin/skills/lisa-github-project-v2/SKILL.md +2 -0
  51. package/plugins/lisa/.codex-plugin/skills/lisa-github-read-issue/SKILL.md +2 -0
  52. package/plugins/lisa/.codex-plugin/skills/lisa-github-sync/SKILL.md +2 -0
  53. package/plugins/lisa/.codex-plugin/skills/lisa-github-to-tracker/SKILL.md +2 -0
  54. package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +2 -0
  55. package/plugins/lisa/.codex-plugin/skills/lisa-github-verify/SKILL.md +2 -0
  56. package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +2 -0
  57. package/plugins/lisa/.codex-plugin/skills/lisa-github-write-prd/SKILL.md +2 -0
  58. package/plugins/lisa/.codex-plugin/skills/lisa-health/SKILL.md +2 -0
  59. package/plugins/lisa/.codex-plugin/skills/lisa-health-drift-cron/SKILL.md +2 -0
  60. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +2 -0
  61. package/plugins/lisa/.codex-plugin/skills/lisa-improve-code-complexity/SKILL.md +2 -0
  62. package/plugins/lisa/.codex-plugin/skills/lisa-improve-harness/SKILL.md +2 -0
  63. package/plugins/lisa/.codex-plugin/skills/lisa-improve-max-lines/SKILL.md +2 -0
  64. package/plugins/lisa/.codex-plugin/skills/lisa-improve-max-lines-per-function/SKILL.md +2 -0
  65. package/plugins/lisa/.codex-plugin/skills/lisa-improve-test-coverage/SKILL.md +2 -0
  66. package/plugins/lisa/.codex-plugin/skills/lisa-improve-tests/SKILL.md +2 -0
  67. package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +2 -0
  68. package/plugins/lisa/.codex-plugin/skills/lisa-intake-explain/SKILL.md +2 -0
  69. package/plugins/lisa/.codex-plugin/skills/lisa-jam-access/SKILL.md +2 -0
  70. package/plugins/lisa/.codex-plugin/skills/lisa-jira-add-journey/SKILL.md +2 -0
  71. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +2 -0
  72. package/plugins/lisa/.codex-plugin/skills/lisa-jira-claim/SKILL.md +2 -0
  73. package/plugins/lisa/.codex-plugin/skills/lisa-jira-create/SKILL.md +2 -0
  74. package/plugins/lisa/.codex-plugin/skills/lisa-jira-evidence/SKILL.md +2 -0
  75. package/plugins/lisa/.codex-plugin/skills/lisa-jira-journey/SKILL.md +2 -0
  76. package/plugins/lisa/.codex-plugin/skills/lisa-jira-read-ticket/SKILL.md +2 -0
  77. package/plugins/lisa/.codex-plugin/skills/lisa-jira-sync/SKILL.md +2 -0
  78. package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +2 -0
  79. package/plugins/lisa/.codex-plugin/skills/lisa-jira-verify/SKILL.md +2 -0
  80. package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +2 -0
  81. package/plugins/lisa/.codex-plugin/skills/lisa-jsdoc-best-practices/SKILL.md +2 -0
  82. package/plugins/lisa/.codex-plugin/skills/lisa-kane-browser/SKILL.md +2 -0
  83. package/plugins/lisa/.codex-plugin/skills/lisa-learnings-audit/SKILL.md +2 -0
  84. package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +2 -0
  85. package/plugins/lisa/.codex-plugin/skills/lisa-linear-add-journey/SKILL.md +2 -0
  86. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +2 -0
  87. package/plugins/lisa/.codex-plugin/skills/lisa-linear-claim/SKILL.md +2 -0
  88. package/plugins/lisa/.codex-plugin/skills/lisa-linear-create/SKILL.md +2 -0
  89. package/plugins/lisa/.codex-plugin/skills/lisa-linear-evidence/SKILL.md +2 -0
  90. package/plugins/lisa/.codex-plugin/skills/lisa-linear-journey/SKILL.md +2 -0
  91. package/plugins/lisa/.codex-plugin/skills/lisa-linear-prd-intake/SKILL.md +2 -0
  92. package/plugins/lisa/.codex-plugin/skills/lisa-linear-read-issue/SKILL.md +2 -0
  93. package/plugins/lisa/.codex-plugin/skills/lisa-linear-sync/SKILL.md +2 -0
  94. package/plugins/lisa/.codex-plugin/skills/lisa-linear-to-tracker/SKILL.md +2 -0
  95. package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +2 -0
  96. package/plugins/lisa/.codex-plugin/skills/lisa-linear-verify/SKILL.md +2 -0
  97. package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +2 -0
  98. package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-prd/SKILL.md +2 -0
  99. package/plugins/lisa/.codex-plugin/skills/lisa-monitor/SKILL.md +2 -0
  100. package/plugins/lisa/.codex-plugin/skills/lisa-nightly-add-test-coverage/SKILL.md +2 -0
  101. package/plugins/lisa/.codex-plugin/skills/lisa-nightly-improve-tests/SKILL.md +2 -0
  102. package/plugins/lisa/.codex-plugin/skills/lisa-nightly-lower-code-complexity/SKILL.md +2 -0
  103. package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +2 -0
  104. package/plugins/lisa/.codex-plugin/skills/lisa-notion-prd-intake/SKILL.md +2 -0
  105. package/plugins/lisa/.codex-plugin/skills/lisa-notion-to-tracker/SKILL.md +2 -0
  106. package/plugins/lisa/.codex-plugin/skills/lisa-notion-write-prd/SKILL.md +2 -0
  107. package/plugins/lisa/.codex-plugin/skills/lisa-parity-code-review/SKILL.md +2 -0
  108. package/plugins/lisa/.codex-plugin/skills/lisa-parity-code-simplifier/SKILL.md +3 -1
  109. package/plugins/lisa/.codex-plugin/skills/lisa-parity-code-simplifier/agents/openai.yaml +2 -2
  110. package/plugins/lisa/.codex-plugin/skills/lisa-parity-coderabbit/SKILL.md +2 -0
  111. package/plugins/lisa/.codex-plugin/skills/lisa-parity-sentry-sdk-setup/SKILL.md +2 -0
  112. package/plugins/lisa/.codex-plugin/skills/lisa-parity-sentry-seer/SKILL.md +2 -0
  113. package/plugins/lisa/.codex-plugin/skills/lisa-parity-skill-creator/SKILL.md +2 -0
  114. package/plugins/lisa/.codex-plugin/skills/lisa-performance-review/SKILL.md +2 -0
  115. package/plugins/lisa/.codex-plugin/skills/lisa-persist-learning/SKILL.md +2 -0
  116. package/plugins/lisa/.codex-plugin/skills/lisa-plan/SKILL.md +2 -0
  117. package/plugins/lisa/.codex-plugin/skills/lisa-plugin-sync-explain/SKILL.md +2 -0
  118. package/plugins/lisa/.codex-plugin/skills/lisa-posthog-access/SKILL.md +2 -0
  119. package/plugins/lisa/.codex-plugin/skills/lisa-prd-backlink/SKILL.md +2 -0
  120. package/plugins/lisa/.codex-plugin/skills/lisa-prd-source-write/SKILL.md +2 -0
  121. package/plugins/lisa/.codex-plugin/skills/lisa-prd-ticket-coverage/SKILL.md +2 -0
  122. package/plugins/lisa/.codex-plugin/skills/lisa-product-walkthrough/SKILL.md +2 -0
  123. package/plugins/lisa/.codex-plugin/skills/lisa-project-ideation/SKILL.md +2 -0
  124. package/plugins/lisa/.codex-plugin/skills/lisa-pull-request-review/SKILL.md +2 -0
  125. package/plugins/lisa/.codex-plugin/skills/lisa-quality-review/SKILL.md +2 -0
  126. package/plugins/lisa/.codex-plugin/skills/lisa-queue-status/SKILL.md +2 -0
  127. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/SKILL.md +2 -0
  128. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +2 -0
  129. package/plugins/lisa/.codex-plugin/skills/lisa-reproduce-bug/SKILL.md +2 -0
  130. package/plugins/lisa/.codex-plugin/skills/lisa-research/SKILL.md +2 -0
  131. package/plugins/lisa/.codex-plugin/skills/lisa-review-implementation/SKILL.md +2 -0
  132. package/plugins/lisa/.codex-plugin/skills/lisa-review-local/SKILL.md +2 -0
  133. package/plugins/lisa/.codex-plugin/skills/lisa-rework-triage/SKILL.md +2 -0
  134. package/plugins/lisa/.codex-plugin/skills/lisa-root-cause-analysis/SKILL.md +2 -0
  135. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +2 -0
  136. package/plugins/lisa/.codex-plugin/skills/lisa-security-review/SKILL.md +2 -0
  137. package/plugins/lisa/.codex-plugin/skills/lisa-security-zap-scan/SKILL.md +2 -0
  138. package/plugins/lisa/.codex-plugin/skills/lisa-sentry-access/SKILL.md +2 -0
  139. package/plugins/lisa/.codex-plugin/skills/lisa-setup-atlassian/SKILL.md +2 -0
  140. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +2 -0
  141. package/plugins/lisa/.codex-plugin/skills/lisa-setup-confluence/SKILL.md +2 -0
  142. package/plugins/lisa/.codex-plugin/skills/lisa-setup-github/SKILL.md +2 -0
  143. package/plugins/lisa/.codex-plugin/skills/lisa-setup-github-repo/SKILL.md +2 -0
  144. package/plugins/lisa/.codex-plugin/skills/lisa-setup-jira/SKILL.md +2 -0
  145. package/plugins/lisa/.codex-plugin/skills/lisa-setup-kane/SKILL.md +2 -0
  146. package/plugins/lisa/.codex-plugin/skills/lisa-setup-linear/SKILL.md +2 -0
  147. package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/SKILL.md +2 -0
  148. package/plugins/lisa/.codex-plugin/skills/lisa-setup-notion/SKILL.md +2 -0
  149. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-aws/SKILL.md +2 -0
  150. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +2 -0
  151. package/plugins/lisa/.codex-plugin/skills/lisa-setup-sonar/SKILL.md +2 -0
  152. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/SKILL.md +2 -0
  153. package/plugins/lisa/.codex-plugin/skills/lisa-sonarcloud-access/SKILL.md +2 -0
  154. package/plugins/lisa/.codex-plugin/skills/lisa-spec-conformance/SKILL.md +2 -0
  155. package/plugins/lisa/.codex-plugin/skills/lisa-starter-sync/SKILL.md +2 -0
  156. package/plugins/lisa/.codex-plugin/skills/lisa-sync-down/SKILL.md +2 -0
  157. package/plugins/lisa/.codex-plugin/skills/lisa-task-decomposition/SKILL.md +2 -0
  158. package/plugins/lisa/.codex-plugin/skills/lisa-task-triage/SKILL.md +2 -0
  159. package/plugins/lisa/.codex-plugin/skills/lisa-tdd-implementation/SKILL.md +2 -0
  160. package/plugins/lisa/.codex-plugin/skills/lisa-tear-down-automations/SKILL.md +2 -0
  161. package/plugins/lisa/.codex-plugin/skills/lisa-test-strategy/SKILL.md +2 -0
  162. package/plugins/lisa/.codex-plugin/skills/lisa-ticket-triage/SKILL.md +2 -0
  163. package/plugins/lisa/.codex-plugin/skills/lisa-track/SKILL.md +2 -0
  164. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-add-journey/SKILL.md +2 -0
  165. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-build-intake/SKILL.md +2 -0
  166. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-claim/SKILL.md +2 -0
  167. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-create/SKILL.md +2 -0
  168. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-evidence/SKILL.md +2 -0
  169. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-journey/SKILL.md +2 -0
  170. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-read/SKILL.md +2 -0
  171. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
  172. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-sync/SKILL.md +2 -0
  173. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-validate/SKILL.md +2 -0
  174. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-verify/SKILL.md +2 -0
  175. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-write/SKILL.md +2 -0
  176. package/plugins/lisa/.codex-plugin/skills/lisa-usage-accounting/SKILL.md +2 -0
  177. package/plugins/lisa/.codex-plugin/skills/lisa-use-the-product/SKILL.md +2 -0
  178. package/plugins/lisa/.codex-plugin/skills/lisa-validate-tracker-mapping/SKILL.md +2 -0
  179. package/plugins/lisa/.codex-plugin/skills/lisa-verification-lifecycle/SKILL.md +2 -0
  180. package/plugins/lisa/.codex-plugin/skills/lisa-verify/SKILL.md +2 -0
  181. package/plugins/lisa/.codex-plugin/skills/lisa-verify-prd/SKILL.md +2 -0
  182. package/plugins/lisa/.codex-plugin/skills/lisa-verify-workflow-change/SKILL.md +2 -0
  183. package/plugins/lisa/.codex-plugin/skills/lisa-wiki-install/SKILL.md +2 -0
  184. package/plugins/lisa/hooks/parity-safety-net.sh +769 -37
  185. package/plugins/lisa/rules/reference/design-source-of-truth.md +1 -1
  186. package/plugins/lisa/rules/reference/rejection-detection.md +1 -1
  187. package/plugins/lisa-agy/hooks/parity-safety-net.sh +769 -37
  188. package/plugins/lisa-agy/plugin.json +1 -1
  189. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  190. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  191. package/plugins/lisa-cdk/.codex-plugin/skills/lisa-setup-aws/SKILL.md +3 -1
  192. package/plugins/lisa-cdk/.codex-plugin/skills/lisa-setup-aws/agents/openai.yaml +2 -2
  193. package/plugins/lisa-cdk/.codex-plugin/skills/setup-aws-accounts/SKILL.md +2 -0
  194. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  195. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  196. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  197. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  198. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +769 -37
  199. package/plugins/lisa-copilot/rules/reference/design-source-of-truth.md +1 -1
  200. package/plugins/lisa-copilot/rules/reference/rejection-detection.md +1 -1
  201. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  202. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +769 -37
  203. package/plugins/lisa-cursor/rules/design-source-of-truth-reference.mdc +1 -1
  204. package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +1 -1
  205. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  206. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  207. package/plugins/lisa-expo/.codex-plugin/skills/add-app-clip/SKILL.md +2 -0
  208. package/plugins/lisa-expo/.codex-plugin/skills/apollo-client/SKILL.md +2 -0
  209. package/plugins/lisa-expo/.codex-plugin/skills/atomic-design-gluestack/SKILL.md +2 -0
  210. package/plugins/lisa-expo/.codex-plugin/skills/building-native-ui/SKILL.md +2 -0
  211. package/plugins/lisa-expo/.codex-plugin/skills/container-view-pattern/SKILL.md +2 -0
  212. package/plugins/lisa-expo/.codex-plugin/skills/cross-platform-compatibility/SKILL.md +2 -0
  213. package/plugins/lisa-expo/.codex-plugin/skills/directory-structure/SKILL.md +2 -0
  214. package/plugins/lisa-expo/.codex-plugin/skills/e2e-coverage-gaps/SKILL.md +2 -0
  215. package/plugins/lisa-expo/.codex-plugin/skills/eas-update-insights/SKILL.md +2 -0
  216. package/plugins/lisa-expo/.codex-plugin/skills/expo-api-routes/SKILL.md +2 -0
  217. package/plugins/lisa-expo/.codex-plugin/skills/expo-brownfield/SKILL.md +2 -0
  218. package/plugins/lisa-expo/.codex-plugin/skills/expo-cicd-workflows/SKILL.md +2 -0
  219. package/plugins/lisa-expo/.codex-plugin/skills/expo-deployment/SKILL.md +2 -0
  220. package/plugins/lisa-expo/.codex-plugin/skills/expo-dev-client/SKILL.md +2 -0
  221. package/plugins/lisa-expo/.codex-plugin/skills/expo-env-config/SKILL.md +2 -0
  222. package/plugins/lisa-expo/.codex-plugin/skills/expo-module/SKILL.md +2 -0
  223. package/plugins/lisa-expo/.codex-plugin/skills/expo-router-best-practices/SKILL.md +2 -0
  224. package/plugins/lisa-expo/.codex-plugin/skills/expo-tailwind-setup/SKILL.md +2 -0
  225. package/plugins/lisa-expo/.codex-plugin/skills/expo-ui-jetpack-compose/SKILL.md +2 -0
  226. package/plugins/lisa-expo/.codex-plugin/skills/expo-ui-swift-ui/SKILL.md +2 -0
  227. package/plugins/lisa-expo/.codex-plugin/skills/gluestack-nativewind/SKILL.md +2 -0
  228. package/plugins/lisa-expo/.codex-plugin/skills/jira-add-journey/SKILL.md +2 -0
  229. package/plugins/lisa-expo/.codex-plugin/skills/jira-create/SKILL.md +2 -0
  230. package/plugins/lisa-expo/.codex-plugin/skills/jira-evidence/SKILL.md +2 -0
  231. package/plugins/lisa-expo/.codex-plugin/skills/jira-journey/SKILL.md +2 -0
  232. package/plugins/lisa-expo/.codex-plugin/skills/jira-verify/SKILL.md +2 -0
  233. package/plugins/lisa-expo/.codex-plugin/skills/lisa-e2e-coverage-gaps/SKILL.md +2 -0
  234. package/plugins/lisa-expo/.codex-plugin/skills/lisa-maestro-mcp-setup/SKILL.md +2 -0
  235. package/plugins/lisa-expo/.codex-plugin/skills/local-state/SKILL.md +2 -0
  236. package/plugins/lisa-expo/.codex-plugin/skills/maestro-mcp-setup/SKILL.md +2 -0
  237. package/plugins/lisa-expo/.codex-plugin/skills/native-data-fetching/SKILL.md +2 -0
  238. package/plugins/lisa-expo/.codex-plugin/skills/ops-browser-uat/SKILL.md +2 -0
  239. package/plugins/lisa-expo/.codex-plugin/skills/ops-check-logs/SKILL.md +2 -0
  240. package/plugins/lisa-expo/.codex-plugin/skills/ops-db-ops/SKILL.md +2 -0
  241. package/plugins/lisa-expo/.codex-plugin/skills/ops-deploy/SKILL.md +2 -0
  242. package/plugins/lisa-expo/.codex-plugin/skills/ops-monitor-errors/SKILL.md +2 -0
  243. package/plugins/lisa-expo/.codex-plugin/skills/ops-performance/SKILL.md +2 -0
  244. package/plugins/lisa-expo/.codex-plugin/skills/ops-run-local/SKILL.md +2 -0
  245. package/plugins/lisa-expo/.codex-plugin/skills/ops-verify-health/SKILL.md +2 -0
  246. package/plugins/lisa-expo/.codex-plugin/skills/owasp-zap/SKILL.md +2 -0
  247. package/plugins/lisa-expo/.codex-plugin/skills/play-store-access/SKILL.md +2 -0
  248. package/plugins/lisa-expo/.codex-plugin/skills/playwright-ci-debugging/SKILL.md +2 -0
  249. package/plugins/lisa-expo/.codex-plugin/skills/playwright-selectors/SKILL.md +2 -0
  250. package/plugins/lisa-expo/.codex-plugin/skills/reduce-complexity/SKILL.md +2 -0
  251. package/plugins/lisa-expo/.codex-plugin/skills/testing-library/SKILL.md +2 -0
  252. package/plugins/lisa-expo/.codex-plugin/skills/upgrading-expo/SKILL.md +2 -0
  253. package/plugins/lisa-expo/.codex-plugin/skills/use-dom/SKILL.md +2 -0
  254. package/plugins/lisa-expo-agy/plugin.json +1 -1
  255. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  256. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  257. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  258. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  259. package/plugins/lisa-harper-fabric/.codex-plugin/skills/e2e-coverage-gaps/SKILL.md +2 -0
  260. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-auth/SKILL.md +2 -0
  261. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-build-and-deploy/SKILL.md +2 -0
  262. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-caching/SKILL.md +2 -0
  263. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-component-model/SKILL.md +2 -0
  264. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-config-yaml/SKILL.md +2 -0
  265. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-operations/SKILL.md +2 -0
  266. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-realtime/SKILL.md +2 -0
  267. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-resources/SKILL.md +2 -0
  268. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-rest-queries/SKILL.md +2 -0
  269. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-schema-graphql/SKILL.md +3 -1
  270. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-schema-graphql/agents/openai.yaml +2 -2
  271. package/plugins/lisa-harper-fabric/.codex-plugin/skills/harper-testing/SKILL.md +2 -0
  272. package/plugins/lisa-harper-fabric/.codex-plugin/skills/lisa-e2e-coverage-gaps/SKILL.md +2 -0
  273. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  274. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  275. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  276. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  277. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  278. package/plugins/lisa-nestjs/.codex-plugin/skills/nestjs-graphql/SKILL.md +2 -0
  279. package/plugins/lisa-nestjs/.codex-plugin/skills/nestjs-rules/SKILL.md +2 -0
  280. package/plugins/lisa-nestjs/.codex-plugin/skills/typeorm-patterns/SKILL.md +2 -0
  281. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  282. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  283. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  284. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  285. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  286. package/plugins/lisa-openclaw/.codex-plugin/skills/lisa-connect-repo-topic/SKILL.md +2 -0
  287. package/plugins/lisa-openclaw/.codex-plugin/skills/lisa-connect-staff-slack/SKILL.md +2 -0
  288. package/plugins/lisa-openclaw/.codex-plugin/skills/lisa-connect-staff-telegram/SKILL.md +2 -0
  289. package/plugins/lisa-openclaw/.codex-plugin/skills/lisa-openclaw-connect-repo-topic/SKILL.md +2 -0
  290. package/plugins/lisa-openclaw/.codex-plugin/skills/lisa-openclaw-connect-staff/SKILL.md +2 -0
  291. package/plugins/lisa-openclaw/.codex-plugin/skills/lisa-openclaw-setup/SKILL.md +2 -0
  292. package/plugins/lisa-openclaw/.codex-plugin/skills/lisa-setup-openclaw/SKILL.md +2 -0
  293. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  294. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  295. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  296. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  297. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  298. package/plugins/lisa-phaser/.codex-plugin/skills/phaser-accessibility/SKILL.md +2 -0
  299. package/plugins/lisa-phaser/.codex-plugin/skills/phaser-asset-pipeline/SKILL.md +2 -0
  300. package/plugins/lisa-phaser/.codex-plugin/skills/phaser-asset-sourcing/SKILL.md +2 -0
  301. package/plugins/lisa-phaser/.codex-plugin/skills/phaser-build-deploy/SKILL.md +2 -0
  302. package/plugins/lisa-phaser/.codex-plugin/skills/phaser-i18n/SKILL.md +2 -0
  303. package/plugins/lisa-phaser/.codex-plugin/skills/phaser-project-structure/SKILL.md +2 -0
  304. package/plugins/lisa-phaser/.codex-plugin/skills/phaser-services/SKILL.md +2 -0
  305. package/plugins/lisa-phaser/.codex-plugin/skills/phaser-testing/SKILL.md +2 -0
  306. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  307. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  308. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  309. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  310. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  311. package/plugins/lisa-rails/.codex-plugin/skills/action-controller-best-practices/SKILL.md +2 -0
  312. package/plugins/lisa-rails/.codex-plugin/skills/action-view-best-practices/SKILL.md +2 -0
  313. package/plugins/lisa-rails/.codex-plugin/skills/active-record-model-best-practices/SKILL.md +2 -0
  314. package/plugins/lisa-rails/.codex-plugin/skills/e2e-coverage-gaps/SKILL.md +2 -0
  315. package/plugins/lisa-rails/.codex-plugin/skills/fix-linter-error/SKILL.md +2 -0
  316. package/plugins/lisa-rails/.codex-plugin/skills/improve-code-complexity/SKILL.md +2 -0
  317. package/plugins/lisa-rails/.codex-plugin/skills/improve-max-lines/SKILL.md +2 -0
  318. package/plugins/lisa-rails/.codex-plugin/skills/improve-max-lines-per-function/SKILL.md +2 -0
  319. package/plugins/lisa-rails/.codex-plugin/skills/improve-test-coverage/SKILL.md +2 -0
  320. package/plugins/lisa-rails/.codex-plugin/skills/jira-add-journey/SKILL.md +2 -0
  321. package/plugins/lisa-rails/.codex-plugin/skills/jira-create/SKILL.md +2 -0
  322. package/plugins/lisa-rails/.codex-plugin/skills/jira-evidence/SKILL.md +2 -0
  323. package/plugins/lisa-rails/.codex-plugin/skills/jira-journey/SKILL.md +2 -0
  324. package/plugins/lisa-rails/.codex-plugin/skills/jira-verify/SKILL.md +2 -0
  325. package/plugins/lisa-rails/.codex-plugin/skills/lisa-e2e-coverage-gaps/SKILL.md +2 -0
  326. package/plugins/lisa-rails/.codex-plugin/skills/ops-check-logs/SKILL.md +2 -0
  327. package/plugins/lisa-rails/.codex-plugin/skills/ops-deploy/SKILL.md +2 -0
  328. package/plugins/lisa-rails/.codex-plugin/skills/ops-run-local/SKILL.md +2 -0
  329. package/plugins/lisa-rails/.codex-plugin/skills/ops-verify-jobs/SKILL.md +2 -0
  330. package/plugins/lisa-rails/.codex-plugin/skills/ops-verify-telemetry/SKILL.md +2 -0
  331. package/plugins/lisa-rails-agy/plugin.json +1 -1
  332. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  333. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  334. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  335. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  336. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  337. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  338. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  339. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  340. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  341. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-add-ingest/SKILL.md +2 -0
  342. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-add-role/SKILL.md +2 -0
  343. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-ingest/SKILL.md +2 -0
  344. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-lint/SKILL.md +2 -0
  345. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-migrate/SKILL.md +2 -0
  346. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-onboard-me/SKILL.md +2 -0
  347. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-query/SKILL.md +2 -0
  348. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-setup/SKILL.md +2 -0
  349. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-setup-wiki/SKILL.md +2 -0
  350. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-status/SKILL.md +2 -0
  351. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-add-ingest/SKILL.md +2 -0
  352. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-add-role/SKILL.md +2 -0
  353. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-connector-confluence/SKILL.md +2 -0
  354. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-connector-docs/SKILL.md +2 -0
  355. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-connector-git/SKILL.md +2 -0
  356. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-connector-jira/SKILL.md +2 -0
  357. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-connector-memory/SKILL.md +2 -0
  358. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-connector-notion/SKILL.md +2 -0
  359. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-connector-roles/SKILL.md +2 -0
  360. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-connector-slack/SKILL.md +2 -0
  361. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-connector-web/SKILL.md +2 -0
  362. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-doctor/SKILL.md +2 -0
  363. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-ingest/SKILL.md +2 -0
  364. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-lint/SKILL.md +2 -0
  365. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-migrate/SKILL.md +2 -0
  366. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-onboard-me/SKILL.md +2 -0
  367. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-query/SKILL.md +2 -0
  368. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-setup/SKILL.md +2 -0
  369. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-setup-automations/SKILL.md +2 -0
  370. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-status/SKILL.md +2 -0
  371. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-tear-down-automations/SKILL.md +2 -0
  372. package/plugins/lisa-wiki/.codex-plugin/skills/lisa-wiki-usage/SKILL.md +2 -0
  373. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  374. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  375. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  376. package/plugins/src/base/hooks/parity-safety-net.sh +769 -37
  377. package/plugins/src/base/rules/reference/design-source-of-truth.md +1 -1
  378. package/plugins/src/base/rules/reference/rejection-detection.md +1 -1
  379. package/scripts/generate-codex-plugin-artifacts.mjs +11 -6
@@ -512,6 +512,8 @@ readonly FOLLOW_MAX_BYTES=262144
512
512
  readonly FOLLOW_MAX_TOTAL_BYTES=524288
513
513
  readonly FOLLOW_MAX_FILES=8
514
514
  readonly FOLLOW_MAX_DEPTH=3
515
+ readonly FOLLOW_MAX_FUNCTION_DEPTH=16
516
+ readonly FOLLOW_MAX_FUNCTION_CALLS=128
515
517
 
516
518
  # Effective working directory established by a literal `cd` in the command, and
517
519
  # the flag that says a `cd` happened whose target could NOT be read literally.
@@ -523,6 +525,8 @@ follow_paths=$'\n'
523
525
  follow_names=""
524
526
  follow_count=0
525
527
  follow_bytes=0
528
+ follow_function_calls=0
529
+ follow_function_bytes=0
526
530
  followed_text=""
527
531
  followed_rm_text=""
528
532
  # Generated from the canonical reviewed supervisor, never from a host manifest.
@@ -724,8 +728,9 @@ follow_may_invoke() {
724
728
  # `;` or `|` inside a quoted string is not a statement boundary, and splitting
725
729
  # there would invent a command position in the middle of a quoted argument and
726
730
  # make prose look like an invocation — the #3604 failure mode, arrived at from
727
- # the other side. Parens are NOT separators, so `eval "$(cat <file>)"` stays one
728
- # statement and its command word is still `eval`.
731
+ # the other side. Literal subshell parentheses carry scope events, while quoted
732
+ # parentheses and substitutions stay in their original statement so `eval
733
+ # "$(cat <file>)"` retains its command position and executed-file inspection.
729
734
  #
730
735
  # awk rather than a bash char loop because this runs on EVERY intercepted Bash
731
736
  # command, and it addresses the text by index instead of accumulating it a
@@ -733,24 +738,229 @@ follow_may_invoke() {
733
738
  # arrives through the ENVIRONMENT: `awk -v` processes backslash escapes in the
734
739
  # value it is given and would rewrite the command before it was ever read.
735
740
  follow_split() {
736
- SN_FOLLOW_TEXT="$1" awk '
741
+ LC_ALL=C SN_FOLLOW_TEXT="$1" awk '
742
+ # Lexical lookahead keeps an asynchronous return from ending its parent
743
+ # scan before the trailing & event. Aggregate work is capped linearly in
744
+ # input size, including overlap from nested scopes; no partial parse passes.
745
+ function lookahead_step() {
746
+ if (++lookahead_work > 8 * n + 1024) { exit 2 }
747
+ }
748
+ function brace_pipe(from, opening, closing, j, c, quote, escape, braces, nesting, tail, tail_cases, tail_header, word, word_end) {
749
+ quote = ""; escape = 0; braces = 1; nesting = 0; brace_redirect = ""; tail_cases = 0; tail_header = 0;
750
+ for (j = from; j <= n; j++) {
751
+ lookahead_step(); c = substr(s, j, 1);
752
+ if (escape) { escape = 0; continue }
753
+ if (c == "\\" && quote != "\047") { escape = 1; continue }
754
+ if (quote != "") { if (c == quote) { quote = "" } continue }
755
+ if (c == "\"" || c == "\047" || c == "`") { quote = c; continue }
756
+ if (braces > 0) {
757
+ # A case-pattern ) is not the closing parenthesis of its shell.
758
+ if (opening == "(" && c ~ /[A-Za-z_]/) {
759
+ word_end = j + 1;
760
+ while (word_end <= n && substr(s, word_end, 1) ~ /[A-Za-z_0-9-]/) { lookahead_step(); word_end++ }
761
+ word = substr(s, j, word_end - j); j = word_end - 1;
762
+ if (word == "case" && (tail_cases == 0 || !tail_patterns[tail_cases])) { tail_header++ }
763
+ if (word == "in" && tail_header > 0) {
764
+ tail_header--; tail_patterns[++tail_cases] = 1; tail_levels[tail_cases] = braces;
765
+ }
766
+ if (word == "esac" && tail_cases > 0) { tail_cases-- }
767
+ continue;
768
+ }
769
+ if (tail_cases > 0 && tail_levels[tail_cases] == braces) {
770
+ if (c == ")" && tail_patterns[tail_cases]) { tail_patterns[tail_cases] = 0; continue }
771
+ if (c == ";" && substr(s, j + 1, 1) == ";") { tail_patterns[tail_cases] = 1; j++; continue }
772
+ }
773
+ if (c == opening) { braces++ }
774
+ if (c == closing && --braces == 0) { tail = j + 1 }
775
+ continue;
776
+ }
777
+ # Redirections belong to the compound command too. Quoted operator
778
+ # bytes and nested substitutions are operands, not the pipeline edge.
779
+ if (c == "(") { nesting++; continue }
780
+ if (c == ")") { if (nesting == 0) { brace_redirect = substr(s, tail, j - tail); return 0 } nesting--; continue }
781
+ if (nesting > 0) { continue }
782
+ if (c == "|") { brace_redirect = substr(s, tail, j - tail); return substr(s, j + 1, 1) != "|" }
783
+ if (c == ";" || c == "\n") { brace_redirect = substr(s, tail, j - tail); return 0 }
784
+ if (c == "&" && substr(s, j - 1, 1) !~ /[<>]/ && substr(s, j + 1, 1) != ">") { brace_redirect = substr(s, tail, j - tail); return 0 }
785
+ }
786
+ if (tail > 0) { brace_redirect = substr(s, tail) }
787
+ return 0;
788
+ }
789
+ function list_async(from, j, c, quote, escape, nesting, braces) {
790
+ quote = ""; escape = 0; nesting = 0; braces = 0;
791
+ for (j = from; j <= n; j++) {
792
+ lookahead_step();
793
+ c = substr(s, j, 1);
794
+ if (escape) { escape = 0; continue }
795
+ if (c == "\\" && quote != "\047") { escape = 1; continue }
796
+ if (quote != "") { if (c == quote) { quote = "" } continue }
797
+ if (c == "\"" || c == "\047" || c == "`") { quote = c; continue }
798
+ if (c == "(") { nesting++; continue }
799
+ if (c == ")") { if (nesting == 0) { return 0 } nesting--; continue }
800
+ if (c == "{") { braces++; continue }
801
+ if (c == "}") { if (braces > 0) { braces-- } continue }
802
+ if (nesting > 0 || braces > 0) { continue }
803
+ if (c == ";" || c == "\n") { return 0 }
804
+ if (c == "&" && substr(s, j - 1, 1) !~ /[&<>]/ && substr(s, j + 1, 1) !~ /[&>]/) { return 1 }
805
+ }
806
+ return 0;
807
+ }
737
808
  BEGIN {
738
809
  s = ENVIRON["SN_FOLLOW_TEXT"]; n = length(s);
739
- q = ""; esc = 0; start = 1; sep = "o";
810
+ q = ""; esc = 0; start = 1; sep = "o"; parens = 0;
811
+ functions = 0; cases = 0; backtick = 0; brace_depth = 0; post_brace = 0;
812
+ printf "L\t%d\n", list_async(1);
740
813
  for (i = 1; i <= n; i++) {
741
814
  c = substr(s, i, 1);
742
- if (esc == 1) { esc = 0; continue }
815
+ if (esc == 1) { if (c == "$") { literal_dollars[i] = 1 } esc = 0; continue }
743
816
  if (c == "\\" && q != "\047") { esc = 1; continue }
744
- if (q != "") { if (c == q) { q = "" } continue }
817
+ if (functions > 0) {
818
+ if (q != "") { if (c == q) { q = "" } continue }
819
+ if (c == "\"" || c == "\047") { q = c; continue }
820
+ if (c == "{") { functions++ }
821
+ if (c == "}" && --functions == 0) {
822
+ printf "N\t%s\t%s\n", sep, function_name;
823
+ body = substr(s, function_start, i - function_start);
824
+ count = split(body, body_lines, "\n");
825
+ for (j = 1; j <= count; j++) { printf "D\t%s\n", body_lines[j] }
826
+ printf "F\t\n"; start = i + 1; sep = "o";
827
+ }
828
+ continue;
829
+ }
830
+ quoted_expression = q == "\"" && ((c == "(" && substr(s, i - 1, 1) == "$" && !literal_dollars[i - 1]) || c == "`");
831
+ if (q != "" && !quoted_expression) { if (c == q) { q = "" } continue }
745
832
  if (c == "\"" || c == "\047") { q = c; continue }
833
+ if (c == "`") {
834
+ if (backtick) {
835
+ piece = substr(s, start, i - start);
836
+ if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
837
+ printf "]\t\n"; start = backtick_start; q = backtick_quote; backtick = 0;
838
+ } else {
839
+ backtick_start = start; backtick_quote = q; q = ""; backtick = 1;
840
+ printf "[\t\nL\t%d\n", list_async(i + 1); start = i + 1;
841
+ }
842
+ continue;
843
+ }
844
+ if (i == start && c ~ /[[:space:]]/) { start = i + 1; continue }
845
+ if (c ~ /[[:space:]]/ && substr(s, i - 1, 1) !~ /[[:space:]]/ && substr(s, start, 4) == "case") {
846
+ piece = substr(s, start, i - start);
847
+ if (piece ~ /^case[[:space:]].*[[:space:]]in$/) {
848
+ case_scopes[++cases] = parens;
849
+ case_patterns[cases] = 1; printf "C\t\n"; start = i + 1; continue;
850
+ }
851
+ }
852
+ if (cases > 0 && c ~ /[[:space:];&|)]/ && substr(s, start, 4) == "esac") {
853
+ piece = substr(s, start, i - start);
854
+ if (piece ~ /^esac[[:space:]]*$/) { cases--; printf "K\t\n" }
855
+ }
856
+ if (cases > 0 && case_scopes[cases] == parens && case_patterns[cases]) {
857
+ if (c == ")") { case_patterns[cases] = 0; start = i + 1 }
858
+ continue;
859
+ }
860
+ if (c == "{") {
861
+ piece = substr(s, start, i - start);
862
+ definition = piece ~ /^[[:space:]]*([A-Za-z_][A-Za-z_0-9]*[[:space:]]*\([[:space:]]*\)|function[[:space:]]+[A-Za-z_][A-Za-z_0-9]*([[:space:]]*\([[:space:]]*\))?)[[:space:]]*$/;
863
+ if (definition) {
864
+ function_name = piece;
865
+ sub(/^[[:space:]]*(function[[:space:]]+)?/, "", function_name);
866
+ sub(/[[:space:]\(].*$/, "", function_name);
867
+ functions = 1; function_start = i + 1;
868
+ continue;
869
+ }
870
+ previous = i > 1 ? substr(s, i - 1, 1) : "";
871
+ group = (i == 1 || previous ~ /[[:space:];&|()]/) && substr(s, i + 1, 1) ~ /[[:space:];]/ &&
872
+ piece ~ /^[[:space:]]*((then|do|else|!|time)[[:space:]]+)*$/;
873
+ brace_groups[++brace_depth] = group;
874
+ if (group) {
875
+ brace_sep = sep;
876
+ if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
877
+ # A pipeline owns the whole compound command, not just the final
878
+ # statement before |. Snapshot before any brace-body cd/return.
879
+ outgoing = brace_pipe(i + 1, "{", "}");
880
+ brace_pipes[brace_depth] = brace_sep == "|" || outgoing;
881
+ if (brace_pipes[brace_depth]) { printf "(\t%s%s\nL\t%d\n", (brace_sep == "|" ? "|" : ""), (outgoing ? "+" : ""), list_async(i + 1) }
882
+ printf "G\t%s\nL\t%d\n", brace_sep, list_async(i + 1);
883
+ if (brace_redirect ~ /[^[:space:]]/) { printf "I\t%s\n", brace_redirect }
884
+ # P retains incoming stdin provenance without wrapping each body
885
+ # statement in another child: all statements share the brace cwd.
886
+ if (brace_sep == "|") { sep = "P" }
887
+ start = i + 1; continue;
888
+ }
889
+ }
890
+ if (c == "}" && brace_depth > 0) {
891
+ group = brace_groups[brace_depth--];
892
+ if (group) {
893
+ piece = substr(s, start, i - start);
894
+ if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
895
+ printf "H\t\n";
896
+ if (brace_pipes[brace_depth + 1]) { printf ")\t\n" }
897
+ start = i + 1; post_brace = 1; continue;
898
+ }
899
+ }
900
+ if (c == "(") {
901
+ previous = i > 1 ? substr(s, i - 1, 1) : "";
902
+ piece = substr(s, start, i - start);
903
+ header = piece ~ /^[[:space:]]*(function[[:space:]]+)?[A-Za-z_][A-Za-z_0-9]*[[:space:]]*$/ && substr(s, i + 1) ~ /^[[:space:]]*\)/;
904
+ expression = previous ~ /[$<>]/ && !literal_dollars[i - 1] && substr(s, i + 1, 1) != "(";
905
+ group = (parens == 0 || scopes[parens] != 0) &&
906
+ (i == 1 || previous ~ /[[:space:];&|()]/) && substr(s, i + 1, 1) != "(" && !header;
907
+ scopes[++parens] = expression ? 2 : group;
908
+ if (expression) {
909
+ expression_starts[parens] = start;
910
+ expression_quotes[parens] = q; q = "";
911
+ printf "[\t\nL\t%d\n", list_async(i + 1); start = i + 1; continue;
912
+ }
913
+ if (group) {
914
+ piece = substr(s, start, i - start);
915
+ if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
916
+ outgoing = brace_pipe(i + 1, "(", ")");
917
+ printf "(\t%s%s\nL\t%d\n", sep, (outgoing ? "+" : ""), list_async(i + 1);
918
+ if (brace_redirect ~ /[^[:space:]]/) { printf "I\t%s\n", brace_redirect }
919
+ if (sep == "|") { sep = "P" }
920
+ start = i + 1; continue;
921
+ }
922
+ }
923
+ if (c == ")" && parens > 0) {
924
+ piece = substr(s, start, i - start);
925
+ group = scopes[parens--];
926
+ if (group == 2) {
927
+ if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
928
+ printf "]\t\n"; start = expression_starts[parens + 1]; q = expression_quotes[parens + 1]; continue;
929
+ }
930
+ if (group) {
931
+ piece = substr(s, start, i - start);
932
+ if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", sep, piece; sep = "o" }
933
+ printf ")\t\n"; start = i + 1; post_brace = 1; continue;
934
+ }
935
+ }
746
936
  if (c == ";" || c == "&" || c == "|" || c == "\n") {
747
- printf "%s\t%s\n", sep, substr(s, start, i - start);
937
+ if (parens > 0 && scopes[parens] == 0) { continue }
938
+ if (c == "&" && (substr(s, i - 1, 1) ~ /[<>]/ || substr(s, i + 1, 1) == ">")) { continue }
939
+ piece = substr(s, start, i - start);
940
+ pipe = c == "|" && substr(s, i + 1, 1) != "|";
941
+ if (pipe || sep == "|") { printf "(\t%s%s\nL\t%d\n", (sep == "|" ? "|" : ""), (pipe ? "+" : ""), list_async(start) }
942
+ if (piece ~ /[^[:space:]]/) { printf "%s\t%s\n", post_brace ? "R" : sep, piece }
943
+ post_brace = 0;
944
+ if (pipe || sep == "|") { printf ")\t\n" }
945
+ if (c == ";" && substr(s, i + 1, 1) == ";" && cases > 0) {
946
+ case_patterns[cases] = 1; printf "A\t\n"; i++;
947
+ }
948
+ if (c == "|" && substr(s, i + 1, 1) == "|") {
949
+ i++; sep = "?";
950
+ } else if (c == "&" && substr(s, i + 1, 1) == "&") {
951
+ i++; sep = "&";
952
+ } else if (c == "|") { sep = "|" } else { sep = "o" }
953
+ if (c == "&" && sep == "o" && substr(s, i - 1, 1) !~ /[&<>]/ && substr(s, i + 1, 1) != ">") {
954
+ printf "B\t\n";
955
+ }
956
+ if ((c == ";" || c == "\n" || c == "&") && sep == "o") { printf "L\t%d\n", list_async(i + 1) }
748
957
  start = i + 1;
749
- if (c == "|") { sep = "|" } else { sep = "o" }
750
958
  continue
751
959
  }
752
960
  }
961
+ if (sep == "|") { printf "(\t|\nL\t%d\n", list_async(start) }
753
962
  printf "%s\t%s\n", sep, substr(s, start, n - start + 1);
963
+ if (sep == "|") { printf ")\t\n" }
754
964
  }
755
965
  '
756
966
  }
@@ -815,7 +1025,12 @@ follow_cd() {
815
1025
  esac
816
1026
  case "$d" in
817
1027
  /*) ;;
818
- *) d="${follow_cwd:-$PWD}/$d" ;;
1028
+ *)
1029
+ # A relative cd cannot recover an unknown base. Falling back to this
1030
+ # hook's PWD would confidently inspect another directory (#4299).
1031
+ [ "$follow_cwd_unknown" -eq 0 ] || return 0
1032
+ d="${follow_cwd:-$PWD}/$d"
1033
+ ;;
819
1034
  esac
820
1035
  follow_normalize "$d"
821
1036
  d="$follow_normalized"
@@ -899,8 +1114,14 @@ follow_locate() {
899
1114
  # is never executed, so scanning it can only manufacture a false refusal.
900
1115
  follow_take() {
901
1116
  local path="$1" depth="$2" size="" content="" rm_content="${3-}"
1117
+ local invocation_kind=child
1118
+ if [ "$#" -eq 3 ]; then invocation_kind="$3"; fi
902
1119
  case "$follow_paths" in
903
- *$'\n'"$path"$'\n'*) return 0 ;;
1120
+ *$'\n'"$path"$'\n'*)
1121
+ # Sourcing executes in the caller each time; a read cache cannot erase
1122
+ # its directory changes or newly defined functions.
1123
+ [ "$invocation_kind" = source ] || return 0
1124
+ ;;
904
1125
  esac
905
1126
  follow_paths="$follow_paths$path"$'\n'
906
1127
  follow_count=$((follow_count + 1))
@@ -928,22 +1149,31 @@ follow_take() {
928
1149
  fi
929
1150
  follow_names="$follow_names $path"
930
1151
  followed_text="$followed_text"$'\n'"$content"
931
- if [ "$#" -lt 3 ]; then
1152
+ if [ "$#" -ne 4 ]; then
932
1153
  rm_content="$content"
933
1154
  else
934
1155
  rm_content="$(printf '%s' "$rm_content" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "supervisor cleanup view could not be read"
935
1156
  fi
936
1157
  followed_rm_text="$followed_rm_text"$'\n'"$rm_content"
937
1158
  if [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ]; then
938
- # A `cd` inside the followed script governs what THAT script goes on to
939
- # execute, and nothing after it in the command that ran the script — the
940
- # child inherits this directory but cannot change the parent's. Saving and
941
- # restoring is what keeps a nested `cd` from silently re-pointing the rest
942
- # of the outer walk.
1159
+ # Child scripts inherit cwd but cannot change their parent. A sourced
1160
+ # script runs in the caller and keeps both cwd and function definitions.
943
1161
  local saved_cwd="$follow_cwd" saved_unknown="$follow_cwd_unknown"
944
- follow_scan "$content" "$((depth + 1))"
945
- follow_cwd="$saved_cwd"
946
- follow_cwd_unknown="$saved_unknown"
1162
+ if [ "$#" -eq 4 ]; then
1163
+ # Only follow_supervisor reaches this form, after exact byte identity and
1164
+ # the public argv profile were authenticated. Its owned bootstrap is
1165
+ # already scanned in full by every text policy; its user payload is
1166
+ # followed separately by follow_supervisor. Re-expanding all bootstrap
1167
+ # functions would re-interpret its internal protocol, not inspect a new
1168
+ # user-controlled file, and multiplied its native hook time fourfold.
1169
+ follow_scan "$content" "$((depth + 1))" 0 1
1170
+ else
1171
+ follow_scan "$content" "$((depth + 1))" 0 0 "$invocation_kind"
1172
+ fi
1173
+ if [ "$invocation_kind" != source ]; then
1174
+ follow_cwd="$saved_cwd"
1175
+ follow_cwd_unknown="$saved_unknown"
1176
+ fi
947
1177
  fi
948
1178
  }
949
1179
 
@@ -951,7 +1181,14 @@ follow_take() {
951
1181
  # nested inside an already-followed script it is the documented residual above.
952
1182
  follow_target() {
953
1183
  local token="$1" depth="$2" path=""
1184
+ local invocation_kind="${3:-child}"
954
1185
  [ -n "$token" ] || return 0
1186
+ if [ "$follow_cwd_unknown" -ne 0 ]; then
1187
+ case "$token" in
1188
+ /* | '~/'*) ;;
1189
+ *) follow_refuse "$token" "the execution directory is unknown" ;;
1190
+ esac
1191
+ fi
955
1192
  # Entry authority belongs to each invocation, not the file-read cache. Only
956
1193
  # follow_supervisor may admit the exact public interpreter/argv profile.
957
1194
  case "$token" in
@@ -964,7 +1201,7 @@ follow_target() {
964
1201
  return 0
965
1202
  fi
966
1203
  path="$follow_located"
967
- follow_take "$path" "$depth"
1204
+ follow_take "$path" "$depth" "$invocation_kind"
968
1205
  }
969
1206
 
970
1207
  # A command word that IS the executable (`./deploy.sh`, `/tmp/x.sh`). Followed
@@ -1021,23 +1258,422 @@ follow_next_operand() {
1021
1258
  follow_operand_index="$j"
1022
1259
  }
1023
1260
 
1261
+ # Resolve a piped file in the PRODUCER's directory, which can differ from the
1262
+ # interpreter's after a subshell closes. Preserve unknown-state provenance too.
1263
+ follow_pipe_target() {
1264
+ local saved_cwd="$follow_cwd" saved_unknown="$follow_cwd_unknown"
1265
+ follow_cwd="$3"
1266
+ follow_cwd_unknown="$4"
1267
+ follow_target "$1" "$2"
1268
+ follow_cwd="$saved_cwd"
1269
+ follow_cwd_unknown="$saved_unknown"
1270
+ }
1271
+
1272
+ # Carry branch uncertainty into sourced declarations as well as cwd changes.
1273
+ # The local projection lasts only for this source invocation, not later scans.
1274
+ follow_source_target() {
1275
+ local follow_source_flow_unknown="$3"
1276
+ follow_target "$1" "$2" source
1277
+ }
1278
+
1279
+ # Extract the literal -c operand without evaluating it or treating positional
1280
+ # arguments after its closing quote as commands. Prefix indexing matches the
1281
+ # whitespace tokens used by follow_scan; the operand keeps its internal bytes.
1282
+ follow_inline() {
1283
+ local stmt="$1" from="$2" depth="$3" body
1284
+ local follow_inline_depth="${follow_inline_depth:-0}"
1285
+ [ "$follow_inline_depth" -lt "$FOLLOW_MAX_FUNCTION_DEPTH" ] || follow_refuse "-c" "inline shells exceed the inspection depth"
1286
+ follow_inline_depth=$((follow_inline_depth + 1))
1287
+ body="$(LC_ALL=C SN_INLINE_TEXT="$stmt" SN_INLINE_FROM="$from" awk '
1288
+ BEGIN {
1289
+ s = ENVIRON["SN_INLINE_TEXT"]; n = length(s); i = 1;
1290
+ for (word = 0; word < ENVIRON["SN_INLINE_FROM"]; word++) {
1291
+ while (i <= n && substr(s, i, 1) ~ /[[:space:]]/) { i++ }
1292
+ while (i <= n && substr(s, i, 1) !~ /[[:space:]]/) { i++ }
1293
+ }
1294
+ while (i <= n && substr(s, i, 1) ~ /[[:space:]]/) { i++ }
1295
+ q = substr(s, i, 1);
1296
+ if (q == "\047" || q == "\"") {
1297
+ start = ++i; esc = 0;
1298
+ for (; i <= n; i++) {
1299
+ c = substr(s, i, 1);
1300
+ if (esc) { esc = 0; continue }
1301
+ if (c == "\\" && q == "\"") { esc = 1; continue }
1302
+ if (c == q) {
1303
+ # Adjacent quoted/unquoted fragments are the same shell operand.
1304
+ # Refuse unsupported concatenation instead of scanning a prefix.
1305
+ if (i < n && substr(s, i + 1, 1) !~ /[[:space:]]/) { exit 1 }
1306
+ printf "%s", substr(s, start, i - start); exit;
1307
+ }
1308
+ }
1309
+ exit 1;
1310
+ }
1311
+ start = i;
1312
+ while (i <= n && substr(s, i, 1) !~ /[[:space:]]/) {
1313
+ c = substr(s, i, 1);
1314
+ if (c == "\047" || c == "\"" || c == "\\") { exit 1 }
1315
+ i++;
1316
+ }
1317
+ printf "%s", substr(s, start, i - start);
1318
+ }
1319
+ ')" || follow_refuse "-c" "its command string could not be determined"
1320
+ local saved_cwd="$follow_cwd" saved_unknown="$follow_cwd_unknown"
1321
+ follow_scan "$body" "$depth"
1322
+ follow_cwd="$saved_cwd"
1323
+ follow_cwd_unknown="$saved_unknown"
1324
+ }
1325
+
1326
+ # Resolve literal descriptor duplication without running the proposed shell.
1327
+ # Compound input is established before its body; output is projected when the
1328
+ # producer reaches the pipe. Unknown descriptors refuse rather than drop bytes.
1329
+ follow_redirect_io() {
1330
+ local statement="$1" mode="$2" result
1331
+ result="$(SN_COMPOUND_REDIRECT="$statement" SN_DESCRIPTOR_MAP="$follow_descriptor_map" SN_REDIRECT_CWD="$follow_cwd" SN_REDIRECT_UNKNOWN="$follow_cwd_unknown" python3 - "$mode" <<'PY'
1332
+ import json
1333
+ import os
1334
+ import sys
1335
+ try:
1336
+ mode = sys.argv[1]
1337
+ text = os.environ['SN_COMPOUND_REDIRECT']
1338
+ tokens = []
1339
+ cursor = 0
1340
+ # Keep syntax provenance and byte adjacency: quoted/escaped > is data,
1341
+ # and `cat 2 >&2` names a file rather than redirecting descriptor 2.
1342
+ while cursor < len(text):
1343
+ if text[cursor].isspace():
1344
+ cursor += 1
1345
+ continue
1346
+ start = cursor
1347
+ syntax = text[cursor] in '<>&'
1348
+ plain = True
1349
+ value = ''
1350
+ quote = ''
1351
+ if syntax:
1352
+ while cursor < len(text) and text[cursor] in '<>&':
1353
+ value += text[cursor]; cursor += 1
1354
+ else:
1355
+ while cursor < len(text):
1356
+ char = text[cursor]
1357
+ if not quote and (char.isspace() or char in '<>&'):
1358
+ break
1359
+ if char == '\\' and quote != "'":
1360
+ plain = False
1361
+ cursor += 1
1362
+ if cursor == len(text):
1363
+ raise ValueError('trailing escape')
1364
+ if text[cursor] == '\n':
1365
+ cursor += 1
1366
+ continue
1367
+ if quote == '"' and text[cursor] not in '$`"\\':
1368
+ value += '\\'
1369
+ value += text[cursor]; cursor += 1
1370
+ continue
1371
+ if char in ('"', "'"):
1372
+ if not quote:
1373
+ quote = char; plain = False; cursor += 1
1374
+ continue
1375
+ if quote == char:
1376
+ quote = ''; cursor += 1
1377
+ continue
1378
+ value += char; cursor += 1
1379
+ if quote:
1380
+ raise ValueError('unterminated quote')
1381
+ tokens.append((value, syntax, plain, start, cursor))
1382
+ descriptors = {int(fd): value for fd, value in json.loads(os.environ['SN_DESCRIPTOR_MAP']).items()}
1383
+ operators = ('<', '>', '>>', '<>', '<&', '>&', '&>', '&>>')
1384
+ index = 0
1385
+ while index < len(tokens):
1386
+ fd = None
1387
+ token, syntax, plain, start, end = tokens[index]
1388
+ if token.isdigit() and plain and index + 1 < len(tokens) and tokens[index + 1][1] and end == tokens[index + 1][3]:
1389
+ fd = int(token); index += 1
1390
+ token, syntax, plain, start, end = tokens[index]
1391
+ if not syntax or token not in operators:
1392
+ if mode == 'cat':
1393
+ index += 1
1394
+ continue
1395
+ sys.exit(2)
1396
+ if index + 1 >= len(tokens):
1397
+ sys.exit(2)
1398
+ operator = token
1399
+ operand, operand_syntax, _, _, _ = tokens[index + 1]
1400
+ if operand_syntax:
1401
+ sys.exit(2)
1402
+ index += 2
1403
+ if fd is None:
1404
+ fd = 0 if operator.startswith('<') else 1
1405
+ if operator in ('<&', '>&'):
1406
+ if operand == '-':
1407
+ descriptors[fd] = {'kind': 'closed'}
1408
+ elif operand.isdigit():
1409
+ descriptors[fd] = descriptors.get(int(operand), {'kind': 'unknown'})
1410
+ else:
1411
+ sys.exit(2)
1412
+ else:
1413
+ descriptors[fd] = {'kind': 'file', 'path': operand, 'cwd': os.environ['SN_REDIRECT_CWD'], 'unknown': int(os.environ['SN_REDIRECT_UNKNOWN'])}
1414
+ if operator.startswith('&>'):
1415
+ descriptors[2] = descriptors[fd]
1416
+ output = descriptors[1]['kind']
1417
+ if output == 'unknown' or descriptors[0]['kind'] == 'unknown':
1418
+ sys.exit(2)
1419
+ print(json.dumps({'input': descriptors[0], 'output': output, 'descriptors': descriptors}))
1420
+ except (ValueError, OSError):
1421
+ sys.exit(2)
1422
+ PY
1423
+ )" || follow_refuse "compound redirection" "its input or pipeline output could not be determined"
1424
+ follow_redirect_input="$(printf '%s' "$result" | jq -er '.input.kind')" || follow_refuse "compound redirection" "invalid input projection"
1425
+ follow_redirect_path="$(printf '%s' "$result" | jq -r '.input.path // ""')" || follow_refuse "compound redirection" "invalid input path"
1426
+ follow_redirect_map="$(printf '%s' "$result" | jq -ce '.descriptors')" || follow_refuse "compound redirection" "invalid descriptor projection"
1427
+ follow_redirect_input_cwd="$(printf '%s' "$result" | jq -r '.input.cwd // ""')" || follow_refuse "compound redirection" "invalid input cwd"
1428
+ follow_redirect_input_unknown="$(printf '%s' "$result" | jq -r '.input.unknown // 1')" || follow_refuse "compound redirection" "invalid input uncertainty"
1429
+ follow_redirect_output="$(printf '%s' "$result" | jq -er '.output')" || follow_refuse "compound redirection" "invalid output projection"
1430
+ }
1431
+
1024
1432
  # Walk one text for the files it executes. Runs in the PARENT shell, never in a
1025
1433
  # command substitution: follow_refuse() must be able to deny, and an exit inside
1026
1434
  # a subshell ends that subshell rather than this hook — the mistake documented
1027
1435
  # on the rm segmentation below.
1028
1436
  follow_scan() {
1029
1437
  local text="$1" depth="$2"
1438
+ local function_call_depth="${3:-0}"
1439
+ local authenticated_bootstrap="${4:-0}"
1440
+ local invocation_kind="${5:-child}"
1441
+ local function_flow_unknown="${6:-0}" function_lookup_allowed statement_conditional function_conditional=0
1442
+ local and_cwd="" and_unknown=1 and_ready=0 join_cwd join_unknown
1443
+ if [ "$invocation_kind" = source ]; then
1444
+ function_flow_unknown="${follow_source_flow_unknown:-0}"
1445
+ fi
1446
+ local return_scope_depth=-1
1447
+ local return_list_depth=-1
1448
+ local return_flow_unknown=0
1449
+ local list_depth=0 list_index
1030
1450
  local split_out line sep stmt t tj i j n cmd_pos eval_mode inline arg_exec
1031
1451
  local heredoc redirected pipe_cat prev_cat pending_operand stdin_mode
1452
+ local pipe_cwd pipe_unknown prev_cwd prev_unknown
1453
+ local follow_stdin_cat="${follow_stdin_cat:-}" follow_stdin_cwd="${follow_stdin_cwd:-}" follow_stdin_unknown="${follow_stdin_unknown:-1}"
1454
+ local follow_redirect_input follow_redirect_path follow_redirect_output follow_redirect_map follow_redirect_input_cwd follow_redirect_input_unknown
1455
+ local follow_descriptor_map="${follow_descriptor_map:-}"
1456
+ if [ -z "$follow_descriptor_map" ]; then
1457
+ follow_descriptor_map='{"0":{"kind":"input"},"1":{"kind":"pipe"},"2":{"kind":"stderr"}}'
1458
+ fi
1032
1459
  local find_mode dispatch_pending
1460
+ local scope_depth=0 case_depth=0 function_name="" function_body="" function_index function_found
1033
1461
  local -a toks=()
1034
- split_out="$(follow_split "$text")"
1462
+ local -a cwd_scopes=() unknown_scopes=() cat_scopes=() cat_cwd_scopes=() cat_unknown_scopes=() function_scopes=() flow_scopes=() return_flows=()
1463
+ local -a list_cwds=() list_unknowns=() list_functions=() list_flows=() list_asyncs=() list_scope_depths=() list_return_flows=() brace_flows=()
1464
+ local -a brace_descriptor_maps=() scope_descriptor_maps=()
1465
+ local -a brace_stdin_cats=() brace_stdin_cwds=() brace_stdin_unknowns=() scope_stdin_cats=() scope_stdin_cwds=() scope_stdin_unknowns=()
1466
+ local -a case_cwds=() case_unknowns=() case_changes=()
1467
+ if [ "$function_call_depth" -eq 0 ] && [ "$invocation_kind" != source ]; then
1468
+ # Ordinary child shells do not inherit unexported functions. Sourcing and
1469
+ # function calls share the caller's definitions instead.
1470
+ local function_count=0
1471
+ local -a function_names=() function_bodies=() function_uncertainties=()
1472
+ fi
1473
+ split_out="$(follow_split "$text")" || follow_refuse "shell scope" "scope parsing exceeds the inspection budget"
1035
1474
  prev_cat=""
1036
1475
  while IFS= read -r line; do
1037
1476
  sep="${line%%$'\t'*}"
1038
1477
  stmt="${line#*$'\t'}"
1039
- pipe_cat=""
1040
- [ "$sep" != "|" ] || pipe_cat="$prev_cat"
1478
+ statement_conditional=0
1479
+ case "$sep" in '&' | '?') statement_conditional=1 ;; esac
1480
+ # A reached && successor observes the preceding successful command's cwd.
1481
+ # Scope/list events invalidate this projection; it must never carry a
1482
+ # child's or a background list's directory into another shell/list.
1483
+ case "$sep" in
1484
+ L | G | H | B | N | F | C | A | K | '(' | '[' | ')' | ']') and_ready=0 ;;
1485
+ esac
1486
+ if [ "$return_list_depth" -ge 0 ]; then
1487
+ case "$sep" in
1488
+ B) [ "$list_depth" -eq "$return_list_depth" ] || continue ;;
1489
+ '(' | '[' | ')' | ']' | G | H) ;;
1490
+ *) continue ;;
1491
+ esac
1492
+ fi
1493
+ if [ "$return_scope_depth" -ge 0 ]; then
1494
+ # A return in a child scope ends that child, not its enclosing function.
1495
+ # Keep only scope events until the matching close restores the parent.
1496
+ case "$sep" in '(' | '[' | ')' | ']' | G | H) ;; *) continue ;; esac
1497
+ fi
1498
+ case "$sep" in
1499
+ L)
1500
+ list_cwds[list_depth]="$follow_cwd"
1501
+ list_unknowns[list_depth]="$follow_cwd_unknown"
1502
+ list_functions[list_depth]=$function_count
1503
+ list_flows[list_depth]=$function_flow_unknown
1504
+ list_asyncs[list_depth]="$stmt"
1505
+ list_scope_depths[list_depth]=$scope_depth
1506
+ list_return_flows[list_depth]=$return_flow_unknown
1507
+ continue
1508
+ ;;
1509
+ G)
1510
+ brace_flows[list_depth]=$function_flow_unknown
1511
+ brace_descriptor_maps[list_depth]="$follow_descriptor_map"
1512
+ brace_stdin_cats[list_depth]="$follow_stdin_cat"
1513
+ brace_stdin_cwds[list_depth]="$follow_stdin_cwd"
1514
+ brace_stdin_unknowns[list_depth]="$follow_stdin_unknown"
1515
+ if [ "$stmt" = "|" ]; then
1516
+ follow_stdin_cat="$prev_cat"; follow_stdin_cwd="${prev_cwd:-}"; follow_stdin_unknown="${prev_unknown:-1}"
1517
+ fi
1518
+ case "$stmt" in '&' | '?') function_flow_unknown=1 ;; esac
1519
+ list_depth=$((list_depth + 1)); continue
1520
+ ;;
1521
+ H)
1522
+ list_depth=$((list_depth - 1))
1523
+ function_flow_unknown="${brace_flows[list_depth]}"
1524
+ follow_descriptor_map="${brace_descriptor_maps[list_depth]}"
1525
+ follow_stdin_cat="${brace_stdin_cats[list_depth]}"
1526
+ follow_stdin_cwd="${brace_stdin_cwds[list_depth]}"
1527
+ follow_stdin_unknown="${brace_stdin_unknowns[list_depth]}"
1528
+ # A conditional return may exit this function/source before commands
1529
+ # after the brace. Unlike a child return, that uncertainty persists.
1530
+ if [ "$return_flow_unknown" -ne 0 ]; then function_flow_unknown=1; fi
1531
+ continue
1532
+ ;;
1533
+ B)
1534
+ # A single & runs the whole AND/OR list asynchronously. Its cwd and
1535
+ # definitions cannot escape into the parent's next list.
1536
+ follow_cwd="${list_cwds[list_depth]}"
1537
+ follow_cwd_unknown="${list_unknowns[list_depth]}"
1538
+ function_flow_unknown="${list_flows[list_depth]}"
1539
+ return_flow_unknown="${list_return_flows[list_depth]}"
1540
+ return_list_depth=-1
1541
+ while [ "$function_count" -gt "${list_functions[list_depth]}" ]; do
1542
+ function_count=$((function_count - 1))
1543
+ function_index=$function_count
1544
+ unset 'function_names[function_index]' 'function_bodies[function_index]' 'function_uncertainties[function_index]'
1545
+ done
1546
+ continue
1547
+ ;;
1548
+ N)
1549
+ function_name="${stmt#*$'\t'}"; function_body=""
1550
+ function_conditional="$function_flow_unknown"
1551
+ case "${stmt%%$'\t'*}" in '&' | '?') function_conditional=1 ;; esac
1552
+ continue
1553
+ ;;
1554
+ D)
1555
+ if [ "$authenticated_bootstrap" -eq 0 ]; then
1556
+ function_body="$function_body$stmt"$'\n'
1557
+ fi
1558
+ continue
1559
+ ;;
1560
+ F)
1561
+ [ "$authenticated_bootstrap" -eq 0 ] || continue
1562
+ function_index=$function_count
1563
+ function_names[function_index]="$function_name"
1564
+ function_bodies[function_index]="$function_body"
1565
+ function_uncertainties[function_index]="$function_conditional"
1566
+ function_count=$((function_count + 1))
1567
+ continue
1568
+ ;;
1569
+ C)
1570
+ function_flow_unknown=1
1571
+ case_cwds[case_depth]="$follow_cwd"
1572
+ case_unknowns[case_depth]="$follow_cwd_unknown"
1573
+ case_changes[case_depth]=0
1574
+ case_depth=$((case_depth + 1))
1575
+ continue
1576
+ ;;
1577
+ A | K)
1578
+ function_index=$((case_depth - 1))
1579
+ if [ "$follow_cwd" != "${case_cwds[function_index]}" ] || [ "$follow_cwd_unknown" != "${case_unknowns[function_index]}" ]; then
1580
+ case_changes[function_index]=1
1581
+ fi
1582
+ follow_cwd="${case_cwds[function_index]}"
1583
+ follow_cwd_unknown="${case_unknowns[function_index]}"
1584
+ if [ "$sep" = K ]; then
1585
+ if [ "${case_changes[function_index]}" -ne 0 ]; then
1586
+ follow_cwd=""; follow_cwd_unknown=1
1587
+ fi
1588
+ case_depth=$((case_depth - 1))
1589
+ fi
1590
+ continue
1591
+ ;;
1592
+ '(' | '[')
1593
+ cwd_scopes[scope_depth]="$follow_cwd"
1594
+ unknown_scopes[scope_depth]="$follow_cwd_unknown"
1595
+ cat_scopes[scope_depth]="$prev_cat"
1596
+ cat_cwd_scopes[scope_depth]="${prev_cwd:-}"
1597
+ cat_unknown_scopes[scope_depth]="${prev_unknown:-1}"
1598
+ scope_descriptor_maps[scope_depth]="$follow_descriptor_map"
1599
+ function_scopes[scope_depth]=$function_count
1600
+ flow_scopes[scope_depth]=$function_flow_unknown
1601
+ return_flows[scope_depth]=$return_flow_unknown
1602
+ scope_stdin_cats[scope_depth]="$follow_stdin_cat"
1603
+ scope_stdin_cwds[scope_depth]="$follow_stdin_cwd"
1604
+ scope_stdin_unknowns[scope_depth]="$follow_stdin_unknown"
1605
+ if [ "$stmt" = "|" ] || [ "$stmt" = "|+" ]; then
1606
+ follow_stdin_cat="$prev_cat"; follow_stdin_cwd="${prev_cwd:-}"; follow_stdin_unknown="${prev_unknown:-1}"
1607
+ follow_descriptor_map="$(printf '%s' "$follow_descriptor_map" | jq -ce '.["0"] = {kind:"input"}')" || follow_refuse "pipeline input" "its descriptor could not be established"
1608
+ fi
1609
+ case "$stmt" in
1610
+ *+)
1611
+ follow_descriptor_map="$(printf '%s' "$follow_descriptor_map" | jq -ce '.["1"] = {kind:"pipe"}')" || follow_refuse "pipeline output" "its descriptor could not be established"
1612
+ ;;
1613
+ esac
1614
+ scope_depth=$((scope_depth + 1))
1615
+ list_depth=$((list_depth + 1))
1616
+ continue
1617
+ ;;
1618
+ ')' | ']')
1619
+ scope_depth=$((scope_depth - 1))
1620
+ list_depth=$((list_depth - 1))
1621
+ follow_cwd="${cwd_scopes[scope_depth]}"
1622
+ follow_descriptor_map="${scope_descriptor_maps[scope_depth]}"
1623
+ follow_cwd_unknown="${unknown_scopes[scope_depth]}"
1624
+ function_flow_unknown="${flow_scopes[scope_depth]}"
1625
+ return_flow_unknown="${return_flows[scope_depth]}"
1626
+ follow_stdin_cat="${scope_stdin_cats[scope_depth]}"
1627
+ follow_stdin_cwd="${scope_stdin_cwds[scope_depth]}"
1628
+ follow_stdin_unknown="${scope_stdin_unknowns[scope_depth]}"
1629
+ while [ "$function_count" -gt "${function_scopes[scope_depth]}" ]; do
1630
+ function_count=$((function_count - 1))
1631
+ function_index=$function_count
1632
+ unset 'function_names[function_index]' 'function_bodies[function_index]' 'function_uncertainties[function_index]'
1633
+ done
1634
+ if [ "$sep" = ']' ]; then
1635
+ prev_cat="${cat_scopes[scope_depth]}"
1636
+ prev_cwd="${cat_cwd_scopes[scope_depth]}"
1637
+ prev_unknown="${cat_unknown_scopes[scope_depth]}"
1638
+ fi
1639
+ if [ "$return_scope_depth" -ge 0 ] && [ "$scope_depth" -lt "$return_scope_depth" ]; then
1640
+ return_scope_depth=-1
1641
+ fi
1642
+ continue
1643
+ ;;
1644
+ I)
1645
+ follow_redirect_io "$stmt" compound
1646
+ follow_descriptor_map="$follow_redirect_map"
1647
+ case "$follow_redirect_input" in
1648
+ file)
1649
+ follow_stdin_cat="$follow_redirect_path"
1650
+ follow_stdin_cwd="$follow_redirect_input_cwd"; follow_stdin_unknown="$follow_redirect_input_unknown"
1651
+ ;;
1652
+ closed) follow_stdin_cat="" ;;
1653
+ input) ;;
1654
+ *) follow_refuse "compound redirection" "its input source could not be determined" ;;
1655
+ esac
1656
+ continue
1657
+ ;;
1658
+ R)
1659
+ # The whole compound tail was applied before its body by I. Its cat
1660
+ # producer already records whether its effective output reaches pipe.
1661
+ continue
1662
+ ;;
1663
+ esac
1664
+ join_cwd="$follow_cwd"; join_unknown="$follow_cwd_unknown"
1665
+ if [ "$sep" = '&' ] && [ "$and_ready" -eq 1 ]; then
1666
+ follow_cwd="$and_cwd"; follow_cwd_unknown="$and_unknown"
1667
+ fi
1668
+ and_ready=0
1669
+ pipe_cat="$follow_stdin_cat"
1670
+ pipe_cwd="$follow_stdin_cwd"
1671
+ pipe_unknown="$follow_stdin_unknown"
1672
+ if [ "$sep" = "|" ]; then
1673
+ pipe_cat="$prev_cat"
1674
+ pipe_cwd="${prev_cwd:-}"
1675
+ pipe_unknown="${prev_unknown:-1}"
1676
+ fi
1041
1677
  prev_cat=""
1042
1678
  case "$stmt" in
1043
1679
  *[![:space:]]*) ;;
@@ -1053,6 +1689,7 @@ follow_scan() {
1053
1689
  [ "$n" -gt 0 ] || continue
1054
1690
  i=0
1055
1691
  cmd_pos=1
1692
+ function_lookup_allowed=1
1056
1693
  eval_mode=0
1057
1694
  arg_exec=0
1058
1695
  pending_operand=0
@@ -1104,7 +1741,65 @@ follow_scan() {
1104
1741
  follow_unwrap "${toks[i]}"
1105
1742
  t="$follow_unwrapped"
1106
1743
  fi
1744
+ function_found=0
1745
+ for ((function_index = function_count - 1; function_lookup_allowed && function_index >= 0; function_index--)); do
1746
+ if [ "$t" = "${function_names[function_index]}" ]; then
1747
+ [ "${function_uncertainties[function_index]}" -eq 0 ] || follow_refuse "$t" "its function definition is conditional"
1748
+ # Calls execute in this shell and do not consume a file-follow depth.
1749
+ # A separate bound refuses recursion without truncating the body.
1750
+ [ "$function_call_depth" -lt "$FOLLOW_MAX_FUNCTION_DEPTH" ] || follow_refuse "$t" "function calls exceed the inspection depth"
1751
+ follow_function_calls=$((follow_function_calls + 1))
1752
+ follow_function_bytes=$((follow_function_bytes + ${#function_bodies[function_index]}))
1753
+ if [ "$follow_function_calls" -gt "$FOLLOW_MAX_FUNCTION_CALLS" ] || [ "$follow_function_bytes" -gt "$FOLLOW_MAX_TOTAL_BYTES" ]; then
1754
+ follow_refuse "$t" "function expansion exceeds the inspection budget"
1755
+ fi
1756
+ local call_cwd="$follow_cwd" call_unknown="$follow_cwd_unknown"
1757
+ follow_scan "${function_bodies[function_index]}" "$depth" "$((function_call_depth + 1))" 0 child "$((function_flow_unknown || statement_conditional))"
1758
+ if { [ "$function_flow_unknown" -ne 0 ] || [ "$statement_conditional" -ne 0 ]; } && { [ "$follow_cwd" != "$call_cwd" ] || [ "$follow_cwd_unknown" != "$call_unknown" ]; }; then
1759
+ follow_cwd=""; follow_cwd_unknown=1
1760
+ fi
1761
+ function_found=1
1762
+ break
1763
+ fi
1764
+ done
1765
+ if [ "$function_found" -eq 1 ]; then
1766
+ cmd_pos=0; i=$((i + 1)); continue
1767
+ fi
1107
1768
  case "$t" in
1769
+ return)
1770
+ if [ "$statement_conditional" -ne 0 ]; then function_flow_unknown=1; fi
1771
+ if { [ "$function_call_depth" -gt 0 ] || [ "$invocation_kind" = source ]; } && [ "$function_flow_unknown" -ne 0 ]; then
1772
+ return_flow_unknown=1
1773
+ fi
1774
+ if { [ "$function_call_depth" -gt 0 ] || [ "$invocation_kind" = source ]; } && [ "$function_flow_unknown" -eq 0 ]; then
1775
+ # Ordinary braces share the shell but have distinct list frames.
1776
+ # A surrounding background brace list ends only its child; do not
1777
+ # cross a literal subshell boundary while finding that parent list.
1778
+ for ((list_index = list_depth; list_index >= 0; list_index--)); do
1779
+ [ "${list_scope_depths[list_index]}" -eq "$scope_depth" ] || break
1780
+ if [ "${list_asyncs[list_index]}" -ne 0 ]; then
1781
+ return_list_depth="$list_index"
1782
+ break
1783
+ fi
1784
+ done
1785
+ if [ "$return_list_depth" -ge 0 ]; then
1786
+ :
1787
+ elif [ "$scope_depth" -eq 0 ]; then
1788
+ return 0
1789
+ else
1790
+ return_scope_depth="$scope_depth"
1791
+ fi
1792
+ fi
1793
+ cmd_pos=0; i=$((i + 1)); continue
1794
+ ;;
1795
+ if | while | until)
1796
+ function_flow_unknown=1
1797
+ i=$((i + 1)); continue
1798
+ ;;
1799
+ for | select)
1800
+ function_flow_unknown=1
1801
+ cmd_pos=0; i=$((i + 1)); continue
1802
+ ;;
1108
1803
  # Prefixes that keep the command position open: assignments, wrappers,
1109
1804
  # and the shell keywords a statement can start with. Without them
1110
1805
  # `if …; then bash <file>` and `env bash <file>` walk past the
@@ -1139,12 +1834,17 @@ follow_scan() {
1139
1834
  # which keeps working when options sit in between — `timeout -s KILL
1140
1835
  # 5 bash <file>` reaches `bash` just as `timeout 5 bash <file>` does.
1141
1836
  pending_operand=1
1837
+ function_lookup_allowed=0
1142
1838
  i=$((i + 1))
1143
1839
  continue
1144
1840
  ;;
1145
1841
  sudo | env | command | exec | nohup | nice | time | builtin \
1146
- | do | then | else | while | until | if | '!' | '{' \
1842
+ | do | then | else | '!' | '{' \
1147
1843
  | */sudo | */env | */nohup | */nice | */time)
1844
+ case "$t" in
1845
+ sudo | env | command | exec | nohup | nice | builtin | */sudo | */env | */nohup | */nice) function_lookup_allowed=0 ;;
1846
+ do | then | else) function_flow_unknown=1 ;;
1847
+ esac
1148
1848
  i=$((i + 1))
1149
1849
  continue
1150
1850
  ;;
@@ -1182,6 +1882,9 @@ follow_scan() {
1182
1882
  # place would resolve later tokens from a directory the shell left.
1183
1883
  follow_cd "${HOME:-}"
1184
1884
  fi
1885
+ if [ "$function_flow_unknown" -ne 0 ]; then
1886
+ follow_cwd=""; follow_cwd_unknown=1
1887
+ fi
1185
1888
  cmd_pos=0
1186
1889
  i=$((i + 1))
1187
1890
  continue
@@ -1191,7 +1894,11 @@ follow_scan() {
1191
1894
  j="$follow_operand_index"
1192
1895
  if [ "$j" -lt "$n" ]; then
1193
1896
  follow_unwrap "${toks[j]}"
1194
- follow_target "$follow_unwrapped" "$depth"
1897
+ local source_cwd="$follow_cwd" source_unknown="$follow_cwd_unknown"
1898
+ follow_source_target "$follow_unwrapped" "$depth" "$((function_flow_unknown || statement_conditional))"
1899
+ if { [ "$function_flow_unknown" -ne 0 ] || [ "$statement_conditional" -ne 0 ]; } && { [ "$follow_cwd" != "$source_cwd" ] || [ "$follow_cwd_unknown" != "$source_unknown" ]; }; then
1900
+ follow_cwd=""; follow_cwd_unknown=1
1901
+ fi
1195
1902
  fi
1196
1903
  cmd_pos=0
1197
1904
  i=$((j + 1))
@@ -1200,14 +1907,32 @@ follow_scan() {
1200
1907
  cat | '<')
1201
1908
  follow_next_operand "$i" "$n"
1202
1909
  j="$follow_operand_index"
1203
- if [ "$j" -lt "$n" ]; then
1204
- follow_unwrap "${toks[j]}"
1910
+ follow_unwrapped=""
1911
+ if [ "$j" -lt "$n" ]; then follow_unwrap "${toks[j]}"; fi
1912
+ if [ "$t" = cat ] && { [ "$j" -ge "$n" ] || [ "$follow_unwrapped" = - ]; }; then
1913
+ # Bare cat and cat - forward the group's named stdin unchanged.
1914
+ prev_cat="$pipe_cat"; prev_cwd="$pipe_cwd"; prev_unknown="$pipe_unknown"
1915
+ if [ "$eval_mode" -eq 1 ] && [ -n "$prev_cat" ]; then
1916
+ follow_pipe_target "$prev_cat" "$depth" "$prev_cwd" "$prev_unknown"
1917
+ fi
1918
+ elif [ "$j" -lt "$n" ]; then
1205
1919
  if [ "$eval_mode" -eq 1 ]; then
1206
1920
  follow_target "$follow_unwrapped" "$depth"
1207
1921
  else
1208
- # Not executed here — but `cat <file> | bash` is, and the
1209
- # interpreter arm below reads this when the pipe hands it over.
1922
+ # Not executed here — the pipe consumer decides whether the
1923
+ # producer's file content becomes an executable shell input.
1210
1924
  prev_cat="$follow_unwrapped"
1925
+ prev_cwd="$follow_cwd"; prev_unknown="$follow_cwd_unknown"
1926
+ fi
1927
+ fi
1928
+ if [ -n "$prev_cat" ] && [ "$eval_mode" -eq 0 ]; then
1929
+ if [[ "$stmt" == *'>'* ]] || [ "$follow_descriptor_map" != '{"0":{"kind":"input"},"1":{"kind":"pipe"},"2":{"kind":"stderr"}}' ]; then
1930
+ follow_redirect_io "$stmt" cat
1931
+ case "$follow_redirect_output" in
1932
+ pipe) ;;
1933
+ file | closed | stderr | input) prev_cat="" ;;
1934
+ *) follow_refuse "cat redirection" "its output destination could not be determined" ;;
1935
+ esac
1211
1936
  fi
1212
1937
  fi
1213
1938
  cmd_pos=0
@@ -1292,18 +2017,17 @@ follow_scan() {
1292
2017
  continue
1293
2018
  fi
1294
2019
  if [ "$inline" -eq 1 ]; then
1295
- # `-c` takes a COMMAND STRING, not a script path. That string is text
1296
- # the guards already read, and it can itself invoke a script, so the
1297
- # walk re-enters it at a fresh command position rather than reading it
1298
- # as a filename — this is what catches `bash -c 'bash <file>'`.
1299
- cmd_pos=1
1300
- i="$j"
2020
+ # The command string runs in a child shell. It gets its own function
2021
+ # map and cwd effects; positional arguments are data, not commands.
2022
+ follow_inline "$stmt" "$j" "$depth"
2023
+ cmd_pos=0
2024
+ i="$n"
1301
2025
  continue
1302
2026
  fi
1303
2027
  if [ "$stdin_mode" -eq 1 ]; then
1304
2028
  # `sh -s -- -y` reads its program from stdin; tokens after `--` are
1305
2029
  # positional arguments to that program, never a script path.
1306
- [ -z "$pipe_cat" ] || follow_target "$pipe_cat" "$depth"
2030
+ [ -z "$pipe_cat" ] || follow_pipe_target "$pipe_cat" "$depth" "$pipe_cwd" "$pipe_unknown"
1307
2031
  cmd_pos=0
1308
2032
  i="$n"
1309
2033
  continue
@@ -1317,7 +2041,7 @@ follow_scan() {
1317
2041
  fi
1318
2042
  # No operand: an interactive or stdin-fed interpreter. Only the
1319
2043
  # `cat <file> | bash` shape names a FILE, and only that is followed.
1320
- [ -z "$pipe_cat" ] || follow_target "$pipe_cat" "$depth"
2044
+ [ -z "$pipe_cat" ] || follow_pipe_target "$pipe_cat" "$depth" "$pipe_cwd" "$pipe_unknown"
1321
2045
  cmd_pos=0
1322
2046
  i="$j"
1323
2047
  continue
@@ -1351,6 +2075,14 @@ follow_scan() {
1351
2075
  cmd_pos=0
1352
2076
  i=$((i + 1))
1353
2077
  done
2078
+ # Keep successful-branch provenance separate from the continuation join.
2079
+ # OR can skip its RHS and still reach a following &&, so it cannot prove
2080
+ # that RHS's cwd. A semicolon observes every possible preceding outcome.
2081
+ and_cwd="$follow_cwd"; and_unknown="$follow_cwd_unknown"
2082
+ if [ "$sep" != '?' ] && [ "$function_flow_unknown" -eq 0 ]; then and_ready=1; fi
2083
+ if [ "$statement_conditional" -ne 0 ] && { [ "$join_unknown" -ne 0 ] || [ "$follow_cwd_unknown" -ne 0 ] || [ "$follow_cwd" != "$join_cwd" ]; }; then
2084
+ follow_cwd=""; follow_cwd_unknown=1
2085
+ fi
1354
2086
  done <<<"$split_out"
1355
2087
  }
1356
2088