rollout-cli 0.4.2__tar.gz → 0.5.1__tar.gz

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 (120) hide show
  1. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/ask-colleague/SKILL.md +13 -0
  2. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +46 -4
  3. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/mass-update/SKILL.md +8 -1
  4. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/mass-update/scripts/mass-update.sh +32 -4
  5. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/CHANGELOG.md +17 -0
  6. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/PKG-INFO +1 -1
  7. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/docs/skill-sources.md +1 -1
  8. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/pyproject.toml +1 -1
  9. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/ask-colleague-revendor/README.md +1 -0
  10. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/ask-colleague-revendor/files/SKILL.md.tmpl +13 -0
  11. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/ask-colleague-revendor/files/scripts/ask-colleague.sh +46 -4
  12. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/ask-colleague-revendor/manifest.toml +8 -6
  13. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_commands/apply.py +5 -1
  14. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/core/bump.py +17 -5
  15. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/sonar-project.properties +11 -0
  16. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_bump.py +13 -0
  17. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_mass_update.py +60 -0
  18. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/uv.lock +1 -1
  19. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/agent-config/SKILL.md +0 -0
  20. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  21. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/agent-config/scripts/show.sh +0 -0
  22. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  23. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  24. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  25. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  26. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  27. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/cicd/SKILL.md +0 -0
  28. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  29. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  30. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  31. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  32. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  33. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/communicate/SKILL.md +0 -0
  34. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  35. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  36. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  37. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  38. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  39. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  40. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  41. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  42. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  43. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  44. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/run-tests/SKILL.md +0 -0
  45. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/run-tests/scripts/test.sh +0 -0
  46. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/sonarclaude/SKILL.md +0 -0
  47. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  48. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  49. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  50. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/think/SKILL.md +0 -0
  51. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/think/scripts/think.sh +0 -0
  52. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/version-bump/SKILL.md +0 -0
  53. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills/version-bump/scripts/bump.py +0 -0
  54. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.claude/skills.local.yaml.example +0 -0
  55. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.devague/frames/mass-update-now-clones-each-target-repo-into-a-git.json +0 -0
  56. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.devague/frames/rollout-cli-ships-rollout-define-an-org-wide-chang.json +0 -0
  57. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.devague/plans/mass-update-now-clones-each-target-repo-into-a-git.json +0 -0
  58. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.devague/plans/rollout-cli-ships-rollout-define-an-org-wide-chang.json +0 -0
  59. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.flake8 +0 -0
  60. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.github/workflows/publish.yml +0 -0
  61. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.github/workflows/tests.yml +0 -0
  62. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.gitignore +0 -0
  63. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/.markdownlint-cli2.yaml +0 -0
  64. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/AGENTS.colleague.md +0 -0
  65. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/CLAUDE.md +0 -0
  66. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/LICENSE +0 -0
  67. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/README.md +0 -0
  68. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/culture.yaml +0 -0
  69. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/docs/plans/2026-06-06-rollout-cli-ships-rollout-define-an-org-wide-chang.md +0 -0
  70. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/docs/plans/2026-06-12-mass-update-now-clones-each-target-repo-into-a-git.md +0 -0
  71. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/docs/specs/2026-06-06-rollout-cli-ships-rollout-define-an-org-wide-chang.md +0 -0
  72. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/docs/specs/2026-06-12-mass-update-now-clones-each-target-repo-into-a-git.md +0 -0
  73. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/apache-relicense/apply.sh +0 -0
  74. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/apache-relicense/check.sh +0 -0
  75. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/apache-relicense/files/LICENSE +0 -0
  76. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/apache-relicense/manifest.toml +0 -0
  77. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/ask-colleague-revendor/apply.sh +0 -0
  78. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/ask-colleague-revendor/check.sh +0 -0
  79. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/ask-colleague-revendor/files/prompts/explore.md +0 -0
  80. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/ask-colleague-revendor/files/prompts/review.md +0 -0
  81. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/recipes/ask-colleague-revendor/files/prompts/write.md +0 -0
  82. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/__init__.py +0 -0
  83. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/__main__.py +0 -0
  84. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/__init__.py +0 -0
  85. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_commands/__init__.py +0 -0
  86. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_commands/cli.py +0 -0
  87. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_commands/doctor.py +0 -0
  88. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_commands/explain.py +0 -0
  89. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_commands/learn.py +0 -0
  90. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_commands/overview.py +0 -0
  91. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_commands/plan.py +0 -0
  92. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_commands/whoami.py +0 -0
  93. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_errors.py +0 -0
  94. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/cli/_output.py +0 -0
  95. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/core/__init__.py +0 -0
  96. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/core/devex_pr.py +0 -0
  97. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/core/gitops.py +0 -0
  98. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/core/orchestrate.py +0 -0
  99. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/core/preflight.py +0 -0
  100. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/core/recipe.py +0 -0
  101. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/core/report.py +0 -0
  102. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/core/targets.py +0 -0
  103. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/explain/__init__.py +0 -0
  104. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/rollout/explain/catalog.py +0 -0
  105. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/__init__.py +0 -0
  106. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_apache_relicense.py +0 -0
  107. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_apply_cmd.py +0 -0
  108. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_cli.py +0 -0
  109. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_cli_introspection.py +0 -0
  110. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_cli_wiring.py +0 -0
  111. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_devex_pr.py +0 -0
  112. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_e2e_apache_relicense.py +0 -0
  113. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_explain_catalog.py +0 -0
  114. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_gitops.py +0 -0
  115. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_orchestrate.py +0 -0
  116. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_plan_cmd.py +0 -0
  117. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_preflight.py +0 -0
  118. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_recipe.py +0 -0
  119. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_report.py +0 -0
  120. {rollout_cli-0.4.2 → rollout_cli-0.5.1}/tests/test_targets.py +0 -0
@@ -10,6 +10,7 @@ description: >
10
10
  hands off a small implementation, `ask-colleague feedback` grades a finished
