rollout-cli 0.6.0__tar.gz → 0.7.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 (137) hide show
  1. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/ask-colleague/SKILL.md +6 -5
  2. {rollout_cli-0.6.0/recipes/ask-colleague-revendor/files → rollout_cli-0.7.2/.claude/skills/ask-colleague}/prompts/explore.md +4 -0
  3. {rollout_cli-0.6.0/recipes/ask-colleague-revendor/files → rollout_cli-0.7.2/.claude/skills/ask-colleague}/prompts/review.md +2 -7
  4. {rollout_cli-0.6.0/recipes/ask-colleague-revendor/files → rollout_cli-0.7.2/.claude/skills/ask-colleague}/scripts/ask-colleague.sh +218 -27
  5. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/assign-to-workforce/SKILL.md +100 -17
  6. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +92 -26
  7. rollout_cli-0.7.2/.claude/skills/challenge/SKILL.md +290 -0
  8. rollout_cli-0.7.2/.claude/skills/deviate/SKILL.md +176 -0
  9. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/recall/SKILL.md +13 -9
  10. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/recall/scripts/recall.sh +45 -22
  11. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/remember/SKILL.md +8 -5
  12. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/remember/scripts/remember.sh +50 -22
  13. rollout_cli-0.7.2/.claude/skills/scope/SKILL.md +167 -0
  14. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/spec-to-plan/SKILL.md +89 -16
  15. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +1 -1
  16. rollout_cli-0.7.2/.claude/skills/summarize-delivery/SKILL.md +440 -0
  17. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/think/SKILL.md +98 -12
  18. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/think/scripts/think.sh +1 -1
  19. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/CHANGELOG.md +34 -1
  20. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/CLAUDE.md +28 -0
  21. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/PKG-INFO +1 -1
  22. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/docs/skill-sources.md +6 -0
  23. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/pyproject.toml +1 -1
  24. rollout_cli-0.7.2/recipes/eidetic-memory/README.md +161 -0
  25. rollout_cli-0.7.2/recipes/eidetic-memory/apply.sh +118 -0
  26. rollout_cli-0.7.2/recipes/eidetic-memory/check.sh +46 -0
  27. rollout_cli-0.7.2/recipes/eidetic-memory/files/CLAUDE.snippet.tmpl +25 -0
  28. rollout_cli-0.7.2/recipes/eidetic-memory/files/SKILL.md.recall.tmpl +185 -0
  29. rollout_cli-0.7.2/recipes/eidetic-memory/files/SKILL.md.remember.tmpl +121 -0
  30. rollout_cli-0.7.2/recipes/eidetic-memory/files/scripts/recall.sh +164 -0
  31. rollout_cli-0.7.2/recipes/eidetic-memory/files/scripts/remember.sh +166 -0
  32. rollout_cli-0.7.2/recipes/eidetic-memory/manifest.toml +51 -0
  33. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/core/devex_pr.py +18 -8
  34. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_devex_pr.py +23 -5
  35. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/uv.lock +1 -1
  36. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/agent-config/SKILL.md +0 -0
  37. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  38. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/agent-config/scripts/show.sh +0 -0
  39. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  40. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/cicd/SKILL.md +0 -0
  41. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  42. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  43. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  44. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  45. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  46. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/communicate/SKILL.md +0 -0
  47. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  48. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  49. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  50. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  51. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  52. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  53. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  54. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  55. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/mass-update/SKILL.md +0 -0
  56. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/mass-update/scripts/mass-update.sh +0 -0
  57. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  58. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  59. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/run-tests/SKILL.md +0 -0
  60. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/run-tests/scripts/test.sh +0 -0
  61. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/sonarclaude/SKILL.md +0 -0
  62. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  63. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/version-bump/SKILL.md +0 -0
  64. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills/version-bump/scripts/bump.py +0 -0
  65. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.claude/skills.local.yaml.example +0 -0
  66. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.devague/frames/mass-update-now-clones-each-target-repo-into-a-git.json +0 -0
  67. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.devague/frames/rollout-cli-ships-rollout-define-an-org-wide-chang.json +0 -0
  68. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.devague/plans/mass-update-now-clones-each-target-repo-into-a-git.json +0 -0
  69. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.devague/plans/rollout-cli-ships-rollout-define-an-org-wide-chang.json +0 -0
  70. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.flake8 +0 -0
  71. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.github/workflows/publish.yml +0 -0
  72. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.github/workflows/tests.yml +0 -0
  73. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.gitignore +0 -0
  74. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/.markdownlint-cli2.yaml +0 -0
  75. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/AGENTS.colleague.md +0 -0
  76. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/LICENSE +0 -0
  77. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/README.md +0 -0
  78. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/culture.yaml +0 -0
  79. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/docs/plans/2026-06-06-rollout-cli-ships-rollout-define-an-org-wide-chang.md +0 -0
  80. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/docs/plans/2026-06-12-mass-update-now-clones-each-target-repo-into-a-git.md +0 -0
  81. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/docs/specs/2026-06-06-rollout-cli-ships-rollout-define-an-org-wide-chang.md +0 -0
  82. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/docs/specs/2026-06-12-mass-update-now-clones-each-target-repo-into-a-git.md +0 -0
  83. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/apache-relicense/apply.sh +0 -0
  84. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/apache-relicense/check.sh +0 -0
  85. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/apache-relicense/files/LICENSE +0 -0
  86. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/apache-relicense/manifest.toml +0 -0
  87. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/ask-colleague-revendor/README.md +0 -0
  88. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/ask-colleague-revendor/apply.sh +0 -0
  89. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/ask-colleague-revendor/check.sh +0 -0
  90. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/ask-colleague-revendor/files/SKILL.md.tmpl +0 -0
  91. {rollout_cli-0.6.0/.claude/skills/ask-colleague → rollout_cli-0.7.2/recipes/ask-colleague-revendor/files}/prompts/explore.md +0 -0
  92. {rollout_cli-0.6.0/.claude/skills/ask-colleague → rollout_cli-0.7.2/recipes/ask-colleague-revendor/files}/prompts/review.md +0 -0
  93. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/ask-colleague-revendor/files/prompts/write.md +0 -0
  94. {rollout_cli-0.6.0/.claude/skills/ask-colleague → rollout_cli-0.7.2/recipes/ask-colleague-revendor/files}/scripts/ask-colleague.sh +0 -0
  95. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/recipes/ask-colleague-revendor/manifest.toml +0 -0
  96. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/__init__.py +0 -0
  97. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/__main__.py +0 -0
  98. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/__init__.py +0 -0
  99. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_commands/__init__.py +0 -0
  100. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_commands/apply.py +0 -0
  101. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_commands/cli.py +0 -0
  102. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_commands/doctor.py +0 -0
  103. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_commands/explain.py +0 -0
  104. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_commands/learn.py +0 -0
  105. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_commands/overview.py +0 -0
  106. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_commands/plan.py +0 -0
  107. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_commands/whoami.py +0 -0
  108. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_errors.py +0 -0
  109. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/cli/_output.py +0 -0
  110. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/core/__init__.py +0 -0
  111. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/core/bump.py +0 -0
  112. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/core/gitops.py +0 -0
  113. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/core/orchestrate.py +0 -0
  114. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/core/preflight.py +0 -0
  115. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/core/recipe.py +0 -0
  116. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/core/report.py +0 -0
  117. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/core/targets.py +0 -0
  118. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/explain/__init__.py +0 -0
  119. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/rollout/explain/catalog.py +0 -0
  120. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/sonar-project.properties +0 -0
  121. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/__init__.py +0 -0
  122. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_apache_relicense.py +0 -0
  123. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_apply_cmd.py +0 -0
  124. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_bump.py +0 -0
  125. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_cli.py +0 -0
  126. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_cli_introspection.py +0 -0
  127. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_cli_wiring.py +0 -0
  128. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_e2e_apache_relicense.py +0 -0
  129. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_explain_catalog.py +0 -0
  130. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_gitops.py +0 -0
  131. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_mass_update.py +0 -0
  132. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_orchestrate.py +0 -0
  133. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_plan_cmd.py +0 -0
  134. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_preflight.py +0 -0
  135. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_recipe.py +0 -0
  136. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_report.py +0 -0
  137. {rollout_cli-0.6.0 → rollout_cli-0.7.2}/tests/test_targets.py +0 -0
@@ -101,6 +101,7 @@ else an install hint.
101
101
  | `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. |
102
102
  | `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. |
103
103
  | `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`. |
104
+ | `plan "<task>"` | Colleague **PLANS** a complex task: it proposes a spec, then a split plan, then fans the waves out to a subagent-colleague workforce (`colleague plan run … --yes`). The *inverse of `/think`* — same arc, but colleague is the planning mind, not Claude. Needs a live backend. | Runs `colleague plan` in `--repo` (not a throwaway worktree): the workforce stage spawns isolated subagent worktrees and can land branches — treat like `write --apply` (gets a user nod). |
104
105
  | `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. |
105
106
  | `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. |
106
107
 
@@ -113,7 +114,7 @@ else an install hint.
113
114
  | `--engine NAME` | Backend plugin (default: `$COLLEAGUE_ENGINE` or `vllm-openai`). |
114
115
  | `--model NAME` | Model (default: `$COLLEAGUE_MODEL` or `sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP`). |
115
116
  | `--base-url URL` | OpenAI base URL (default: `$COLLEAGUE_BASE_URL` or `http://localhost:8001/v1`). |
116
- | `--max-steps N` | Loop step budget (default: 20). |
117
+ | `--max-steps N` | Loop step budget (default: 20). `explore`/`review` select colleague's own native **`explore`**/**`review`** mode profile (`colleague/profiles.py`, applied via `colleague work --mode`) instead of a wrapper-side override — today that profile defaults to 30, since read-only mapping fans out across more files. An explicit `--max-steps N` always overrides the profile's default, in either direction. If the resolved `colleague` predates `--mode` (a stale install on `PATH`), the wrapper falls back to the old caller-side `--max-steps 30` + reserved-steps behavior so it keeps working. |
117
118
  | `--apply` | (`write`) apply the change in place (work branch) instead of previewing. |
118
119
  | `--allow-dirty` | (`write`) allow running on a dirty tree (only matters with `--apply` / `--pr`). |
119
120
  | `--pr` | (`write`) push + open a PR instead of a local work branch (implies `--apply`). |
@@ -207,7 +208,7 @@ stop requests take effect on the next iteration rather than interrupting mid-ste
207
208
 
208
209
  ## Provenance
209
210
 
