rollout-cli 0.4.1__tar.gz → 0.4.2__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 (121) hide show
  1. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/ask-colleague/SKILL.md +36 -14
  2. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +101 -36
  3. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/CHANGELOG.md +6 -0
  4. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/PKG-INFO +1 -1
  5. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/pyproject.toml +1 -1
  6. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/ask-colleague-revendor/apply.sh +6 -6
  7. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/ask-colleague-revendor/check.sh +1 -1
  8. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/ask-colleague-revendor/files/SKILL.md.tmpl +5 -1
  9. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/ask-colleague-revendor/files/scripts/ask-colleague.sh +101 -36
  10. rollout_cli-0.4.2/recipes/ask-colleague-revendor/manifest.toml +27 -0
  11. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/uv.lock +1 -1
  12. rollout_cli-0.4.1/recipes/ask-colleague-revendor/manifest.toml +0 -25
  13. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/agent-config/SKILL.md +0 -0
  14. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  15. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/agent-config/scripts/show.sh +0 -0
  16. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  17. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  18. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  19. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  20. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  21. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/cicd/SKILL.md +0 -0
  22. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  23. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  24. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  25. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  26. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  27. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/communicate/SKILL.md +0 -0
  28. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  29. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  30. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  31. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  32. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  33. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  34. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  35. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  36. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/mass-update/SKILL.md +0 -0
  37. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/mass-update/scripts/mass-update.sh +0 -0
  38. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  39. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  40. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/run-tests/SKILL.md +0 -0
  41. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/run-tests/scripts/test.sh +0 -0
  42. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/sonarclaude/SKILL.md +0 -0
  43. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  44. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  45. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  46. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/think/SKILL.md +0 -0
  47. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/think/scripts/think.sh +0 -0
  48. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/version-bump/SKILL.md +0 -0
  49. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills/version-bump/scripts/bump.py +0 -0
  50. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.claude/skills.local.yaml.example +0 -0
  51. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.devague/frames/mass-update-now-clones-each-target-repo-into-a-git.json +0 -0
  52. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.devague/frames/rollout-cli-ships-rollout-define-an-org-wide-chang.json +0 -0
  53. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.devague/plans/mass-update-now-clones-each-target-repo-into-a-git.json +0 -0
  54. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.devague/plans/rollout-cli-ships-rollout-define-an-org-wide-chang.json +0 -0
  55. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.flake8 +0 -0
  56. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.github/workflows/publish.yml +0 -0
  57. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.github/workflows/tests.yml +0 -0
  58. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.gitignore +0 -0
  59. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/.markdownlint-cli2.yaml +0 -0
  60. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/AGENTS.colleague.md +0 -0
  61. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/CLAUDE.md +0 -0
  62. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/LICENSE +0 -0
  63. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/README.md +0 -0
  64. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/culture.yaml +0 -0
  65. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/docs/plans/2026-06-06-rollout-cli-ships-rollout-define-an-org-wide-chang.md +0 -0
  66. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/docs/plans/2026-06-12-mass-update-now-clones-each-target-repo-into-a-git.md +0 -0
  67. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/docs/skill-sources.md +0 -0
  68. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/docs/specs/2026-06-06-rollout-cli-ships-rollout-define-an-org-wide-chang.md +0 -0
  69. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/docs/specs/2026-06-12-mass-update-now-clones-each-target-repo-into-a-git.md +0 -0
  70. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/apache-relicense/apply.sh +0 -0
  71. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/apache-relicense/check.sh +0 -0
  72. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/apache-relicense/files/LICENSE +0 -0
  73. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/apache-relicense/manifest.toml +0 -0
  74. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/ask-colleague-revendor/README.md +0 -0
  75. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/ask-colleague-revendor/files/prompts/explore.md +0 -0
  76. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/ask-colleague-revendor/files/prompts/review.md +0 -0
  77. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/recipes/ask-colleague-revendor/files/prompts/write.md +0 -0
  78. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/__init__.py +0 -0
  79. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/__main__.py +0 -0
  80. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/__init__.py +0 -0
  81. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_commands/__init__.py +0 -0
  82. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_commands/apply.py +0 -0
  83. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_commands/cli.py +0 -0
  84. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_commands/doctor.py +0 -0
  85. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_commands/explain.py +0 -0
  86. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_commands/learn.py +0 -0
  87. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_commands/overview.py +0 -0
  88. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_commands/plan.py +0 -0
  89. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_commands/whoami.py +0 -0
  90. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_errors.py +0 -0
  91. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/cli/_output.py +0 -0
  92. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/core/__init__.py +0 -0
  93. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/core/bump.py +0 -0
  94. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/core/devex_pr.py +0 -0
  95. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/core/gitops.py +0 -0
  96. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/core/orchestrate.py +0 -0
  97. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/core/preflight.py +0 -0
  98. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/core/recipe.py +0 -0
  99. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/core/report.py +0 -0
  100. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/core/targets.py +0 -0
  101. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/explain/__init__.py +0 -0
  102. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/rollout/explain/catalog.py +0 -0
  103. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/sonar-project.properties +0 -0
  104. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/__init__.py +0 -0
  105. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_apache_relicense.py +0 -0
  106. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_apply_cmd.py +0 -0
  107. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_bump.py +0 -0
  108. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_cli.py +0 -0
  109. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_cli_introspection.py +0 -0
  110. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_cli_wiring.py +0 -0
  111. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_devex_pr.py +0 -0
  112. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_e2e_apache_relicense.py +0 -0
  113. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_explain_catalog.py +0 -0
  114. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_gitops.py +0 -0
  115. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_mass_update.py +0 -0
  116. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_orchestrate.py +0 -0
  117. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_plan_cmd.py +0 -0
  118. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_preflight.py +0 -0
  119. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_recipe.py +0 -0
  120. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_report.py +0 -0
  121. {rollout_cli-0.4.1 → rollout_cli-0.4.2}/tests/test_targets.py +0 -0