11
11
  work item (the ROI loop), and `ask-colleague clean` reaps stale/corrupt
12
12
  `colleague/*` branches a crashed run left behind (which can break `git fetch`).
13
+ Pilot a running work item with `monitor`/`guide`/`stop`.
13
14
  Reach for it REFLEXIVELY, the way you'd lean over to the
14
15
  teammate at the next desk — not only when asked: before you present or open a PR
15
16
  on a non-trivial committed diff, run `review` for a diverse second opinion; for a
@@ -154,6 +155,18 @@ keeping stdout valid JSON for a machine consumer while diagnostics stay on stder
154
155
  (or `colleague clean`) to reap it — start with `--dry-run` to see what it would
155
156
  remove. It only ever touches `colleague/*` refs and `.colleague/` artifacts.
156
157
 
158
+ ## Piloting a flight
159
+
160
+ Dispatch a drive with `--watch` (on `explore`, `review`, or `write`) to make the
161
+ work item watchable. While it runs you can:
162
+
163
+ - **`ask-colleague monitor <task-id>`** — watch the flight's live feed
164
+ - **`ask-colleague guide <task-id> "<message>"`** — send mid-flight guidance
165
+ - **`ask-colleague stop <task-id>`** — cooperatively ask the flight to stop
166
+
167
+ Control is applied at the running loop's next turn boundary, so guidance and
168
+ stop requests take effect on the next iteration rather than interrupting mid-step.
169
+
157
170
  ## Hard rules (do not violate)
158
171
 
159
172
  - **explore and review are read-only.** They run in a throwaway `git worktree`
@@ -84,6 +84,9 @@ Usage:
84
84
  ask-colleague feedback <id|last> [--rating N] Grade a past drive (ROI loop); with --rating records, without shows
85
85
  ask-colleague feedback list List every recorded drive by request + grade (find one by its request)
