rollout-cli 0.3.0__tar.gz → 0.3.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 (112) hide show
  1. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +105 -46
  2. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.markdownlint-cli2.yaml +3 -0
  3. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/CHANGELOG.md +34 -0
  4. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/PKG-INFO +1 -1
  5. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/docs/skill-sources.md +6 -0
  6. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/pyproject.toml +1 -1
  7. rollout_cli-0.3.1/recipes/ask-colleague-revendor/README.md +84 -0
  8. rollout_cli-0.3.1/recipes/ask-colleague-revendor/apply.sh +39 -0
  9. rollout_cli-0.3.1/recipes/ask-colleague-revendor/check.sh +31 -0
  10. rollout_cli-0.3.1/recipes/ask-colleague-revendor/files/SKILL.md.tmpl +196 -0
  11. rollout_cli-0.3.1/recipes/ask-colleague-revendor/files/prompts/explore.md +25 -0
  12. rollout_cli-0.3.1/recipes/ask-colleague-revendor/files/prompts/review.md +36 -0
  13. rollout_cli-0.3.1/recipes/ask-colleague-revendor/files/prompts/write.md +15 -0
  14. rollout_cli-0.3.1/recipes/ask-colleague-revendor/files/scripts/ask-colleague.sh +568 -0
  15. rollout_cli-0.3.1/recipes/ask-colleague-revendor/manifest.toml +25 -0
  16. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/uv.lock +1 -1
  17. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/agent-config/SKILL.md +0 -0
  18. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  19. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/agent-config/scripts/show.sh +0 -0
  20. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/ask-colleague/SKILL.md +0 -0
  21. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  22. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  23. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  24. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  25. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  26. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/cicd/SKILL.md +0 -0
  27. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  28. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  29. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  30. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  31. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  32. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/communicate/SKILL.md +0 -0
  33. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  34. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  35. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  36. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  37. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  38. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  39. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  40. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  41. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  42. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  43. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/run-tests/SKILL.md +0 -0
  44. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/run-tests/scripts/test.sh +0 -0
  45. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/sonarclaude/SKILL.md +0 -0
  46. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  47. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  48. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  49. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/think/SKILL.md +0 -0
  50. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/think/scripts/think.sh +0 -0
  51. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/version-bump/SKILL.md +0 -0
  52. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills/version-bump/scripts/bump.py +0 -0
  53. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.claude/skills.local.yaml.example +0 -0
  54. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.devague/frames/rollout-cli-ships-rollout-define-an-org-wide-chang.json +0 -0
  55. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.devague/plans/rollout-cli-ships-rollout-define-an-org-wide-chang.json +0 -0
  56. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.flake8 +0 -0
  57. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.github/workflows/publish.yml +0 -0
  58. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.github/workflows/tests.yml +0 -0
  59. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/.gitignore +0 -0
  60. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/CLAUDE.md +0 -0
  61. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/LICENSE +0 -0
  62. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/README.md +0 -0
  63. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/culture.yaml +0 -0
  64. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/docs/plans/2026-06-06-rollout-cli-ships-rollout-define-an-org-wide-chang.md +0 -0
  65. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/docs/specs/2026-06-06-rollout-cli-ships-rollout-define-an-org-wide-chang.md +0 -0
  66. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/recipes/apache-relicense/apply.sh +0 -0
  67. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/recipes/apache-relicense/check.sh +0 -0
  68. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/recipes/apache-relicense/files/LICENSE +0 -0
  69. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/recipes/apache-relicense/manifest.toml +0 -0
  70. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/__init__.py +0 -0
  71. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/__main__.py +0 -0
  72. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/__init__.py +0 -0
  73. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_commands/__init__.py +0 -0
  74. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_commands/apply.py +0 -0
  75. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_commands/cli.py +0 -0
  76. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_commands/doctor.py +0 -0
  77. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_commands/explain.py +0 -0
  78. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_commands/learn.py +0 -0
  79. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_commands/overview.py +0 -0
  80. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_commands/plan.py +0 -0
  81. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_commands/whoami.py +0 -0
  82. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_errors.py +0 -0
  83. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/cli/_output.py +0 -0
  84. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/core/__init__.py +0 -0
  85. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/core/bump.py +0 -0
  86. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/core/devex_pr.py +0 -0
  87. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/core/gitops.py +0 -0
  88. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/core/orchestrate.py +0 -0
  89. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/core/preflight.py +0 -0
  90. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/core/recipe.py +0 -0
  91. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/core/report.py +0 -0
  92. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/core/targets.py +0 -0
  93. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/explain/__init__.py +0 -0
  94. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/rollout/explain/catalog.py +0 -0
  95. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/sonar-project.properties +0 -0
  96. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/__init__.py +0 -0
  97. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_apache_relicense.py +0 -0
  98. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_apply_cmd.py +0 -0
  99. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_bump.py +0 -0
  100. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_cli.py +0 -0
  101. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_cli_introspection.py +0 -0
  102. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_cli_wiring.py +0 -0
  103. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_devex_pr.py +0 -0
  104. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_e2e_apache_relicense.py +0 -0
  105. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_explain_catalog.py +0 -0
  106. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_gitops.py +0 -0
  107. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_orchestrate.py +0 -0
  108. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_plan_cmd.py +0 -0
  109. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_preflight.py +0 -0
  110. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_recipe.py +0 -0
  111. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_report.py +0 -0
  112. {rollout_cli-0.3.0 → rollout_cli-0.3.1}/tests/test_targets.py +0 -0
