@mmerterden/multi-agent-pipeline 16.18.0 → 16.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +5 -5
  3. package/README.tr.md +2 -2
  4. package/docs/FIGMA_PIPELINE.md +1 -1
  5. package/docs/adr/0001-three-model-triage.md +4 -2
  6. package/docs/architecture.md +2 -2
  7. package/docs/ecosystem.md +13 -11
  8. package/docs/features.md +2 -2
  9. package/index.js +1 -1
  10. package/install/_codex-agents.mjs +2 -2
  11. package/install/_common.mjs +25 -1
  12. package/install/_dev-only-files.mjs +3 -2
  13. package/install/_mcp-register.mjs +4 -3
  14. package/install/_plugin-skills.mjs +1 -3
  15. package/install/copilot.mjs +18 -9
  16. package/install/index.mjs +2 -4
  17. package/install/templates/copilot-instructions.md +7 -7
  18. package/package.json +4 -3
  19. package/pipeline/agents/android-architect.md +1 -0
  20. package/pipeline/agents/backend-architect.md +1 -0
  21. package/pipeline/agents/code-reviewer.md +1 -0
  22. package/pipeline/agents/dev-critic.md +2 -1
  23. package/pipeline/agents/explorer.md +1 -0
  24. package/pipeline/agents/ios-architect.md +1 -0
  25. package/pipeline/agents/security-auditor.md +1 -0
  26. package/pipeline/agents/task-clarifier.md +1 -0
  27. package/pipeline/claude-md-template.md +2 -2
  28. package/pipeline/commands/multi-agent/SKILL.md +5 -5
  29. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +1 -1
  30. package/pipeline/commands/multi-agent/design-check/SKILL.md +1 -1
  31. package/pipeline/commands/multi-agent/help/SKILL.md +2 -2
  32. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -1
  33. package/pipeline/commands/multi-agent/manual-test/SKILL.md +5 -1
  34. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
  35. package/pipeline/commands/multi-agent/review/SKILL.md +27 -14
  36. package/pipeline/commands/multi-agent/review-analysis/SKILL.md +1 -1
  37. package/pipeline/commands/multi-agent/store-ready/SKILL.md +24 -2
  38. package/pipeline/commands/multi-agent/sync/SKILL.md +2 -2
  39. package/pipeline/commands/sim-test.md +64 -20
  40. package/pipeline/lib/credential-inventory.sh +15 -2
  41. package/pipeline/lib/credential-store-resolver.sh +14 -4
  42. package/pipeline/lib/credential-store.sh +8 -2
  43. package/pipeline/lib/extract-conventions.sh +1 -14
  44. package/pipeline/lib/fetch-confluence.sh +12 -4
  45. package/pipeline/lib/fetch-crashlytics.sh +11 -8
  46. package/pipeline/lib/fetch-document.sh +1 -1
  47. package/pipeline/lib/fetch-figma-annotations.sh +7 -5
  48. package/pipeline/lib/fetch-fortify.sh +5 -3
  49. package/pipeline/lib/fetch-graylog.sh +5 -3
  50. package/pipeline/lib/figma-mcp-refresh.sh +1 -1
  51. package/pipeline/lib/figma-screenshot.sh +27 -24
  52. package/pipeline/lib/figma-token.sh +8 -4
  53. package/pipeline/lib/issue-fetcher.sh +0 -1
  54. package/pipeline/lib/jira-publish.sh +7 -5
  55. package/pipeline/lib/md2confluence-v3.py +13 -7
  56. package/pipeline/lib/multi-repo-pipeline.sh +18 -8
  57. package/pipeline/lib/plan-todos.sh +11 -0
  58. package/pipeline/lib/post-pr-review.sh +9 -2
  59. package/pipeline/lib/repo-cache.sh +18 -10
  60. package/pipeline/lib/review-watch.sh +60 -14
  61. package/pipeline/lib/shadow-git.sh +8 -4
  62. package/pipeline/lib/vercel-deploy.sh +2 -2
  63. package/pipeline/multi-agent-refs/_dev-context.md +5 -2
  64. package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
  65. package/pipeline/multi-agent-refs/analysis/render.md +1 -1
  66. package/pipeline/multi-agent-refs/channels/pr.md +22 -4
  67. package/pipeline/multi-agent-refs/cross-cli-contract.md +1 -1
  68. package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +15 -2
  69. package/pipeline/multi-agent-refs/features/review-delta.md +89 -0
  70. package/pipeline/multi-agent-refs/features/scope-check.md +41 -0
  71. package/pipeline/multi-agent-refs/features/verify-by-test.md +6 -5
  72. package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
  73. package/pipeline/multi-agent-refs/outside-the-pipeline.md +6 -6
  74. package/pipeline/multi-agent-refs/payload-contracts.md +1 -1
  75. package/pipeline/multi-agent-refs/phases/log-format.md +1 -1
  76. package/pipeline/multi-agent-refs/phases/modes.md +1 -1
  77. package/pipeline/multi-agent-refs/phases/phase-0-init.md +4 -2
  78. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +3 -3
  79. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
  80. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +17 -2
  81. package/pipeline/multi-agent-refs/phases/phase-4-review.md +67 -24
  82. package/pipeline/multi-agent-refs/phases/phase-5-test.md +10 -0
  83. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +2 -0
  84. package/pipeline/multi-agent-refs/phases/phase-7-report.md +4 -2
  85. package/pipeline/multi-agent-refs/rules.md +2 -2
  86. package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
  87. package/pipeline/rules/figma-pipeline.md +18 -72
  88. package/pipeline/rules/outside-the-pipeline.md +4 -3
  89. package/pipeline/schemas/agent-state.schema.json +130 -1
  90. package/pipeline/schemas/dev-critic-output.schema.json +5 -0
  91. package/pipeline/schemas/prefs.schema.json +47 -0
  92. package/pipeline/schemas/reviewer-output.schema.json +7 -2
  93. package/pipeline/schemas/scope-check.schema.json +55 -0
  94. package/pipeline/schemas/token-budget.json +3 -3
  95. package/pipeline/schemas/triage-output.schema.json +12 -2
  96. package/pipeline/scripts/README.md +3 -2
  97. package/pipeline/scripts/_fingerprint.mjs +173 -0
  98. package/pipeline/scripts/_stack-routing.mjs +1 -1
  99. package/pipeline/scripts/agent-guard.py +102 -21
  100. package/pipeline/scripts/anonymize-findings.mjs +7 -6
  101. package/pipeline/scripts/build-skills-index.mjs +14 -3
  102. package/pipeline/scripts/cost-budget-check.mjs +5 -3
  103. package/pipeline/scripts/cost-lib.sh +0 -15
  104. package/pipeline/scripts/diff-explain.mjs +22 -12
  105. package/pipeline/scripts/evidence-gate.mjs +73 -5
  106. package/pipeline/scripts/finding-fingerprint.mjs +101 -0
  107. package/pipeline/scripts/gc-refs.sh +6 -2
  108. package/pipeline/scripts/gc-tmp.sh +1 -1
  109. package/pipeline/scripts/gc-worktrees.sh +1 -1
  110. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
  111. package/pipeline/scripts/github-ssh-setup.sh +7 -2
  112. package/pipeline/scripts/graph-build.mjs +2 -2
  113. package/pipeline/scripts/jira-wiki-escape.mjs +2 -1
  114. package/pipeline/scripts/keychain.py +12 -11
  115. package/pipeline/scripts/learning-curve.mjs +1 -1
  116. package/pipeline/scripts/migrate-prefs.mjs +1 -1
  117. package/pipeline/scripts/output-quality-check.sh +3 -1
  118. package/pipeline/scripts/phase-tracker.sh +1 -1
  119. package/pipeline/scripts/phase0-exit-gate.mjs +2 -1
  120. package/pipeline/scripts/plan-coverage-gate.mjs +2 -1
  121. package/pipeline/scripts/pre-commit-check.sh +23 -13
  122. package/pipeline/scripts/prune-logs.sh +1 -1
  123. package/pipeline/scripts/render-agent-log-cost.sh +3 -1
  124. package/pipeline/scripts/render-cost-summary.sh +4 -2
  125. package/pipeline/scripts/render-work-summary.sh +5 -3
  126. package/pipeline/scripts/repo-map.mjs +3 -2
  127. package/pipeline/scripts/review-delta.mjs +217 -0
  128. package/pipeline/scripts/run-metrics.mjs +20 -0
  129. package/pipeline/scripts/scan-skills.sh +6 -2
  130. package/pipeline/scripts/scope-check-gate.mjs +90 -0
  131. package/pipeline/scripts/search-logs.sh +8 -6
  132. package/pipeline/scripts/sign-skills.sh +3 -1
  133. package/pipeline/scripts/smoke-cross-cli-behavior.sh +12 -5
  134. package/pipeline/scripts/triage-memory.mjs +25 -4
  135. package/pipeline/scripts/uninstall.mjs +20 -12
  136. package/pipeline/scripts/update-check.sh +2 -2
  137. package/pipeline/scripts/update-issue-progress.sh +6 -5
  138. package/pipeline/scripts/validate-analysis-doc.mjs +6 -6
  139. package/pipeline/scripts/validate-reviewer.mjs +6 -0
  140. package/pipeline/scripts/validate-triage.mjs +20 -0
  141. package/pipeline/scripts/verify-skills.sh +3 -1
  142. package/pipeline/scripts/worktree-finalize.sh +22 -10
  143. package/pipeline/skills/.skill-manifest.json +81 -57
  144. package/pipeline/skills/.skills-index.json +19 -19
  145. package/pipeline/skills/shared/README.md +8 -8
  146. package/pipeline/skills/shared/core/multi-agent/SKILL.md +7 -7
  147. package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +1 -1
  148. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +2 -2
  149. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +1 -1
  150. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +2 -2
  151. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +23 -14
  152. package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +5 -0
  153. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
  154. package/pipeline/skills/shared/external/NOTICE-dimillian-skills.md +56 -0
  155. package/pipeline/skills/shared/external/accessibility-compliance-accessibility-audit/SKILL.md +0 -4
  156. package/pipeline/skills/shared/external/api-patterns/SKILL.md +12 -24
  157. package/pipeline/skills/shared/external/app-store-changelog/references/release-notes-guidelines.md +34 -0
  158. package/pipeline/skills/shared/external/app-store-changelog/scripts/collect_release_changes.sh +33 -0
  159. package/pipeline/skills/shared/external/architecture/SKILL.md +7 -9
  160. package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +0 -4
  161. package/pipeline/skills/shared/external/fastapi-pro/SKILL.md +0 -1
  162. package/pipeline/skills/shared/external/github-actions-templates/SKILL.md +0 -14
  163. package/pipeline/skills/shared/external/hig-components-content/SKILL.md +13 -13
  164. package/pipeline/skills/shared/external/hig-components-layout/SKILL.md +16 -16
  165. package/pipeline/skills/shared/external/hig-components-status/SKILL.md +6 -6
  166. package/pipeline/skills/shared/external/hig-components-system/SKILL.md +13 -13
  167. package/pipeline/skills/shared/external/hig-foundations/SKILL.md +23 -23
  168. package/pipeline/skills/shared/external/hig-inputs/SKILL.md +18 -18
  169. package/pipeline/skills/shared/external/hig-patterns/SKILL.md +30 -30
  170. package/pipeline/skills/shared/external/hig-platforms/SKILL.md +11 -11
  171. package/pipeline/skills/shared/external/hig-technologies/SKILL.md +33 -33
  172. package/pipeline/skills/shared/external/ios-coding-standard/references/STANDARD.md +52 -52
  173. package/pipeline/skills/shared/external/ios-coding-standard/references/lint-local.sh +1 -1
  174. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +11 -11
  175. package/pipeline/skills/shared/external/ios-developer/SKILL.md +0 -1
  176. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +7 -3
  177. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +9 -15
  178. package/pipeline/skills/shared/external/macos-spm-app-packaging/SKILL.md +0 -5
  179. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/bootstrap/Package.swift +17 -0
  180. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/bootstrap/Sources/MyApp/Resources/.keep +0 -0
  181. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/bootstrap/Sources/MyApp/main.swift +11 -0
  182. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/bootstrap/version.env +2 -0
  183. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/build_icon.sh +49 -0
  184. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/compile_and_run.sh +63 -0
  185. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/launch.sh +28 -0
  186. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/make_appcast.sh +82 -0
  187. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +206 -0
  188. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +52 -0
  189. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +52 -0
  190. package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/version.env +2 -0
  191. package/pipeline/skills/shared/external/macos-spm-app-packaging/references/packaging.md +17 -0
  192. package/pipeline/skills/shared/external/macos-spm-app-packaging/references/release.md +32 -0
  193. package/pipeline/skills/shared/external/macos-spm-app-packaging/references/scaffold.md +79 -0
  194. package/pipeline/skills/shared/external/monorepo-architect/SKILL.md +0 -1
  195. package/pipeline/skills/shared/external/nodejs-backend-patterns/SKILL.md +0 -4
  196. package/pipeline/skills/shared/external/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
  197. package/pipeline/skills/shared/external/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
  198. package/pipeline/skills/shared/external/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
  199. package/pipeline/skills/shared/external/swiftui-performance-audit/references/code-smells.md +150 -0
  200. package/pipeline/skills/shared/external/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
  201. package/pipeline/skills/shared/external/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
  202. package/pipeline/skills/shared/external/swiftui-performance-audit/references/profiling-intake.md +44 -0
  203. package/pipeline/skills/shared/external/swiftui-performance-audit/references/report-template.md +47 -0
  204. package/pipeline/skills/shared/external/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
  205. package/pipeline/skills/shared/external/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
  206. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/app-wiring.md +201 -0
  207. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/async-state.md +96 -0
  208. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/components-index.md +46 -0
  209. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/controls.md +57 -0
  210. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/deeplinks.md +66 -0
  211. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/focus.md +90 -0
  212. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/form.md +97 -0
  213. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/grids.md +71 -0
  214. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/haptics.md +71 -0
  215. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  216. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  217. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/list.md +86 -0
  218. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  219. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/macos-settings.md +71 -0
  220. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  221. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/media.md +73 -0
  222. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/menu-bar.md +101 -0
  223. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/navigationstack.md +159 -0
  224. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/overlay.md +45 -0
  225. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/performance.md +62 -0
  226. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/previews.md +48 -0
  227. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  228. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/scrollview.md +87 -0
  229. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/searchable.md +71 -0
  230. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/sheets.md +155 -0
  231. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/split-views.md +72 -0
  232. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/tabview.md +114 -0
  233. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/theming.md +71 -0
  234. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/title-menus.md +93 -0
  235. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/top-bar.md +49 -0
  236. package/pipeline/skills/shared/external/swiftui-view-refactor/references/mv-patterns.md +161 -0
  237. package/pipeline/skills/skills-index.md +8 -8
  238. package/pipeline/skills/shared/external/help-skills/SKILL.md +0 -166