@@ -7,18 +7,20 @@ description: >
7
7
  isn't a stronger model; it's a second, independent mind, and that diversity is
8
8
  the value: `ask-colleague review` gets a candid second opinion on a diff,
9
9
  `ask-colleague explore` gets a fresh read of an area, `ask-colleague write`
10
- hands off a small implementation, and `ask-colleague feedback` grades a finished
11
- work item (the ROI loop). Reach for it REFLEXIVELY, the way you'd lean over to the
10
+ hands off a small implementation, `ask-colleague feedback` grades a finished
11
+ work item (the ROI loop), and `ask-colleague clean` reaps stale/corrupt
12
+ `colleague/*` branches a crashed run left behind (which can break `git fetch`).
13
+ Reach for it REFLEXIVELY, the way you'd lean over to the
12
14
  teammate at the next desk — not only when asked: before you present or open a PR
13
15
  on a non-trivial committed diff, run `review` for a diverse second opinion; for a
14
16
  fresh read of an unfamiliar area whose answer is independent of your current
15
17
  context, run `explore`. Both are read-only — isolated in a throwaway git
16
- worktree, zero side effects — so the reflex is always safe; the side-effecting
17
- `write --apply` / `write --pr` still needs the user's go-ahead. Triggers when the
18
+ worktree, zero side effects to your tree/branch — so the reflex is always safe; the
19
+ side-effecting `write --apply` / `write --pr` still needs the user's go-ahead. Triggers when the
18
20
  user says "ask colleague", "ask a colleague to review/explore/write this", "have
19
21
  colleague take a look", "get a second opinion", "ask the other model", "rate that
20
- work item" — and still on the legacy "outsource this". Colleague's output is a second
21
- opinion to verify and own, never authority.
22
+ work item", "clean up a crashed colleague run" — and still on the legacy "outsource this".
23
+ Colleague's output is a second opinion to verify and own, never authority.
22
24
  ---
23
25
 
24
26
  # ask-colleague — lean on colleague as a different mind
@@ -95,10 +97,11 @@ else an install hint.
95
97
 
96
98
  | Verb | What it does | Side effects |
97
99
  |------|--------------|--------------|
98
- | `explore "<question or area>"` | Read-only investigation of the repo; the model reads and reports findings. | **None** — runs in a throwaway worktree at HEAD. |
99
- | `review "<what to focus on>" [--base main]` | A diverse second opinion on the **committed** diff (`<base>...HEAD`). | **None** — throwaway worktree; reviews committed changes only. |
100
- | `write "<task>" [--apply\|--pr]` | Implement a change. **Previews by default** (throwaway worktree, prints the would-be diff); `--apply` lands a work branch in place; `--pr` pushes + opens a PR. | **None** by default (preview); a `colleague/<id>` work branch / PR only with `--apply` / `--pr`. |
100
+ | `explore "<question or area>"` | Read-only investigation of the repo; the model reads and reports findings. | **None** to your working tree / branch — runs in a throwaway worktree at HEAD; writes only a gradable run artifact under the gitignored `.colleague/` bookkeeping dir. |
101
+ | `review "<what to focus on>" [--base main]` | A diverse second opinion on the **committed** diff (`<base>...HEAD`). | **None** to your working tree / branch — throwaway worktree, committed changes only; writes only a gradable run artifact under the gitignored `.colleague/` bookkeeping dir. |
102
+ | `write "<task>" [--apply\|--pr]` | Implement a change. **Previews by default** (throwaway worktree, prints the would-be diff); `--apply` lands a work branch in place; `--pr` pushes + opens a PR. | **None** to your working tree / branch by default (preview); a `colleague/<id>` work branch / PR only with `--apply` / `--pr`. |
101
103
  | `feedback <id\|last> [--rating N]` | **Grade a finished work item** (the ROI loop). With `--rating N` (1–5, plus `--notes`) it records feedback; without, it shows the work item's existing feedback. `last` resolves the most recent work item in `--repo`. | Writes `.colleague/<id>.feedback.json` only when `--rating` is given; read-only otherwise. |