@@ -11,6 +11,7 @@
11
11
  # ask-colleague write "<task>" [--apply] implement a change (preview by default)
12
12
  # ask-colleague feedback <id|last> --rating N grade a past drive (ROI loop); no rating -> show
13
13
  # ask-colleague feedback list list every recorded drive by request + grade
14
+ # ask-colleague clean reap stale colleague/* branches + artifacts (#162)
14
15
  #
15
16
  # explore/review run in a throwaway `git worktree` at HEAD, so they can never
16
17
  # touch your working tree or branch (any stray write is discarded). write also
@@ -22,6 +23,18 @@
22
23
  # most recent consequential write. Grade a probe by its printed task-id (every
23
24
  # drive prints `task:` and a `grade:` hint), or find it with `feedback list`.
24
25
  #
26
+ # A crashed/interrupted `write --apply` can leave a dangling colleague/<id>
27
+ # branch (and 0-byte .colleague/ artifacts) that breaks `git fetch`. The
28
+ # EXIT-trap cleanup below only reaps the *current* read-only run's worktree, not
29
+ # a *prior* crashed run — `ask-colleague clean` (which shells out to `colleague
30
+ # clean`) reaps those, scoped strictly to colleague/* so an unrelated branch is
31
+ # never touched.
32
+ #
33
+ # Exit-code policy (matches colleague's CLI contract, #161): 0 success · 1
34
+ # user-input error (bad/missing verb, flag, arg, or path; dirty-tree state guard
35
+ # — same class as the runtime's EXIT_USER_ERROR guard) · 2 environment/setup
36
+ # error (missing required tool, colleague CLI not found, missing prompt template).
37
+ #
25
38
  set -euo pipefail
26
39
 
27
40
  SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -29,24 +42,28 @@ PROMPTS_DIR="$SKILL_DIR/prompts"
29
42
 
30
43
  # ── resolve the colleague CLI (installed, then local-dev fallback) ─────────
31
44
  COLLEAGUE=()
32
- resolve_colleague() {
33
- if command -v colleague >/dev/null 2>&1; then
34
- COLLEAGUE=(colleague) # installed tool — the normal case
35
- return 0
36
- fi
37
- # Local-dev fallback: inside the colleague checkout, run via uv.
38
- local dir="$PWD"
45
+
46
+ _colleague_via_uv() {
47
+ local dir="$1"
39
48
  while [[ -n "$dir" ]] && [[ "$dir" != "/" ]]; do
40
49
  if [[ -f "$dir/pyproject.toml" ]] \
41
50
  && grep -q '^name = "colleague"' "$dir/pyproject.toml" 2>/dev/null; then
42
- if command -v uv >/dev/null 2>&1; then
43
- COLLEAGUE=(uv run colleague)
44
- return 0
45
- fi
46
- break
51
+ command -v uv >/dev/null 2>&1 || return 1
52
+ COLLEAGUE=(uv run --project "$dir" colleague)
53
+ return 0
47
54
  fi
48
55
  dir=$(dirname "$dir")
49
56
  done
57
+ return 1
58
+ }
59
+
60
+ resolve_colleague() {
61
+ if command -v colleague >/dev/null 2>&1; then
62
+ COLLEAGUE=(colleague) # installed tool — the normal case
63
+ return 0
64
+ fi
65
+ _colleague_via_uv "$PWD" && return 0
66
+ _colleague_via_uv "$REPO" && return 0
50
67
  cat >&2 <<'EOF'
51
68
  error: colleague CLI not found.
52
69
  hint: install it with `uv tool install colleague` (or `pipx install colleague`),
@@ -66,9 +83,11 @@ Usage:
66
83
  ask-colleague write "<task>" [--apply|--pr] Implement a change (preview by default; --apply lands it)
67
84
  ask-colleague feedback <id|last> [--rating N] Grade a past drive (ROI loop); with --rating records, without shows
68
85
  ask-colleague feedback list List every recorded drive by request + grade (find one by its request)
86
+ ask-colleague clean [--dry-run] Reap stale/corrupt colleague/* branches + orphaned .colleague/ artifacts (#162)
69
87
 
70
88
  Options:
71
89
  --repo PATH Target repo (default: .)
90
+ --dry-run (clean) report what would be reaped without changing anything
72
91
  --base BRANCH Base for `review` diff (default: main)
73
92
  --engine NAME Backend plugin (default: $COLLEAGUE_ENGINE or vllm-openai)
74
93
  --model NAME Model (default: $COLLEAGUE_MODEL or sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP)
@@ -89,19 +108,22 @@ feedback grades a finished drive: stats (in the artifact) say what it cost,
89
108
  feedback says how good it was — together, the ROI of outsourcing. explore/review
90
109
  do not move `last` (they are read-only) — grade them by their printed task-id, or
91
110
  run `ask-colleague feedback list` to find a drive by its request.
111
+ clean recovers a repo a crashed run wedged: it reaps stale/corrupt colleague/*
112
+ branches + orphaned .colleague/ artifacts (scoped to colleague/* only). For the
113
+ full flag set (--merged / --older-than) call `colleague clean` directly.
92
114
  EOF
93
115
  }
94
116
 
95
117
  # ── parse the verb ──────────────────────────────────────────────────────────
96
118
  VERB="${1:-}"
97
119
  case "$VERB" in
98
- explore | review | write | feedback) shift ;;
120
+ explore | review | write | feedback | clean) shift ;;
99
121
  -h | --help) usage; exit 0 ;;
100
- "") usage >&2; exit 2 ;;
122
+ "") usage >&2; exit 1 ;; # missing arg -> user-input error (#161)
101
123
  *)
102
- echo "error: unknown verb '$VERB' (expected explore|review|write|feedback)" >&2
124
+ echo "error: unknown verb '$VERB' (expected explore|review|write|feedback|clean)" >&2
103
125
  echo "hint: run 'ask-colleague --help'" >&2
104
- exit 2
126
+ exit 1 # bad verb -> user-input error (#161)
105
127
  ;;
106
128
  esac
107
129
 
@@ -125,7 +147,7 @@ need_value() { # $1 = remaining arg count ($#), $2 = flag name
125
147
  [[ "$1" -ge 2 ]] || {
126
148
  echo "error: $2 requires a value" >&2
127
149
  echo "hint: run 'ask-colleague --help'" >&2
128
- exit 2
150
+ exit 1 # missing flag value -> user-input error (#161)
129
151
  }
130
152
  }
131
153
 
@@ -143,6 +165,7 @@ TIMEOUT="${COLLEAGUE_TIMEOUT:-${CONVERTIBLE_TIMEOUT:-300}}"
143
165
  ALLOW_DIRTY=0
144
166
  APPLY=0
145
167
  OPEN_PR=0
168
+ DRY_RUN=0
146
169
  RATING=""
147
170
  NOTES=""
148
171
  BY=""
@@ -160,33 +183,41 @@ while [[ $# -gt 0 ]]; do
160
183
  --apply) APPLY=1; shift ;;
161
184
  --allow-dirty) ALLOW_DIRTY=1; shift ;;
162
185
  --pr) OPEN_PR=1; shift ;;
186
+ --dry-run) DRY_RUN=1; shift ;;
163
187
  --rating) need_value "$#" "$1"; RATING="$2"; shift 2 ;;
164
188
  --notes) need_value "$#" "$1"; NOTES="$2"; shift 2 ;;
165
189
  --by) need_value "$#" "$1"; BY="$2"; shift 2 ;;
166
190
  -h | --help) usage; exit 0 ;;
167
191
  --) shift; while [[ $# -gt 0 ]]; do ARG="${ARG:+$ARG }$1"; shift; done ;;
168
- -*) echo "error: unknown option '$1'" >&2; echo "hint: run 'ask-colleague --help'" >&2; exit 2 ;;
192
+ # unknown option -> user-input error (#161)
193
+ -*) echo "error: unknown option '$1'" >&2; echo "hint: run 'ask-colleague --help'" >&2; exit 1 ;;
169
194
  *) ARG="${ARG:+$ARG }$1"; shift ;;
170
195
  esac
171
196
  done
172
197
 
173
- [[ -n "$ARG" ]] || { echo "error: $VERB needs a description argument" >&2; usage >&2; exit 2; }
174
- [[ -d "$REPO" ]] || { echo "error: --repo is not a directory: $REPO" >&2; exit 2; }
198
+ # clean takes no description argument; every other verb requires one. (All of the
199
+ # guards below are user-input errors -> exit 1, per the policy comment at the top.)
200
+ if [[ "$VERB" == "clean" ]]; then
201
+ [[ -z "$ARG" ]] || { echo "error: clean takes no description argument" >&2; exit 1; }
202
+ else
203
+ [[ -n "$ARG" ]] || { echo "error: $VERB needs a description argument" >&2; usage >&2; exit 1; }
204
+ fi
205
+ [[ -d "$REPO" ]] || { echo "error: --repo is not a directory: $REPO" >&2; exit 1; }
175
206
  REPO="$(cd "$REPO" && pwd)"
176
207
 
177
208
  # One git-repo guard for every verb: --repo is a runtime target like `git -C`, but
178
209
  # it must at least be a real git work tree. Fail fast with a clear message instead
179
- # of an opaque mid-drive error (and every verb here needs git: read-only verbs add
180
- # a worktree, write commits a drive branch).
210
+ # of an opaque mid-drive error (read-only verbs add a worktree, write commits a
211
+ # drive branch, clean reaps colleague/* refs).
181
212
  git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 \
182
- || { echo "error: --repo is not a git repository: $REPO" >&2; exit 2; }
213
+ || { echo "error: --repo is not a git repository: $REPO" >&2; exit 1; }
183
214
 
184
215
  # review interpolates --base into the LLM instruction ("git diff $BASE...HEAD"),
185
216
  # so reject a value that is not a real commit/ref before it is rendered into the
186
217
  # prompt — fail fast rather than hand the model a bogus (or injected) ref.
187
218
  if [[ "$VERB" == "review" ]]; then
188
219
  git -C "$REPO" rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null 2>&1 \
189
- || { echo "error: --base is not a valid commit/ref in $REPO: $BASE" >&2; exit 2; }
220
+ || { echo "error: --base is not a valid commit/ref in $REPO: $BASE" >&2; exit 1; }
190
221
  fi
191
222
 
192
223
  resolve_colleague || exit 2
@@ -229,7 +260,9 @@ print_result() {
229
260
  # real repo) -> print a copy-paste `grade:` hint with the explicit task-id, so
230
261
  # the caller never has to rely on `last` (issue #132). A preview leaves it
231
262
  # empty (its artifact was discarded with the worktree, so it is not gradable).
232
- ASK_COLLEAGUE_REAL_ARTIFACT_DIR="${1:-}" ASK_COLLEAGUE_GRADABLE="${2:-}" python3 -c '
263
+ # $3 (optional): exit code from the colleague drive command, propagated to
264
+ # the caller when the drive itself failed.
265
+ ASK_COLLEAGUE_REAL_ARTIFACT_DIR="${1:-}" ASK_COLLEAGUE_GRADABLE="${2:-}" ASK_COLLEAGUE_DRIVE_RC="${3:-}" python3 -c '
233
266
  import sys, json, os
234
267
  raw = sys.stdin.read().strip()
235
268
  if not raw:
@@ -268,7 +301,7 @@ ap = d.get("artifacts_path")
268
301
  real_dir = os.environ.get("ASK_COLLEAGUE_REAL_ARTIFACT_DIR") or ""
269
302
  if ap and real_dir:
270
303
  ap = os.path.join(real_dir, os.path.basename(ap))
271
- if ap:
304
+ if ap and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
272
305
  print("artifact:", ap, file=out)
273
306
  # A drive is gradable whenever its artifact survives — including a FAILED drive
274
307
  # (colleague writes an artifact on failure too, h5): a failure rated 1/5 is exactly
@@ -276,7 +309,14 @@ if ap:
276
309
  # `out` (stderr on failure), matching the rest of the failure digest.
277
310
  if tid and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
278
311
  print("grade:", "ask-colleague feedback", tid, "--rating N", file=out)
279
- sys.exit(0 if ok else 1)
312
+ if ok:
313
+ sys.exit(0)
314
+ else:
315
+ rc = os.environ.get("ASK_COLLEAGUE_DRIVE_RC")
316
+ if rc in ("1", "2"):
317
+ sys.exit(int(rc))
318
+ else:
319
+ sys.exit(1)
280
320
  '
281
321
  }
282
322
 
@@ -394,8 +434,8 @@ _add_worktree() {
394
434
  run_readonly() {
395
435
  local instruction="$1"
396
436
  _add_worktree
397
- local out
398
- out="$("${COLLEAGUE[@]}" drive "$instruction" --repo "$_WT" --no-pr "${COMMON_FLAGS[@]}")" || true
437
+ local out rc=0
438
+ out="$("${COLLEAGUE[@]}" drive "$instruction" --repo "$_WT" --no-pr "${COMMON_FLAGS[@]}")" || rc=$?
399
439
  _DRIVE_BRANCH="$(printf '%s' "$out" | _extract_branch)"
400
440
  # Preserve the artifact to the real repo BEFORE the EXIT trap removes the
401
441
  # worktree, so the drive can be graded by its task-id (`ask-colleague feedback
@@ -409,7 +449,7 @@ run_readonly() {
409
449
  real_dir="$REPO/.colleague"
410
450
  gradable="1"
411
451
  fi
412
- printf '%s' "$out" | print_result "$real_dir" "$gradable"
452
+ printf '%s' "$out" | print_result "$real_dir" "$gradable" "$rc"
413
453
  }
414
454
 
415
455
  # ── write preview (default): drive in a throwaway worktree, show the would-be ──
@@ -418,8 +458,8 @@ run_readonly() {
418
458
  run_preview() {
419
459
  local instruction="$1"
420
460
  _add_worktree
421
- local out
422
- out="$("${COLLEAGUE[@]}" drive "$instruction" --repo "$_WT" --no-pr "${COMMON_FLAGS[@]}")" || true
461
+ local out rc=0
462
+ out="$("${COLLEAGUE[@]}" drive "$instruction" --repo "$_WT" --no-pr "${COMMON_FLAGS[@]}")" || rc=$?
423
463
  _DRIVE_BRANCH="$(printf '%s' "$out" | _extract_branch)"
424
464
 
425
465
  # Capture the would-be patch before _cleanup_worktree deletes the drive branch.
@@ -428,9 +468,9 @@ run_preview() {
428
468
  patch="$(git -C "$REPO" diff "HEAD..$_DRIVE_BRANCH" 2>/dev/null || true)"
429
469
  fi
430
470
 
431
- local rc=0
432
- printf '%s' "$out" | print_result || rc=$?
433
- if [[ "$rc" -eq 0 ]]; then
471
+ local prc=0
472
+ printf '%s' "$out" | print_result "" "" "$rc" || prc=$?
473
+ if [[ "$prc" -eq 0 ]]; then
434
474
  if [[ -n "$patch" ]]; then
435
475
  printf '\n--- preview diff (NOT applied — pass --apply to land it) ---\n'
436
476
  printf '%s\n' "$patch"
@@ -438,7 +478,7 @@ run_preview() {
438
478
  printf '\n(preview: no file changes reported; NOT applied)\n'
439
479
  fi
440
480
  fi
441
- return "$rc"
481
+ return "$prc"
442
482
  }
443
483
 
444
484
  # ── write verb: preview by default; --apply lands a drive branch; --pr opens a ─
@@ -454,7 +494,10 @@ run_write() {
454
494
  && [[ -n "$(git -C "$REPO" status --porcelain 2>/dev/null)" ]]; then
455
495
  echo "error: working tree is dirty — commit/stash first, or pass --allow-dirty" >&2
456
496
  echo "hint: 'colleague drive --no-pr' commits uncommitted edits onto the drive branch" >&2
457
- exit 2
497
+ # User-fixable state guard -> exit 1, matching the runtime's own
498
+ # dirty-tree guard (colleague/handoff.py _guard_clean_tree =
499
+ # EXIT_USER_ERROR), not exit 2 (#161).
500
+ exit 1
458
501
  fi
459
502
  # The runtime now enforces its own dirty-tree guard (colleague#149); pass
460
503
  # --allow-dirty through so an operator opt-in here isn't re-refused by the
@@ -462,20 +505,22 @@ run_write() {
462
505
  # top — read-only/preview verbs run in a clean worktree and must not get it),
463
506
  # which sidesteps the `set -u` empty-array expansion hazard.
464
507
  [[ "$ALLOW_DIRTY" -eq 1 ]] && COMMON_FLAGS+=(--allow-dirty)
465
- # `|| true`: a failed drive (`colleague drive` returns 1 when status != ok,
466
- # printing the result JSON to stdout) must still flow into print_result so the
467
- # digest is emitted (to stderr) and the wrapper exits non-zero not aborted by
468
- # `set -e` at the assignment, which would swallow the digest. Matches the
469
- # read-only / preview paths, which already guard this way.
470
- local out
508
+ # `|| rc=$?`: a failed drive (`colleague drive` exits non-zero, printing the
509
+ # result JSON to stdout) must still flow into print_result so the digest is
510
+ # emitted (to stderr) and the wrapper propagates the drive's real exit code.
511
+ # The rc is captured (declaration split from the assignment so `local` doesn't
512
+ # mask it) rather than discarded with `|| true`, which would have collapsed the
513
+ # tri-state rc; `set -e` does not abort at the assignment. Matches the
514
+ # read-only / preview paths, which guard this way.
515
+ local out rc=0
471
516
  if [[ "$OPEN_PR" -eq 1 ]]; then
472
- out="$("${COLLEAGUE[@]}" drive "$instruction" --repo "$REPO" "${COMMON_FLAGS[@]}")" || true
517
+ out="$("${COLLEAGUE[@]}" drive "$instruction" --repo "$REPO" "${COMMON_FLAGS[@]}")" || rc=$?
473
518
  else
474
- out="$("${COLLEAGUE[@]}" drive "$instruction" --repo "$REPO" --no-pr "${COMMON_FLAGS[@]}")" || true
519
+ out="$("${COLLEAGUE[@]}" drive "$instruction" --repo "$REPO" --no-pr "${COMMON_FLAGS[@]}")" || rc=$?
475
520
  fi
476
521
  # A landed write persists its artifact in the real repo and moves `last`, so it
477
522
  # is gradable — print the `grade:` hint (with the explicit task-id).
478
- printf '%s' "$out" | print_result "" "1"
523
+ printf '%s' "$out" | print_result "" "1" "$rc"
479
524
  }
480
525
 
481
526
  # ── feedback verb: grade a finished drive (the ROI loop) ────────────────────
@@ -501,9 +546,23 @@ run_feedback() {
501
546
  "${cmd[@]}"
502
547
  }
503
548
 
549
+ # ── clean verb: recover a repo a crashed run wedged (#162) ──────────────────
550
+ # A thin pass-through to `colleague clean`: the runtime reaps stale/corrupt
551
+ # colleague/* branches + orphaned .colleague/ artifacts, scoped strictly to
552
+ # colleague/* (so an unrelated branch is never touched) and conservative with
553
+ # .git/objects. No worktree, no engine — colleague owns the reap + its own
554
+ # stdout/stderr/exit. The full flag set (--merged / --older-than) lives on
555
+ # `colleague clean`; the skill forwards the common --dry-run.
556
+ run_clean() {
557
+ local cmd=("${COLLEAGUE[@]}" clean --repo "$REPO")
558
+ [[ "$DRY_RUN" -eq 1 ]] && cmd+=(--dry-run)
559
+ "${cmd[@]}"
560
+ }
561
+
504
562
  case "$VERB" in
505
563
  explore) run_readonly "$(render_prompt explore)" ;;
506
564
  review) run_readonly "$(render_prompt review)" ;;
507
565
  write) run_write "$(render_prompt write)" ;;
508
566
  feedback) run_feedback "$ARG" ;;
567
+ clean) run_clean ;;
509
568
  esac
@@ -23,6 +23,9 @@ ignores:
23
23
  - ".teken/**"
24
24
  # Vendored skills are cited verbatim from guildmaster — do not reformat them.
25
25
  - ".claude/skills/**"
26
+ # Recipe payload snapshots are cited verbatim (cite-don't-import) and copied
27
+ # into target repos byte-for-byte — do not reformat them.
28
+ - "recipes/**/files/**"
26
29
  # devague-generated convergence artifacts (the exact `devague export` output;
27
30
  # headings carry the announcement verbatim and claims embed <placeholder>s).
28
31
  - "docs/specs/**"
@@ -5,6 +5,40 @@ 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.3.1] - 2026-06-12
9
+
10
+ ### Added
11
+
12
+ - **`ask-colleague-revendor` recipe** (`recipes/ask-colleague-revendor/`) —
13
+ propagates an upstream `ask-colleague` wrapper change from its origin
14
+ (colleague) to downstream consumers, and adds the skill fresh where absent.
15
+ Existing installs get `scripts/ask-colleague.sh` overwritten **only**
16
+ (wrapper-only, matching colleague#183; a consumer's `SKILL.md` provenance
17
+ token is never clobbered); absent installs get the full skill with the repo
18
+ nick substituted in. The recipe `README.md` carries the **downstream-consumer
19
+ ledger** and the **reconciliation procedure**, making rollout-cli the owner of
20
+ that propagation contract — closes #3.
21
+
22
+ ### Changed
23
+
24
+ - Re-vendored rollout-cli's own `ask-colleague` wrapper from colleague#183
25
+ (`resolve_colleague()` honors `--repo`; `drive` tri-state exit propagated;
26
+ no dead `artifact:` line). `SKILL.md`/prompts untouched.
27
+ - `.markdownlint-cli2.yaml` ignores `recipes/**/files/**` — recipe payload
28
+ snapshots are cited verbatim and must not be reformatted.
29
+ - `docs/skill-sources.md` points to the recipe as the owner of downstream
30
+ `ask-colleague` propagation + consumer tracking.
31
+
32
+ ### Fixed
33
+
34
+ - Hardened the `ask-colleague-revendor` recipe after review (Qodo on #5):
35
+ `check.sh` now requires a **quoted semver** `version` line and an existing
36
+ `CHANGELOG.md` (matching what `apply` actually needs, so a repo can't pass
37
+ `plan` then fail mid-`apply`; also drops a commented-`version` false positive);
38
+ `apply.sh` escapes `sed` replacement metacharacters in the repo nick; and
39
+ `manifest.toml` `touched_files` now declares the `SKILL.md`/prompt paths that
40
+ a fresh add writes.
41
+
8
42
  ## [0.3.0] - 2026-06-06
9
43
 
10
44
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rollout-cli
3
- Version: 0.3.0
3
+ Version: 0.3.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
@@ -105,6 +105,12 @@ cp -R ../colleague/.claude/skills/ask-colleague .claude/skills/
105
105
  Once guildmaster re-broadcasts `ask-colleague`, switch the upstream column back
106
106
  to `../guildmaster/.claude/skills/ask-colleague/` and re-sync from there.
107
107
 
108
+ The procedure above re-syncs *this* repo's own copy. Propagating an upstream
109
+ `ask-colleague` change **out to the other downstream consumers** — and the
110
+ ledger of who those consumers are — is owned by the
111
+ [`ask-colleague-revendor`](../recipes/ask-colleague-revendor/README.md) rollout
112
+ recipe (rollout-cli#3).
113
+
108
114
  ## Tooling prerequisites
109
115
 
110
116
  - **`devex`** (>=0.21) on PATH — `cicd` delegates the PR lifecycle to `devex pr`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rollout-cli"
3
- version = "0.3.0"
3
+ version = "0.3.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"
@@ -0,0 +1,84 @@
1
+ # ask-colleague-revendor
2
+
3
+ Propagate an upstream `ask-colleague` wrapper change from its origin repo
4
+ (**colleague**) to every downstream repo that vendors the skill, and add the
5
+ skill fresh where it is absent.
6
+
7
+ This recipe is also rollout-cli's answer to
8
+ [agentculture/rollout-cli#3](https://github.com/agentculture/rollout-cli/issues/3):
9
+ the **reconciliation procedure** and the **downstream-consumer ledger** live
10
+ here, in the propagation tool, rather than as an outbound ledger forced into the
11
+ origin repo (colleague).
12
+
13
+ ## What it does
14
+
15
+ `ask-colleague` is first-party to **colleague**; downstream repos vendor it
16
+ `cite, don't import`. The wrapper (`scripts/ask-colleague.sh`) is meant to be
17
+ **verbatim** across consumers; the `SKILL.md` is **not** — each consumer
18
+ substitutes its own nick into one provenance clause
19
+ (`which <nick> vendors *from* guildmaster`) and may lag the origin on prose.
20
+
21
+ So the recipe is deliberately split:
22
+
23
+ - **Existing install** → overwrite `scripts/ask-colleague.sh` **only**. This
24
+ matches how upstream changes land (colleague#183 was wrapper-only) and never
25
+ clobbers a consumer's `SKILL.md` provenance token or local divergence.
26
+ - **Skill absent** → lay down the full skill (`wrapper` + `SKILL.md` +
27
+ `prompts/`), substituting the target repo's nick into the `SKILL.md`
28
+ provenance line via the `__REPO_NICK__` token in `files/SKILL.md.tmpl`.
29
+
30
+ `check.sh` classifies a repo `done` (wrapper already byte-matches the vendored
31
+ snapshot), `ok` (wrapper missing or stale → apply), or `needs-attention` (no
32
+ version-bumpable `pyproject.toml`).
33
+
34
+ ## Downstream-consumer ledger
35
+
36
+ Origin: **colleague** (`agentculture/colleague`,
37
+ `.claude/skills/ask-colleague/`). Known consumers that re-vendor the wrapper:
38
+
39
+ | Consumer | Repo | Vendoring | Notes |
40
+ |----------|------|-----------|-------|
41
+ | culture | `agentculture/culture` | re-vendor wrapper | merged at pre-#183; carries `which culture …` token |
42
+ | agentirc | `agentculture/agentirc` | re-vendor wrapper | merged at pre-#183; no `culture.yaml` (identity differs) |
43
+ | steward | `agentculture/steward` | re-vendor wrapper | merged at pre-#183; carries `which steward …` token |
44
+ | rollout-cli | `agentculture/rollout-cli` | re-vendor wrapper | this repo ("self"); carries `which rollout-cli …` token |
45
+ | dgx-spark-cli | `agentculture/dgx-spark-cli` | fresh add | skill was absent |
46
+ | reterminal-cli | `agentculture/reterminal-cli` | re-vendor wrapper | merged at pre-#183 |
47
+
48
+ Keep this table current when a consumer is added or dropped.
49
+
50
+ ## Reconciliation procedure (when colleague lands an upstream change)
51
+
52
+ 1. **Refresh the snapshot** from the origin checkout so the recipe carries the
53
+ new wrapper (and, for fresh-adds, current `SKILL.md`/prompts):
54
+
55
+ ```bash
56
+ O=../colleague/.claude/skills/ask-colleague
57
+ cp "$O/scripts/ask-colleague.sh" recipes/ask-colleague-revendor/files/scripts/
58
+ cp "$O"/prompts/*.md recipes/ask-colleague-revendor/files/prompts/
59
+ cp "$O/SKILL.md" recipes/ask-colleague-revendor/files/SKILL.md.tmpl
60
+ # re-apply the token: in SKILL.md.tmpl, `which colleague vendors *from*`
61
+ # → `which __REPO_NICK__ vendors *from*`
62
+ ```
63
+
64
+ 2. **Dry-run** the classification across consumers:
65
+
66
+ ```bash
67
+ uv run rollout-cli plan ask-colleague-revendor \
68
+ --repos culture,agentirc,steward,dgx-spark-cli,reterminal-cli --base <clones-dir>
69
+ ```
70
+
71
+ 3. **Apply** — branch, patch wrapper, bump version, open one PR per repo
72
+ (operator squash-merges; rollout never merges):
73
+
74
+ ```bash
75
+ uv run rollout-cli apply ask-colleague-revendor \
76
+ --repos culture,agentirc,steward,dgx-spark-cli,reterminal-cli --base <clones-dir> --yes
77
+ ```
78
+
79
+ Run against **fresh clones** (not live agent checkouts) so a running agent is
80
+ never disrupted; rollout requires each target clean and on `main`.
81
+
82
+ 4. Update the consumer ledger above if the set changed.
83
+
84
+ Refs: colleague#183 (wrapper fixes), colleague#180/#181, rollout-cli#3.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # apply.sh — re-vendor the ask-colleague skill wrapper from colleague (#183).
3
+ # Usage: apply.sh [REPO_DIR] (default: current directory)
4
+ #
5
+ # Idempotent. Two cases, one code path:
6
+ # * Existing install — overwrite scripts/ask-colleague.sh ONLY. This mirrors
7
+ # colleague#183, which was wrapper-only; a downstream's SKILL.md carries a
8
+ # repo-specific provenance token (and may intentionally diverge), so it is
9
+ # never clobbered here.
10
+ # * Skill absent — lay down the full skill (wrapper + SKILL.md + prompts),
11
+ # substituting the target repo's nick into SKILL.md's provenance line.
12
+ set -euo pipefail
13
+
14
+ REPO="${1:-.}"
15
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
16
+ SRC="${SCRIPT_DIR}/files"
17
+ DEST="${REPO}/.claude/skills/ask-colleague"
18
+
19
+ mkdir -p "${DEST}/scripts" "${DEST}/prompts"
20
+
21
+ # (1) Wrapper — always overwrite verbatim (the #183 payload).
22
+ cp "${SRC}/scripts/ask-colleague.sh" "${DEST}/scripts/ask-colleague.sh"
23
+ chmod 0755 "${DEST}/scripts/ask-colleague.sh"
24
+
25
+ # (2) SKILL.md — only when absent (fresh add); preserve an existing copy.
26
+ if [ ! -f "${DEST}/SKILL.md" ]; then
27
+ nick="$(basename "$(cd "${REPO}" && pwd)")"
28
+ # Escape sed replacement metacharacters (& \ and the / delimiter) so a repo
29
+ # basename containing them can't corrupt or break the substitution.
30
+ nick_esc="$(printf '%s' "${nick}" | sed -e 's/[&/\\]/\\&/g')"
31
+ sed "s/__REPO_NICK__/${nick_esc}/g" "${SRC}/SKILL.md.tmpl" > "${DEST}/SKILL.md"
32
+ fi
33
+
34
+ # (3) Prompts — only when absent (fresh add); never disturb existing ones.
35
+ for p in explore review write; do
36
+ if [ ! -f "${DEST}/prompts/${p}.md" ]; then
37
+ cp "${SRC}/prompts/${p}.md" "${DEST}/prompts/${p}.md"
38
+ fi
39
+ done
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ # check.sh — classify a target repo for the ask-colleague-revendor recipe.
3
+ # Usage: check.sh [REPO_DIR] (default: current directory)
4
+ # Prints exactly one status word to stdout and exits 0:
5
+ # "done" — wrapper already byte-matches the vendored #183 wrapper
6
+ # "ok" — wrapper missing or stale (recipe adds / refreshes it)
7
+ # "needs-attention" — not a version-bumpable agent repo (no pyproject version)
8
+ set -euo pipefail
9
+
10
+ REPO="${1:-.}"
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ SRC_WRAPPER="${SCRIPT_DIR}/files/scripts/ask-colleague.sh"
13
+ DEST_WRAPPER="${REPO}/.claude/skills/ask-colleague/scripts/ask-colleague.sh"
14
+
15
+ # apply unconditionally bumps pyproject.toml's version and prepends to
16
+ # CHANGELOG.md, so the preflight must match what those steps require:
17
+ # * a *quoted* semver version line (rollout/core/bump.py `_LINE_RE`/`_VERSION_RE`)
18
+ # * an existing CHANGELOG.md (rollout/core/bump.py prepend_changelog)
19
+ # A looser check lets a repo pass `plan` then fail mid-`apply`.
20
+ if [ ! -f "${REPO}/pyproject.toml" ] \
21
+ || ! grep -Eq '^[[:space:]]*version[[:space:]]*=[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+"' "${REPO}/pyproject.toml" \
22
+ || [ ! -f "${REPO}/CHANGELOG.md" ]; then
23
+ echo "needs-attention"
24
+ exit 0
25
+ fi
26
+
27
+ if [ -f "${DEST_WRAPPER}" ] && cmp -s "${DEST_WRAPPER}" "${SRC_WRAPPER}"; then
28
+ echo "done"
29
+ else
30
+ echo "ok"
31
+ fi