210
- This is a **first-party** colleague skill — colleague is its origin. It is
211
- the inverse of the other skills under `.claude/skills/`, which rollout-cli
212
- vendors *from* guildmaster. See `docs/skill-sources.md`. The `cite, don't import`
213
- policy holds: downstream repos copy it, they don't symlink or depend on it.
211
+ This is a **first-party** colleague skill — colleague is its origin. See
212
+ `docs/skill-sources.md` for the consumer's per-repo skill ledger. The `cite,
213
+ don't import` policy holds: downstream repos copy it, they don't symlink or
214
+ depend on it.
@@ -18,6 +18,10 @@ Rules:
18
18
  report (or you are within a few steps of the budget), STOP reading and call
19
19
  `finish`. Err on the side of finishing early — a focused finding beats endless
20
20
  reading.
21
+ - For a WIDE codebase map (many folders/modules), do NOT read every file in series
22
+ — that exhausts the step budget. Partition the surface by folder and delegate the
23
+ per-folder sub-surveys to the `subagents` tool (one child per folder/subtree, each
24
+ returning its findings), then synthesize their results into your report.
21
25
 
22
26
  When you are done, call finish with a structured findings report:
23
27
  1. What it is / how it works (with file:line references).
@@ -5,13 +5,8 @@ Focus the review on:
5
5
 
6
6
  $ARGUMENTS
7
7
 
8
- The change under review is the committed diff on this branch versus its base
9
- (`$BASE`). Start by running, read-only:
10
-
11
- git diff $BASE...HEAD --stat
12
- git diff $BASE...HEAD
13
-
14
- then read the touched files for the context you need.
8
+ The diff for this change is ALREADY PROVIDED below the instructions (filtered +
9
+ capped). Read specific files only if you need more context.
15
10
 
16
11
  Rules:
17
12
  - READ-ONLY. Do NOT modify, create, or delete any file. Only read and run
@@ -43,11 +43,19 @@ PROMPTS_DIR="$SKILL_DIR/prompts"
43
43
  # ── resolve the colleague CLI (installed, then local-dev fallback) ─────────
44
44
  COLLEAGUE=()
45
45
 
46
+ _pyproject_is_colleague() {
47
+ local line
48
+ while IFS= read -r line || [[ -n "$line" ]]; do
49
+ [[ "$line" == 'name = "colleague"'* ]] && return 0
50
+ done < "$1"
51
+ return 1
52
+ }
53
+
46
54
  _colleague_via_uv() {
47
55
  local dir="$1"
48
56
  while [[ -n "$dir" ]] && [[ "$dir" != "/" ]]; do
49
57
  if [[ -f "$dir/pyproject.toml" ]] \
50
- && grep -q '^name = "colleague"' "$dir/pyproject.toml" 2>/dev/null; then
58
+ && _pyproject_is_colleague "$dir/pyproject.toml"; then
51
59
  command -v uv >/dev/null 2>&1 || return 1
52
60
  COLLEAGUE=(uv run --project "$dir" colleague)
53
61
  return 0
@@ -81,10 +89,11 @@ Usage:
81
89
  ask-colleague explore "<question or area>" Read-only investigation -> findings (no side effects)
82
90
  ask-colleague review "<what to focus on>" Diverse second-opinion on the committed diff (no side effects)
83
91
  ask-colleague write "<task>" [--apply|--pr] Implement a change (preview by default; --apply lands it)
92
+ ask-colleague plan "<task>" [--no-workforce] Colleague PLANS a complex task (spec -> plan -> subagent workforce)
84
93
  ask-colleague feedback <id|last> [--rating N] Grade a past drive (ROI loop); with --rating records, without shows
85
94
  ask-colleague feedback list List every recorded drive by request + grade (find one by its request)
86
95
  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
96
+ ask-colleague monitor <task-id> Stream a running flight's live feed (--follow)
88
97
  ask-colleague guide <task-id> "<msg>" Send mid-flight guidance to a running flight
89
98
  ask-colleague stop <task-id> Ask a running flight to stop (cooperative)
90
99
 
@@ -94,9 +103,14 @@ Options:
94
103
  --base BRANCH Base for `review` diff (default: main)
95
104
  --engine NAME Backend plugin (default: $COLLEAGUE_ENGINE or vllm-openai)
96
105
  --model NAME Model (default: $COLLEAGUE_MODEL or sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP)
106
+ --role NAME Typed subagent role (e.g. explorer, reviewer, writer)
97
107
  --base-url URL OpenAI base URL (default: $COLLEAGUE_BASE_URL or http://localhost:8001/v1)
98
- --max-steps N Loop step budget (default: 20)
108
+ --max-steps N Loop step budget (default: 20; explore/review default from
109
+ colleague's own "explore"/"review" mode profile, today 30 —
110
+ an explicit --max-steps always overrides, lower or higher)
99
111
  --timeout N Per-request timeout, seconds (default: $COLLEAGUE_TIMEOUT or 300)
112
+ --no-workforce (plan) deliver the spec+plan only, skip the workforce fan-out (#215)
113
+ --quick / --no-spec (plan) skip the spec stage, plan directly from the request (#199)
100
114
  --apply (write) apply the change in place (drive branch) instead of previewing
101
115
  --allow-dirty (write) allow running on a dirty tree (only with --apply/--pr)
102
116
  --pr (write) push + open a PR instead of a local drive branch (implies --apply)
@@ -123,11 +137,11 @@ EOF
123
137
  # ── parse the verb ──────────────────────────────────────────────────────────
124
138
  VERB="${1:-}"
125
139
  case "$VERB" in
126
- explore | review | write | feedback | clean | monitor | guide | stop) shift ;;
140
+ explore | review | write | plan | feedback | clean | monitor | guide | stop) shift ;;
127
141
  -h | --help) usage; exit 0 ;;
128
142
  "") usage >&2; exit 1 ;; # missing arg -> user-input error (#161)
129
143
  *)
130
- echo "error: unknown verb '$VERB' (expected explore|review|write|feedback|clean|monitor|guide|stop)" >&2
144
+ echo "error: unknown verb '$VERB' (expected explore|review|write|plan|feedback|clean|monitor|guide|stop)" >&2
131
145
  echo "hint: run 'ask-colleague --help'" >&2
132
146
  exit 1 # bad verb -> user-input error (#161)
133
147
  ;;
@@ -139,9 +153,7 @@ esac
139
153
  # feedback/clean are thin pass-throughs to `colleague` plus the shared git
140
154
  # work-tree guard, so they need only git; the drive verbs (explore/review/write)
141
155
  # also render a prompt and parse colleague's --json result via python3, and the
142
- # worktree-isolated paths additionally need mktemp. grep is only used by the
143
- # uv-fallback resolver, which degrades to the clear "colleague not found" message
144
- # when absent, so it is not a hard requirement here.
156
+ # worktree-isolated paths additionally need mktemp.
145
157
  require_tools() { # $@ = tool names this verb path needs
146
158
  local missing=() t
147
159
  for t in "$@"; do
@@ -172,6 +184,7 @@ ENGINE="${COLLEAGUE_ENGINE:-${CONVERTIBLE_ENGINE:-vllm-openai}}"
172
184
  MODEL="${COLLEAGUE_MODEL:-${CONVERTIBLE_MODEL:-sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP}}"
173
185
  BASE_URL="${COLLEAGUE_BASE_URL:-${CONVERTIBLE_BASE_URL:-http://localhost:8001/v1}}"
174
186
  MAX_STEPS=20
187
+ MAX_STEPS_EXPLICIT=0
175
188
  TIMEOUT="${COLLEAGUE_TIMEOUT:-${CONVERTIBLE_TIMEOUT:-300}}"
176
189
  ALLOW_DIRTY=0
177
190
  APPLY=0
@@ -183,6 +196,9 @@ NOTES=""
183
196
  BY=""
184
197
  ARG=""
185
198
  JSON_OUT=0
199
+ ROLE=""
200
+ QUICK=0
201
+ NO_WORKFORCE=0
186
202
 
187
203
  while [[ $# -gt 0 ]]; do
188
204
  case "$1" in
@@ -190,14 +206,17 @@ while [[ $# -gt 0 ]]; do
190
206
  --base) need_value "$#" "$1"; BASE="$2"; shift 2 ;;
191
207
  --engine) need_value "$#" "$1"; ENGINE="$2"; shift 2 ;;
192
208
  --model) need_value "$#" "$1"; MODEL="$2"; shift 2 ;;
209
+ --role) need_value "$#" "$1"; ROLE="$2"; shift 2 ;;
193
210
  --base-url) need_value "$#" "$1"; BASE_URL="$2"; shift 2 ;;
194
- --max-steps) need_value "$#" "$1"; MAX_STEPS="$2"; shift 2 ;;
211
+ --max-steps) need_value "$#" "$1"; MAX_STEPS="$2"; MAX_STEPS_EXPLICIT=1; shift 2 ;;
195
212
  --timeout) need_value "$#" "$1"; TIMEOUT="$2"; shift 2 ;;
196
213
  --apply) APPLY=1; shift ;;
197
214
  --watch) WATCH=1; shift ;;
198
215
  --allow-dirty) ALLOW_DIRTY=1; shift ;;
199
216
  --pr) OPEN_PR=1; shift ;;
200
217
  --dry-run) DRY_RUN=1; shift ;;
218
+ --quick | --no-spec) QUICK=1; shift ;; # plan: skip the spec stage (#199)
219
+ --no-workforce) NO_WORKFORCE=1; shift ;; # plan: deliver spec+plan only (#215)
201
220
  --rating) need_value "$#" "$1"; RATING="$2"; shift 2 ;;
202
221
  --notes) need_value "$#" "$1"; NOTES="$2"; shift 2 ;;
203
222
  --by) need_value "$#" "$1"; BY="$2"; shift 2 ;;
@@ -210,13 +229,27 @@ while [[ $# -gt 0 ]]; do
210
229
  esac
211
230
  done
212
231
 
232
+ # Per-verb default: explore and review get a modest higher budget (30) for wider
233
+ # surveys and read-heavy diffs; write keeps the global default (20). Only apply
234
+ # when the user did NOT pass an explicit --max-steps.
235
+ #
236
+ # t4/spec R1: this number now ALSO matches colleague's own native "explore"/
237
+ # "review" mode profile (colleague/profiles.py), which is what actually drives
238
+ # the step budget at runtime once `--mode` is selected below — this wrapper-side
239
+ # value survives only so the "widen --max-steps" re-run hint (print_result)
240
+ # still names the right number; whether --max-steps is actually FORWARDED to
241
+ # colleague is decided later, once mode support is known.
242
+ if [[ ( "$VERB" == "explore" || "$VERB" == "review" ) && "$MAX_STEPS_EXPLICIT" -eq 0 ]]; then
243
+ MAX_STEPS=30
244
+ fi
245
+
213
246
  # Now that the verb and its flags are known, require only the tools THIS path
214
247
  # uses. feedback/clean shell straight to `colleague` (+ the git work-tree guard
215
248
  # below), so they need only git — not python3/mktemp (qodo: the old blanket check
216
249
  # failed those verbs in minimal envs). write --apply/--pr lands in place with no
217
250
  # throwaway worktree, so it needs no mktemp either.
218
251
  case "$VERB" in
219
- feedback | clean) require_tools git ;;
252
+ feedback | clean | plan) require_tools git ;;
220
253
  monitor | guide | stop) : ;;
221
254
  write)
222
255
  if [[ "$APPLY" -eq 1 || "$OPEN_PR" -eq 1 ]]; then
@@ -265,10 +298,99 @@ resolve_colleague || exit 2
265
298
  # Per-request timeout is config (no drive flag); EngineConfig reads it from env.
266
299
  # A local model can be slow on a growing context, so default generously.
267
300
  export COLLEAGUE_TIMEOUT="$TIMEOUT"
268
- COMMON_FLAGS=(--engine "$ENGINE" --model "$MODEL" --base-url "$BASE_URL" --max-steps "$MAX_STEPS" --json)
301
+
302
+ # ── plan: colleague does the planning (the inverse of /think) ────────────────
303
+ # plan delegates the WHOLE planning arc to colleague via the `colleague plan`
304
+ # verb — no throwaway worktree, no prompt template. Gated non-interactively
305
+ # (--yes) so a delegating agent can hand off the arc and read the
306
+ # spec -> plan -> workforce result. colleague is the planning mind; /think keeps
307
+ # Claude as the planner (same arc, a different mind).
308
+ if [[ "$VERB" == "plan" ]]; then
309
+ plan_flags=(--repo "$REPO" --engine "$ENGINE" --model "$MODEL" --base-url "$BASE_URL" --yes)
310
+ # --quick skips the spec stage (#199); --no-workforce delivers spec+plan only,
311
+ # skipping the timeout-prone fan-out (#215). --timeout already applies via the
312
+ # COLLEAGUE_TIMEOUT export above. These are forwarded, never auto-applied.
313
+ [[ "$QUICK" -eq 1 ]] && plan_flags+=(--quick)
314
+ [[ "$NO_WORKFORCE" -eq 1 ]] && plan_flags+=(--no-workforce)
315
+ [[ "${JSON_OUT:-0}" -eq 1 ]] && plan_flags+=(--json)
316
+ plan_rc=0
317
+ "${COLLEAGUE[@]}" plan run "$ARG" "${plan_flags[@]}" || plan_rc=$?
318
+ # No silent auto-degrade: name the recovery levers (those not already set) and
319
+ # let the caller choose. Human hints are TEXT-mode only — in --json mode the
320
+ # consumer is a machine and colleague's own structured {code,message,remediation}
321
+ # error already went to stderr, so suppress the prose there (Qodo #230 F4).
322
+ if [[ "$plan_rc" -ne 0 && "${JSON_OUT:-0}" -ne 1 ]]; then
323
+ echo "hint: plan mode did not complete on this backend. Recovery options (no auto-degrade):" >&2
324
+ [[ "$NO_WORKFORCE" -eq 0 ]] && echo "hint: --no-workforce deliver the spec+plan only, skip the timeout-prone workforce fan-out (#215)" >&2
325
+ [[ "$QUICK" -eq 0 ]] && echo "hint: --quick skip the spec stage, plan directly from the request (#199)" >&2
326
+ echo "hint: --timeout N widen the per-request window (currently ${TIMEOUT}s)" >&2
327
+ fi
328
+ exit "$plan_rc"
329
+ fi
330
+
331
+ # ── explore/review adopt colleague's native mode profile (t4/spec R1) ───────
332
+ # explore/review's step budget + synthesis-reserve tail used to be caller-side
333
+ # overrides baked into this script (a fixed --max-steps 30 plus an exported
334
+ # COLLEAGUE_SYNTHESIS_RESERVE_STEPS=3). Those numbers now live in colleague's
335
+ # own mode-profile catalog (colleague/profiles.py "explore"/"review") and are
336
+ # applied runtime-side by `colleague work --mode explore|review`
337
+ # (colleague/config.py apply_mode_profile) — so this wrapper's job shrinks to
338
+ # "select the mode" instead of re-deriving the profile's numbers itself; a
339
+ # future retune of the profile then reaches this wrapper for free.
340
+ #
341
+ # Honest limit: this wrapper prefers an installed `colleague` on PATH ahead of
342
+ # a local checkout, and an installed CLI can be older than the one this repo
343
+ # ships (it can predate --mode entirely). Detect that cheaply by checking the
344
+ # resolved CLI's OWN --help text for the literal flag with a plain bash
345
+ # substring match (no external text-search tool — the resolver stays as
346
+ # dependency-free as the uv-fallback path above, #190); fall back to the old
347
+ # caller-side defaults when --mode isn't there, so the wrapper keeps working
348
+ # unmodified against a stale CLI.
349
+ #
350
+ # NOTE: the substring must be anchored past the flag name — "--mode" is
351
+ # itself a literal prefix of "--model" (already a flag on every version), so
352
+ # a naive `*--mode*` match false-positives on a stale --help that only has
353
+ # --model. Requiring the next character NOT be a letter (or end-of-string)
354
+ # distinguishes a genuine "--mode ..."/"--mode]" occurrence from "--model".
355
+ MODE_SUPPORTED=0
356
+ if [[ "$VERB" == "explore" || "$VERB" == "review" ]]; then
357
+ help_out="$("${COLLEAGUE[@]}" work --help 2>/dev/null)" || help_out=""
358
+ if [[ "$help_out" == *"--mode"[!a-zA-Z]* || "$help_out" == *--mode ]]; then
359
+ MODE_SUPPORTED=1
360
+ fi
361
+ fi
362
+
363
+ if [[ ( "$VERB" == "explore" || "$VERB" == "review" ) && "$MODE_SUPPORTED" -eq 0 ]]; then
364
+ # Legacy fallback (stale CLI without --mode): reserve a few steps for the
365
+ # final verdict turn so a big-diff review/explore yields findings instead
366
+ # of dying mid-read (#197). Caller env wins.
367
+ : "${COLLEAGUE_SYNTHESIS_RESERVE_STEPS:=3}"
368
+ export COLLEAGUE_SYNTHESIS_RESERVE_STEPS
369
+ fi
370
+
371
+ COMMON_FLAGS=(--engine "$ENGINE" --model "$MODEL" --base-url "$BASE_URL" --json)
372
+ # --max-steps: forwarded unless this is a native-mode-capable explore/review run
373
+ # with no explicit --max-steps — then the flag is withheld so colleague's own
374
+ # mode profile supplies the step budget as a DEFAULT at runtime. An explicit
375
+ # --max-steps always wins, in EITHER direction (lower or higher than the
376
+ # profile's own number): it is still forwarded here, and the runtime resolves
377
+ # an explicit flag ahead of any profile default (colleague/config.py
378
+ # apply_mode_profile's explicit-knobs precedence).
379
+ if [[ "$MAX_STEPS_EXPLICIT" -eq 1 || "$MODE_SUPPORTED" -eq 0 || ( "$VERB" != "explore" && "$VERB" != "review" ) ]]; then
380
+ COMMON_FLAGS+=(--max-steps "$MAX_STEPS")
381
+ fi
382
+ # --mode: select colleague's native explore/review constraint profile when the
383
+ # resolved CLI supports it (detected above). write carries no --mode — its
384
+ # profile is behavior-neutral (identical to no mode at all), so the numbers
385
+ # stay exactly as documented (--max-steps 20 default, no reserve override).
386
+ if [[ "$MODE_SUPPORTED" -eq 1 && ( "$VERB" == "explore" || "$VERB" == "review" ) ]]; then
387
+ COMMON_FLAGS+=(--mode "$VERB")
388
+ fi
269
389
  # --watch arms a flight for EVERY drive verb (explore/review/write), so it lives on
270
390
  # the shared flag list — not inside one verb's path — so monitor/guide/stop work.
271
391
  [[ "${WATCH:-0}" -eq 1 ]] && COMMON_FLAGS+=(--watch)
392
+ # --role forwards a typed-subagent role to the work item.
393
+ [[ -n "${ROLE:-}" ]] && COMMON_FLAGS+=(--role "$ROLE")
272
394
 
273
395
  # ── render an instruction from a prompt template ────────────────────────────
274
396
  render_prompt() {
@@ -305,20 +427,38 @@ print_result() {
305
427
  # empty (its artifact was discarded with the worktree, so it is not gradable).
306
428
  # $3 (optional): exit code from the colleague drive command, propagated to
307
429
  # the caller when the drive itself failed.
308
- ASK_COLLEAGUE_REAL_ARTIFACT_DIR="${1:-}" ASK_COLLEAGUE_GRADABLE="${2:-}" ASK_COLLEAGUE_DRIVE_RC="${3:-}" ASK_COLLEAGUE_JSON="${JSON_OUT:-0}" python3 -c '
430
+ ASK_COLLEAGUE_REAL_ARTIFACT_DIR="${1:-}" ASK_COLLEAGUE_GRADABLE="${2:-}" ASK_COLLEAGUE_DRIVE_RC="${3:-}" ASK_COLLEAGUE_JSON="${JSON_OUT:-0}" ASK_COLLEAGUE_MAX_STEPS="${MAX_STEPS:-20}" python3 -c '
309
431
  import sys, json, os
310
432
  raw = sys.stdin.read().strip()
311
433
  json_mode = os.environ.get("ASK_COLLEAGUE_JSON") == "1"
434
+ def _fail(code, message, remediation, detail=None):
435
+ # #226: in --json mode emit a structured {code, message, remediation} object
436
+ # on stderr (matching colleague CliError shape) so a machine consumer parses
437
+ # a failure the same way it parses success; in text mode keep the standard
438
+ # error:/hint: contract. stdout stays clean (no result) on every path.
439
+ if json_mode:
440
+ obj = {"code": code, "message": message, "remediation": remediation}
441
+ if detail is not None:
442
+ obj["detail"] = detail
443
+ sys.stderr.write(json.dumps(obj) + "\n")
444
+ else:
445
+ sys.stderr.write("error: " + message + "\n")
446
+ if detail is not None:
447
+ sys.stderr.write(detail + "\n")
448
+ if remediation:
449
+ sys.stderr.write("hint: " + remediation + "\n")
450
+ sys.exit(code)
312
451
  if not raw:
313
- sys.stderr.write("error: colleague produced no result on stdout (see diagnostics above)\n")
314
- sys.exit(2)
452
+ _fail(2, "colleague produced no result on stdout (see diagnostics above)",
453
+ "re-run; if it persists, check the backend with colleague doctor --probe")
315
454
  try:
316
455
  d = json.loads(raw)
317
456
  except Exception:
318
- sys.stderr.write("error: could not parse colleague --json output:\n")
319
- sys.stderr.write(raw[:2000] + "\n")
320
- sys.exit(2)
457
+ _fail(2, "could not parse colleague --json output",
458
+ "the backend may have emitted non-JSON; see the raw output and diagnostics above",
459
+ detail=raw[:2000])
321
460
  ok = d.get("status") == "ok"
461
+ inc = d.get("incompletion") if isinstance(d.get("incompletion"), dict) else None
322
462
  tid = d.get("task_id") or ""
323
463
  # Resolve the artifact path to the preserved copy when the drive ran in a
324
464
  # throwaway worktree (read-only verbs); the raw JSON points into the now-deleted
@@ -332,11 +472,37 @@ if ap and real_dir:
332
472
  # trailing off mid-task. Warn so the caller treats it as a partial, not a verdict.
333
473
  # The warning is a DIAGNOSTIC -> always stderr (never stdout), so both the digest
334
474
  # and --json keep a clean, machine-readable stdout (no single quotes in this body).
475
+ # Enriched partial warnings: include reached step count and a concrete larger
476
+ # --max-steps to retry with, so the hint is actionable (#194).
477
+ max_steps = int(os.environ.get("ASK_COLLEAGUE_MAX_STEPS", "20"))
478
+ stats = d.get("stats") or {}
479
+ model_turns = stats.get("model_turns")
480
+ step_count = stats.get("step_count")
335
481
  if d.get("stopped_without_finish"):
336
- print("warning: drive ended without calling finish treat the summary as a", file=sys.stderr)
337
- print(" partial (the model stopped mid-task), not an authoritative result.", file=sys.stderr)
482
+ reached = model_turns if model_turns is not None else step_count
483
+ if reached is not None:
484
+ print(f"warning: drive ended without calling finish (reached {reached} model turns of {max_steps}) — re-run with --max-steps {2 * max_steps} to go deeper.", file=sys.stderr)
485
+ else:
486
+ print("warning: drive ended without calling finish — treat the summary as a", file=sys.stderr)
487
+ print(" partial (the model stopped mid-task), not an authoritative result.", file=sys.stderr)
338
488
  elif d.get("not_finished"):
339
- print("warning: drive ran out of steps without finishing summary is partial.", file=sys.stderr)
489
+ reached = model_turns if model_turns is not None else step_count
490
+ if reached is not None:
491
+ print(f"warning: drive ran out of steps without finishing (reached {reached} model turns of {max_steps}) — re-run with --max-steps {2 * max_steps} to go deeper.", file=sys.stderr)
492
+ else:
493
+ print("warning: drive ran out of steps without finishing — summary is partial.", file=sys.stderr)
494
+ # No-result sentinel: the drive produced nothing actionable (#192).
495
+ summary = d.get("summary") or ""
496
+ if summary == "__COLLEAGUE_NO_RESULT_PRODUCED__":
497
+ print("warning: no result produced — widen --max-steps or narrow the scope.", file=sys.stderr)
498
+ # Incompletion: when the run did NOT deliver (status != "ok") and colleague
499
+ # carried an incompletion record, surface it as a diagnostic to stderr so the
500
+ # delegating caller knows WHY the run failed and what to do next. Always stderr
501
+ # (never stdout) so --json stdout stays pure.
502
+ if not ok and inc:
503
+ reason = inc.get("reason", "")
504
+ recommendation = inc.get("recommendation", "")
505
+ print(f"incomplete: {reason} — {recommendation}", file=sys.stderr)
340
506
  if json_mode:
341
507
  # --json contract: stdout carries ONLY the TaskResult JSON; every
342
508
  # human/diagnostic line already went to stderr above. The exit code still
@@ -359,7 +525,10 @@ if json_mode:
359
525
  # task_id is already in the payload, but echoing the copy-paste grade hint
360
526
  # keeps the convention every work item follows (rule 907536) without breaking
361
527
  # the stdout contract. Gated on gradable, exactly like the digest below.
362
- if tid and gradable:
528
+ # Skip the grade hint for the no-result sentinel (#192). A FAILED/incomplete but
529
+ # gradable drive still gets the hint — a failure rated 1/5 is the ROI signal
530
+ # (#139); the incompletion diagnostic above already says it did not succeed.
531
+ if tid and gradable and summary != "__COLLEAGUE_NO_RESULT_PRODUCED__":
363
532
  print("task:", tid, file=sys.stderr)
364
533
  print("grade:", "ask-colleague feedback", tid, "--rating N", file=sys.stderr)
365
534
  else:
@@ -380,7 +549,10 @@ else:
380
549
  # drive (colleague writes an artifact on failure too, h5): a failure rated
381
550
  # 1/5 is exactly the ROI signal, so the hint must not be gated on `ok` (#139
382
551
  # qodo). It prints to `out` (stderr on failure), matching the failure digest.
383
- if tid and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1":
552
+ # Skip the grade footer for the no-result sentinel (#192): a drive that
553
+ # produced nothing is not worth grading.
554
+ summary = d.get("summary") or ""
555
+ if tid and os.environ.get("ASK_COLLEAGUE_GRADABLE") == "1" and summary != "__COLLEAGUE_NO_RESULT_PRODUCED__":
384
556
  print("grade:", "ask-colleague feedback", tid, "--rating N", file=out)
385
557
  if ok:
386
558
  sys.exit(0)
@@ -474,7 +646,7 @@ _preserve_artifact() {
474
646
  local art_name="$1"
475
647
  [[ -n "$art_name" && -n "$_WT" ]] || return 1
476
648
  if ! _valid_segment "$art_name"; then
477
- printf 'ask-colleague: refusing to preserve unsafe artifact name %q\n' "$art_name" >&2
649
+ printf 'error: refusing to preserve unsafe artifact name %q\n' "$art_name" >&2
478
650
  return 1
479
651
  fi
480
652
  local src="$_WT/.colleague"
@@ -484,7 +656,7 @@ _preserve_artifact() {
484
656
  # The JSON artifact is the record of the drive — surface a copy failure rather
485
657
  # than swallow it, so the caller can fall back to honest path reporting.
486
658
  if ! cp -f "$src/$art_name" "$dst/$art_name"; then
487
- printf 'ask-colleague: could not preserve artifact %s\n' "$art_name" >&2
659
+ printf 'error: could not preserve artifact %s\n' "$art_name" >&2
488
660
  return 1
489
661
  fi
490
662
  # The trace shares the artifact stem (.json -> .trace.jsonl); a best-effort
@@ -568,7 +740,7 @@ run_write() {
568
740
  return
569
741
  fi
570
742
  if [[ "$ALLOW_DIRTY" -eq 0 ]] \
571
- && [[ -n "$(git -C "$REPO" status --porcelain 2>/dev/null)" ]]; then
743
+ && [[ -n "$(git -C "$REPO" status --porcelain --untracked-files=no 2>/dev/null)" ]]; then
572
744
  echo "error: working tree is dirty — commit/stash first, or pass --allow-dirty" >&2
573
745
  echo "hint: 'colleague drive --no-pr' commits uncommitted edits onto the drive branch" >&2
574
746
  # User-fixable state guard -> exit 1, matching the runtime's own
@@ -641,13 +813,32 @@ run_clean() {
641
813
  "${cmd[@]}"
642
814
  }
643
815
 
816
+ # ── front-load a filtered, capped diff into the review instruction ──────────
817
+ # Issue #220a: print a diffstat + filtered diff body so the model does not waste
818
+ # turns running `git diff` itself. Lockfile/vendored noise is excluded; the body
819
+ # is capped at ${COLLEAGUE_MAX_OUTPUT_CHARS:-100000} characters.
820
+ front_load_review_diff() {
821
+ local cap="${COLLEAGUE_MAX_OUTPUT_CHARS:-100000}"
822
+ printf '%s\n' "--- DIFF UNDER REVIEW (filtered + capped; read specific files for more) ---"
823
+ git -C "$REPO" diff "$BASE"...HEAD --stat
824
+ printf '\n'
825
+ local diff_body
826
+ diff_body="$(git -C "$REPO" diff "$BASE"...HEAD -- . ':(exclude)*.lock' ':(exclude)**/*.lock' ':(exclude)package-lock.json' ':(exclude)**/package-lock.json' ':(exclude)*.min.js' ':(exclude)**/*.min.js' 2>/dev/null || true)"
827
+ if [[ "${#diff_body}" -gt "$cap" ]]; then
828
+ printf '%s\n' "${diff_body:0:$cap}"
829
+ printf '%s\n' "[... diff body truncated at ${cap} chars; read specific files for the rest ...]"
830
+ else
831
+ printf '%s\n' "$diff_body"
832
+ fi
833
+ }
834
+
644
835
  # ── piloting verbs: thin passthroughs to the `colleague flight` noun ─────────
645
836
  _flight_json_flag() { [[ "$JSON_OUT" -eq 1 ]] && printf -- '--json'; }
646
837
 
647
838
  run_monitor() {
648
839
  local fid="${ARG%% *}"
649
840
  [[ -z "$fid" ]] && { echo "error: monitor needs a flight task-id" >&2; exit 1; }
650
- "${COLLEAGUE[@]}" flight status "$fid" --repo "$REPO" $(_flight_json_flag)
841
+ "${COLLEAGUE[@]}" flight status "$fid" --repo "$REPO" --follow $(_flight_json_flag)
651
842
  }
652
843
 
653
844
  run_stop() {
@@ -665,7 +856,7 @@ run_guide() {
665
856
 
666
857
  case "$VERB" in
667
858
  explore) run_readonly "$(render_prompt explore)" ;;
668
- review) run_readonly "$(render_prompt review)" ;;
859
+ review) run_readonly "$(render_prompt review)"$'\n\n'"$(front_load_review_diff)" ;;
669
860
  write) run_write "$(render_prompt write)" ;;
670
861
  feedback) run_feedback "$ARG" ;;
671
862
  clean) run_clean ;;