86
86
  ask-colleague clean [--dry-run] Reap stale/corrupt colleague/* branches + orphaned .colleague/ artifacts (#162)
87
+ ask-colleague monitor <task-id> Watch a running flight's live feed
88
+ ask-colleague guide <task-id> "<msg>" Send mid-flight guidance to a running flight
89
+ ask-colleague stop <task-id> Ask a running flight to stop (cooperative)
87
90
 
88
91
  Options:
89
92
  --repo PATH Target repo (default: .)
@@ -100,6 +103,7 @@ Options:
100
103
  --rating N (feedback) record a 1-5 quality rating for the drive
101
104
  --notes "..." (feedback) free-text notes to store with the rating
102
105
  --by NAME (feedback) who is grading (default: colleague's resolved identity)
106
+ --watch (explore/review/write) arm a flight so you can monitor/guide/stop it
103
107
  --json Machine-readable output: stdout carries only the result JSON,
104
108
  every diagnostic/digest line goes to stderr (any verb)
105
109
 
@@ -119,11 +123,11 @@ EOF
119
123
  # ── parse the verb ──────────────────────────────────────────────────────────
120
124
  VERB="${1:-}"
121
125
  case "$VERB" in
122
- explore | review | write | feedback | clean) shift ;;
126
+ explore | review | write | feedback | clean | monitor | guide | stop) shift ;;
123
127
  -h | --help) usage; exit 0 ;;
124
128
  "") usage >&2; exit 1 ;; # missing arg -> user-input error (#161)
125
129
  *)
126
- echo "error: unknown verb '$VERB' (expected explore|review|write|feedback|clean)" >&2
130
+ echo "error: unknown verb '$VERB' (expected explore|review|write|feedback|clean|monitor|guide|stop)" >&2
127
131
  echo "hint: run 'ask-colleague --help'" >&2
128
132
  exit 1 # bad verb -> user-input error (#161)
129
133
  ;;
@@ -173,6 +177,7 @@ ALLOW_DIRTY=0
173
177
  APPLY=0
174
178
  OPEN_PR=0
175
179
  DRY_RUN=0
180
+ WATCH=0
176
181
  RATING=""
177
182
  NOTES=""
178
183
  BY=""
@@ -189,6 +194,7 @@ while [[ $# -gt 0 ]]; do
189
194
  --max-steps) need_value "$#" "$1"; MAX_STEPS="$2"; shift 2 ;;
190
195
  --timeout) need_value "$#" "$1"; TIMEOUT="$2"; shift 2 ;;
191
196
  --apply) APPLY=1; shift ;;
197
+ --watch) WATCH=1; shift ;;
192
198
  --allow-dirty) ALLOW_DIRTY=1; shift ;;
193
199
  --pr) OPEN_PR=1; shift ;;
194
200
  --dry-run) DRY_RUN=1; shift ;;
@@ -211,6 +217,7 @@ done
211
217
  # throwaway worktree, so it needs no mktemp either.
212
218
  case "$VERB" in
213
219
  feedback | clean) require_tools git ;;
220
+ monitor | guide | stop) : ;;
214
221
  write)
215
222
  if [[ "$APPLY" -eq 1 || "$OPEN_PR" -eq 1 ]]; then
216
223
  require_tools git python3
@@ -235,8 +242,15 @@ REPO="$(cd "$REPO" && pwd)"
235
242
  # it must at least be a real git work tree. Fail fast with a clear message instead
236
243
  # of an opaque mid-drive error (read-only verbs add a worktree, write commits a
237
244
  # drive branch, clean reaps colleague/* refs).
238
- git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 \
239
- || { echo "error: --repo is not a git repository: $REPO" >&2; exit 1; }
245
+ # The pilot verbs (monitor/guide/stop) are pure .colleague/flight/ file I/O — they
246
+ # need no git work tree, so they are exempt from this fail-fast guard.
247
+ case "$VERB" in
248
+ monitor | guide | stop) : ;;
249
+ *)
250
+ git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 \
251
+ || { echo "error: --repo is not a git repository: $REPO" >&2; exit 1; }
252
+ ;;
253
+ esac
240
254
 
241
255
  # review interpolates --base into the LLM instruction ("git diff $BASE...HEAD"),
242
256
  # so reject a value that is not a real commit/ref before it is rendered into the
@@ -252,6 +266,9 @@ resolve_colleague || exit 2
252
266
  # A local model can be slow on a growing context, so default generously.
253
267
  export COLLEAGUE_TIMEOUT="$TIMEOUT"
254
268
  COMMON_FLAGS=(--engine "$ENGINE" --model "$MODEL" --base-url "$BASE_URL" --max-steps "$MAX_STEPS" --json)
269
+ # --watch arms a flight for EVERY drive verb (explore/review/write), so it lives on
270
+ # the shared flag list — not inside one verb's path — so monitor/guide/stop work.
271
+ [[ "${WATCH:-0}" -eq 1 ]] && COMMON_FLAGS+=(--watch)
255
272
 
256
273
  # ── render an instruction from a prompt template ────────────────────────────
257
274
  render_prompt() {
@@ -624,10 +641,35 @@ run_clean() {
624
641
  "${cmd[@]}"
625
642
  }
626
643
 
644
+ # ── piloting verbs: thin passthroughs to the `colleague flight` noun ─────────
645
+ _flight_json_flag() { [[ "$JSON_OUT" -eq 1 ]] && printf -- '--json'; }
646
+
647
+ run_monitor() {
648
+ local fid="${ARG%% *}"
649
+ [[ -z "$fid" ]] && { echo "error: monitor needs a flight task-id" >&2; exit 1; }
650
+ "${COLLEAGUE[@]}" flight status "$fid" --repo "$REPO" $(_flight_json_flag)
651
+ }
652
+
653
+ run_stop() {
654
+ local fid="${ARG%% *}"
655
+ [[ -z "$fid" ]] && { echo "error: stop needs a flight task-id" >&2; exit 1; }
656
+ "${COLLEAGUE[@]}" flight stop "$fid" --repo "$REPO" $(_flight_json_flag)
657
+ }
658
+
659
+ run_guide() {
660
+ local fid="${ARG%% *}"
661
+ local msg="${ARG#* }"
662
+ [[ -z "$fid" || "$msg" == "$fid" ]] && { echo "error: guide needs <task-id> <message>" >&2; exit 1; }
663
+ "${COLLEAGUE[@]}" flight guide "$fid" "$msg" --repo "$REPO" $(_flight_json_flag)
664
+ }
665
+
627
666
  case "$VERB" in
628
667
  explore) run_readonly "$(render_prompt explore)" ;;
629
668
  review) run_readonly "$(render_prompt review)" ;;
630
669
  write) run_write "$(render_prompt write)" ;;
631
670
  feedback) run_feedback "$ARG" ;;
632
671
  clean) run_clean ;;
672
+ monitor) run_monitor ;;
673
+ guide) run_guide ;;
674
+ stop) run_stop ;;
633
675
  esac
@@ -43,12 +43,18 @@ gone. The new mechanism:
43
43
 
44
44
  ```bash
45
45
  bash .claude/skills/mass-update/scripts/mass-update.sh \
46
- --repos <owner/repo[,owner/repo...]> <ops> [--pr] [--keep-clone]
46
+ --repos <owner/repo[,owner/repo...]> <ops> [--slug SLUG] [--pr] [--keep-clone]
47
47
  ```
48
48
 
49
49
  Run it from the rollout-cli repo root. `--repos` takes comma-separated GitHub
50
50
  slugs (e.g. `agentculture/widget-cli`).
51
51
 
52
+ The work branch defaults to `mass-update/<owner>-<repo>`. Pass `--slug SLUG` to
53
+ suffix it as `mass-update/<owner>-<repo>-<slug>`, so several independent updates
54
+ to the same repo can coexist as distinct branches (and distinct PRs) instead of
55
+ colliding on the one default branch name. `SLUG` must be ref-safe — letters,
56
+ digits, `.`, `_`, `-`; start with a letter or digit; no trailing `.` or `-`.
57
+
52
58
  ## Op shapes (combinable in one call)
53
59
 
54
60
  | Op | What it does |
@@ -65,6 +71,7 @@ All ops given in one call are applied to each clone. At least one op is required
65
71
  |------|--------|
66
72
  | *(none)* | **PREVIEW.** Clone, apply ops, print diff, cleanup. **Zero outward effect.** Always start here. |
67
73
  | `--pr` | Clone, apply, commit, push, open PR, file a companion tracking issue via `communicate/scripts/post-issue.sh`, cleanup. Both the PR URL and issue URL are printed. |
74
+ | `--slug SLUG` | Suffix the work branch: `mass-update/<owner>-<repo>-<slug>` (default has no suffix). Use when a repo may receive more than one independent mass-update so the branches don't collide. |
68
75
  | `--keep-clone` | Retain the scratch clone on exit (debug). |
69
76
 
70
77
  ## Worked example — propagate a LICENSE to a sibling repo
@@ -28,6 +28,9 @@ Targets:
28
28
  --repos a/b,c/d Comma-separated GitHub slugs (owner/repo). Required.
29
29
 
30
30
  Other:
31
+ --slug SLUG Suffix the work branch: mass-update/<owner>-<repo>-SLUG
32
+ (default: mass-update/<owner>-<repo>). Lets several
33
+ independent updates to the same repo coexist.
31
34
  -h, --help Show this help.
32
35
  EOF
33
36
  }
@@ -47,6 +50,7 @@ EDITS=()
47
50
  DO_PR=0
48
51
  KEEP_CLONE=0
49
52
  PENDING_FILE=""
53
+ SLUG=""
50
54
 
51
55
  _push_file() {
52
56
  [[ -n "$PENDING_FILE" ]] || { echo "error: $([[ "$1" == "from" ]] && echo --from || echo --content) given before any --file DEST" >&2; exit 2; }
@@ -93,6 +97,11 @@ while [[ $# -gt 0 ]]; do
93
97
  EDITS+=("$2")
94
98
  shift 2
95
99
  ;;
100
+ --slug)
101
+ need_value "$#" "$1"
102
+ SLUG="$2"
103
+ shift 2
104
+ ;;
96
105
  --pr)
97
106
  DO_PR=1
98
107
  shift
@@ -133,6 +142,22 @@ if [[ ${#FILE_DESTS[@]} -eq 0 && ${#EDITS[@]} -eq 0 ]]; then
133
142
  exit 2
134
143
  fi
135
144
 
145
+ # Validate the optional branch slug — it becomes a git ref suffix, so it must be
146
+ # a valid ref *component*, not merely a "nice-looking" string. Beyond the safe
147
+ # charset and leading/trailing dash/dot rules, git also rejects a '..' sequence
148
+ # and a '.lock' suffix in a ref name; allowing them here would pass validation
149
+ # but blow up later at `checkout -b` / `push` / `pr create --head` (where the
150
+ # failure is swallowed by `|| true`), so reject them up front with a clear error.
151
+ if [[ -n "$SLUG" ]]; then
152
+ if [[ ! "$SLUG" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ \
153
+ || "$SLUG" == *. || "$SLUG" == *- \
154
+ || "$SLUG" == *..* || "$SLUG" == *.lock ]]; then
155
+ echo "error: '$SLUG' is not a valid --slug (must be a valid git ref component)" >&2
156
+ echo "hint: letters, digits, '.', '_', '-'; start with a letter/digit; no trailing '.'/'-', no '..' sequence, no '.lock' suffix" >&2
157
+ exit 2
158
+ fi
159
+ fi
160
+
136
161
  # ── validate slug format ────────────────────────────────────────────────────
137
162
  validate_slug() {
138
163
  local slug="$1"
@@ -314,6 +339,9 @@ for raw in "${TARGETS[@]}"; do
314
339
  repo="${target#*/}"