104
+ | `clean [--dry-run]` | **Reap what a crashed run left behind** (#162): stale/corrupt `colleague/*` branches + orphaned 0-byte `.colleague/` artifacts that can wedge `git fetch`. Scoped strictly to `colleague/*` (never touches an unrelated branch); conservative with `.git/objects` (reports 0-byte loose objects + suggests `git prune`, never deletes them). A thin pass-through to `colleague clean`. | Deletes corrupt `colleague/<id>` refs + 0-byte `.colleague/` artifacts in `--repo`; `--dry-run` changes nothing. |
102
105
 
103
106
  ### Options
104
107
 
@@ -116,10 +119,15 @@ else an install hint.
116
119
  | `--rating N` | (`feedback`) record a 1–5 quality rating for the work item. |
117
120
  | `--notes "..."` | (`feedback`) free-text notes stored with the rating. |
118
121
  | `--by NAME` | (`feedback`) who is grading (default: colleague's resolved identity). |
122
+ | `--dry-run` | (`clean`) report what would be reaped without changing anything. |
123
+ | `--json` | (any verb) machine-readable output: stdout carries **only** the result JSON, every diagnostic/digest line goes to stderr. |
119
124
 
120
125
  The result printed to stdout is the work item's `TaskResult.summary` (plus
121
126
  `changed_files` / work branch for `write`), parsed from `colleague work
122
- --json`. Per-step progress streams to stderr while it runs.
127
+ --json`. Per-step progress streams to stderr while it runs. Pass `--json` to get
128
+ the raw `TaskResult` on stdout instead of the human digest (the drive verbs emit
129
+ the normalized `TaskResult`; `feedback` / `clean` forward `--json` to colleague),
130
+ keeping stdout valid JSON for a machine consumer while diagnostics stay on stderr.
123
131
 
124
132
  ## When to reach for which verb
125
133
 
@@ -140,6 +148,11 @@ The result printed to stdout is the work item's `TaskResult.summary` (plus
140
148
  says how *good* it was — together they let you compute the **ROI of asking
141
149
  colleague** and decide whether to ask again (and which backend). Grade the most
142
150
  recent work item with `ask-colleague feedback last --rating 4 --notes "…"`.
151
+ - **clean** — recovery, not routine. A crashed / interrupted `write --apply` can
152
+ leave a dangling `colleague/<id>` branch pointing at half-written (0-byte)
153
+ objects that **breaks `git fetch` / `git pull`**. Run `ask-colleague clean`
154
+ (or `colleague clean`) to reap it — start with `--dry-run` to see what it would
155
+ remove. It only ever touches `colleague/*` refs and `.colleague/` artifacts.
143
156
 
144
157
  ## Hard rules (do not violate)
145
158
 
@@ -168,11 +181,20 @@ The result printed to stdout is the work item's `TaskResult.summary` (plus
168
181
  uncommitted work, commit it first.
169
182
  - The default backend is whatever single model is running locally; a multi-model
170
183
  fleet (different model per verb) is separate infrastructure.
184
+ - **Every verb writes bookkeeping under `.colleague/`** (run artifacts for
185
+ explore/review/write; feedback records; the `last_work` pointer) — none of it
186
+ in your tracked tree, but in a repo that does **not** already gitignore
187
+ `.colleague/` it shows up as untracked files. **Add `.colleague/` to your
188
+ `.gitignore`** (keep `!/.colleague/commands/` if you commit command templates).
189
+ - **A crashed run can wedge `git fetch`.** A `write --apply` interrupted
190
+ mid-commit can leave a dangling `colleague/<id>` branch + 0-byte artifacts;
191
+ `ask-colleague clean` recovers it. A SIGKILL/OOM *during* the commit can still
192
+ corrupt git objects (git/filesystem durability, not the skill's to guarantee)
193
+ — which is exactly what `clean` is for.
171
194
 
172
195
  ## Provenance
173
196
 
174
197
  This is a **first-party** colleague skill — colleague is its origin. It is
175
- the inverse of the other skills under `.claude/skills/`, which
176
- rollout-cli vendors *from* guildmaster. See `docs/skill-sources.md`.
177
- The `cite, don't import` policy holds: downstream repos copy it, they don't
178
- symlink or depend on it.
198
+ the inverse of the other skills under `.claude/skills/`, which rollout-cli
199
+ vendors *from* guildmaster. See `docs/skill-sources.md`. The `cite, don't import`
200
+ policy holds: downstream repos copy it, they don't symlink or depend on it.
@@ -100,6 +100,8 @@ Options:
100
100
  --rating N (feedback) record a 1-5 quality rating for the drive
101
101
  --notes "..." (feedback) free-text notes to store with the rating
102
102
  --by NAME (feedback) who is grading (default: colleague's resolved identity)
103
+ --json Machine-readable output: stdout carries only the result JSON,
104
+ every diagnostic/digest line goes to stderr (any verb)
103
105
 
104
106
  explore/review run in a throwaway git worktree at HEAD — they cannot touch your
105
107
  working tree or branch. review compares <base>...HEAD (committed changes only).
@@ -127,16 +129,23 @@ case "$VERB" in
127
129
  ;;
128
130
  esac
129
131
 
130
- # Required external tools fail fast with a clear message, not an opaque
131
- # mid-run error, if the environment is missing one.
132
- require_tools() {
132
+ # Verify the external tools a given verb path actually needs are on PATH — fail
133
+ # fast with a clear message, not an opaque mid-run error. The required set is
134
+ # verb-specific (the caller passes it once the verb + flags are known, below):
135
+ # feedback/clean are thin pass-throughs to `colleague` plus the shared git
136
+ # work-tree guard, so they need only git; the drive verbs (explore/review/write)
137
+ # also render a prompt and parse colleague's --json result via python3, and the
138
+ # worktree-isolated paths additionally need mktemp. grep is only used by the
139
+ # uv-fallback resolver, which degrades to the clear "colleague not found" message
140
+ # when absent, so it is not a hard requirement here.
141
+ require_tools() { # $@ = tool names this verb path needs
133
142
  local missing=() t
134
- for t in python3 git grep mktemp; do
143
+ for t in "$@"; do
135
144
  command -v "$t" >/dev/null 2>&1 || missing+=("$t")
136
145
  done
137
146
  if [[ ${#missing[@]} -gt 0 ]]; then
138
147
  echo "error: missing required tool(s): ${missing[*]}" >&2
139
- echo "hint: ask-colleague needs python3, git, grep, and mktemp on PATH." >&2
148
+ echo "hint: '$VERB' needs these on PATH: $*" >&2
140
149
  exit 2
141
150
  fi
142
151
  }
@@ -151,8 +160,6 @@ need_value() { # $1 = remaining arg count ($#), $2 = flag name
151
160
  }
152
161
  }
153
162
 
154
- require_tools
155
-
156
163
  # ── defaults + flag parsing ─────────────────────────────────────────────────
157
164
  REPO="."
158
165
  BASE="main"
@@ -170,6 +177,7 @@ RATING=""
170
177
  NOTES=""
171
178
  BY=""
172
179
  ARG=""
180
+ JSON_OUT=0
173
181
 
174
182
  while [[ $# -gt 0 ]]; do
175
183
  case "$1" in
@@ -187,6 +195,7 @@ while [[ $# -gt 0 ]]; do
187
195
  --rating) need_value "$#" "$1"; RATING="$2"; shift 2 ;;
188
196
  --notes) need_value "$#" "$1"; NOTES="$2"; shift 2 ;;
189
197
  --by) need_value "$#" "$1"; BY="$2"; shift 2 ;;
198
+ --json) JSON_OUT=1; shift ;;
190
199
  -h | --help) usage; exit 0 ;;
191
200
  --) shift; while [[ $# -gt 0 ]]; do ARG="${ARG:+$ARG }$1"; shift; done ;;
192
201
  # unknown option -> user-input error (#161)
@@ -195,6 +204,23 @@ while [[ $# -gt 0 ]]; do
195
204
  esac
196
205
  done
197
206
 
207
+ # Now that the verb and its flags are known, require only the tools THIS path
208
+ # uses. feedback/clean shell straight to `colleague` (+ the git work-tree guard
209
+ # below), so they need only git — not python3/mktemp (qodo: the old blanket check
210
+ # failed those verbs in minimal envs). write --apply/--pr lands in place with no
211
+ # throwaway worktree, so it needs no mktemp either.
212
+ case "$VERB" in
213
+ feedback | clean) require_tools git ;;
214
+ write)
215
+ if [[ "$APPLY" -eq 1 || "$OPEN_PR" -eq 1 ]]; then
216
+ require_tools git python3
217
+ else
218
+ require_tools git python3 mktemp # preview runs in a throwaway worktree
219
+ fi
220
+ ;;
221
+ *) require_tools git python3 mktemp ;; # explore / review
222
+ esac
223
+
198
224
  # clean takes no description argument; every other verb requires one. (All of the
199
225
  # guards below are user-input errors -> exit 1, per the policy comment at the top.)
200
226
  if [[ "$VERB" == "clean" ]]; then
@@ -262,9 +288,10 @@ print_result() {
262
288
  # empty (its artifact was discarded with the worktree, so it is not gradable).
263
289
  # $3 (optional): exit code from the colleague drive command, propagated to
264
290
  # 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 '
291
+ ASK_COLLEAGUE_REAL_ARTIFACT_DIR="${1:-}" ASK_COLLEAGUE_GRADABLE="${2:-}" ASK_COLLEAGUE_DRIVE_RC="${3:-}" ASK_COLLEAGUE_JSON="${JSON_OUT:-0}" python3 -c '
266
292
  import sys, json, os
267
293
  raw = sys.stdin.read().strip()
294
+ json_mode = os.environ.get("ASK_COLLEAGUE_JSON") == "1"
268
295
  if not raw:
269
296
  sys.stderr.write("error: colleague produced no result on stdout (see diagnostics above)\n")
270
297
  sys.exit(2)
@@ -275,40 +302,69 @@ except Exception:
275
302
  sys.stderr.write(raw[:2000] + "\n")
276
303
  sys.exit(2)
277
304
  ok = d.get("status") == "ok"
278
- out = sys.stdout if ok else sys.stderr
279
305
  tid = d.get("task_id") or ""
280
- print("status:", d.get("status"), file=out)
281
- if tid:
282
- print("task:", tid, file=out)
306
+ # Resolve the artifact path to the preserved copy when the drive ran in a
307
+ # throwaway worktree (read-only verbs); the raw JSON points into the now-deleted
308
+ # worktree, so both the digest and the --json output report the real location.
309
+ ap = d.get("artifacts_path")
310
+ real_dir = os.environ.get("ASK_COLLEAGUE_REAL_ARTIFACT_DIR") or ""
311
+ if ap and real_dir:
312
+ ap = os.path.join(real_dir, os.path.basename(ap))
283
313
  # A drive that stopped without calling finish (colleague#142) or exhausted its
284
314
  # step budget did NOT deliver an authoritative result — its summary is the model
285
315
  # trailing off mid-task. Warn so the caller treats it as a partial, not a verdict.
286
- # The warning is a DIAGNOSTIC -> always stderr (never stdout), so a successful
287
- # drive keeps a clean, machine-readable stdout (no single quotes in this body).
316
+ # The warning is a DIAGNOSTIC -> always stderr (never stdout), so both the digest
317
+ # and --json keep a clean, machine-readable stdout (no single quotes in this body).
288
318
  if d.get("stopped_without_finish"):
289
319
  print("warning: drive ended without calling finish — treat the summary as a", file=sys.stderr)
290
320
  print(" partial (the model stopped mid-task), not an authoritative result.", file=sys.stderr)
291
321
  elif d.get("not_finished"):
292
322
  print("warning: drive ran out of steps without finishing — summary is partial.", file=sys.stderr)
293
- print(file=out)
294
- print((d.get("summary") or "").rstrip(), file=out)
295
- cf = d.get("changed_files") or []
296
- if cf:
297
- print("\nchanged files:", ", ".join(cf), file=out)
298
- if d.get("branch"):
299
- print("drive branch:", d["branch"], file=out)
300
- ap = d.get("artifacts_path")
301
- real_dir = os.environ.get("ASK_COLLEAGUE_REAL_ARTIFACT_DIR") or ""
302
- if ap and real_dir:
303
- ap = os.path.join(real_dir, os.path.basename(ap))
304
- if ap and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
305
- print("artifact:", ap, file=out)
306
- # A drive is gradable whenever its artifact survives — including a FAILED drive
307
- # (colleague writes an artifact on failure too, h5): a failure rated 1/5 is exactly
308
- # the ROI signal, so the hint must not be gated on `ok` (#139 qodo). It prints to
309
- # `out` (stderr on failure), matching the rest of the failure digest.
310
- if tid and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
311
- print("grade:", "ask-colleague feedback", tid, "--rating N", file=out)
323
+ if json_mode:
324
+ # --json contract: stdout carries ONLY the TaskResult JSON; every
325
+ # human/diagnostic line already went to stderr above. The exit code still
326
+ # reflects drive success/failure.
327
+ #
328
+ # artifacts_path mirrors the digest gate (ASK_COLLEAGUE_GRADABLE): it is only
329
+ # meaningful when the artifact SURVIVES. A preview (run_preview passes empty
330
+ # gradable) drives in a throwaway worktree the EXIT trap deletes, so the raw
331
+ # path points at a dir that is gone by the time the caller reads it — drop it
332
+ # rather than hand a machine consumer a dead path (#186 qodo finding-3). When
333
+ # gradable, rewrite it to the preserved copy in the real repo.
334
+ gradable = os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1"
335
+ if not gradable:
336
+ d.pop("artifacts_path", None)
337
+ elif ap:
338
+ d["artifacts_path"] = ap
339
+ json.dump(d, sys.stdout)
340
+ sys.stdout.write("\n")
341
+ # The task:/grade: hints are diagnostics -> stderr (stdout stays pure JSON).
342
+ # task_id is already in the payload, but echoing the copy-paste grade hint
343
+ # keeps the convention every work item follows (rule 907536) without breaking
344
+ # the stdout contract. Gated on gradable, exactly like the digest below.
345
+ if tid and gradable:
346
+ print("task:", tid, file=sys.stderr)
347
+ print("grade:", "ask-colleague feedback", tid, "--rating N", file=sys.stderr)
348
+ else:
349
+ out = sys.stdout if ok else sys.stderr
350
+ print("status:", d.get("status"), file=out)
351
+ if tid:
352
+ print("task:", tid, file=out)
353
+ print(file=out)
354
+ print((d.get("summary") or "").rstrip(), file=out)
355
+ cf = d.get("changed_files") or []
356
+ if cf:
357
+ print("\nchanged files:", ", ".join(cf), file=out)
358
+ if d.get("branch"):
359
+ print("drive branch:", d["branch"], file=out)
360
+ if ap and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
361
+ print("artifact:", ap, file=out)
362
+ # A drive is gradable whenever its artifact survives — including a FAILED
363
+ # drive (colleague writes an artifact on failure too, h5): a failure rated
364
+ # 1/5 is exactly the ROI signal, so the hint must not be gated on `ok` (#139
365
+ # qodo). It prints to `out` (stderr on failure), matching the failure digest.
366
+ if tid and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
367
+ print("grade:", "ask-colleague feedback", tid, "--rating N", file=out)
312
368
  if ok:
313
369
  sys.exit(0)
314
370
  else:
@@ -471,11 +527,15 @@ run_preview() {
471
527
  local prc=0
472
528
  printf '%s' "$out" | print_result "" "" "$rc" || prc=$?
473
529
  if [[ "$prc" -eq 0 ]]; then
530
+ # In --json mode stdout is reserved for the result JSON print_result just
531
+ # emitted, so the would-be patch is a diagnostic -> stderr (fd 2).
532
+ local diff_fd=1
533
+ [[ "$JSON_OUT" -eq 1 ]] && diff_fd=2
474
534
  if [[ -n "$patch" ]]; then
475
- printf '\n--- preview diff (NOT applied — pass --apply to land it) ---\n'
476
- printf '%s\n' "$patch"
535
+ printf '\n--- preview diff (NOT applied — pass --apply to land it) ---\n' >&"$diff_fd"
536
+ printf '%s\n' "$patch" >&"$diff_fd"
477
537
  else
478
- printf '\n(preview: no file changes reported; NOT applied)\n'
538
+ printf '\n(preview: no file changes reported; NOT applied)\n' >&"$diff_fd"
479
539
  fi
480
540
  fi
481
541
  return "$prc"
@@ -543,6 +603,9 @@ run_feedback() {
543
603
  cmd+=(show "$ref")
544
604
  fi
545
605
  cmd+=(--repo "$REPO")
606
+ # colleague feedback supports --json natively; forward the operator's request
607
+ # so stdout stays machine-readable end-to-end.
608
+ [[ "$JSON_OUT" -eq 1 ]] && cmd+=(--json)
546
609
  "${cmd[@]}"
547
610
  }
548
611
 
@@ -556,6 +619,8 @@ run_feedback() {
556
619
  run_clean() {
557
620
  local cmd=("${COLLEAGUE[@]}" clean --repo "$REPO")
558
621
  [[ "$DRY_RUN" -eq 1 ]] && cmd+=(--dry-run)
622
+ # colleague clean supports --json natively; forward it for machine-readable output.
623
+ [[ "$JSON_OUT" -eq 1 ]] && cmd+=(--json)
559
624
  "${cmd[@]}"
560
625
  }
561
626
 
@@ -5,6 +5,12 @@ 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.4.2] - 2026-06-13
9
+
10
+ ### Changed
11
+
12
+ - ask-colleague: sync SKILL.md + script to the canonical colleague source (documents the `clean` verb; script +137 lines). rollout-cli's copy was stale vs its origin (agentculture/colleague). Provenance line kept repo-correct.
13
+
8
14
  ## [0.4.1] - 2026-06-13
9
15
 
10
16
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rollout-cli
3
- Version: 0.4.1
3
+ Version: 0.4.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rollout-cli"
3
- version = "0.4.1"
3
+ version = "0.4.2"
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"
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env bash
2
- # apply.sh — re-vendor the ask-colleague skill wrapper from colleague (#183).
2
+ # apply.sh — re-vendor the ask-colleague skill wrapper from its canonical
3
+ # colleague source.
3
4
  # Usage: apply.sh [REPO_DIR] (default: current directory)
4
5
  #
5
6
  # 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.
7
+ # * Existing install — overwrite scripts/ask-colleague.sh ONLY (wrapper-only
8
+ # re-vendor); a downstream's SKILL.md carries a repo-specific provenance token
9
+ # (and may intentionally diverge), so it is never clobbered here.
10
10
  # * Skill absent — lay down the full skill (wrapper + SKILL.md + prompts),
11
11
  # substituting the target repo's nick into SKILL.md's provenance line.
12
12
  set -euo pipefail
@@ -18,7 +18,7 @@ DEST="${REPO}/.claude/skills/ask-colleague"
18
18
 
19
19
  mkdir -p "${DEST}/scripts" "${DEST}/prompts"
20
20
 
21
- # (1) Wrapper — always overwrite verbatim (the #183 payload).
21
+ # (1) Wrapper — always overwrite verbatim (the canonical wrapper payload).
22
22
  cp "${SRC}/scripts/ask-colleague.sh" "${DEST}/scripts/ask-colleague.sh"
23
23
  chmod 0755 "${DEST}/scripts/ask-colleague.sh"
24
24
 
@@ -2,7 +2,7 @@
2
2
  # check.sh — classify a target repo for the ask-colleague-revendor recipe.
3
3
  # Usage: check.sh [REPO_DIR] (default: current directory)
4
4
  # Prints exactly one status word to stdout and exits 0:
5
- # "done" — wrapper already byte-matches the vendored #183 wrapper
5
+ # "done" — wrapper already byte-matches the vendored canonical wrapper
6
6
  # "ok" — wrapper missing or stale (recipe adds / refreshes it)
7
7
  # "needs-attention" — not a version-bumpable agent repo (no pyproject version)
8
8
  set -euo pipefail
@@ -120,10 +120,14 @@ else an install hint.
120
120
  | `--notes "..."` | (`feedback`) free-text notes stored with the rating. |
121
121
  | `--by NAME` | (`feedback`) who is grading (default: colleague's resolved identity). |
122
122
  | `--dry-run` | (`clean`) report what would be reaped without changing anything. |
123
+ | `--json` | (any verb) machine-readable output: stdout carries **only** the result JSON, every diagnostic/digest line goes to stderr. |
123
124
 
124
125
  The result printed to stdout is the work item's `TaskResult.summary` (plus
125
126
  `changed_files` / work branch for `write`), parsed from `colleague work
126
- --json`. Per-step progress streams to stderr while it runs.
127
+ --json`. Per-step progress streams to stderr while it runs. Pass `--json` to get
128
+ the raw `TaskResult` on stdout instead of the human digest (the drive verbs emit
129
+ the normalized `TaskResult`; `feedback` / `clean` forward `--json` to colleague),
130
+ keeping stdout valid JSON for a machine consumer while diagnostics stay on stderr.
127
131
 
128
132
  ## When to reach for which verb
129
133
 
@@ -100,6 +100,8 @@ Options:
100
100
  --rating N (feedback) record a 1-5 quality rating for the drive
101
101
  --notes "..." (feedback) free-text notes to store with the rating
102
102
  --by NAME (feedback) who is grading (default: colleague's resolved identity)
103
+ --json Machine-readable output: stdout carries only the result JSON,
104
+ every diagnostic/digest line goes to stderr (any verb)
103
105
 
104
106
  explore/review run in a throwaway git worktree at HEAD — they cannot touch your
105
107
  working tree or branch. review compares <base>...HEAD (committed changes only).
@@ -127,16 +129,23 @@ case "$VERB" in
127
129
  ;;
128
130
  esac
129
131
 
130
- # Required external tools fail fast with a clear message, not an opaque
131
- # mid-run error, if the environment is missing one.
132
- require_tools() {
132
+ # Verify the external tools a given verb path actually needs are on PATH — fail
133
+ # fast with a clear message, not an opaque mid-run error. The required set is
134
+ # verb-specific (the caller passes it once the verb + flags are known, below):
135
+ # feedback/clean are thin pass-throughs to `colleague` plus the shared git
136
+ # work-tree guard, so they need only git; the drive verbs (explore/review/write)
137
+ # also render a prompt and parse colleague's --json result via python3, and the
138
+ # worktree-isolated paths additionally need mktemp. grep is only used by the
139
+ # uv-fallback resolver, which degrades to the clear "colleague not found" message
140
+ # when absent, so it is not a hard requirement here.
141
+ require_tools() { # $@ = tool names this verb path needs
133
142
  local missing=() t
134
- for t in python3 git grep mktemp; do
143
+ for t in "$@"; do
135
144
  command -v "$t" >/dev/null 2>&1 || missing+=("$t")
136
145
  done
137
146
  if [[ ${#missing[@]} -gt 0 ]]; then
138
147
  echo "error: missing required tool(s): ${missing[*]}" >&2
139
- echo "hint: ask-colleague needs python3, git, grep, and mktemp on PATH." >&2
148
+ echo "hint: '$VERB' needs these on PATH: $*" >&2
140
149
  exit 2
141
150
  fi
142
151
  }
@@ -151,8 +160,6 @@ need_value() { # $1 = remaining arg count ($#), $2 = flag name
151
160
  }
152
161
  }
153
162
 
154
- require_tools
155
-
156
163
  # ── defaults + flag parsing ─────────────────────────────────────────────────
157
164
  REPO="."
158
165
  BASE="main"
@@ -170,6 +177,7 @@ RATING=""
170
177
  NOTES=""
171
178
  BY=""
172
179
  ARG=""
180
+ JSON_OUT=0
173
181
 
174
182
  while [[ $# -gt 0 ]]; do
175
183
  case "$1" in
@@ -187,6 +195,7 @@ while [[ $# -gt 0 ]]; do
187
195
  --rating) need_value "$#" "$1"; RATING="$2"; shift 2 ;;
188
196
  --notes) need_value "$#" "$1"; NOTES="$2"; shift 2 ;;
189
197
  --by) need_value "$#" "$1"; BY="$2"; shift 2 ;;
198
+ --json) JSON_OUT=1; shift ;;
190
199
  -h | --help) usage; exit 0 ;;
191
200
  --) shift; while [[ $# -gt 0 ]]; do ARG="${ARG:+$ARG }$1"; shift; done ;;
192
201
  # unknown option -> user-input error (#161)
@@ -195,6 +204,23 @@ while [[ $# -gt 0 ]]; do
195
204
  esac
196
205
  done
197
206
 
207
+ # Now that the verb and its flags are known, require only the tools THIS path
208
+ # uses. feedback/clean shell straight to `colleague` (+ the git work-tree guard
209
+ # below), so they need only git — not python3/mktemp (qodo: the old blanket check
210
+ # failed those verbs in minimal envs). write --apply/--pr lands in place with no
211
+ # throwaway worktree, so it needs no mktemp either.
212
+ case "$VERB" in
213
+ feedback | clean) require_tools git ;;
214
+ write)
215
+ if [[ "$APPLY" -eq 1 || "$OPEN_PR" -eq 1 ]]; then
216
+ require_tools git python3
217
+ else
218
+ require_tools git python3 mktemp # preview runs in a throwaway worktree
219
+ fi
220
+ ;;
221
+ *) require_tools git python3 mktemp ;; # explore / review
222
+ esac
223
+
198
224
  # clean takes no description argument; every other verb requires one. (All of the
199
225
  # guards below are user-input errors -> exit 1, per the policy comment at the top.)
200
226
  if [[ "$VERB" == "clean" ]]; then
@@ -262,9 +288,10 @@ print_result() {
262
288
  # empty (its artifact was discarded with the worktree, so it is not gradable).
263
289
  # $3 (optional): exit code from the colleague drive command, propagated to
264
290
  # 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 '
291
+ ASK_COLLEAGUE_REAL_ARTIFACT_DIR="${1:-}" ASK_COLLEAGUE_GRADABLE="${2:-}" ASK_COLLEAGUE_DRIVE_RC="${3:-}" ASK_COLLEAGUE_JSON="${JSON_OUT:-0}" python3 -c '
266
292
  import sys, json, os
267
293
  raw = sys.stdin.read().strip()
294
+ json_mode = os.environ.get("ASK_COLLEAGUE_JSON") == "1"
268
295
  if not raw:
269
296
  sys.stderr.write("error: colleague produced no result on stdout (see diagnostics above)\n")
270
297
  sys.exit(2)
@@ -275,40 +302,69 @@ except Exception:
275
302
  sys.stderr.write(raw[:2000] + "\n")
276
303
  sys.exit(2)
277
304
  ok = d.get("status") == "ok"
278
- out = sys.stdout if ok else sys.stderr
279
305
  tid = d.get("task_id") or ""
280
- print("status:", d.get("status"), file=out)
281
- if tid:
282
- print("task:", tid, file=out)
306
+ # Resolve the artifact path to the preserved copy when the drive ran in a
307
+ # throwaway worktree (read-only verbs); the raw JSON points into the now-deleted
308
+ # worktree, so both the digest and the --json output report the real location.
309
+ ap = d.get("artifacts_path")
310
+ real_dir = os.environ.get("ASK_COLLEAGUE_REAL_ARTIFACT_DIR") or ""
311
+ if ap and real_dir:
312
+ ap = os.path.join(real_dir, os.path.basename(ap))
283
313
  # A drive that stopped without calling finish (colleague#142) or exhausted its
284
314
  # step budget did NOT deliver an authoritative result — its summary is the model
285
315
  # trailing off mid-task. Warn so the caller treats it as a partial, not a verdict.
286
- # The warning is a DIAGNOSTIC -> always stderr (never stdout), so a successful
287
- # drive keeps a clean, machine-readable stdout (no single quotes in this body).
316
+ # The warning is a DIAGNOSTIC -> always stderr (never stdout), so both the digest
317
+ # and --json keep a clean, machine-readable stdout (no single quotes in this body).
288
318
  if d.get("stopped_without_finish"):
289
319
  print("warning: drive ended without calling finish — treat the summary as a", file=sys.stderr)
290
320
  print(" partial (the model stopped mid-task), not an authoritative result.", file=sys.stderr)
291
321
  elif d.get("not_finished"):
292
322
  print("warning: drive ran out of steps without finishing — summary is partial.", file=sys.stderr)
293
- print(file=out)
294
- print((d.get("summary") or "").rstrip(), file=out)
295
- cf = d.get("changed_files") or []
296
- if cf:
297
- print("\nchanged files:", ", ".join(cf), file=out)
298
- if d.get("branch"):
299
- print("drive branch:", d["branch"], file=out)
300
- ap = d.get("artifacts_path")
301
- real_dir = os.environ.get("ASK_COLLEAGUE_REAL_ARTIFACT_DIR") or ""
302
- if ap and real_dir:
303
- ap = os.path.join(real_dir, os.path.basename(ap))
304
- if ap and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
305
- print("artifact:", ap, file=out)
306
- # A drive is gradable whenever its artifact survives — including a FAILED drive
307
- # (colleague writes an artifact on failure too, h5): a failure rated 1/5 is exactly
308
- # the ROI signal, so the hint must not be gated on `ok` (#139 qodo). It prints to
309
- # `out` (stderr on failure), matching the rest of the failure digest.
310
- if tid and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
311
- print("grade:", "ask-colleague feedback", tid, "--rating N", file=out)
323
+ if json_mode:
324
+ # --json contract: stdout carries ONLY the TaskResult JSON; every
325
+ # human/diagnostic line already went to stderr above. The exit code still
326
+ # reflects drive success/failure.
327
+ #
328
+ # artifacts_path mirrors the digest gate (ASK_COLLEAGUE_GRADABLE): it is only
329
+ # meaningful when the artifact SURVIVES. A preview (run_preview passes empty
330
+ # gradable) drives in a throwaway worktree the EXIT trap deletes, so the raw
331
+ # path points at a dir that is gone by the time the caller reads it — drop it
332
+ # rather than hand a machine consumer a dead path (#186 qodo finding-3). When
333
+ # gradable, rewrite it to the preserved copy in the real repo.
334
+ gradable = os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1"
335
+ if not gradable:
336
+ d.pop("artifacts_path", None)
337
+ elif ap:
338
+ d["artifacts_path"] = ap
339
+ json.dump(d, sys.stdout)
340
+ sys.stdout.write("\n")
341
+ # The task:/grade: hints are diagnostics -> stderr (stdout stays pure JSON).
342
+ # task_id is already in the payload, but echoing the copy-paste grade hint
343
+ # keeps the convention every work item follows (rule 907536) without breaking
344
+ # the stdout contract. Gated on gradable, exactly like the digest below.
345
+ if tid and gradable:
346
+ print("task:", tid, file=sys.stderr)
347
+ print("grade:", "ask-colleague feedback", tid, "--rating N", file=sys.stderr)
348
+ else:
349
+ out = sys.stdout if ok else sys.stderr
350
+ print("status:", d.get("status"), file=out)
351
+ if tid:
352
+ print("task:", tid, file=out)
353
+ print(file=out)
354
+ print((d.get("summary") or "").rstrip(), file=out)
355
+ cf = d.get("changed_files") or []
356
+ if cf:
357
+ print("\nchanged files:", ", ".join(cf), file=out)
358
+ if d.get("branch"):
359
+ print("drive branch:", d["branch"], file=out)
360
+ if ap and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
361
+ print("artifact:", ap, file=out)
362
+ # A drive is gradable whenever its artifact survives — including a FAILED
363
+ # drive (colleague writes an artifact on failure too, h5): a failure rated
364
+ # 1/5 is exactly the ROI signal, so the hint must not be gated on `ok` (#139
365
+ # qodo). It prints to `out` (stderr on failure), matching the failure digest.
366
+ if tid and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
367
+ print("grade:", "ask-colleague feedback", tid, "--rating N", file=out)
312
368
  if ok:
313
369
  sys.exit(0)
314
370
  else:
@@ -471,11 +527,15 @@ run_preview() {
471
527
  local prc=0
472
528
  printf '%s' "$out" | print_result "" "" "$rc" || prc=$?
473
529
  if [[ "$prc" -eq 0 ]]; then
530
+ # In --json mode stdout is reserved for the result JSON print_result just
531
+ # emitted, so the would-be patch is a diagnostic -> stderr (fd 2).
532
+ local diff_fd=1
533
+ [[ "$JSON_OUT" -eq 1 ]] && diff_fd=2
474
534
  if [[ -n "$patch" ]]; then
475
- printf '\n--- preview diff (NOT applied — pass --apply to land it) ---\n'
476
- printf '%s\n' "$patch"
535
+ printf '\n--- preview diff (NOT applied — pass --apply to land it) ---\n' >&"$diff_fd"
536
+ printf '%s\n' "$patch" >&"$diff_fd"
477
537
  else
478
- printf '\n(preview: no file changes reported; NOT applied)\n'
538
+ printf '\n(preview: no file changes reported; NOT applied)\n' >&"$diff_fd"
479
539
  fi
480
540
  fi
481
541
  return "$prc"
@@ -543,6 +603,9 @@ run_feedback() {
543
603
  cmd+=(show "$ref")
544
604
  fi
545
605
  cmd+=(--repo "$REPO")
606
+ # colleague feedback supports --json natively; forward the operator's request
607
+ # so stdout stays machine-readable end-to-end.
608
+ [[ "$JSON_OUT" -eq 1 ]] && cmd+=(--json)
546
609
  "${cmd[@]}"
547
610
  }
548
611
 
@@ -556,6 +619,8 @@ run_feedback() {
556
619
  run_clean() {
557
620
  local cmd=("${COLLEAGUE[@]}" clean --repo "$REPO")
558
621
  [[ "$DRY_RUN" -eq 1 ]] && cmd+=(--dry-run)
622
+ # colleague clean supports --json natively; forward it for machine-readable output.
623
+ [[ "$JSON_OUT" -eq 1 ]] && cmd+=(--json)
559
624
  "${cmd[@]}"
560
625
  }
561
626
 
@@ -0,0 +1,27 @@
1
+ name = "ask-colleague-revendor"
2
+ # The wrapper is always rewritten. The remaining paths are written only on a
3
+ # fresh add (skill absent); declared here so recipe introspection covers every
4
+ # file apply.sh may touch.
5
+ touched_files = [
6
+ ".claude/skills/ask-colleague/scripts/ask-colleague.sh",
7
+ ".claude/skills/ask-colleague/SKILL.md",
8
+ ".claude/skills/ask-colleague/prompts/explore.md",
9
+ ".claude/skills/ask-colleague/prompts/review.md",
10
+ ".claude/skills/ask-colleague/prompts/write.md",
11
+ ]
12
+ bump_level = "patch"
13
+ changelog_text = """\
14
+ ### Changed
15
+
16
+ - **Re-vendored the `ask-colleague` skill wrapper from its canonical colleague
17
+ source** (`.claude/skills/ask-colleague/scripts/ask-colleague.sh`): the wrapper
18
+ now supports `--json` machine-readable output — stdout 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
24
+ `SKILL.md` is left untouched (it carries a repo-specific provenance token and
25
+ may diverge). Where the skill was absent, it is added fresh (wrapper +
26
+ `SKILL.md` + prompts).
27
+ """
@@ -430,7 +430,7 @@ wheels = [
430
430
 
431
431
  [[package]]
432
432
  name = "rollout-cli"
433
- version = "0.4.1"
433
+ version = "0.4.2"
434
434
  source = { editable = "." }
435
435
 
436
436
  [package.dev-dependencies]
@@ -1,25 +0,0 @@
1
- name = "ask-colleague-revendor"
2
- # The wrapper is always rewritten. The remaining paths are written only on a
3
- # fresh add (skill absent); declared here so recipe introspection covers every
4
- # file apply.sh may touch.
5
- touched_files = [
6
- ".claude/skills/ask-colleague/scripts/ask-colleague.sh",
7
- ".claude/skills/ask-colleague/SKILL.md",
8
- ".claude/skills/ask-colleague/prompts/explore.md",
9
- ".claude/skills/ask-colleague/prompts/review.md",
10
- ".claude/skills/ask-colleague/prompts/write.md",
11
- ]
12
- bump_level = "patch"
13
- changelog_text = """\
14
- ### Changed
15
-
16
- - **Re-vendored the `ask-colleague` skill wrapper from colleague#183**
17
- (`.claude/skills/ask-colleague/scripts/ask-colleague.sh`): `resolve_colleague()`
18
- now honors `--repo` for the `uv` local-dev fallback, the `colleague drive`
19
- tri-state exit code (0/1/2) propagates end-to-end instead of collapsing to 1,
20
- and a non-preserved read-only run no longer prints a dead `artifact:` line into
21
- a throwaway worktree. Wrapper-only — an existing `SKILL.md` and prompt
22
- templates are left untouched (they carry a repo-specific provenance token and
23
- may diverge). Where the skill was absent, it is added fresh (wrapper +
24
- `SKILL.md` + prompts). Refs: colleague#183, #180, #181.
25
- """
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes