agent-toolkit-cli 1.11.0__py3-none-win_amd64.whl

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 (438) hide show
  1. agent_toolkit/__init__.py +4 -0
  2. agent_toolkit/__main__.py +5 -0
  3. agent_toolkit/_paths.py +145 -0
  4. agent_toolkit/bin/.gitkeep +0 -0
  5. agent_toolkit/bin/agent-toolkit.exe +0 -0
  6. agent_toolkit/cli/__init__.py +0 -0
  7. agent_toolkit/cli/__main__.py +5 -0
  8. agent_toolkit/cli/build.py +278 -0
  9. agent_toolkit/cli/completion.py +297 -0
  10. agent_toolkit/cli/context_budget.py +455 -0
  11. agent_toolkit/cli/devcompanion.py +923 -0
  12. agent_toolkit/cli/devcompanion_queue.py +235 -0
  13. agent_toolkit/cli/diff.py +176 -0
  14. agent_toolkit/cli/doctor.py +1052 -0
  15. agent_toolkit/cli/insights.py +771 -0
  16. agent_toolkit/cli/install.py +706 -0
  17. agent_toolkit/cli/main.py +171 -0
  18. agent_toolkit/cli/mcp.py +537 -0
  19. agent_toolkit/cli/memory.py +739 -0
  20. agent_toolkit/cli/plugin.py +224 -0
  21. agent_toolkit/cli/project.py +459 -0
  22. agent_toolkit/cli/release.py +178 -0
  23. agent_toolkit/cli/skills.py +461 -0
  24. agent_toolkit/cli/uninstall.py +166 -0
  25. agent_toolkit/cli/update.py +298 -0
  26. agent_toolkit/cli/workspace.py +1713 -0
  27. agent_toolkit/compiler/hook_registry.py +110 -0
  28. agent_toolkit/compiler/loader.py +338 -0
  29. agent_toolkit/compiler/mcp_registry.py +87 -0
  30. agent_toolkit/compiler/model.py +182 -0
  31. agent_toolkit/compiler/provenance.py +140 -0
  32. agent_toolkit/compiler/registry_emit.py +263 -0
  33. agent_toolkit/compiler/target_registry.py +122 -0
  34. agent_toolkit/compiler/targets/__init__.py +1 -0
  35. agent_toolkit/compiler/targets/agent_plugins.py +160 -0
  36. agent_toolkit/compiler/targets/base.py +229 -0
  37. agent_toolkit/compiler/targets/claude_code.py +241 -0
  38. agent_toolkit/compiler/targets/codex.py +261 -0
  39. agent_toolkit/compiler/targets/copilot.py +275 -0
  40. agent_toolkit/compiler/targets/cursor.py +207 -0
  41. agent_toolkit/compiler/targets/gemini_cli.py +213 -0
  42. agent_toolkit/compiler/targets/muse_code.py +113 -0
  43. agent_toolkit/compiler/targets/opencode.py +204 -0
  44. agent_toolkit/compiler/targets/pi.py +263 -0
  45. agent_toolkit/compiler/targets/windsurf.py +267 -0
  46. agent_toolkit/compiler/tool_mapping.py +83 -0
  47. agent_toolkit/data/.gitignore +4 -0
  48. agent_toolkit/data/__init__.py +1 -0
  49. agent_toolkit/data/agents/agentic-security-reviewer/AGENT.md +60 -0
  50. agent_toolkit/data/agents/architect/AGENT.md +30 -0
  51. agent_toolkit/data/agents/assistant/AGENT.md +59 -0
  52. agent_toolkit/data/agents/build-error-resolver/AGENT.md +30 -0
  53. agent_toolkit/data/agents/client-workflow-bootstrap/AGENT.md +60 -0
  54. agent_toolkit/data/agents/code-reviewer/AGENT.md +49 -0
  55. agent_toolkit/data/agents/database-reviewer/AGENT.md +44 -0
  56. agent_toolkit/data/agents/docs-lookup/AGENT.md +28 -0
  57. agent_toolkit/data/agents/e2e-runner/AGENT.md +38 -0
  58. agent_toolkit/data/agents/performance-optimizer/AGENT.md +52 -0
  59. agent_toolkit/data/agents/planner/AGENT.md +41 -0
  60. agent_toolkit/data/agents/refactor-cleaner/AGENT.md +43 -0
  61. agent_toolkit/data/agents/reference-lookup/AGENT.md +33 -0
  62. agent_toolkit/data/agents/security-reviewer/AGENT.md +47 -0
  63. agent_toolkit/data/agents/tdd-guide/AGENT.md +58 -0
  64. agent_toolkit/data/agents/tech-assistant/AGENT.md +11 -0
  65. agent_toolkit/data/agents/typescript-reviewer/AGENT.md +61 -0
  66. agent_toolkit/data/capabilities/hooks/README.md +6 -0
  67. agent_toolkit/data/capabilities/hooks/pre-commit-validate.yaml +26 -0
  68. agent_toolkit/data/capabilities/hooks/scripts/pre-commit-validate.sh +41 -0
  69. agent_toolkit/data/capabilities/hooks/scripts/session-start-context.sh +11 -0
  70. agent_toolkit/data/capabilities/hooks/session-start-context.yaml +23 -0
  71. agent_toolkit/data/capabilities/targets/registry.yaml +90 -0
  72. agent_toolkit/data/capabilities/upstream.lock +118 -0
  73. agent_toolkit/data/catalogs/agent-catalog.yaml +81 -0
  74. agent_toolkit/data/catalogs/loop-catalog.yaml +55 -0
  75. agent_toolkit/data/catalogs/skill-catalog.yaml +520 -0
  76. agent_toolkit/data/catalogs/skills-layout.json +391 -0
  77. agent_toolkit/data/distributions/products.yaml +190 -0
  78. agent_toolkit/data/distributions/targets/claude-code.yaml +47 -0
  79. agent_toolkit/data/distributions/targets/cursor.yaml +42 -0
  80. agent_toolkit/data/distributions/targets/opencode.yaml +55 -0
  81. agent_toolkit/data/loops/changelog-drafter/loop.yaml +40 -0
  82. agent_toolkit/data/loops/ci-sweeper/loop.yaml +53 -0
  83. agent_toolkit/data/loops/daily-triage/loop.yaml +47 -0
  84. agent_toolkit/data/loops/dep-sweeper/loop.yaml +46 -0
  85. agent_toolkit/data/loops/issue-triage/loop.yaml +40 -0
  86. agent_toolkit/data/loops/oss-daily-briefing/loop.yaml +65 -0
  87. agent_toolkit/data/loops/oss-pr-monitor/loop.yaml +86 -0
  88. agent_toolkit/data/loops/oss-triage/loop.yaml +67 -0
  89. agent_toolkit/data/loops/post-merge-cleanup/loop.yaml +44 -0
  90. agent_toolkit/data/loops/pr-babysitter/loop.yaml +46 -0
  91. agent_toolkit/data/mcp/registry/chrome-devtools.yaml +61 -0
  92. agent_toolkit/data/mcp/registry/clickup.yaml +48 -0
  93. agent_toolkit/data/mcp/registry/figma.yaml +44 -0
  94. agent_toolkit/data/mcp/registry/github.yaml +52 -0
  95. agent_toolkit/data/mcp/registry/linear.yaml +47 -0
  96. agent_toolkit/data/mcp/registry/notion.yaml +48 -0
  97. agent_toolkit/data/mcp/registry/slack.yaml +46 -0
  98. agent_toolkit/data/mcp/templates/chrome-devtools/README.md +57 -0
  99. agent_toolkit/data/mcp/templates/chrome-devtools/config.template.json +6 -0
  100. agent_toolkit/data/mcp/templates/clickup/README.md +45 -0
  101. agent_toolkit/data/mcp/templates/clickup/config.template.json +8 -0
  102. agent_toolkit/data/mcp/templates/clickup/wrapper.sh +23 -0
  103. agent_toolkit/data/mcp/templates/figma/README.md +54 -0
  104. agent_toolkit/data/mcp/templates/figma/config.template.json +9 -0
  105. agent_toolkit/data/mcp/templates/github/README.md +16 -0
  106. agent_toolkit/data/mcp/templates/github/config.template.json +15 -0
  107. agent_toolkit/data/mcp/templates/github/wrapper.sh +6 -0
  108. agent_toolkit/data/mcp/templates/linear/README.md +49 -0
  109. agent_toolkit/data/mcp/templates/linear/config.template.json +10 -0
  110. agent_toolkit/data/mcp/templates/notion/README.md +7 -0
  111. agent_toolkit/data/mcp/templates/notion/config.template.json +8 -0
  112. agent_toolkit/data/mcp/templates/notion/wrapper.sh +23 -0
  113. agent_toolkit/data/mcp/templates/slack/README.md +8 -0
  114. agent_toolkit/data/mcp/templates/slack/config.template.json +9 -0
  115. agent_toolkit/data/mcp/templates/slack/wrapper.sh +23 -0
  116. agent_toolkit/data/packs/README.md +66 -0
  117. agent_toolkit/data/packs/agentic-security/README.md +19 -0
  118. agent_toolkit/data/packs/agentic-security/config.yaml +25 -0
  119. agent_toolkit/data/packs/architecture/README.md +22 -0
  120. agent_toolkit/data/packs/architecture/config.yaml +27 -0
  121. agent_toolkit/data/packs/code-quality/README.md +19 -0
  122. agent_toolkit/data/packs/code-quality/config.yaml +21 -0
  123. agent_toolkit/data/packs/delivery-discipline/README.md +19 -0
  124. agent_toolkit/data/packs/delivery-discipline/config.yaml +42 -0
  125. agent_toolkit/data/packs/design-engineering/README.md +19 -0
  126. agent_toolkit/data/packs/design-engineering/config.yaml +41 -0
  127. agent_toolkit/data/packs/engineering-workflow/README.md +21 -0
  128. agent_toolkit/data/packs/engineering-workflow/config.yaml +57 -0
  129. agent_toolkit/data/packs/oss-maintenance/README.md +245 -0
  130. agent_toolkit/data/packs/oss-maintenance/config.yaml +75 -0
  131. agent_toolkit/data/profiles/claude-code/CLAUDE.md +110 -0
  132. agent_toolkit/data/profiles/claude-code/agents/assistant.md +59 -0
  133. agent_toolkit/data/profiles/claude-code/agents/build-error-resolver.md +30 -0
  134. agent_toolkit/data/profiles/claude-code/agents/client-workflow-bootstrap.md +60 -0
  135. agent_toolkit/data/profiles/claude-code/agents/contribution-planner.md +39 -0
  136. agent_toolkit/data/profiles/claude-code/agents/database-reviewer.md +44 -0
  137. agent_toolkit/data/profiles/claude-code/agents/docs-lookup.md +28 -0
  138. agent_toolkit/data/profiles/claude-code/agents/e2e-runner.md +38 -0
  139. agent_toolkit/data/profiles/claude-code/agents/performance-optimizer.md +52 -0
  140. agent_toolkit/data/profiles/claude-code/agents/planner.md +41 -0
  141. agent_toolkit/data/profiles/claude-code/agents/refactor-cleaner.md +43 -0
  142. agent_toolkit/data/profiles/claude-code/agents/reference-lookup.md +33 -0
  143. agent_toolkit/data/profiles/claude-code/agents/security-reviewer.md +47 -0
  144. agent_toolkit/data/profiles/claude-code/agents/tdd-guide.md +58 -0
  145. agent_toolkit/data/profiles/claude-code/agents/typescript-reviewer.md +61 -0
  146. agent_toolkit/data/profiles/claude-code/settings.json +86 -0
  147. agent_toolkit/data/profiles/copilot/copilot-instructions.md +15 -0
  148. agent_toolkit/data/profiles/cursor/README.md +33 -0
  149. agent_toolkit/data/profiles/cursor/rules/architect.mdc +23 -0
  150. agent_toolkit/data/profiles/cursor/rules/assistant.mdc +61 -0
  151. agent_toolkit/data/profiles/cursor/rules/build-error-resolver.mdc +20 -0
  152. agent_toolkit/data/profiles/cursor/rules/code-reviewer.mdc +21 -0
  153. agent_toolkit/data/profiles/cursor/rules/database-reviewer.mdc +29 -0
  154. agent_toolkit/data/profiles/cursor/rules/docs-lookup.mdc +15 -0
  155. agent_toolkit/data/profiles/cursor/rules/e2e-runner.mdc +30 -0
  156. agent_toolkit/data/profiles/cursor/rules/performance-optimizer.mdc +27 -0
  157. agent_toolkit/data/profiles/cursor/rules/planner.mdc +28 -0
  158. agent_toolkit/data/profiles/cursor/rules/refactor-cleaner.mdc +26 -0
  159. agent_toolkit/data/profiles/cursor/rules/reference-lookup.mdc +20 -0
  160. agent_toolkit/data/profiles/cursor/rules/security-reviewer.mdc +20 -0
  161. agent_toolkit/data/profiles/cursor/rules/tdd-guide.mdc +26 -0
  162. agent_toolkit/data/profiles/cursor/rules/typescript-reviewer.mdc +28 -0
  163. agent_toolkit/data/profiles/muse-code/README.md +35 -0
  164. agent_toolkit/data/profiles/opencode/README.md +44 -0
  165. agent_toolkit/data/profiles/opencode/agents/architect.md +30 -0
  166. agent_toolkit/data/profiles/opencode/agents/assistant.md +96 -0
  167. agent_toolkit/data/profiles/opencode/agents/build-error-resolver.md +31 -0
  168. agent_toolkit/data/profiles/opencode/agents/client-workflow-bootstrap.md +66 -0
  169. agent_toolkit/data/profiles/opencode/agents/code-reviewer.md +47 -0
  170. agent_toolkit/data/profiles/opencode/agents/contribution-planner.md +44 -0
  171. agent_toolkit/data/profiles/opencode/agents/database-reviewer.md +43 -0
  172. agent_toolkit/data/profiles/opencode/agents/docs-lookup.md +28 -0
  173. agent_toolkit/data/profiles/opencode/agents/e2e-runner.md +33 -0
  174. agent_toolkit/data/profiles/opencode/agents/performance-optimizer.md +34 -0
  175. agent_toolkit/data/profiles/opencode/agents/planner.md +27 -0
  176. agent_toolkit/data/profiles/opencode/agents/refactor-cleaner.md +27 -0
  177. agent_toolkit/data/profiles/opencode/agents/reference-lookup.md +27 -0
  178. agent_toolkit/data/profiles/opencode/agents/security-reviewer.md +27 -0
  179. agent_toolkit/data/profiles/opencode/agents/tdd-guide.md +27 -0
  180. agent_toolkit/data/profiles/opencode/agents/typescript-reviewer.md +30 -0
  181. agent_toolkit/data/profiles/opencode/opencode.json +3 -0
  182. agent_toolkit/data/profiles/pi/skills/architect/skill.md +5 -0
  183. agent_toolkit/data/profiles/pi/skills/assistant/skill.md +3 -0
  184. agent_toolkit/data/profiles/pi/skills/code-reviewer/skill.md +5 -0
  185. agent_toolkit/data/profiles/pi/skills/planner/skill.md +3 -0
  186. agent_toolkit/data/profiles/pi/skills/security-reviewer/skill.md +3 -0
  187. agent_toolkit/data/profiles/windsurf/memories/global_rules.md +114 -0
  188. agent_toolkit/data/profiles/windsurf/rules/architect.mdc +23 -0
  189. agent_toolkit/data/profiles/windsurf/rules/assistant.mdc +61 -0
  190. agent_toolkit/data/profiles/windsurf/rules/build-error-resolver.mdc +20 -0
  191. agent_toolkit/data/profiles/windsurf/rules/code-reviewer.mdc +21 -0
  192. agent_toolkit/data/profiles/windsurf/rules/database-reviewer.mdc +29 -0
  193. agent_toolkit/data/profiles/windsurf/rules/docs-lookup.mdc +15 -0
  194. agent_toolkit/data/profiles/windsurf/rules/e2e-runner.mdc +30 -0
  195. agent_toolkit/data/profiles/windsurf/rules/performance-optimizer.mdc +27 -0
  196. agent_toolkit/data/profiles/windsurf/rules/planner.mdc +28 -0
  197. agent_toolkit/data/profiles/windsurf/rules/refactor-cleaner.mdc +26 -0
  198. agent_toolkit/data/profiles/windsurf/rules/reference-lookup.mdc +20 -0
  199. agent_toolkit/data/profiles/windsurf/rules/security-reviewer.mdc +20 -0
  200. agent_toolkit/data/profiles/windsurf/rules/tdd-guide.mdc +26 -0
  201. agent_toolkit/data/profiles/windsurf/rules/typescript-reviewer.mdc +28 -0
  202. agent_toolkit/data/skills/accessibility/review/SKILL.md +116 -0
  203. agent_toolkit/data/skills/accessibility/review/references/a11y-checklist.md +25 -0
  204. agent_toolkit/data/skills/accessibility/review/references/a11y-findings-template.md +56 -0
  205. agent_toolkit/data/skills/accessibility/review/references/research.md +47 -0
  206. agent_toolkit/data/skills/agentic-security/mcp-audit/SKILL.md +116 -0
  207. agent_toolkit/data/skills/agentic-security/mcp-audit/references/mcp-audit-template.md +51 -0
  208. agent_toolkit/data/skills/agentic-security/owasp-agentic-review/SKILL.md +73 -0
  209. agent_toolkit/data/skills/agentic-security/owasp-agentic-review/references/owasp-agentic-template.md +32 -0
  210. agent_toolkit/data/skills/agentic-security/supply-chain-audit/SKILL.md +142 -0
  211. agent_toolkit/data/skills/agentic-security/threat-modeling/SKILL.md +100 -0
  212. agent_toolkit/data/skills/agentic-security/threat-modeling/references/threat-model-template.md +55 -0
  213. agent_toolkit/data/skills/architecture/c4-model/SKILL.md +49 -0
  214. agent_toolkit/data/skills/cloud/aws-well-architected-review/SKILL.md +37 -0
  215. agent_toolkit/data/skills/cloud/cloud-design-patterns/SKILL.md +38 -0
  216. agent_toolkit/data/skills/core/assistant/SKILL.md +216 -0
  217. agent_toolkit/data/skills/core/assistant/references/AGENTS_TEMPLATE.md +0 -0
  218. agent_toolkit/data/skills/core/assistant/references/ORCHESTRATION.md +0 -0
  219. agent_toolkit/data/skills/core/assistant/references/REPO_INSPECTION.md +0 -0
  220. agent_toolkit/data/skills/core/dev-companion/SKILL.md +65 -0
  221. agent_toolkit/data/skills/core/dev-companion/references/LOOP_GUARDRAILS.md +0 -0
  222. agent_toolkit/data/skills/core/onboarding/SKILL.md +122 -0
  223. agent_toolkit/data/skills/core/output-handshake/SKILL.md +26 -0
  224. agent_toolkit/data/skills/core/pr-fallback/SKILL.md +35 -0
  225. agent_toolkit/data/skills/core/pr-fallback/references/pr-body-default.md +0 -0
  226. agent_toolkit/data/skills/core/project/SKILL.md +89 -0
  227. agent_toolkit/data/skills/core/workspace/SKILL.md +103 -0
  228. agent_toolkit/data/skills/core/workspace-knowledge-sync/SKILL.md +140 -0
  229. agent_toolkit/data/skills/data/dbt-validation/SKILL.md +41 -0
  230. agent_toolkit/data/skills/data/snowflake-validation/SKILL.md +38 -0
  231. agent_toolkit/data/skills/delivery/adr/SKILL.md +56 -0
  232. agent_toolkit/data/skills/delivery/adr/references/default-template.md +35 -0
  233. agent_toolkit/data/skills/delivery/adr/references/example-001-graphql-adoption.md +66 -0
  234. agent_toolkit/data/skills/delivery/agreement/SKILL.md +24 -0
  235. agent_toolkit/data/skills/delivery/agreement/references/default-template.md +17 -0
  236. agent_toolkit/data/skills/delivery/agreement/references/example-api-versioning.md +29 -0
  237. agent_toolkit/data/skills/delivery/bug/SKILL.md +26 -0
  238. agent_toolkit/data/skills/delivery/bug/references/default-template.md +26 -0
  239. agent_toolkit/data/skills/delivery/bug/references/example-search-filter-bug.md +58 -0
  240. agent_toolkit/data/skills/delivery/decision-log/SKILL.md +28 -0
  241. agent_toolkit/data/skills/delivery/decision-log/references/default-template.md +17 -0
  242. agent_toolkit/data/skills/delivery/decision-log/references/example-repository-pattern.md +27 -0
  243. agent_toolkit/data/skills/delivery/development-workflow/SKILL.md +34 -0
  244. agent_toolkit/data/skills/delivery/development-workflow/references/default-workflow-checklist.md +44 -0
  245. agent_toolkit/data/skills/delivery/epic/SKILL.md +22 -0
  246. agent_toolkit/data/skills/delivery/epic/references/default-template.md +26 -0
  247. agent_toolkit/data/skills/delivery/epic/references/example-unified-dashboard.md +40 -0
  248. agent_toolkit/data/skills/delivery/incident/SKILL.md +24 -0
  249. agent_toolkit/data/skills/delivery/incident/references/default-template.md +29 -0
  250. agent_toolkit/data/skills/delivery/incident/references/example-payment-outage.md +62 -0
  251. agent_toolkit/data/skills/delivery/management-unit-assessment/SKILL.md +104 -0
  252. agent_toolkit/data/skills/delivery/management-unit-assessment/references/default-template.md +24 -0
  253. agent_toolkit/data/skills/delivery/management-unit-assessment/references/example-team-assessment.md +126 -0
  254. agent_toolkit/data/skills/delivery/meeting-minutes/SKILL.md +25 -0
  255. agent_toolkit/data/skills/delivery/meeting-minutes/references/default-template.md +22 -0
  256. agent_toolkit/data/skills/delivery/meeting-minutes/references/example-weekly-sync.md +72 -0
  257. agent_toolkit/data/skills/delivery/planning/SKILL.md +31 -0
  258. agent_toolkit/data/skills/delivery/planning/references/default-template.md +25 -0
  259. agent_toolkit/data/skills/delivery/planning/references/example-sprint-planning.md +62 -0
  260. agent_toolkit/data/skills/delivery/prd/SKILL.md +46 -0
  261. agent_toolkit/data/skills/delivery/prd/references/default-template.md +31 -0
  262. agent_toolkit/data/skills/delivery/prd/references/example-unified-api.md +71 -0
  263. agent_toolkit/data/skills/delivery/project-assessment/SKILL.md +90 -0
  264. agent_toolkit/data/skills/delivery/project-assessment/references/default-template.md +26 -0
  265. agent_toolkit/data/skills/delivery/project-assessment/references/example-platform-assessment.md +122 -0
  266. agent_toolkit/data/skills/delivery/project-assessment-evidence/SKILL.md +88 -0
  267. agent_toolkit/data/skills/delivery/project-assessment-evidence/references/default-template.md +24 -0
  268. agent_toolkit/data/skills/delivery/project-assessment-evidence/references/example-evidence-map.md +46 -0
  269. agent_toolkit/data/skills/delivery/spike/SKILL.md +24 -0
  270. agent_toolkit/data/skills/delivery/spike/references/default-template.md +29 -0
  271. agent_toolkit/data/skills/delivery/spike/references/example-graphql-federation.md +79 -0
  272. agent_toolkit/data/skills/delivery/task/SKILL.md +21 -0
  273. agent_toolkit/data/skills/delivery/task/references/default-template.md +21 -0
  274. agent_toolkit/data/skills/delivery/task/references/example-nav-system.md +33 -0
  275. agent_toolkit/data/skills/delivery/technical-unit-assessment/SKILL.md +58 -0
  276. agent_toolkit/data/skills/delivery/technical-unit-assessment/references/default-template.md +26 -0
  277. agent_toolkit/data/skills/delivery/technical-unit-assessment/references/example-frontend-assessment.md +112 -0
  278. agent_toolkit/data/skills/delivery/technical-unit-assessment/references/indicator-groups.md +65 -0
  279. agent_toolkit/data/skills/delivery/trd/SKILL.md +49 -0
  280. agent_toolkit/data/skills/delivery/trd/references/default-template.md +31 -0
  281. agent_toolkit/data/skills/delivery/trd/references/example-api-migration.md +122 -0
  282. agent_toolkit/data/skills/delivery/user-story/SKILL.md +21 -0
  283. agent_toolkit/data/skills/delivery/user-story/references/default-template.md +21 -0
  284. agent_toolkit/data/skills/delivery/user-story/references/example-advanced-filtering.md +37 -0
  285. agent_toolkit/data/skills/delivery/work-item/SKILL.md +31 -0
  286. agent_toolkit/data/skills/delivery/work-item/references/default-template.md +21 -0
  287. agent_toolkit/data/skills/delivery/work-item/references/example-routing-dashboard.md +23 -0
  288. agent_toolkit/data/skills/delivery/workflow-client-bootstrap/SKILL.md +148 -0
  289. agent_toolkit/data/skills/delivery/workflow-client-bootstrap/questions.yaml +165 -0
  290. agent_toolkit/data/skills/delivery/workflow-generic-project/SKILL.md +79 -0
  291. agent_toolkit/data/skills/design/design-assessment/SKILL.md +150 -0
  292. agent_toolkit/data/skills/design/design-assessment/references/design-scorecard-template.md +78 -0
  293. agent_toolkit/data/skills/design/design-improvement/SKILL.md +167 -0
  294. agent_toolkit/data/skills/design/design-improvement/references/improvement-plan-template.md +113 -0
  295. agent_toolkit/data/skills/design/figma/LICENSE.txt +2 -0
  296. agent_toolkit/data/skills/design/figma/NOTICE.txt +25 -0
  297. agent_toolkit/data/skills/design/figma/SKILL.md +98 -0
  298. agent_toolkit/data/skills/design/figma/references/figma-mcp-config.md +82 -0
  299. agent_toolkit/data/skills/design/figma/references/figma-tools-and-prompts.md +34 -0
  300. agent_toolkit/data/skills/design/figma-code-connect-components/LICENSE.txt +2 -0
  301. agent_toolkit/data/skills/design/figma-code-connect-components/NOTICE.txt +18 -0
  302. agent_toolkit/data/skills/design/figma-code-connect-components/SKILL.md +354 -0
  303. agent_toolkit/data/skills/design/figma-code-connect-components/references/mapping-checklist.md +7 -0
  304. agent_toolkit/data/skills/design/figma-code-connect-components/scripts/normalize_node_id.py +25 -0
  305. agent_toolkit/data/skills/design/figma-create-design-system-rules/LICENSE.txt +2 -0
  306. agent_toolkit/data/skills/design/figma-create-design-system-rules/NOTICE.txt +19 -0
  307. agent_toolkit/data/skills/design/figma-create-design-system-rules/SKILL.md +554 -0
  308. agent_toolkit/data/skills/design/figma-create-design-system-rules/references/rule-template.md +15 -0
  309. agent_toolkit/data/skills/design/figma-create-design-system-rules/scripts/check_agents_md.sh +9 -0
  310. agent_toolkit/data/skills/design/figma-create-new-file/LICENSE.txt +2 -0
  311. agent_toolkit/data/skills/design/figma-create-new-file/NOTICE.txt +17 -0
  312. agent_toolkit/data/skills/design/figma-create-new-file/SKILL.md +72 -0
  313. agent_toolkit/data/skills/design/figma-implement-design/LICENSE.txt +2 -0
  314. agent_toolkit/data/skills/design/figma-implement-design/NOTICE.txt +19 -0
  315. agent_toolkit/data/skills/design/figma-implement-design/SKILL.md +268 -0
  316. agent_toolkit/data/skills/design/frontend-design/LICENSE.txt +177 -0
  317. agent_toolkit/data/skills/design/frontend-design/SKILL.md +82 -0
  318. agent_toolkit/data/skills/design/frontend-design/UPSTREAM.md +14 -0
  319. agent_toolkit/data/skills/design/frontend-design-review/LICENSE +21 -0
  320. agent_toolkit/data/skills/design/frontend-design-review/SKILL.md +194 -0
  321. agent_toolkit/data/skills/design/frontend-design-review/references/pattern-examples.md +21 -0
  322. agent_toolkit/data/skills/design/frontend-design-review/references/quick-checklist.md +38 -0
  323. agent_toolkit/data/skills/design/frontend-design-review/references/review-output-format.md +68 -0
  324. agent_toolkit/data/skills/design/frontend-design-review/references/review-type-modifiers.md +31 -0
  325. agent_toolkit/data/skills/design/web-design-guidelines/SKILL.md +78 -0
  326. agent_toolkit/data/skills/design/web-design-guidelines/references/LICENSE +21 -0
  327. agent_toolkit/data/skills/design/web-design-guidelines/references/web-interface-guidelines.md +180 -0
  328. agent_toolkit/data/skills/forge/gh-address-comments/LICENSE.txt +202 -0
  329. agent_toolkit/data/skills/forge/gh-address-comments/NOTICE.txt +20 -0
  330. agent_toolkit/data/skills/forge/gh-address-comments/SKILL.md +72 -0
  331. agent_toolkit/data/skills/forge/gh-address-comments/scripts/fetch_comments.py +239 -0
  332. agent_toolkit/data/skills/forge/gh-contribution-planner/SKILL.md +160 -0
  333. agent_toolkit/data/skills/forge/gh-contribution-planner/scripts/inspect_contributions.py +818 -0
  334. agent_toolkit/data/skills/forge/gh-fix-ci/LICENSE.txt +201 -0
  335. agent_toolkit/data/skills/forge/gh-fix-ci/NOTICE.txt +20 -0
  336. agent_toolkit/data/skills/forge/gh-fix-ci/SKILL.md +74 -0
  337. agent_toolkit/data/skills/forge/gh-fix-ci/scripts/inspect_pr_checks.py +509 -0
  338. agent_toolkit/data/skills/forge/github-cli-workflow/SKILL.md +49 -0
  339. agent_toolkit/data/skills/forge/gitlab-cli-workflow/SKILL.md +44 -0
  340. agent_toolkit/data/skills/forge/workflow-client-bootstrap/SKILL.md +14 -0
  341. agent_toolkit/data/skills/forge/workflow-generic-project/SKILL.md +14 -0
  342. agent_toolkit/data/skills/forge/worktree/SKILL.md +113 -0
  343. agent_toolkit/data/skills/integrations/clickup-cli/SKILL.md +78 -0
  344. agent_toolkit/data/skills/integrations/clickup-cli/references/docs-management.md +57 -0
  345. agent_toolkit/data/skills/integrations/clickup-cli/references/task-management.md +131 -0
  346. agent_toolkit/data/skills/integrations/clickup-cli/references/time-tracking.md +38 -0
  347. agent_toolkit/data/skills/integrations/linear/LICENSE.txt +202 -0
  348. agent_toolkit/data/skills/integrations/linear/NOTICE.txt +23 -0
  349. agent_toolkit/data/skills/integrations/linear/SKILL.md +134 -0
  350. agent_toolkit/data/skills/integrations/mcp/SKILL.md +88 -0
  351. agent_toolkit/data/skills/integrations/slack-assistant/SKILL.md +138 -0
  352. agent_toolkit/data/skills/integrations/slack-cli/SKILL.md +109 -0
  353. agent_toolkit/data/skills/integrations/slack-cli/references/INSTALLATION.md +42 -0
  354. agent_toolkit/data/skills/loops/loop-runner/SKILL.md +92 -0
  355. agent_toolkit/data/skills/ops/docs-generator/SKILL.md +120 -0
  356. agent_toolkit/data/skills/ops/llm-cost-advisor/SKILL.md +87 -0
  357. agent_toolkit/data/skills/ops/swarm/SKILL.md +108 -0
  358. agent_toolkit/data/skills/ops/swarm-handoff/SKILL.md +112 -0
  359. agent_toolkit/data/skills/ops/swarm-observer/SKILL.md +102 -0
  360. agent_toolkit/data/skills/ops/triage/SKILL.md +119 -0
  361. agent_toolkit/data/skills/quality/codeql/SKILL.md +106 -0
  362. agent_toolkit/data/skills/quality/codeql/references/codeql-queries.md +43 -0
  363. agent_toolkit/data/skills/quality/megalinter/SKILL.md +162 -0
  364. agent_toolkit/data/skills/quality/megalinter/references/megalinter-images.md +36 -0
  365. agent_toolkit/data/skills/quality/megalinter/references/megalinter-license.md +41 -0
  366. agent_toolkit/data/skills/quality/megalinter/references/megalinter-targets.md +66 -0
  367. agent_toolkit/data/skills/tooling/chrome-devtools/SKILL.md +146 -0
  368. agent_toolkit/data/skills/tooling/herdr/SKILL.md +109 -0
  369. agent_toolkit/data/skills/tooling/inventory/SKILL.md +81 -0
  370. agent_toolkit/data/skills/tooling/jupyter-notebook/LICENSE.txt +201 -0
  371. agent_toolkit/data/skills/tooling/jupyter-notebook/NOTICE.txt +27 -0
  372. agent_toolkit/data/skills/tooling/jupyter-notebook/SKILL.md +145 -0
  373. agent_toolkit/data/skills/tooling/jupyter-notebook/assets/experiment-template.ipynb +117 -0
  374. agent_toolkit/data/skills/tooling/jupyter-notebook/assets/tutorial-template.ipynb +115 -0
  375. agent_toolkit/data/skills/tooling/jupyter-notebook/references/experiment-patterns.md +10 -0
  376. agent_toolkit/data/skills/tooling/jupyter-notebook/references/notebook-structure.md +17 -0
  377. agent_toolkit/data/skills/tooling/jupyter-notebook/references/quality-checklist.md +11 -0
  378. agent_toolkit/data/skills/tooling/jupyter-notebook/references/tutorial-patterns.md +9 -0
  379. agent_toolkit/data/skills/tooling/jupyter-notebook/scripts/new_notebook.py +132 -0
  380. agent_toolkit/data/skills/tooling/mermaid/SKILL.md +36 -0
  381. agent_toolkit/data/skills/tooling/playwright-cli/LICENSE.txt +201 -0
  382. agent_toolkit/data/skills/tooling/playwright-cli/NOTICE.txt +29 -0
  383. agent_toolkit/data/skills/tooling/playwright-cli/SKILL.md +154 -0
  384. agent_toolkit/data/skills/tooling/playwright-cli/references/cli.md +116 -0
  385. agent_toolkit/data/skills/tooling/playwright-cli/references/workflows.md +98 -0
  386. agent_toolkit/data/skills/tooling/playwright-cli/scripts/executable_playwright_cli.sh +25 -0
  387. agent_toolkit/data_cache.py +79 -0
  388. agent_toolkit/data_sync.py +144 -0
  389. agent_toolkit/installer/__init__.py +0 -0
  390. agent_toolkit/installer/merge.py +59 -0
  391. agent_toolkit/installer/receipt.py +89 -0
  392. agent_toolkit/installer/sources.py +92 -0
  393. agent_toolkit/installer/tracking.py +63 -0
  394. agent_toolkit/launcher.py +69 -0
  395. agent_toolkit/loop/__init__.py +0 -0
  396. agent_toolkit/loop/budget.py +107 -0
  397. agent_toolkit/loop/gh_gate.py +923 -0
  398. agent_toolkit/loop/loop-gh-gate +923 -0
  399. agent_toolkit/loop/pack.py +65 -0
  400. agent_toolkit/loop/runner.py +2722 -0
  401. agent_toolkit/runner/__init__.py +0 -0
  402. agent_toolkit/runner/dispatcher.py +163 -0
  403. agent_toolkit/runner/policy.py +356 -0
  404. agent_toolkit/runner/providers/__init__.py +21 -0
  405. agent_toolkit/runner/providers/anthropic_provider.py +109 -0
  406. agent_toolkit/runner/providers/base.py +53 -0
  407. agent_toolkit/runner/providers/ollama_provider.py +108 -0
  408. agent_toolkit/runner/providers/openai_provider.py +103 -0
  409. agent_toolkit/runner/providers/opencode_provider.py +127 -0
  410. agent_toolkit/swarm/__init__.py +5 -0
  411. agent_toolkit/swarm/approvals.py +121 -0
  412. agent_toolkit/swarm/backends/__init__.py +596 -0
  413. agent_toolkit/swarm/budget.py +83 -0
  414. agent_toolkit/swarm/cli.py +2448 -0
  415. agent_toolkit/swarm/config.py +351 -0
  416. agent_toolkit/swarm/handoff.py +174 -0
  417. agent_toolkit/swarm/models.py +230 -0
  418. agent_toolkit/swarm/prompts.py +177 -0
  419. agent_toolkit/swarm/recipes.py +304 -0
  420. agent_toolkit/swarm/runner.py +267 -0
  421. agent_toolkit/swarm/state.py +93 -0
  422. agent_toolkit/swarm/store.py +148 -0
  423. agent_toolkit/swarm/worktree.py +105 -0
  424. agent_toolkit/templates/workspace/.gitignore +19 -0
  425. agent_toolkit/templates/workspace/AGENTS.md +82 -0
  426. agent_toolkit/templates/workspace/knowledge/README.md +22 -0
  427. agent_toolkit/templates/workspace/knowledge/learnings/general.md +3 -0
  428. agent_toolkit/templates/workspace/knowledge/todos/pending.md +7 -0
  429. agent_toolkit/templates/workspace/packs/README.md +24 -0
  430. agent_toolkit/templates/workspace/personas/architect.md +12 -0
  431. agent_toolkit/templates/workspace/personas/implementer.md +12 -0
  432. agent_toolkit/templates/workspace/personas/researcher.md +12 -0
  433. agent_toolkit/templates/workspace/personas/reviewer.md +12 -0
  434. agent_toolkit_cli-1.11.0.dist-info/METADATA +363 -0
  435. agent_toolkit_cli-1.11.0.dist-info/RECORD +438 -0
  436. agent_toolkit_cli-1.11.0.dist-info/WHEEL +4 -0
  437. agent_toolkit_cli-1.11.0.dist-info/entry_points.txt +4 -0
  438. agent_toolkit_cli-1.11.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,923 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """
7
+ bin/loop-gh-gate — Hard autonomy gate for `gh` during loop runs.
8
+
9
+ Installed on PATH as a `gh` shim while a loop runner is active. Intercepts
10
+ mutating GitHub CLI commands and enforces LOOP.md allowlist / deny / tier
11
+ rules. Merge and close additionally require a verifier receipt under the
12
+ active run directory.
13
+
14
+ Environment (set by agent-toolkit loop / install_gh_shim):
15
+ LOOP_GATE_REAL_GH Absolute path to the real `gh` binary
16
+ LOOP_GATE_RUN_DIR Active run artifacts directory
17
+ LOOP_GATE_TIER L1 | L2 | L3
18
+ LOOP_GATE_ALLOWLIST Comma-separated allowlisted actions
19
+ LOOP_GATE_DENY Comma-separated denied actions
20
+ LOOP_GATE_VERIFIER Verifier skill/agent name (optional)
21
+ LOOP_GATE_RECEIPT_SECRET If set, verifier receipts must carry a matching HMAC
22
+ LOOP_GATE_DISABLED If "1", pass through without checks (tests only)
23
+ LOOP_GATE_ATTRIBUTION "1" (default) or "0" to disable comment attribution
24
+ LOOP_GATE_LOOP_NAME Loop name for attribution footer (optional)
25
+ LOOP_GATE_GITHUB_LOGIN Authenticated GitHub login (optional; resolved at install)
26
+ LOOP_GATE_ATTRIBUTION_TEMPLATE Optional custom prefix template with
27
+ {login} and {loop} placeholders
28
+
29
+ Usage (normally via shim):
30
+ loop-gh-gate -- pr merge 123 --repo owner/repo --squash
31
+ loop-gh-gate --classify pr merge 123 --repo owner/repo
32
+ loop-gh-gate --check-receipt merge --repo owner/repo --number 123
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import hashlib
38
+ import hmac
39
+ import json
40
+ import os
41
+ import re
42
+ import subprocess
43
+ import sys
44
+ from datetime import datetime, timezone
45
+ from pathlib import Path
46
+ from typing import Any
47
+
48
+ # Actions that mutate GitHub state and must be gated.
49
+ MUTATING_ACTIONS = frozenset(
50
+ {
51
+ "merge",
52
+ "close",
53
+ "comment",
54
+ "label",
55
+ "assign",
56
+ "approve",
57
+ "push",
58
+ "commit",
59
+ "force-push",
60
+ "delete",
61
+ }
62
+ )
63
+
64
+ # Merge/close always need a verifier receipt at L2+ (and are denied at L1).
65
+ RECEIPT_REQUIRED = frozenset({"merge", "close"})
66
+
67
+ # Max age for a verifier receipt (seconds).
68
+ RECEIPT_MAX_AGE_SEC = 3600
69
+
70
+ # Outbound prose posted via gh that should carry AI attribution (default ON).
71
+ ATTRIBUTION_ACTIONS = frozenset({"comment"})
72
+
73
+ ATTRIBUTION_MARKER = "> 🤖 AI-assisted"
74
+
75
+ DEFAULT_ATTRIBUTION_TEMPLATE = (
76
+ "> 🤖 AI-assisted message posted as `@{login}` by "
77
+ "[agent-toolkit](https://github.com/ulises-jeremias/agent-toolkit)"
78
+ "{loop_suffix}."
79
+ )
80
+
81
+ AGENT_TOOLKIT_REPO_URL = "https://github.com/ulises-jeremias/agent-toolkit"
82
+
83
+
84
+ def _split_csv(value: str | None) -> list[str]:
85
+ if not value:
86
+ return []
87
+ return [p.strip() for p in value.split(",") if p.strip()]
88
+
89
+
90
+ def _env_flag_enabled(name: str, *, default: bool = True) -> bool:
91
+ raw = os.environ.get(name)
92
+ if raw is None or raw == "":
93
+ return default
94
+ return raw.strip().lower() not in {"0", "false", "no", "off"}
95
+
96
+
97
+ def gate_config_from_env() -> dict[str, Any]:
98
+ return {
99
+ "real_gh": os.environ.get("LOOP_GATE_REAL_GH", ""),
100
+ "run_dir": Path(os.environ["LOOP_GATE_RUN_DIR"])
101
+ if os.environ.get("LOOP_GATE_RUN_DIR")
102
+ else None,
103
+ "tier": (os.environ.get("LOOP_GATE_TIER") or "L1").upper(),
104
+ "allowlist": _split_csv(os.environ.get("LOOP_GATE_ALLOWLIST")),
105
+ "deny": _split_csv(os.environ.get("LOOP_GATE_DENY")),
106
+ "verifier": os.environ.get("LOOP_GATE_VERIFIER") or "",
107
+ "receipt_secret": os.environ.get("LOOP_GATE_RECEIPT_SECRET") or "",
108
+ "disabled": os.environ.get("LOOP_GATE_DISABLED") == "1",
109
+ "attribution_enabled": _env_flag_enabled("LOOP_GATE_ATTRIBUTION", default=True),
110
+ "loop_name": os.environ.get("LOOP_GATE_LOOP_NAME") or "",
111
+ "github_login": os.environ.get("LOOP_GATE_GITHUB_LOGIN") or "",
112
+ "attribution_template": os.environ.get("LOOP_GATE_ATTRIBUTION_TEMPLATE") or "",
113
+ }
114
+
115
+
116
+ _SENSITIVE_FLAGS = frozenset(
117
+ {
118
+ "-H",
119
+ "--header",
120
+ "-b",
121
+ "--body",
122
+ "--body-file",
123
+ "-F",
124
+ "-f",
125
+ "--raw-field",
126
+ "--field",
127
+ "--input",
128
+ "-i",
129
+ "--jq",
130
+ }
131
+ )
132
+
133
+
134
+ def redact_argv(argv: list[str]) -> list[str]:
135
+ """Return argv with secret-bearing values replaced by <redacted>."""
136
+ out: list[str] = []
137
+ skip_next = False
138
+ for a in argv:
139
+ if skip_next:
140
+ out.append("<redacted>")
141
+ skip_next = False
142
+ continue
143
+ if a in _SENSITIVE_FLAGS:
144
+ out.append(a)
145
+ skip_next = True
146
+ continue
147
+ if "=" in a and a.startswith("-"):
148
+ flag, _, _val = a.partition("=")
149
+ # Redact inline sensitive values (Authorization headers, body=, etc.)
150
+ low = flag.lower()
151
+ if any(s in low for s in ("header", "body", "field", "input", "token", "auth")):
152
+ out.append(f"{flag}=<redacted>")
153
+ continue
154
+ out.append(a)
155
+ return out
156
+
157
+
158
+ def classify_gh_argv(argv: list[str]) -> tuple[str | None, dict[str, Any]]:
159
+ """Classify a `gh` argv into an action + metadata.
160
+
161
+ Returns (action_or_None_if_readonly, meta).
162
+ Unknown mutating forms return a gated action (typically ``push``) rather
163
+ than falling through as read-only.
164
+ """
165
+ args = [a for a in argv if a != "--"]
166
+ meta: dict[str, Any] = {"repo": None, "number": None, "raw": list(args)}
167
+
168
+ if not args:
169
+ return None, meta
170
+
171
+ # Collect --repo / -R anywhere in argv (gh accepts them mid-command).
172
+ i = 0
173
+ while i < len(args):
174
+ a = args[i]
175
+ if a in ("-R", "--repo") and i + 1 < len(args):
176
+ meta["repo"] = args[i + 1]
177
+ i += 2
178
+ continue
179
+ if a.startswith("--repo="):
180
+ meta["repo"] = a.split("=", 1)[1]
181
+ i += 1
182
+ continue
183
+ i += 1
184
+
185
+ # Strip leading global flags for subcommand detection.
186
+ i = 0
187
+ while i < len(args):
188
+ a = args[i]
189
+ if a in ("-R", "--repo") and i + 1 < len(args):
190
+ i += 2
191
+ continue
192
+ if a.startswith("--repo="):
193
+ i += 1
194
+ continue
195
+ break
196
+
197
+ rest = args[i:]
198
+ if not rest:
199
+ return None, meta
200
+
201
+ # gh pr ...
202
+ if rest[0] == "pr" and len(rest) >= 2:
203
+ sub = rest[1]
204
+ if len(rest) >= 3 and rest[2].isdigit():
205
+ meta["number"] = int(rest[2])
206
+ if sub == "merge":
207
+ return "merge", meta
208
+ if sub == "close":
209
+ return "close", meta
210
+ if sub == "comment":
211
+ return "comment", meta
212
+ if sub == "create":
213
+ return "push", meta # opening a PR is a write
214
+ if sub == "review":
215
+ joined = " ".join(rest)
216
+ if "--approve" in rest or " --approve" in f" {joined}":
217
+ return "approve", meta
218
+ return "comment", meta # request-changes / comment reviews
219
+ if sub == "edit":
220
+ if "--add-label" in rest or "--remove-label" in rest:
221
+ return "label", meta
222
+ if "--add-assignee" in rest or "--remove-assignee" in rest:
223
+ return "assign", meta
224
+ # title/body/base/state edits are mutating but not label/assign
225
+ if any(
226
+ f in rest or any(a.startswith(f"{f}=") for a in rest)
227
+ for f in ("--title", "--body", "--body-file", "--base", "--state")
228
+ ):
229
+ return "push", meta
230
+ return "push", meta # unknown pr edit → deny by default (fail closed)
231
+ if sub in ("ready", "reopen", "lock", "unlock", "delete"):
232
+ return "push" if sub != "delete" else "delete", meta
233
+ if sub in ("list", "view", "status", "diff", "checks"):
234
+ return None, meta
235
+ return "push", meta # unknown pr subcommand → treat as mutating
236
+
237
+ # gh issue ...
238
+ if rest[0] == "issue" and len(rest) >= 2:
239
+ sub = rest[1]
240
+ if len(rest) >= 3 and rest[2].isdigit():
241
+ meta["number"] = int(rest[2])
242
+ if sub == "comment":
243
+ return "comment", meta
244
+ if sub == "create":
245
+ if "--label" in rest or "-l" in rest:
246
+ return "label", meta
247
+ return "push", meta
248
+ if sub == "edit":
249
+ if "--add-assignee" in rest or "--remove-assignee" in rest:
250
+ return "assign", meta
251
+ if "--add-label" in rest or "--remove-label" in rest:
252
+ return "label", meta
253
+ # --state closed / --state open
254
+ for i, a in enumerate(rest):
255
+ if a == "--state" and i + 1 < len(rest) and rest[i + 1] == "closed":
256
+ return "close", meta
257
+ if a.startswith("--state=") and a.split("=", 1)[1] == "closed":
258
+ return "close", meta
259
+ if any(
260
+ f in rest or any(a.startswith(f"{f}=") for a in rest)
261
+ for f in ("--title", "--body", "--body-file")
262
+ ):
263
+ return "push", meta
264
+ return "push", meta
265
+ if sub in ("close",):
266
+ return "close", meta
267
+ if sub in ("list", "view", "status"):
268
+ return None, meta
269
+ return "push", meta
270
+
271
+ # gh api ...
272
+ if rest[0] == "api":
273
+ method = "GET"
274
+ method_explicit = False
275
+ path = ""
276
+ has_field = False
277
+ skip_next = False
278
+ for a in rest[1:]:
279
+ if skip_next:
280
+ skip_next = False
281
+ continue
282
+ if a in ("-X", "--method"):
283
+ skip_next = True
284
+ continue
285
+ if a.startswith("--method="):
286
+ method = a.split("=", 1)[1].upper()
287
+ method_explicit = True
288
+ continue
289
+ if a.startswith("-"):
290
+ if a in ("-F", "-f", "--field", "--raw-field", "-H", "--header", "--input", "-i"):
291
+ if a in ("-F", "-f", "--field", "--raw-field"):
292
+ has_field = True
293
+ skip_next = True
294
+ elif a.startswith(("-F", "-f")) and "=" in a:
295
+ has_field = True
296
+ continue
297
+ if not path:
298
+ path = a
299
+ # Re-scan for method if -X was used
300
+ for i, a in enumerate(rest[1:], 1):
301
+ if a in ("-X", "--method") and i + 1 < len(rest):
302
+ method = rest[i + 1].upper()
303
+ method_explicit = True
304
+ elif a.startswith("--method="):
305
+ method = a.split("=", 1)[1].upper()
306
+ method_explicit = True
307
+ # gh api switches to POST when -f/-F are present unless method is set.
308
+ if has_field and not method_explicit:
309
+ method = "POST"
310
+ if method in ("POST", "PUT", "PATCH", "DELETE"):
311
+ if "/merge" in path:
312
+ return "merge", meta
313
+ if "/comments" in path:
314
+ return "comment", meta
315
+ if "/assignees" in path or "assignees" in path:
316
+ return "assign", meta
317
+ if "/labels" in path:
318
+ return "label", meta
319
+ if re.search(r"/pulls/\d+$", path) and method == "PATCH":
320
+ return "close", meta
321
+ if re.search(r"/issues/\d+$", path) and method == "PATCH":
322
+ # Could be close or assign; treat as push unless assignees/labels
323
+ return "push", meta
324
+ if method == "DELETE":
325
+ return "delete", meta
326
+ return "push", meta # unknown mutating API → typically denied
327
+ return None, meta
328
+
329
+ # Other top-level mutating commands
330
+ if rest[0] in ("release", "gist", "repo", "secret", "variable", "workflow"):
331
+ if len(rest) >= 2 and rest[1] in ("list", "view", "status", "get"):
332
+ return None, meta
333
+ return "push", meta
334
+
335
+ return None, meta
336
+
337
+
338
+ def tier_forbids(tier: str, action: str) -> str | None:
339
+ """Return a reason if the tier itself forbids the action."""
340
+ t = tier.upper()
341
+ if t.startswith("L1") or t == "1":
342
+ return f"L1 report-only forbids '{action}'"
343
+ if (t.startswith("L2") or t == "2") and action in (
344
+ "merge",
345
+ "close",
346
+ "approve",
347
+ "push",
348
+ "commit",
349
+ "force-push",
350
+ "delete",
351
+ ):
352
+ return f"L2 assisted forbids '{action}' (allow comment/label/assign only)"
353
+ return None
354
+
355
+
356
+ def evaluate_action(
357
+ action: str,
358
+ *,
359
+ tier: str,
360
+ allowlist: list[str],
361
+ deny: list[str],
362
+ ) -> tuple[bool, str]:
363
+ """Return (allowed, reason)."""
364
+ if action not in MUTATING_ACTIONS:
365
+ return True, "non-mutating"
366
+
367
+ reason = tier_forbids(tier, action)
368
+ if reason:
369
+ return False, reason
370
+
371
+ if action in deny:
372
+ return False, f"action '{action}' is on deny list"
373
+
374
+ if action not in allowlist:
375
+ return False, f"action '{action}' is not on allowlist"
376
+
377
+ return True, "allowlisted"
378
+
379
+
380
+ def receipts_dir(run_dir: Path) -> Path:
381
+ d = run_dir / "verifier-receipts"
382
+ d.mkdir(parents=True, exist_ok=True)
383
+ return d
384
+
385
+
386
+ def write_denial(run_dir: Path | None, record: dict[str, Any]) -> None:
387
+ if run_dir is None:
388
+ return
389
+ path = run_dir / "gate-denials.jsonl"
390
+ if "argv" in record:
391
+ record = {**record, "argv": redact_argv(list(record["argv"]))}
392
+ record = {**record, "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")}
393
+ with path.open("a", encoding="utf-8") as fh:
394
+ fh.write(json.dumps(record) + "\n")
395
+
396
+
397
+ def receipt_canonical_payload(data: dict[str, Any]) -> str:
398
+ """Stable JSON payload used for HMAC (excludes `sig`)."""
399
+ payload = {
400
+ "action": data.get("action"),
401
+ "repo": data.get("repo"),
402
+ "number": data.get("number"),
403
+ "approved": bool(data.get("approved")),
404
+ "verifier": data.get("verifier"),
405
+ "rationale": data.get("rationale"),
406
+ "ts": data.get("ts"),
407
+ }
408
+ return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
409
+
410
+
411
+ def sign_receipt(data: dict[str, Any], secret: str) -> str:
412
+ digest = hmac.new(
413
+ secret.encode("utf-8"),
414
+ receipt_canonical_payload(data).encode("utf-8"),
415
+ hashlib.sha256,
416
+ ).hexdigest()
417
+ return digest
418
+
419
+
420
+ def verify_receipt_signature(data: dict[str, Any], secret: str) -> bool:
421
+ expected = data.get("sig")
422
+ if not expected or not isinstance(expected, str):
423
+ return False
424
+ actual = sign_receipt(data, secret)
425
+ return hmac.compare_digest(actual, expected)
426
+
427
+
428
+ def find_verifier_receipt(
429
+ run_dir: Path,
430
+ action: str,
431
+ *,
432
+ repo: str | None = None,
433
+ number: int | None = None,
434
+ verifier: str = "",
435
+ receipt_secret: str = "",
436
+ max_age_sec: int = RECEIPT_MAX_AGE_SEC,
437
+ ) -> dict[str, Any] | None:
438
+ """Find a matching approved receipt for this action/target."""
439
+ d = receipts_dir(run_dir)
440
+ now = datetime.now(timezone.utc)
441
+ candidates: list[tuple[datetime, Path, dict[str, Any]]] = []
442
+
443
+ for path in sorted(d.glob("*.json")):
444
+ try:
445
+ data = json.loads(path.read_text(encoding="utf-8"))
446
+ except (OSError, json.JSONDecodeError):
447
+ continue
448
+ if data.get("action") != action:
449
+ continue
450
+ if not data.get("approved"):
451
+ continue
452
+ # Exact binding when the command targets a specific repo/number.
453
+ if number is not None and data.get("number") != number:
454
+ continue
455
+ if repo is not None and data.get("repo") != repo:
456
+ continue
457
+ if verifier and data.get("verifier") != verifier:
458
+ continue
459
+ ts_raw = data.get("ts") or ""
460
+ try:
461
+ ts = datetime.fromisoformat(str(ts_raw).replace("Z", "+00:00"))
462
+ except ValueError:
463
+ continue
464
+ if ts.tzinfo is None:
465
+ continue
466
+ if (now - ts).total_seconds() > max_age_sec or (now - ts).total_seconds() < -60:
467
+ continue
468
+ if receipt_secret and not verify_receipt_signature(data, receipt_secret):
469
+ continue
470
+ candidates.append((ts, path, data))
471
+
472
+ if not candidates:
473
+ return None
474
+ # Prefer newest when multiple match.
475
+ candidates.sort(key=lambda item: item[0].timestamp(), reverse=True)
476
+ return candidates[0][2]
477
+
478
+
479
+ def require_receipt(
480
+ run_dir: Path,
481
+ action: str,
482
+ *,
483
+ repo: str | None,
484
+ number: int | None,
485
+ verifier: str,
486
+ receipt_secret: str = "",
487
+ ) -> tuple[bool, str]:
488
+ if action not in RECEIPT_REQUIRED:
489
+ return True, "receipt not required"
490
+ if repo is None or number is None:
491
+ return False, "merge/close require --repo and PR/issue number for receipt binding"
492
+ receipt = find_verifier_receipt(
493
+ run_dir,
494
+ action,
495
+ repo=repo,
496
+ number=number,
497
+ verifier=verifier,
498
+ receipt_secret=receipt_secret,
499
+ )
500
+ if receipt is None:
501
+ sig_hint = ', "sig":"<hmac-sha256>"' if receipt_secret else ""
502
+ return (
503
+ False,
504
+ f"missing verifier receipt for {action} {repo}#{number}"
505
+ + f" — write {run_dir / 'verifier-receipts'}/<id>.json "
506
+ f'{{"action":"{action}","repo":"{repo}","number":{number},'
507
+ f'"approved":true,"verifier":"{verifier or "configured-verifier"}",'
508
+ f'"rationale":"...","ts":"<ISO8601>Z"{sig_hint}}}',
509
+ )
510
+ return True, "receipt ok"
511
+
512
+
513
+ def check_command(
514
+ argv: list[str],
515
+ cfg: dict[str, Any] | None = None,
516
+ ) -> tuple[bool, str, str | None, dict[str, Any]]:
517
+ """Full check. Returns (ok, reason, action, meta)."""
518
+ cfg = cfg or gate_config_from_env()
519
+ if cfg.get("disabled"):
520
+ return True, "gate disabled", None, {}
521
+
522
+ action, meta = classify_gh_argv(argv)
523
+ if action is None:
524
+ return True, "readonly / ungated", None, meta
525
+
526
+ ok, reason = evaluate_action(
527
+ action,
528
+ tier=str(cfg.get("tier") or "L1"),
529
+ allowlist=list(cfg.get("allowlist") or []),
530
+ deny=list(cfg.get("deny") or []),
531
+ )
532
+ if not ok:
533
+ return False, reason, action, meta
534
+
535
+ run_dir = cfg.get("run_dir")
536
+ if action in RECEIPT_REQUIRED:
537
+ if run_dir is None:
538
+ return False, "LOOP_GATE_RUN_DIR not set (cannot verify receipt)", action, meta
539
+ ok_r, reason_r = require_receipt(
540
+ Path(run_dir),
541
+ action,
542
+ repo=meta.get("repo"),
543
+ number=meta.get("number"),
544
+ verifier=str(cfg.get("verifier") or ""),
545
+ receipt_secret=str(cfg.get("receipt_secret") or ""),
546
+ )
547
+ if not ok_r:
548
+ return False, reason_r, action, meta
549
+
550
+ return True, reason, action, meta
551
+
552
+
553
+ def body_already_attributed(body: str) -> bool:
554
+ """Return True when the body already starts with the AI-assisted marker."""
555
+ return body.lstrip().startswith(ATTRIBUTION_MARKER)
556
+
557
+
558
+ def format_attribution_prefix(
559
+ *,
560
+ login: str = "",
561
+ loop_name: str = "",
562
+ template: str = "",
563
+ ) -> str:
564
+ """Build the markdown attribution blockquote line(s)."""
565
+ login_s = (login or "unknown").lstrip("@")
566
+ loop_s = (loop_name or "").strip()
567
+ loop_suffix = f" (`{loop_s}`)" if loop_s else ""
568
+ raw = (template or "").strip() or DEFAULT_ATTRIBUTION_TEMPLATE
569
+ try:
570
+ text = raw.format(
571
+ login=login_s,
572
+ loop=loop_s or "loop",
573
+ loop_suffix=loop_suffix,
574
+ url=AGENT_TOOLKIT_REPO_URL,
575
+ )
576
+ except (KeyError, ValueError, IndexError):
577
+ text = DEFAULT_ATTRIBUTION_TEMPLATE.format(
578
+ login=login_s,
579
+ loop_suffix=loop_suffix,
580
+ )
581
+ text = text.rstrip()
582
+ if not text.startswith(">"):
583
+ text = f"> {text}"
584
+ return text
585
+
586
+
587
+ def apply_attribution_to_body(body: str, prefix: str) -> str:
588
+ """Prepend attribution when missing. Idempotent."""
589
+ if body_already_attributed(body):
590
+ return body
591
+ prefix = prefix.rstrip()
592
+ if not body:
593
+ return f"{prefix}\n"
594
+ return f"{prefix}\n\n{body.lstrip()}"
595
+
596
+
597
+ def resolve_github_login(real_gh: str | None = None) -> str:
598
+ """Resolve the authenticated GitHub login via `gh api user`."""
599
+ cached = os.environ.get("LOOP_GATE_GITHUB_LOGIN") or ""
600
+ if cached:
601
+ return cached
602
+ real = real_gh or shutil_which_gh() or os.environ.get("LOOP_GATE_REAL_GH") or ""
603
+ if not real:
604
+ return ""
605
+ try:
606
+ result = subprocess.run(
607
+ [real, "api", "user", "-q", ".login"],
608
+ capture_output=True,
609
+ text=True,
610
+ timeout=15,
611
+ check=False,
612
+ )
613
+ except (OSError, subprocess.SubprocessError):
614
+ return ""
615
+ if result.returncode != 0:
616
+ return ""
617
+ return (result.stdout or "").strip()
618
+
619
+
620
+ def _rewrite_flag_value(
621
+ argv: list[str],
622
+ *,
623
+ flags: tuple[str, ...],
624
+ new_value: str,
625
+ ) -> tuple[list[str], bool]:
626
+ """Replace the value for any of the given CLI flags. Returns (argv, changed)."""
627
+ out = list(argv)
628
+ changed = False
629
+ i = 0
630
+ while i < len(out):
631
+ a = out[i]
632
+ for flag in flags:
633
+ if a == flag and i + 1 < len(out):
634
+ out[i + 1] = new_value
635
+ changed = True
636
+ i += 2
637
+ break
638
+ if a.startswith(f"{flag}="):
639
+ out[i] = f"{flag}={new_value}"
640
+ changed = True
641
+ i += 1
642
+ break
643
+ else:
644
+ i += 1
645
+ return out, changed
646
+
647
+
648
+ def _rewrite_field_body(argv: list[str], new_body: str) -> tuple[list[str], bool]:
649
+ """Rewrite `-f body=` / `-F body=` / `--raw-field body=` style args."""
650
+ out = list(argv)
651
+ changed = False
652
+ field_flags = ("-f", "-F", "--field", "--raw-field")
653
+ i = 0
654
+ while i < len(out):
655
+ a = out[i]
656
+ if a in field_flags and i + 1 < len(out) and out[i + 1].startswith("body="):
657
+ out[i + 1] = f"body={new_body}"
658
+ changed = True
659
+ i += 2
660
+ continue
661
+ for flag in field_flags:
662
+ prefix = f"{flag}body="
663
+ # gh allows -fbody=value without space
664
+ if a.startswith(prefix) or a.startswith(f"{flag} body="):
665
+ out[i] = f"{flag}body={new_body}" if not a.startswith(f"{flag} ") else a
666
+ if a.startswith(prefix):
667
+ out[i] = f"{prefix}{new_body}"
668
+ changed = True
669
+ elif a.startswith("-f") and a[2:].startswith("body="):
670
+ out[i] = f"-fbody={new_body}"
671
+ changed = True
672
+ elif a.startswith("-F") and a[2:].startswith("body="):
673
+ out[i] = f"-Fbody={new_body}"
674
+ changed = True
675
+ i += 1
676
+ return out, changed
677
+
678
+
679
+ def inject_attribution_argv(
680
+ argv: list[str],
681
+ *,
682
+ prefix: str,
683
+ run_dir: Path | None = None,
684
+ ) -> tuple[list[str], bool]:
685
+ """Rewrite gh argv so comment/review bodies include attribution.
686
+
687
+ Supports ``--body`` / ``-b``, ``--body-file``, and ``-f/-F body=``.
688
+ Returns (new_argv, injected).
689
+ """
690
+ # --body / -b
691
+ body_flags = ("--body", "-b")
692
+ for i, a in enumerate(argv):
693
+ for flag in body_flags:
694
+ if a == flag and i + 1 < len(argv):
695
+ new_body = apply_attribution_to_body(argv[i + 1], prefix)
696
+ if new_body == argv[i + 1]:
697
+ return list(argv), False
698
+ out, _ = _rewrite_flag_value(argv, flags=body_flags, new_value=new_body)
699
+ return out, True
700
+ if a.startswith(f"{flag}="):
701
+ raw = a.split("=", 1)[1]
702
+ new_body = apply_attribution_to_body(raw, prefix)
703
+ if new_body == raw:
704
+ return list(argv), False
705
+ out, _ = _rewrite_flag_value(argv, flags=body_flags, new_value=new_body)
706
+ return out, True
707
+
708
+ # --body-file
709
+ for i, a in enumerate(argv):
710
+ path_s: str | None = None
711
+ flag_eq = False
712
+ if a in ("--body-file",) and i + 1 < len(argv):
713
+ path_s = argv[i + 1]
714
+ elif a.startswith("--body-file="):
715
+ path_s = a.split("=", 1)[1]
716
+ flag_eq = True
717
+ if path_s is None:
718
+ continue
719
+ try:
720
+ original = Path(path_s).read_text(encoding="utf-8")
721
+ except OSError:
722
+ return list(argv), False
723
+ new_body = apply_attribution_to_body(original, prefix)
724
+ if new_body == original:
725
+ return list(argv), False
726
+ dest_dir = (run_dir / ".gate") if run_dir else Path(path_s).parent
727
+ dest_dir.mkdir(parents=True, exist_ok=True)
728
+ dest = dest_dir / f"attributed-body-{hashlib.sha256(new_body.encode()).hexdigest()[:10]}.md"
729
+ dest.write_text(new_body, encoding="utf-8")
730
+ if flag_eq:
731
+ out = list(argv)
732
+ out[i] = f"--body-file={dest}"
733
+ return out, True
734
+ out, _ = _rewrite_flag_value(argv, flags=("--body-file",), new_value=str(dest))
735
+ return out, True
736
+
737
+ # -f / -F body=
738
+ for i, a in enumerate(argv):
739
+ field_flags = ("-f", "-F", "--field", "--raw-field")
740
+ if a in field_flags and i + 1 < len(argv) and argv[i + 1].startswith("body="):
741
+ raw = argv[i + 1].split("=", 1)[1]
742
+ new_body = apply_attribution_to_body(raw, prefix)
743
+ if new_body == raw:
744
+ return list(argv), False
745
+ return _rewrite_field_body(argv, new_body)
746
+ for flag in ("-f", "-F"):
747
+ prefix_f = f"{flag}body="
748
+ if a.startswith(prefix_f):
749
+ raw = a[len(prefix_f) :]
750
+ new_body = apply_attribution_to_body(raw, prefix)
751
+ if new_body == raw:
752
+ return list(argv), False
753
+ return _rewrite_field_body(argv, new_body)
754
+
755
+ return list(argv), False
756
+
757
+
758
+ def install_gh_shim(
759
+ run_dir: Path,
760
+ *,
761
+ tier: str,
762
+ allowlist: list[str],
763
+ deny: list[str],
764
+ verifier: str = "",
765
+ gate_script: Path | None = None,
766
+ real_gh: str | None = None,
767
+ attribution_enabled: bool = True,
768
+ loop_name: str = "",
769
+ github_login: str = "",
770
+ attribution_template: str = "",
771
+ ) -> dict[str, str]:
772
+ """Create a PATH-first `gh` shim and return env vars for the runner."""
773
+ real = real_gh or shutil_which_gh()
774
+ if not real:
775
+ raise RuntimeError("real `gh` binary not found on PATH")
776
+
777
+ gate_script = gate_script or Path(__file__).resolve()
778
+ shim_dir = run_dir / ".gate" / "bin"
779
+ shim_dir.mkdir(parents=True, exist_ok=True)
780
+ receipts_dir(run_dir)
781
+
782
+ shim = shim_dir / "gh"
783
+ shim.write_text(
784
+ "#!/usr/bin/env bash\n"
785
+ "set -euo pipefail\n"
786
+ f'exec "{sys.executable}" "{gate_script}" -- "$@"\n',
787
+ encoding="utf-8",
788
+ )
789
+ shim.chmod(0o755)
790
+
791
+ login = github_login or resolve_github_login(real)
792
+
793
+ env = os.environ.copy()
794
+ env["PATH"] = f"{shim_dir}{os.pathsep}{env.get('PATH', '')}"
795
+ env["LOOP_GATE_REAL_GH"] = real
796
+ env["LOOP_GATE_RUN_DIR"] = str(run_dir)
797
+ env["LOOP_GATE_TIER"] = tier
798
+ env["LOOP_GATE_ALLOWLIST"] = ",".join(allowlist)
799
+ env["LOOP_GATE_DENY"] = ",".join(deny)
800
+ env["LOOP_GATE_VERIFIER"] = verifier
801
+ env["LOOP_GATE_ATTRIBUTION"] = "1" if attribution_enabled else "0"
802
+ if loop_name:
803
+ env["LOOP_GATE_LOOP_NAME"] = loop_name
804
+ if login:
805
+ env["LOOP_GATE_GITHUB_LOGIN"] = login
806
+ if attribution_template:
807
+ env["LOOP_GATE_ATTRIBUTION_TEMPLATE"] = attribution_template
808
+ # Preserve receipt secret from the parent environment when present.
809
+ if os.environ.get("LOOP_GATE_RECEIPT_SECRET"):
810
+ env["LOOP_GATE_RECEIPT_SECRET"] = os.environ["LOOP_GATE_RECEIPT_SECRET"]
811
+ env.pop("LOOP_GATE_DISABLED", None)
812
+ return env
813
+
814
+
815
+ def shutil_which_gh() -> str | None:
816
+ import shutil
817
+
818
+ # Prefer a real gh that is NOT our shim (avoid recursion if already gated).
819
+ path = os.environ.get("PATH", "")
820
+ for directory in path.split(os.pathsep):
821
+ candidate = Path(directory) / "gh"
822
+ if candidate.is_file() and os.access(candidate, os.X_OK):
823
+ # Skip if it is our shim (calls loop-gh-gate).
824
+ try:
825
+ text = candidate.read_text(encoding="utf-8", errors="ignore")
826
+ except OSError:
827
+ text = ""
828
+ if "loop-gh-gate" in text or "loop_gh_gate" in text:
829
+ continue
830
+ return str(candidate)
831
+ found = shutil.which("gh")
832
+ return found
833
+
834
+
835
+ def main(argv: list[str] | None = None) -> int:
836
+ argv = list(sys.argv[1:] if argv is None else argv)
837
+
838
+ if argv and argv[0] == "--classify":
839
+ action, meta = classify_gh_argv(argv[1:])
840
+ print(json.dumps({"action": action, "meta": meta}))
841
+ return 0
842
+
843
+ if argv and argv[0] == "--check":
844
+ ok, reason, action, meta = check_command(argv[1:])
845
+ print(json.dumps({"ok": ok, "reason": reason, "action": action, "meta": meta}))
846
+ return 0 if ok else 2
847
+
848
+ # Default: act as gh shim. Expect `--` then gh args, or raw gh args.
849
+ if argv and argv[0] == "--":
850
+ gh_args = argv[1:]
851
+ else:
852
+ gh_args = argv
853
+
854
+ cfg = gate_config_from_env()
855
+ ok, reason, action, meta = check_command(gh_args, cfg)
856
+ if not ok:
857
+ write_denial(
858
+ cfg.get("run_dir"),
859
+ {
860
+ "kind": "denied",
861
+ "action": action,
862
+ "reason": reason,
863
+ "argv": gh_args,
864
+ "meta": meta,
865
+ "tier": cfg.get("tier"),
866
+ "allowlist": cfg.get("allowlist"),
867
+ "deny": cfg.get("deny"),
868
+ },
869
+ )
870
+ print(f"[loop-gh-gate] DENIED {action or 'unknown'}: {reason}", file=sys.stderr)
871
+ return 78 # EX_CONFIG — distinctive for gate denial
872
+
873
+ real = cfg.get("real_gh") or shutil_which_gh()
874
+ if not real:
875
+ print("[loop-gh-gate] real gh not found (LOOP_GATE_REAL_GH unset)", file=sys.stderr)
876
+ return 127
877
+
878
+ attribution_injected = False
879
+ if (
880
+ action in ATTRIBUTION_ACTIONS
881
+ and cfg.get("attribution_enabled", True)
882
+ and not cfg.get("disabled")
883
+ ):
884
+ login = str(cfg.get("github_login") or "") or resolve_github_login(str(real))
885
+ prefix = format_attribution_prefix(
886
+ login=login,
887
+ loop_name=str(cfg.get("loop_name") or ""),
888
+ template=str(cfg.get("attribution_template") or ""),
889
+ )
890
+ run_dir = cfg.get("run_dir")
891
+ gh_args, attribution_injected = inject_attribution_argv(
892
+ gh_args,
893
+ prefix=prefix,
894
+ run_dir=Path(run_dir) if run_dir else None,
895
+ )
896
+
897
+ if action:
898
+ # Audit allowed mutations.
899
+ run_dir = cfg.get("run_dir")
900
+ if run_dir:
901
+ audit = Path(run_dir) / "gate-allow.jsonl"
902
+ with audit.open("a", encoding="utf-8") as fh:
903
+ fh.write(
904
+ json.dumps(
905
+ {
906
+ "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
907
+ "kind": "allowed",
908
+ "action": action,
909
+ "reason": reason,
910
+ "argv": redact_argv(gh_args),
911
+ "meta": {k: v for k, v in meta.items() if k != "raw"},
912
+ "attribution_injected": attribution_injected,
913
+ }
914
+ )
915
+ + "\n"
916
+ )
917
+
918
+ result = subprocess.run([real, *gh_args])
919
+ return int(result.returncode)
920
+
921
+
922
+ if __name__ == "__main__":
923
+ sys.exit(main())