315
340
  clone_name="${owner}-${repo}"
316
341
  clone_dir="$CLONES_DIR/$clone_name"
342
+ # Work branch: mass-update/<owner>-<repo>, with an optional -<slug> suffix so
343
+ # several independent updates to the same repo get distinct branches.
344
+ branch="mass-update/${clone_name}${SLUG:+-$SLUG}"
317
345
 
318
346
  # Check auth before cloning
319
347
  if ! gh auth status >/dev/null 2>&1; then
@@ -337,8 +365,8 @@ for raw in "${TARGETS[@]}"; do
337
365
  fi
338
366
 
339
367
  # Create work branch
340
- git -C "$clone_dir" checkout -b "mass-update/$clone_name" 2>/dev/null || \
341
- git -C "$clone_dir" switch -c "mass-update/$clone_name" 2>/dev/null || true
368
+ git -C "$clone_dir" checkout -b "$branch" 2>/dev/null || \
369
+ git -C "$clone_dir" switch -c "$branch" 2>/dev/null || true
342
370
 
343
371
  # Apply file ops
344
372
  apply_file_ops "$clone_dir" || { overall_rc=1; continue; }
@@ -366,7 +394,7 @@ for raw in "${TARGETS[@]}"; do
366
394
  fi
367
395
 
368
396
  # Push work branch — a failed push must not be swallowed.
369
- if ! git -C "$clone_dir" push origin "mass-update/$clone_name"; then
397
+ if ! git -C "$clone_dir" push origin "$branch"; then
370
398
  echo "error: push failed for $target" >&2
371
399
  overall_rc=1
372
400
  continue
@@ -377,7 +405,7 @@ for raw in "${TARGETS[@]}"; do
377
405
  --repo "$target" \
378
406
  --title "mass-update: deterministic update" \
379
407
  --body "Automated update via mass-update skill." \
380
- --head "mass-update/$clone_name")" || [[ -z "$pr_url" ]]; then
408
+ --head "$branch")" || [[ -z "$pr_url" ]]; then
381
409
  echo "error: gh pr create failed for $target" >&2
382
410
  overall_rc=1