@@ -67,13 +67,15 @@ ISSUE_ID=""
67
67
  SESSION_ID=""
68
68
  TIMEOUT="${FIREBASE_TIMEOUT_SECONDS:-20}"
69
69
 
70
+ need_value() { [ $# -ge 2 ] || { echo "ERR: $1 needs a value" >&2; exit 4; }; }
71
+
70
72
  while [ $# -gt 0 ]; do
71
73
  case "$1" in
72
- --project) PROJECT_ID="$2"; shift 2 ;;
73
- --platform) PLATFORM="$2"; shift 2 ;;
74
- --bundle) BUNDLE="$2"; shift 2 ;;
75
- --issue-id) ISSUE_ID="$2"; shift 2 ;;
76
- --session-id) SESSION_ID="$2"; shift 2 ;;
74
+ --project) need_value "$@"; PROJECT_ID="$2"; shift 2 ;;
75
+ --platform) need_value "$@"; PLATFORM="$2"; shift 2 ;;
76
+ --bundle) need_value "$@"; BUNDLE="$2"; shift 2 ;;
77
+ --issue-id) need_value "$@"; ISSUE_ID="$2"; shift 2 ;;
78
+ --session-id) need_value "$@"; SESSION_ID="$2"; shift 2 ;;
77
79
  -h|--help)
78
80
  echo "usage: $0 <issue-url> | $0 --project ... --platform ... --bundle ... --issue-id ... [--session-id ...]" >&2
79
81
  exit 4 ;;
@@ -234,6 +236,7 @@ fi
234
236
  # 1. Build header + claim, base64url
235
237
  # 2. Sign with the SA private key
236
238
  # 3. POST to oauth2.googleapis.com/token
239
+ TOKEN_RC=0
237
240
  ACCESS_TOKEN=$(SA_JSON_IN="$SA_JSON" python3 - <<'PY'
238
241
  import base64, json, os, sys, time, urllib.parse, urllib.request
239
242
 
@@ -302,10 +305,10 @@ except Exception as e:
302
305
  print("", file=sys.stderr)
303
306
  sys.exit(2)
304
307
  PY
305
- )
308
+ ) || TOKEN_RC=$?
306
309
 
307
- if [ -z "$ACCESS_TOKEN" ]; then
308
- echo "ERR: Firebase token exchange failed" >&2
310
+ if [ "$TOKEN_RC" -ne 0 ] || [ -z "$ACCESS_TOKEN" ]; then
311
+ echo "ERR: Firebase token exchange failed (exchange rc=$TOKEN_RC)" >&2
309
312
  exit 3
310
313
  fi
311
314
 
@@ -31,7 +31,7 @@
31
31
  # }
32
32
  #
33
33
  # Exit codes:
34
- # 0 success (may be degraded-empty for a pdf with no converter)
34
+ # 0 success
35
35
  # 3 download or read failure
36
36
  # 4 bad usage / unsupported extension
37
37
  # 6 converter not available (pdf only; the orchestrator treats this as
@@ -89,13 +89,15 @@ BATCH=4
89
89
  log() { printf '%s\n' "$*" >&2; }
90
90
  die() { local code="$1"; shift; log "fetch-figma-annotations: $*"; exit "$code"; }
91
91
 
92
+ need_value() { [ $# -ge 2 ] || die 1 "$1 requires a value"; }
93
+
92
94
  while [ $# -gt 0 ]; do
93
95
  case "$1" in
94
- --file-key) FILE_KEY="${2:-}"; shift 2 ;;
95
- --node-id) NODE_IDS="${2:-}"; shift 2 ;;
96
- --url) URL="${2:-}"; shift 2 ;;
97
- --lang-prefixes) LANG_PREFIXES="${2:-TR,EN}"; shift 2 ;;
98
- --batch) BATCH="${2:-4}"; shift 2 ;;
96
+ --file-key) need_value "$@"; FILE_KEY="$2"; shift 2 ;;
97
+ --node-id) need_value "$@"; NODE_IDS="$2"; shift 2 ;;
98
+ --url) need_value "$@"; URL="$2"; shift 2 ;;
99
+ --lang-prefixes) need_value "$@"; LANG_PREFIXES="${2:-TR,EN}"; shift 2 ;;
100
+ --batch) need_value "$@"; BATCH="${2:-4}"; shift 2 ;;
99
101
  -h|--help) grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'; exit 0 ;;
100
102
  *) die 1 "unknown argument: $1" ;;
101
103
  esac
@@ -62,11 +62,13 @@ ISSUE_ID=""
62
62
  TIMEOUT="${FORTIFY_TIMEOUT_SECONDS:-25}"
63
63
  AUTH_SCHEME="${FORTIFY_AUTH_SCHEME:-auto}"
64
64
 
65
+ need_value() { [ $# -ge 2 ] || { echo "ERR: $1 needs a value" >&2; exit 4; }; }
66
+
65
67
  while [ $# -gt 0 ]; do
66
68
  case "$1" in
67
- --version-id) VERSION_ID="$2"; shift 2 ;;
68
- --issue) ISSUE_ID="$2"; shift 2 ;;
69
- --instance-id) ISSUE_ID="$2"; shift 2 ;;
69
+ --version-id) need_value "$@"; VERSION_ID="$2"; shift 2 ;;
70
+ --issue) need_value "$@"; ISSUE_ID="$2"; shift 2 ;;
71
+ --instance-id) need_value "$@"; ISSUE_ID="$2"; shift 2 ;;
70
72
  -h|--help)
71
73
  echo "usage: $0 <ssc-url> | $0 --version-id <id> [--issue <id>] | $0 --instance-id <id>" >&2
72
74
  exit 4 ;;
@@ -73,11 +73,13 @@ TIMEOUT="${GRAYLOG_TIMEOUT_SECONDS:-25}"
73
73
  RANGE="${GRAYLOG_RANGE_SECONDS:-86400}"
74
74
  LIMIT="${GRAYLOG_LIMIT:-200}"
75
75
 
76
+ need_value() { [ $# -ge 2 ] || { echo "ERR: $1 needs a value" >&2; exit 4; }; }
77
+
76
78
  while [ $# -gt 0 ]; do
77
79
  case "$1" in
78
- --trx) TRX_ID="$2"; shift 2 ;;
79
- --conv) CONV_ID="$2"; shift 2 ;;
80
- --env) ENVIRONMENT="$2"; shift 2 ;;
80
+ --trx) need_value "$@"; TRX_ID="$2"; shift 2 ;;
81
+ --conv) need_value "$@"; CONV_ID="$2"; shift 2 ;;
82
+ --env) need_value "$@"; ENVIRONMENT="$2"; shift 2 ;;
81
83
  -h|--help)
82
84
  echo "usage: $0 --trx <transaction-id> | --conv <conversation-id> [--env auto|prod|test]" >&2
83
85
  exit 4 ;;
@@ -85,7 +85,7 @@ refresh_grant_cfg() {
85
85
 
86
86
  RESPONSE=$(curl -s --max-time 20 -X POST "$TOKEN_ENDPOINT" \
87
87
  -H "Content-Type: application/x-www-form-urlencoded" \
88
- -K <(refresh_grant_cfg))
88
+ -K <(refresh_grant_cfg)) || RESPONSE=""
89
89
 
90
90
  NEW_ACCESS=$(printf '%s' "$RESPONSE" | jq -r '.access_token // empty' 2>/dev/null || true)
91
91
  NEW_REFRESH=$(printf '%s' "$RESPONSE" | jq -r '.refresh_token // empty' 2>/dev/null || true)
@@ -81,6 +81,8 @@ if ! command -v resolve_figma_token >/dev/null 2>&1; then
81
81
  fi
82
82
 
83
83
  FIGMA_API="https://api.figma.com/v1"
84
+ WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/figma-screenshot.XXXXXX")
85
+ trap 'rm -rf "$WORK_DIR"' EXIT INT TERM
84
86
  PARALLEL_DL=4
85
87
  HTTP_TIMEOUT=60
86
88
  MAX_IDS_PER_CALL=50
@@ -126,7 +128,8 @@ Options:
126
128
  hold the other channel's design" before a run may write that a
127
129
  channel has none. Emits JSON, downloads nothing.
128
130
  --feature-slug Feature name to match against, for --discover-sections.
129
- --output-dir Output directory. Default: /tmp/figma-screenshots-<ts>.
131
+ --output-dir Output directory. Default: a fresh mktemp dir
132
+ ($TMPDIR/figma-screenshots.XXXXXX).
130
133
  --scale Render scale (1-4). Default: 2.
131
134
  --format Image format. Default: png.
132
135
  --help Show this help text and exit.
@@ -256,7 +259,7 @@ fetch_nodes_metadata() {
256
259
  fetch_image_urls() {
257
260
  local ids="$1"
258
261
  local tmp
259
- tmp=$(mktemp)
262
+ tmp=$(mktemp "$WORK_DIR/images.XXXXXX")
260
263
  local url="$FIGMA_API/images/$FILE_KEY?ids=$ids&format=$FORMAT&scale=$SCALE"
261
264
  local code
262
265
  code=$(curl_figma "$tmp" "$url")
@@ -281,7 +284,7 @@ render_and_download() {
281
284
 
282
285
  # Emit a job list (nodeId<TAB>url<TAB>outPath) for parallel download.
283
286
  local jobs
284
- jobs=$(mktemp)
287
+ jobs=$(mktemp "$WORK_DIR/jobs.XXXXXX")
285
288
  local key_prefix="${FILE_KEY:0:4}"
286
289
  printf '%s' "$urls_json" | jq -r 'to_entries[] | "\(.key)\t\(.value)"' \
287
290
  | while IFS=$'\t' read -r nid url; do
@@ -292,7 +295,7 @@ render_and_download() {
292
295
 
293
296
  # Parallel download. xargs -P keeps the channel count bounded.
294
297
  local results
295
- results=$(mktemp)
298
+ results=$(mktemp "$WORK_DIR/results.XXXXXX")
296
299
  # shellcheck disable=SC2016
297
300
  awk -F'\t' '{printf "%s\037%s\037%s\n", $1,$2,$3}' "$jobs" \
298
301
  | xargs -P "$PARALLEL_DL" -I{} bash -c '
@@ -389,17 +392,19 @@ if [ "$#" -eq 0 ]; then
389
392
  exit 1
390
393
  fi
391
394
 
395
+ need_value() { [ "$#" -ge 2 ] || die 1 "$1 requires a value"; }
396
+
392
397
  while [ "$#" -gt 0 ]; do
393
398
  case "$1" in
394
399
  --help|-h) print_help; exit 0 ;;
395
- --file-key) FILE_KEY="${2:-}"; shift 2 ;;
396
- --node-id) NODE_ID="${2:-}"; shift 2 ;;
397
- --section) SECTION_URL="${2:-}"; shift 2 ;;
400
+ --file-key) need_value "$@"; FILE_KEY="$2"; shift 2 ;;
401
+ --node-id) need_value "$@"; NODE_ID="$2"; shift 2 ;;
402
+ --section) need_value "$@"; SECTION_URL="$2"; shift 2 ;;
398
403
  --discover-sections) DISCOVER=1; shift ;;
399
- --feature-slug) FEATURE_SLUG="${2:-}"; shift 2 ;;
400
- --output-dir) OUTPUT_DIR="${2:-}"; shift 2 ;;
401
- --scale) SCALE="${2:-2}"; shift 2 ;;
402
- --format) FORMAT="${2:-png}"; shift 2 ;;
404
+ --feature-slug) need_value "$@"; FEATURE_SLUG="$2"; shift 2 ;;
405
+ --output-dir) need_value "$@"; OUTPUT_DIR="$2"; shift 2 ;;
406
+ --scale) need_value "$@"; SCALE="${2:-2}"; shift 2 ;;
407
+ --format) need_value "$@"; FORMAT="${2:-png}"; shift 2 ;;
403
408
  *) die 1 "unknown argument: $1" ;;
404
409
  esac
405
410
  done
@@ -431,7 +436,7 @@ if [ -z "${FIGMA_TOKEN:-}" ]; then
431
436
  fi
432
437
 
433
438
  if [ -z "$OUTPUT_DIR" ]; then
434
- OUTPUT_DIR="/tmp/figma-screenshots-$(date +%Y%m%d-%H%M%S)"
439
+ OUTPUT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/figma-screenshots.XXXXXX")
435
440
  fi
436
441
  mkdir -p "$OUTPUT_DIR"
437
442
 
@@ -444,14 +449,12 @@ mkdir -p "$OUTPUT_DIR"
444
449
  # "Elite Kart" on one section and "Elit Kart" on its sibling.
445
450
 
446
451
  run_discover_mode() {
447
- local body
448
- # Same as fetch_with_retry: the token goes through a curl config on a process
449
- # substitution, never argv, because argv is readable from ps.
450
- body=$(curl -sS --max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
451
- -K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
452
- "https://api.figma.com/v1/files/${FILE_KEY}?depth=3") || die 3 "file tree fetch failed"
453
-
454
- printf '%s' "$body" | FEATURE_SLUG="$FEATURE_SLUG" FILE_KEY="$FILE_KEY" python3 -c '
452
+ local tree code
453
+ tree=$(mktemp "$WORK_DIR/tree.XXXXXX")
454
+ code=$(curl_figma "$tree" "$FIGMA_API/files/${FILE_KEY}?depth=3")
455
+ handle_http_code "$code" "file tree for $FILE_KEY"
456
+
457
+ FEATURE_SLUG="$FEATURE_SLUG" FILE_KEY="$FILE_KEY" python3 -c '
455
458
  import json, os, re, sys, unicodedata
456
459
 
457
460
  def fold(t):
@@ -475,11 +478,11 @@ def channel_of(name):
475
478
  return label
476
479
  return None
477
480
 
478
- raw = sys.stdin.read()
481
+ raw = open(sys.argv[1]).read()
479
482
  try:
480
483
  doc = json.loads(raw).get("document", {})
481
484
  except Exception:
482
- print(json.dumps({"error": "unparseable file response"})); sys.exit(3)
485
+ print(json.dumps({"error": "unparseable file response"})); sys.exit(6)
483
486
 
484
487
  slug_words = [w for w in fold(os.environ["FEATURE_SLUG"]).split() if len(w) > 2]
485
488
  out, weak, pages = [], [], 0
@@ -516,7 +519,7 @@ print(json.dumps({
516
519
  "candidates": sorted(out, key=lambda c: (c["channelGuess"] or "zz", c["id"])),
517
520
  "weakMatches": sorted(weak, key=lambda c: c["id"]),
518
521
  }, ensure_ascii=False, indent=2))
519
- '
522
+ ' "$tree"
520
523
  }
521
524
 
522
525
  # --- Section mode -----------------------------------------------------------
@@ -570,7 +573,7 @@ PY
570
573
 
571
574
  # Collect ids and chunk to MAX_IDS_PER_CALL.
572
575
  local ids_file
573
- ids_file=$(mktemp)
576
+ ids_file=$(mktemp "$WORK_DIR/ids.XXXXXX")
574
577
  awk -F'\t' '{print $1}' "$lookup" > "$ids_file"
575
578
 
576
579
  local rows
@@ -31,8 +31,9 @@
31
31
  # An unset or non-executable CRED_STORE is tolerated: resolution falls through to
32
32
  # the env fallback so a keychain-less CI box still works.
33
33
 
34
- # The canonical key first, the legacy key second. Space-separated so the loop
35
- # below stays POSIX-ish and works under bash 3.2 (stock macOS).
34
+ # The canonical key first, the legacy key second. Space-separated; the loop
35
+ # below splits explicitly because zsh (the pipeline's Bash tool) does not
36
+ # word-split an unquoted parameter, so `for key in $KEYS` saw one key there.
36
37
  FIGMA_TOKEN_LOGICAL_KEYS="${FIGMA_TOKEN_LOGICAL_KEYS:-figma figma_pat}"
37
38
 
38
39
  # resolve_figma_token
@@ -40,13 +41,16 @@ FIGMA_TOKEN_LOGICAL_KEYS="${FIGMA_TOKEN_LOGICAL_KEYS:-figma figma_pat}"
40
41
  resolve_figma_token() {
41
42
  local key tok
42
43
  if [ -n "${CRED_STORE:-}" ] && [ -x "${CRED_STORE:-}" ]; then
43
- for key in $FIGMA_TOKEN_LOGICAL_KEYS; do
44
+ while IFS= read -r key; do
45
+ [ -n "$key" ] || continue
44
46
  tok=$("$CRED_STORE" get "$key" 2>/dev/null || true)
45
47
  if [ -n "$tok" ]; then
46
48
  printf '%s' "$tok"
47
49
  return 0
48
50
  fi
49
- done
51
+ done <<EOF_KEYS
52
+ $(printf '%s\n' "$FIGMA_TOKEN_LOGICAL_KEYS" | tr ' ' '\n')
53
+ EOF_KEYS
50
54
  fi
51
55
  if [ -n "${FIGMA_PAT:-}" ]; then
52
56
  printf '%s' "$FIGMA_PAT"
@@ -43,7 +43,6 @@
43
43
  # Required env:
44
44
  # ACCOUNT_JIRA_TOKEN_KEY keychain service name (optional)
45
45
  # ACCOUNT_JIRA_HOST e.g. jira.example.com (optional)
46
- # ACCOUNT_GH_TOKEN_KEY keychain service name (optional)
47
46
  # ACCOUNT_DEFAULT_OWNER fallback GitHub owner (optional)
48
47
  # ACCOUNT_DEFAULT_REPO fallback GitHub repo (optional)
49
48
  # PROMPT_LANG tr|en - controls maturity.summary language (default: tr)
@@ -54,13 +54,15 @@ BACKUP_DIR="${JIRA_BACKUP_DIR:-$HOME/.claude/logs/multi-agent/jira-backups}"
54
54
 
55
55
  die() { echo "$1" >&2; exit "${2:-64}"; }
56
56
 
57
+ need_value() { [ "$#" -ge 2 ] || die "$1 requires a value"; }
58
+
57
59
  while [ "$#" -gt 0 ]; do
58
60
  case "$1" in
59
- --issue) ISSUE="${2:-}"; shift 2 ;;
60
- --body-file) BODY_FILE="${2:-}"; shift 2 ;;
61
- --target) TARGET="${2:-}"; shift 2 ;;
62
- --mode) MODE="${2:-}"; shift 2 ;;
63
- --backup-dir) BACKUP_DIR="${2:-}"; shift 2 ;;
61
+ --issue) need_value "$@"; ISSUE="$2"; shift 2 ;;
62
+ --body-file) need_value "$@"; BODY_FILE="$2"; shift 2 ;;
63
+ --target) need_value "$@"; TARGET="$2"; shift 2 ;;
64
+ --mode) need_value "$@"; MODE="$2"; shift 2 ;;
65
+ --backup-dir) need_value "$@"; BACKUP_DIR="$2"; shift 2 ;;
64
66
  --confirm-overwrite) CONFIRM=1; shift ;;
65
67
  --dry-run) DRY_RUN=1; shift ;;
66
68
  -h|--help) sed -n '2,40p' "$0"; exit 0 ;;
@@ -69,6 +69,11 @@ PUNCT_MAP_DEFAULT_TR = {
69
69
  PUNCT_MAP_EN = dict(PUNCT_MAP_DEFAULT_TR)
70
70
  PUNCT_MAP_EN["\u00A7"] = "section"
71
71
 
72
+ # Typographic dashes (U+2010 to U+2015), quotes (U+2018 to U+201F), the
73
+ # ellipsis and the section sign, including the variants the maps above do
74
+ # not rewrite, so a residual can actually be reported.
75
+ FORBIDDEN_PUNCT_RE = re.compile("[\u2010-\u2015\u2018-\u201F\u2026\u00A7]")
76
+
72
77
 
73
78
  def detect_language(markdown: str, front_matter_lang: str | None) -> str:
74
79
  if front_matter_lang:
@@ -76,12 +81,11 @@ def detect_language(markdown: str, front_matter_lang: str | None) -> str:
76
81
  if lang.startswith("en"):
77
82
  return "en"
78
83
  return "tr"
79
- # Heuristic: presence of TR-only characters in headings
80
- for line in markdown.splitlines():
81
- if line.startswith("#"):
82
- if re.search(r"[ğışçöü]", line):
83
- return "tr"
84
- return "tr"
84
+ # Heuristic: a Turkish document carries Turkish-only letters somewhere in
85
+ # its body; a text without a single one is English.
86
+ if re.search(r"[ğışçöüĞİŞÇÖÜ]", markdown):
87
+ return "tr"
88
+ return "en"
85
89
 
86
90
 
87
91
  def apply_punctuation_gate(text: str, language: str) -> tuple[str, list[str]]:
@@ -91,7 +95,9 @@ def apply_punctuation_gate(text: str, language: str) -> tuple[str, list[str]]:
91
95
  out = out.replace(src, dst)
92
96
 
93
97
  warnings: list[str] = []
94
- leftover = [ch for ch in mapping.keys() if ch in out]
98
+ # The residual check must look wider than the mapping: every mapped
99
+ # character was just replaced, so testing those again can never fire.
100
+ leftover = sorted(set(FORBIDDEN_PUNCT_RE.findall(out)))
95
101
  if leftover:
96
102
  warnings.append(
97
103
  "punctuation gate: residual forbidden characters: "
@@ -33,7 +33,7 @@ set -euo pipefail
33
33
  CMD="${1:-}"
34
34
  STATE="${2:-}"
35
35
  [ -z "$CMD" ] || [ -z "$STATE" ] && {
36
- echo "usage: $0 <prepare|commit|push|pr|teardown> <state.json>" >&2
36
+ echo "usage: $0 <prepare|bridge|commit|push|pr|teardown> <state.json>" >&2
37
37
  exit 1
38
38
  }
39
39
  [ ! -f "$STATE" ] && { echo "ERR: state file not found: $STATE" >&2; exit 2; }
@@ -54,6 +54,8 @@ TASK_ID=$(python3 -c "import json; d=json.load(open('$STATE')); print(d['taskId'
54
54
  COMMIT_TYPE=$(python3 -c "import json; d=json.load(open('$STATE')); print(d.get('commitType','feat'))")
55
55
  ISSUE_KEY=$(python3 -c "import json; d=json.load(open('$STATE')); print(d['issueRef'].get('key') or '')")
56
56
  ISSUE_TITLE=$(python3 -c "import json; d=json.load(open('$STATE')); print(d['issueRef'].get('title') or '')")
57
+ IDENTITY_NAME=$(python3 -c "import json; d=json.load(open('$STATE')); print((d.get('identity') or {}).get('name') or '')")
58
+ IDENTITY_EMAIL=$(python3 -c "import json; d=json.load(open('$STATE')); print((d.get('identity') or {}).get('email') or '')")
57
59
 
58
60
  cmd_prepare() {
59
61
  mkdir -p "$WORKTREE_ROOT"
@@ -90,24 +92,32 @@ cmd_commit() {
90
92
  # loop - a piped `while` runs in a subshell and the summary exit would
91
93
  # always be 0 regardless of per-repo failures.
92
94
  local failed=0
95
+ # state.identity is the commit author/committer, as documented in the schema
96
+ # above. Git's own config is the fallback when the picker recorded none.
97
+ local identity_args=()
98
+ if [ -n "$IDENTITY_NAME" ] && [ -n "$IDENTITY_EMAIL" ]; then
99
+ identity_args=(-c "user.name=$IDENTITY_NAME" -c "user.email=$IDENTITY_EMAIL")
100
+ fi
93
101
  while IFS=$'\t' read -r name url provider; do
94
102
  local target="$WORKTREE_ROOT/$name"
95
103
  [ ! -d "$target" ] && continue
96
104
  cd "$target"
97
- # Use status --porcelain so untracked files count as "changes" too -
98
- # dev sessions usually add brand-new files alongside edits, and `git diff`
99
- # alone misses them.
100
- if [ -z "$(git status --porcelain 2>/dev/null)" ]; then
101
- echo "[$name] no changes, skipping commit"
105
+ # Only what the dev session staged is committed. A blanket `git add -A`
106
+ # here swept build output and nested-worktree gitlinks into the commit.
107
+ if git diff --cached --quiet 2>/dev/null; then
108
+ if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
109
+ echo "[$name] nothing staged (unstaged or untracked changes present) - stage what belongs in the commit, skipping"
110
+ else
111
+ echo "[$name] no changes, skipping commit"
112
+ fi
102
113
  cd - >/dev/null
103
114
  continue
104
115
  fi
105
- git add -A
106
116
  local subject="$COMMIT_TYPE($name): $ISSUE_TITLE"
107
117
  [ -n "$ISSUE_KEY" ] && subject="$subject [$ISSUE_KEY]"
108
118
  # Trim subject to 72 chars
109
119
  subject=$(printf '%s' "$subject" | cut -c1-72)
110
- if git commit --quiet -m "$subject"; then
120
+ if git ${identity_args[@]+"${identity_args[@]}"} commit --quiet -m "$subject"; then
111
121
  echo "[$name] committed: $subject"
112
122
  else
113
123
  echo "ERR: [$name] commit failed" >&2
@@ -155,10 +155,19 @@ do_start() {
155
155
  write_state "$state_path" "$new"
156
156
  }
157
157
 
158
+ require_todo() {
159
+ # $1 state path, $2 todo_id - exits 1 when the id is not in the plan.
160
+ if ! jq -e --arg id "$2" '(.plan.todos // []) | map(select(.id == $id)) | length > 0' "$1" >/dev/null 2>&1; then
161
+ echo "plan-todos: todo '$2' not found" >&2
162
+ exit 1
163
+ fi
164
+ }
165
+
158
166
  do_complete() {
159
167
  local task_id="${1:-}"; local todo_id="${2:-}"; local notes="${3:-}"
160
168
  [ -z "$task_id" ] || [ -z "$todo_id" ] && { echo "usage: complete <task-id> <todo-id> [notes]" >&2; exit 64; }
161
169
  local state_path; state_path=$(resolve_state "$task_id")
170
+ require_todo "$state_path" "$todo_id"
162
171
  local now; now=$(iso_now)
163
172
  local new
164
173
  new=$(jq --arg id "$todo_id" --arg t "$now" --arg n "$notes" '
@@ -176,6 +185,7 @@ do_fail() {
176
185
  local task_id="${1:-}"; local todo_id="${2:-}"; local reason="${3:-}"
177
186
  [ -z "$task_id" ] || [ -z "$todo_id" ] || [ -z "$reason" ] && { echo "usage: fail <task-id> <todo-id> <reason>" >&2; exit 64; }
178
187
  local state_path; state_path=$(resolve_state "$task_id")
188
+ require_todo "$state_path" "$todo_id"
179
189
  local now; now=$(iso_now)
180
190
  local new
181
191
  new=$(jq --arg id "$todo_id" --arg t "$now" --arg r "$reason" '
@@ -190,6 +200,7 @@ do_skip() {
190
200
  local task_id="${1:-}"; local todo_id="${2:-}"; local reason="${3:-}"
191
201
  [ -z "$task_id" ] || [ -z "$todo_id" ] || [ -z "$reason" ] && { echo "usage: skip <task-id> <todo-id> <reason>" >&2; exit 64; }
192
202
  local state_path; state_path=$(resolve_state "$task_id")
203
+ require_todo "$state_path" "$todo_id"
193
204
  local now; now=$(iso_now)
194
205
  local new
195
206
  new=$(jq --arg id "$todo_id" --arg t "$now" --arg r "$reason" '
@@ -408,9 +408,16 @@ post_bitbucket_server() {
408
408
  needs_work) status_payload='{"status":"NEEDS_WORK"}' ;;
409
409
  esac
410
410
 
411
- curl -sS -m 30 -K <(bb_auth_cfg "$auth_user" "$auth_token") -X PUT \
411
+ local http
412
+ http=$(curl -sS -m 30 -K <(bb_auth_cfg "$auth_user" "$auth_token") -X PUT \
412
413
  -H "Content-Type: application/json" "$base/participants/$user_slug" \
413
- -d "$status_payload" >/dev/null
414
+ -d "$status_payload" -o /dev/null -w '%{http_code}' || echo 000)
415
+ case "$http" in
416
+ 200|201) ;;
417
+ *)
418
+ echo "post-pr-review: bitbucket verdict PUT failed (HTTP $http) for PR #$PR_NUM - $DECISION was NOT recorded" >&2
419
+ return 4 ;;
420
+ esac
414
421
  }
415
422
 
416
423
  # --- Main switch ------------------------------------------------------------
@@ -18,12 +18,13 @@
18
18
  # ./repo-cache.sh bitbucket <workspace> [--refresh]
19
19
  #
20
20
  # Required env per-provider:
21
- # github -> GH_TOKEN_KEY (keychain service)
21
+ # github -> (none) - uses the gh CLI's own auth; GH_USER selects the account
22
22
  # bitbucket -> BB_TOKEN_KEY, BB_USER_KEY, BB_HOST (e.g. bitbucket.example.com)
23
23
  # local -> (none) - scopes are treated as filesystem roots; "scope=$HOME"
24
24
  # walks $HOME with maxdepth, scope=<path> walks just that path.
25
- # LOCAL_SCAN_ROOTS env can override the default root list.
26
- # LOCAL_SCAN_MAXDEPTH env (default 4) bounds traversal cost.
25
+ # LOCAL_SCAN_ROOTS env (colon-separated, like PATH) overrides the
26
+ # default root list. LOCAL_SCAN_MAXDEPTH env (default 4) bounds
27
+ # traversal cost.
27
28
 
28
29
  set -euo pipefail
29
30
 
@@ -211,16 +212,23 @@ print(json.dumps(out))
211
212
  local)
212
213
  # Scan filesystem roots for git repos. Default roots cover the common
213
214
  # places personal/work checkouts live; override via LOCAL_SCAN_ROOTS.
214
- DEFAULT_ROOTS="$HOME $HOME/Documents $HOME/Developer $HOME/projects $HOME/code $HOME/work"
215
- ROOTS="${LOCAL_SCAN_ROOTS:-$DEFAULT_ROOTS}"
216
- MAXDEPTH="${LOCAL_SCAN_MAXDEPTH:-4}"
217
- # When SCOPE is an absolute path use it directly; otherwise treat SCOPE
218
- # as a label and walk the configured roots.
215
+ # Roots are held in an array, never a whitespace-split string: a HOME with
216
+ # a space in it split into two nonexistent roots and the scan found nothing.
217
+ ROOTS=()
219
218
  if [ -d "$SCOPE" ]; then
220
- ROOTS="$SCOPE"
219
+ ROOTS=("$SCOPE")
220
+ elif [ -n "${LOCAL_SCAN_ROOTS:-}" ]; then
221
+ while IFS= read -r root; do
222
+ [ -n "$root" ] && ROOTS+=("$root")
223
+ done <<EOF_ROOTS
224
+ $(printf '%s\n' "$LOCAL_SCAN_ROOTS" | tr ':' '\n')
225
+ EOF_ROOTS
226
+ else
227
+ ROOTS=("$HOME" "$HOME/Documents" "$HOME/Developer" "$HOME/projects" "$HOME/code" "$HOME/work")
221
228
  fi
229
+ MAXDEPTH="${LOCAL_SCAN_MAXDEPTH:-4}"
222
230
  : > "$CACHE_TMP.list"
223
- for root in $ROOTS; do
231
+ for root in ${ROOTS[@]+"${ROOTS[@]}"}; do
224
232
  [ -d "$root" ] || continue
225
233
  # -prune avoids descending into already-found .git trees (nested submodules
226
234
  # show up via submodule-detector, not the top-level picker).
@@ -10,11 +10,16 @@
10
10
  # - Reads watched repos from prefs.global.reviewWatch.repos[] (or --repos).
11
11
  # - For each repo, lists open PRs since the last seen cursor (state in
12
12
  # ~/.claude/state/review-watch/<owner-repo>.json).
13
- # - For each PR not yet reviewed (or updated since last review), invokes
14
- # `bash $HOME/.claude/lib/post-pr-review.sh <task-id>` after writing the
15
- # PR input to agent-state.json. The reviewer dispatch itself is the
16
- # responsibility of /multi-agent:review's LLM loop; review-watch is just
17
- # the trigger.
13
+ # - For each PR not yet reviewed (or updated since last review), writes the
14
+ # PR input to a per-PR agent-state.json (see Storage) and prints the
15
+ # dispatch line. The review itself is /multi-agent:review's LLM loop,
16
+ # which fills `.review.findings` for that task id; review-watch never
17
+ # spends tokens.
18
+ # - On a later scan, a PR whose agent-state carries findings for the
19
+ # current head SHA is posted through post-pr-review.sh, and only a
20
+ # successful post stamps reviewed_prs[<n>] = <sha>. A PR is never marked
21
+ # reviewed without a review having been posted.
22
+ # REVIEW_WATCH_DRY_RUN=1 reports what would be posted and stamps nothing.
18
23
  # - Honors labelFilter (only review PRs carrying the label, default: all).
19
24
  # - Skips PRs the user authored (we don't review our own PRs through this
20
25
  # surface - that's what the in-pipeline Phase 4 / standalone /review is
@@ -36,13 +41,26 @@
36
41
  # ~/.claude/state/review-watch/<owner__repo>.json
37
42
  # { last_seen_iso: "2026-05-11T12:34:56Z",
38
43
  # reviewed_prs: { "1234": "head-sha-abc" } }
44
+ # ~/.claude/logs/multi-agent/review-watch/<owner__repo>-pr<n>/agent-state.json
45
+ # { taskId, review: { input: {...}, headCommitSha, findings? } }
46
+ # (task id <owner__repo>-pr<n>; post-pr-review.sh reads it by that id)
39
47
 
40
48
  set -euo pipefail
41
49
 
42
50
  PREFS="$HOME/.claude/multi-agent-preferences.json"
43
51
  STATE_DIR="$HOME/.claude/state/review-watch"
52
+ REVIEW_LOGS="$HOME/.claude/logs/multi-agent/review-watch"
44
53
  mkdir -p "$STATE_DIR"
45
54
 
55
+ POSTER=""
56
+ for _cand in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/post-pr-review.sh" \
57
+ "$HOME/.claude/lib/post-pr-review.sh" \
58
+ "$HOME/.copilot/lib/post-pr-review.sh" \
59
+ "$HOME/.codex/lib/post-pr-review.sh"; do
60
+ [ -f "$_cand" ] && { POSTER="$_cand"; break; }
61
+ done
62
+ unset _cand
63
+
46
64
  CMD="${1:-}"
47
65
  shift || true
48
66
 
@@ -192,16 +210,44 @@ review_one_repo() {
192
210
  continue
193
211
  fi
194
212
 
213
+ local task_id state_file
214
+ task_id="${repo//\//__}-pr${pr_num}"
215
+ state_file="$REVIEW_LOGS/$task_id/agent-state.json"
216
+
217
+ # Findings already written for THIS head by /multi-agent:review: post them.
218
+ # The stamp below is the only place reviewed_prs is written, and it is
219
+ # written only after post-pr-review.sh returned 0.
220
+ if [ -f "$state_file" ] \
221
+ && [ "$(jq -r '.review.headCommitSha // ""' "$state_file" 2>/dev/null)" = "$head_sha" ] \
222
+ && jq -e '.review.findings | type == "array"' "$state_file" >/dev/null 2>&1; then
223
+ if [ "${REVIEW_WATCH_DRY_RUN:-0}" = "1" ]; then
224
+ printf ' dry-run: would post the review for PR #%s (task %s) via post-pr-review.sh\n' "$pr_num" "$task_id"
225
+ elif [ -z "$POSTER" ]; then
226
+ err "post-pr-review.sh not found - PR #$pr_num stays pending"
227
+ elif bash "$POSTER" "$task_id"; then
228
+ printf ' posted review for PR #%s at %s\n' "$pr_num" "$head_sha"
229
+ state=$(jq --arg n "$pr_num" --arg s "$head_sha" \
230
+ '.reviewed_prs[$n] = $s' <<<"$state")
231
+ else
232
+ err "post-pr-review.sh failed for $repo PR #$pr_num - stays pending, not stamped"
233
+ fi
234
+ continue
235
+ fi
236
+
237
+ # No review for this head yet: write the input /multi-agent:review reads
238
+ # and hand off. review-watch does not run the LLM itself, because token
239
+ # cost decisions stay with the user; the PR stays un-stamped until a
240
+ # review is actually posted.
195
241
  printf ' dispatch review: %s PR #%s - %s\n' "$repo" "$pr_num" "$title"
196
- printf ' url=%s head=%s\n' "$url" "$head_sha"
197
-
198
- # Record that we reviewed this head SHA. The actual dispatch happens
199
- # via /multi-agent:review on the surface that invoked review-watch
200
- # (Claude Code session or future webhook handler). review-watch only
201
- # produces the dispatch list - it does not run the LLM itself, because
202
- # token cost decisions stay with the user.
203
- state=$(jq --arg n "$pr_num" --arg s "$head_sha" \
204
- '.reviewed_prs[$n] = $s' <<<"$state")
242
+ printf ' url=%s head=%s task=%s\n' "$url" "$head_sha" "$task_id"
243
+ mkdir -p "$(dirname "$state_file")"
244
+ jq -n --arg repo "$repo" --arg n "$pr_num" --arg sha "$head_sha" \
245
+ --arg url "$url" --arg title "$title" --arg t "$task_id" '
246
+ { schemaVersion: "2.1.0", taskId: $t,
247
+ review: { input: { kind: "pr", provider: "github", host: "github.com",
248
+ orgRepo: $repo, prNumber: ($n | tonumber),
249
+ url: $url, title: $title },
250
+ headCommitSha: $sha, iterationNumber: 1 } }' > "$state_file"
205
251
  done < <(jq -c '.[]' <<<"$prs_json")
206
252
 
207
253
  # Persist the NEWEST updatedAt seen this scan - the previous per-PR