383
411
  continue
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/). This project
6
6
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.1] - 2026-06-18
9
+
10
+ ### Changed
11
+
12
+ - ask-colleague: re-vendor the wrapper + recipe snapshot to the canonical colleague source (colleague 1.7.0) — adds the `colleague flight` pilot verbs `monitor` / `guide` / `stop` plus the `--watch` flag that arms a drive (explore/review/write) so it can be watched, guided mid-flight, and cooperatively stopped. Updates the bundled `SKILL.md`, the recipe `SKILL.md.tmpl` + wrapper snapshot, the manifest changelog text, and the `docs/skill-sources.md` provenance row (sync date → 2026-06-14). Adds discord-bot-cli to the ask-colleague-revendor consumer ledger.
13
+
14
+ ### Fixed
15
+
16
+ - rollout apply: date the prepended changelog entry the day the change lands (`date.today()`) instead of the frozen `2026-06-06` placeholder the apply CLI never overrode.
17
+ - `rollout/core/bump.py` `prepend_changelog`: no longer emits consecutive blank lines (markdownlint MD012) when inserting before an existing entry — every cross-repo rollout target previously inherited a lint-failing CHANGELOG (caught on discord-bot-cli#6). Controls the blank lines at the insertion seams and guarantees a single trailing newline (MD047).
18
+
19
+ ## [0.5.0] - 2026-06-13
20
+
21
+ ### Added
22
+
23
+ - mass-update: --slug SLUG flag suffixes the work branch as `mass-update/<owner>-<repo>-<slug>`, so several independent updates to the same repo get distinct branches/PRs instead of colliding on the default branch name. Slug is validated as a valid git ref component — rejecting not just bad charset / leading-trailing dash-or-dot, but also the `..` sequences and `.lock` suffix git refuses (which would otherwise pass validation and only fail later at checkout/push/PR). (Qodo PR #10 bug 1.)
24
+
8
25
  ## [0.4.2] - 2026-06-13
9
26
 
10
27
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rollout-cli
3
- Version: 0.4.2
3
+ Version: 0.5.1
4
4
  Summary: Cross-repo propagation workflow built on refactor-cli — apply a transformation across many repos at once.
5
5
  Project-URL: Homepage, https://github.com/agentculture/rollout-cli
6
6
  Project-URL: Issues, https://github.com/agentculture/rollout-cli/issues
@@ -33,7 +33,7 @@ is load-bearing, even where guildmaster's upstream copy omits it.
33
33
  | `think` | `../guildmaster/.claude/skills/think/` | **devague** (re-broadcast via guildmaster) | idea→spec leg of the devague workflow chain. Verbatim (already carried `type: command` at guildmaster). Origin/broadcast prose left verbatim. | 2026-05-26 (guildmaster 0.6.0) |
34
34
  | `spec-to-plan` | `../guildmaster/.claude/skills/spec-to-plan/` | **devague** (re-broadcast via guildmaster) | spec→plan leg of the devague workflow chain. Verbatim (already carried `type: command`). | 2026-05-26 (guildmaster 0.6.0) |
35
35
  | `assign-to-workforce` | `../guildmaster/.claude/skills/assign-to-workforce/` | **devague** (re-broadcast via guildmaster) | plan→parallel-implementation leg of the devague workflow chain. Verbatim (already carried `type: command`). | 2026-05-26 (guildmaster 0.6.0) |
36
- | `ask-colleague` | `../colleague/.claude/skills/ask-colleague/` | **colleague** (renamed from convertible; vendored directly — guildmaster re-broadcast pending) | The first-party front door to the `colleague` CLI: hand a scoped task to a *different* engine/mind via `explore` / `review` / `write`, and grade a finished work item via `feedback` (the ROI loop). `explore`/`review` run isolated in a throwaway `git worktree`; `write` **previews by default** (throwaway worktree, no side effects) and refuses a dirty tree only when applying (`--apply` / `--pr`). Verbatim except one consumer-identifying clause in the Provenance paragraph (`colleague vendors from guildmaster` → `rollout-cli vendors from guildmaster`); already carried `type: command`. Optional runtime dep: **`colleague`** on PATH. | 2026-06-06 (colleague 0.39.2, direct) |
36
+ | `ask-colleague` | `../colleague/.claude/skills/ask-colleague/` | **colleague** (renamed from convertible; vendored directly — guildmaster re-broadcast pending) | The first-party front door to the `colleague` CLI: hand a scoped task to a *different* engine/mind via `explore` / `review` / `write`, and grade a finished work item via `feedback` (the ROI loop); pilot a running drive with `monitor` / `guide` / `stop` (arm it with `--watch`). `explore`/`review` run isolated in a throwaway `git worktree`; `write` **previews by default** (throwaway worktree, no side effects) and refuses a dirty tree only when applying (`--apply` / `--pr`). Verbatim except one consumer-identifying clause in the Provenance paragraph (`colleague vendors from guildmaster` → `rollout-cli vendors from guildmaster`); already carried `type: command`. Optional runtime dep: **`colleague`** on PATH. | 2026-06-14 (colleague 1.7.0, direct) |
37
37
 
38
38
  ## Re-sync procedure
39
39
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rollout-cli"
3
- version = "0.4.2"
3
+ version = "0.5.1"
4
4
  description = "Cross-repo propagation workflow built on refactor-cli — apply a transformation across many repos at once."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -44,6 +44,7 @@ Origin: **colleague** (`agentculture/colleague`,
44
44
  | rollout-cli | `agentculture/rollout-cli` | re-vendor wrapper | this repo ("self"); carries `which rollout-cli …` token |
45
45
  | dgx-spark-cli | `agentculture/dgx-spark-cli` | fresh add | skill was absent |
46
46
  | reterminal-cli | `agentculture/reterminal-cli` | re-vendor wrapper | merged at pre-#183 |
47
+ | discord-bot-cli | `agentculture/discord-bot-cli` | fresh add | skill was absent; carries `which discord-bot-cli …` token; landed at flight-verbs snapshot (discord-bot-cli#6) |
47
48
 
48
49
  Keep this table current when a consumer is added or dropped.
49
50
 
@@ -10,6 +10,7 @@ description: >
10
10
  hands off a small implementation, `ask-colleague feedback` grades a finished
11
11
  work item (the ROI loop), and `ask-colleague clean` reaps stale/corrupt
12
12
  `colleague/*` branches a crashed run left behind (which can break `git fetch`).
13
+ Pilot a running work item with `monitor`/`guide`/`stop`.
13
14
  Reach for it REFLEXIVELY, the way you'd lean over to the
14
15
  teammate at the next desk — not only when asked: before you present or open a PR
15
16
  on a non-trivial committed diff, run `review` for a diverse second opinion; for a
@@ -154,6 +155,18 @@ keeping stdout valid JSON for a machine consumer while diagnostics stay on stder
154
155
  (or `colleague clean`) to reap it — start with `--dry-run` to see what it would
155
156
  remove. It only ever touches `colleague/*` refs and `.colleague/` artifacts.
156
157
 
158
+ ## Piloting a flight
159
+
160
+ Dispatch a drive with `--watch` (on `explore`, `review`, or `write`) to make the
161
+ work item watchable. While it runs you can:
162
+
163
+ - **`ask-colleague monitor <task-id>`** — watch the flight's live feed
164
+ - **`ask-colleague guide <task-id> "<message>"`** — send mid-flight guidance
165
+ - **`ask-colleague stop <task-id>`** — cooperatively ask the flight to stop
166
+
167
+ Control is applied at the running loop's next turn boundary, so guidance and
168
+ stop requests take effect on the next iteration rather than interrupting mid-step.
169
+
157
170
  ## Hard rules (do not violate)
158
171
 
159
172
  - **explore and review are read-only.** They run in a throwaway `git worktree`
@@ -84,6 +84,9 @@ Usage:
84
84
  ask-colleague feedback <id|last> [--rating N] Grade a past drive (ROI loop); with --rating records, without shows
85
85
  ask-colleague feedback list List every recorded drive by request + grade (find one by its request)
86
86
  ask-colleague clean [--dry-run] Reap stale/corrupt colleague/* branches + orphaned .colleague/ artifacts (#162)
87
+ ask-colleague monitor <task-id> Watch a running flight's live feed
88
+ ask-colleague guide <task-id> "<msg>" Send mid-flight guidance to a running flight
89
+ ask-colleague stop <task-id> Ask a running flight to stop (cooperative)
87
90
 
88
91
  Options:
89
92
  --repo PATH Target repo (default: .)
@@ -100,6 +103,7 @@ Options:
100
103
  --rating N (feedback) record a 1-5 quality rating for the drive
101
104
  --notes "..." (feedback) free-text notes to store with the rating
102
105
  --by NAME (feedback) who is grading (default: colleague's resolved identity)
106
+ --watch (explore/review/write) arm a flight so you can monitor/guide/stop it
103
107
  --json Machine-readable output: stdout carries only the result JSON,
104
108
  every diagnostic/digest line goes to stderr (any verb)
105
109
 
@@ -119,11 +123,11 @@ EOF
119
123
  # ── parse the verb ──────────────────────────────────────────────────────────
120
124
  VERB="${1:-}"
121
125
  case "$VERB" in
122
- explore | review | write | feedback | clean) shift ;;
126
+ explore | review | write | feedback | clean | monitor | guide | stop) shift ;;
123
127
  -h | --help) usage; exit 0 ;;
124
128
  "") usage >&2; exit 1 ;; # missing arg -> user-input error (#161)
125
129
  *)
126
- echo "error: unknown verb '$VERB' (expected explore|review|write|feedback|clean)" >&2
130
+ echo "error: unknown verb '$VERB' (expected explore|review|write|feedback|clean|monitor|guide|stop)" >&2
127
131
  echo "hint: run 'ask-colleague --help'" >&2
128
132
  exit 1 # bad verb -> user-input error (#161)
129
133
  ;;
@@ -173,6 +177,7 @@ ALLOW_DIRTY=0
173
177
  APPLY=0
174
178
  OPEN_PR=0
175
179
  DRY_RUN=0
180
+ WATCH=0
176
181
  RATING=""
177
182
  NOTES=""
178
183
  BY=""
@@ -189,6 +194,7 @@ while [[ $# -gt 0 ]]; do
189
194
  --max-steps) need_value "$#" "$1"; MAX_STEPS="$2"; shift 2 ;;
190
195
  --timeout) need_value "$#" "$1"; TIMEOUT="$2"; shift 2 ;;
191
196
  --apply) APPLY=1; shift ;;
197
+ --watch) WATCH=1; shift ;;
192
198
  --allow-dirty) ALLOW_DIRTY=1; shift ;;
193
199
  --pr) OPEN_PR=1; shift ;;
194
200
  --dry-run) DRY_RUN=1; shift ;;
@@ -211,6 +217,7 @@ done
211
217
  # throwaway worktree, so it needs no mktemp either.
212
218
  case "$VERB" in
213
219
  feedback | clean) require_tools git ;;
220
+ monitor | guide | stop) : ;;
214
221
  write)
215
222
  if [[ "$APPLY" -eq 1 || "$OPEN_PR" -eq 1 ]]; then
216
223
  require_tools git python3
@@ -235,8 +242,15 @@ REPO="$(cd "$REPO" && pwd)"
235
242
  # it must at least be a real git work tree. Fail fast with a clear message instead
236
243
  # of an opaque mid-drive error (read-only verbs add a worktree, write commits a
237
244
  # drive branch, clean reaps colleague/* refs).
238
- git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 \
239
- || { echo "error: --repo is not a git repository: $REPO" >&2; exit 1; }
245
+ # The pilot verbs (monitor/guide/stop) are pure .colleague/flight/ file I/O — they
246
+ # need no git work tree, so they are exempt from this fail-fast guard.
247
+ case "$VERB" in
248
+ monitor | guide | stop) : ;;
249
+ *)
250
+ git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 \
251
+ || { echo "error: --repo is not a git repository: $REPO" >&2; exit 1; }
252
+ ;;
253
+ esac
240
254
 
241
255
  # review interpolates --base into the LLM instruction ("git diff $BASE...HEAD"),
242
256
  # so reject a value that is not a real commit/ref before it is rendered into the
@@ -252,6 +266,9 @@ resolve_colleague || exit 2
252
266
  # A local model can be slow on a growing context, so default generously.
253
267
  export COLLEAGUE_TIMEOUT="$TIMEOUT"
254
268
  COMMON_FLAGS=(--engine "$ENGINE" --model "$MODEL" --base-url "$BASE_URL" --max-steps "$MAX_STEPS" --json)
269
+ # --watch arms a flight for EVERY drive verb (explore/review/write), so it lives on
270
+ # the shared flag list — not inside one verb's path — so monitor/guide/stop work.
271
+ [[ "${WATCH:-0}" -eq 1 ]] && COMMON_FLAGS+=(--watch)
255
272
 
256
273
  # ── render an instruction from a prompt template ────────────────────────────
257
274
  render_prompt() {
@@ -624,10 +641,35 @@ run_clean() {
624
641
  "${cmd[@]}"
625
642
  }
626
643
 
644
+ # ── piloting verbs: thin passthroughs to the `colleague flight` noun ─────────
645
+ _flight_json_flag() { [[ "$JSON_OUT" -eq 1 ]] && printf -- '--json'; }
646
+
647
+ run_monitor() {
648
+ local fid="${ARG%% *}"
649
+ [[ -z "$fid" ]] && { echo "error: monitor needs a flight task-id" >&2; exit 1; }
650
+ "${COLLEAGUE[@]}" flight status "$fid" --repo "$REPO" $(_flight_json_flag)
651
+ }
652
+
653
+ run_stop() {
654
+ local fid="${ARG%% *}"
655
+ [[ -z "$fid" ]] && { echo "error: stop needs a flight task-id" >&2; exit 1; }
656
+ "${COLLEAGUE[@]}" flight stop "$fid" --repo "$REPO" $(_flight_json_flag)
657
+ }
658
+
659
+ run_guide() {
660
+ local fid="${ARG%% *}"
661
+ local msg="${ARG#* }"
662
+ [[ -z "$fid" || "$msg" == "$fid" ]] && { echo "error: guide needs <task-id> <message>" >&2; exit 1; }
663
+ "${COLLEAGUE[@]}" flight guide "$fid" "$msg" --repo "$REPO" $(_flight_json_flag)
664
+ }
665
+
627
666
  case "$VERB" in
628
667
  explore) run_readonly "$(render_prompt explore)" ;;
629
668
  review) run_readonly "$(render_prompt review)" ;;
630
669
  write) run_write "$(render_prompt write)" ;;
631
670
  feedback) run_feedback "$ARG" ;;
632
671
  clean) run_clean ;;
672
+ monitor) run_monitor ;;
673
+ guide) run_guide ;;
674
+ stop) run_stop ;;
633
675
  esac
@@ -15,12 +15,14 @@ changelog_text = """\
15
15
 
16
16
  - **Re-vendored the `ask-colleague` skill wrapper from its canonical colleague
17
17
  source** (`.claude/skills/ask-colleague/scripts/ask-colleague.sh`): the wrapper
18
- now supports `--json` machine-readable outputstdout carries **only** the
19
- result JSON (the drive verbs emit the normalized `TaskResult`; `feedback` /
20
- `clean` forward `--json` to colleague) while every diagnostic/digest line goes
21
- to stderr, keeping stdout valid JSON for a machine consumer. This supersedes the
22
- earlier wrapper-only colleague#183 snapshot (which predated `--json`). The
23
- bundled `SKILL.md` template documents the new verb/flag; an existing downstream
18
+ now drives the `colleague flight` pilot verbs `monitor` / `guide` / `stop`
19
+ plus the `--watch` flag that arms a drive (`explore` / `review` / `write`) so it
20
+ can be watched, guided mid-flight, and cooperatively stopped. This supersedes the
21
+ earlier colleague#186 snapshot (which predated flights). It keeps the `--json`
22
+ machine-readable output added then stdout carries **only** the result JSON (the
23
+ drive verbs emit the normalized `TaskResult`; `feedback` / `clean` forward
24
+ `--json` to colleague) while every diagnostic/digest line goes to stderr. The
25
+ bundled `SKILL.md` template documents the new verbs/flag; an existing downstream
24
26
  `SKILL.md` is left untouched (it carries a repo-specific provenance token and
25
27
  may diverge). Where the skill was absent, it is added fresh (wrapper +
26
28
  `SKILL.md` + prompts).
@@ -9,6 +9,7 @@ as "needs-attention"). Never force-apply or merge.
9
9
  from __future__ import annotations
10
10
 
11
11
  import argparse
12
+ import datetime
12
13
  import pathlib
13
14
 
14
15
  from rollout.cli._output import emit_result
@@ -53,13 +54,16 @@ def cmd_apply(args: argparse.Namespace) -> int:
53
54
  return 0
54
55
 
55
56
  # --- classify and apply ----------------------------------------------
57
+ # Date the changelog entry the day the change actually lands (not the
58
+ # apply_one frozen-default placeholder, which is only for hermetic tests).
59
+ today = datetime.date.today().isoformat()
56
60
  report = Report()
57
61
  for name in targets_list:
58
62
  repo_path = pathlib.Path(base) / pathlib.Path(name).name
59
63
  try:
60
64
  status = preflight.classify(repo_path, recipe)
61
65
  if status == preflight.Status.CLEAN:
62
- row = orchestrate.apply_one(repo_path, recipe, repo_slug=name)
66
+ row = orchestrate.apply_one(repo_path, recipe, repo_slug=name, date=today)
63
67
  report.add_row(name, row.status, row.pr_url)
64
68
  else:
65
69
  report.add_row(name, _STATUS_MAP[status])
@@ -103,16 +103,28 @@ def prepend_changelog(
103
103
  if f"## [{version}]" in text:
104
104
  return
105
105
 
106
- # Build the new section.
107
- new_section = f"\n## [{version}] - {date}\n\n{body}\n"
106
+ # Build the new entry. Normalise the body and control the surrounding
107
+ # blank lines here (exactly one before and after) rather than inheriting
108
+ # the caller's / file's trailing whitespace — otherwise the junction with
109
+ # the intro or an existing entry yields consecutive blank lines, which
110
+ # markdownlint rejects (MD012/no-multiple-blanks).
111
+ body_clean = re.sub(r"\n{3,}", "\n\n", body.strip())
112
+ entry = f"## [{version}] - {date}\n\n{body_clean}\n"
108
113
 
109
114
  # Find the first existing release entry (## [X.Y.Z] ...)
110
115
  first_entry = _ENTRY_RE.search(text)
111
116
  if first_entry:
112
- # Insert just before the first existing entry.
113
- new_text = text[: first_entry.start()] + new_section + text[first_entry.start() :]
117
+ # Insert just before the first existing entry, with exactly one blank
118
+ # line separating the intro from the new entry and the new entry from
119
+ # the existing one.
120
+ prefix = text[: first_entry.start()].rstrip()
121
+ suffix = text[first_entry.start() :].lstrip("\n")
122
+ new_text = f"{prefix}\n\n{entry}\n{suffix}"
114
123
  else:
115
124
  # No existing entries — append after the intro.
116
- new_text = text.rstrip() + new_section
125
+ new_text = f"{text.rstrip()}\n\n{entry}"
126
+
127
+ # Exactly one trailing newline (MD047).
128
+ new_text = new_text.rstrip("\n") + "\n"
117
129
 
118
130
  path.write_text(new_text, encoding="utf-8")
@@ -16,6 +16,17 @@ sonar.python.version=3.12
16
16
  # Don't analyze noise.
17
17
  sonar.exclusions=**/__pycache__/**,**/.venv/**,uv.lock,.claude/skills/**
18
18
 
19
+ # False positive: pythonsecurity:S2083 (path injection) fires on
20
+ # prepend_changelog's `path.write_text` in rollout/core/bump.py because it traces
21
+ # the changelog path back to the `--repos` slug. That slug is sanitized at the
22
+ # boundary — apply.py derives the write dir as `<base>/<pathlib.Path(name).name>`,
23
+ # and `.name` strips any traversal — and the sole caller (orchestrate.apply_one)
24
+ # always passes `<repo_path>/CHANGELOG.md`. This is a local, operator-run devtool
25
+ # writing a CHANGELOG, not an attacker-facing surface. Ignore S2083 in bump.py.
26
+ sonar.issue.ignore.multicriteria=e1
27
+ sonar.issue.ignore.multicriteria.e1.ruleKey=pythonsecurity:S2083
28
+ sonar.issue.ignore.multicriteria.e1.resourceKey=**/bump.py
29
+
19
30
  # Block CI on a failing quality gate. The scanner polls SonarCloud and exits
20
31
  # non-zero on a red gate (coverage regression, new bugs/vulnerabilities). Only
21
32
  # takes effect when the SonarCloud Scan step runs — that step is guarded by
@@ -152,6 +152,19 @@ def test_prepend_changelog_before_existing_entry(tmp_path: pathlib.Path) -> None
152
152
  assert new_pos < old_pos
153
153
 
154
154
 
155
+ def test_prepend_changelog_no_consecutive_blank_lines(tmp_path: pathlib.Path) -> None:
156
+ """Inserting never produces MD012 double blanks at the intro/entry junctions."""
157
+ existing = "## [0.1.0] - 2025-01-01\n\n### Added\n\n- old thing\n"
158
+ path = _write_changelog(tmp_path, existing=existing)
159
+ # A body padded with stray blank lines must still normalise cleanly.
160
+ prepend_changelog(path, "0.2.0", "2026-01-01", "\n### Changed\n\n\n- new thing\n\n")
161
+ content = path.read_text()
162
+ assert "\n\n\n" not in content, "markdownlint MD012: consecutive blank lines"
163
+ assert content.endswith("\n") and not content.endswith("\n\n")
164
+ assert content.index("## [0.2.0]") < content.index("## [0.1.0]")
165
+ assert "- new thing" in content
166
+
167
+
155
168
  # ---------------------------------------------------------------------------
156
169
  # prepend_changelog — idempotency
157
170
  # ---------------------------------------------------------------------------
@@ -478,6 +478,66 @@ def test_pr_writes_no_tempfiles_outside_repo(repo, fake_bin, tmp_path):
478
478
  assert not (repo / ".mass-update").exists()
479
479
 
480
480
 
481
+ # --------------------------------------------------------------------------- #
482
+ # --slug: suffix the work branch so several independent updates to the same repo
483
+ # get distinct branches (mass-update/<owner>-<repo>-<slug>).
484
+ # --------------------------------------------------------------------------- #
485
+
486
+
487
+ def _current_branch(clone: Path) -> str:
488
+ return subprocess.run(
489
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
490
+ cwd=clone,
491
+ capture_output=True,
492
+ text=True,
493
+ ).stdout.strip()
494
+
495
+
496
+ def test_default_branch_has_no_slug_suffix(repo, fake_bin):
497
+ r = run(repo, fake_bin, "--repos", SLUG, "--file", "NOTICE", "--content", "x\n", "--keep-clone")
498
+ assert r.returncode == 0, r.stderr
499
+ assert _current_branch(_clone_dir(repo)) == "mass-update/agentculture-widget-cli"
500
+
501
+
502
+ def test_slug_suffixes_the_work_branch(repo, fake_bin):
503
+ r = run(
504
+ repo,
505
+ fake_bin,
506
+ "--repos",
507
+ SLUG,
508
+ "--file",
509
+ "NOTICE",
510
+ "--content",
511
+ "x\n",
512
+ "--slug",
513
+ "notice-v2",
514
+ "--keep-clone",
515
+ )
516
+ assert r.returncode == 0, r.stderr
517
+ assert _current_branch(_clone_dir(repo)) == "mass-update/agentculture-widget-cli-notice-v2"
518
+
519
+
520
+ @pytest.mark.parametrize(
521
+ "bad",
522
+ [
523
+ "-leading",
524
+ "trailing-",
525
+ "has space",
526
+ "has/slash",
527
+ "trailing.",
528
+ "@bad",
529
+ # git rejects these as ref names even though they look benign:
530
+ "dot..dot", # '..' sequence
531
+ "foo.lock", # '.lock' suffix
532
+ ],
533
+ )
534
+ def test_invalid_slug_is_rejected(repo, fake_bin, bad):
535
+ r = run(repo, fake_bin, "--repos", SLUG, "--file", "NOTICE", "--content", "x\n", "--slug", bad)
536
+ assert r.returncode == 2, r.stderr
537
+ assert "slug" in (r.stdout + r.stderr).lower()
538
+ assert not (repo / ".mass-update").exists()
539
+
540
+
481
541
  # finding 2: when communicate's post-issue.sh is absent, --pr must error (route
482
542
  # issues only through /communicate), not silently fall back to `agtag` directly.
483
543
  def test_missing_post_issue_script_errors_no_agtag_fallback(repo, fake_bin):
@@ -430,7 +430,7 @@ wheels = [
430
430
 
431
431
  [[package]]
432
432
  name = "rollout-cli"
433
- version = "0.4.2"
433
+ version = "0.5.1"
434
434
  source = { editable = "." }
435
435
 
436
436
  [package.dev-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes