flow-workflows 0.86.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -173,6 +173,7 @@ Symmetrically, `guided`/`auto` never ask about the flow's own machinery (panels,
173
173
  | `/flow:work:try` | Point the main checkout at a branch to test it, and print the manual test plan once it is up (then `--back`) |
174
174
  | `/flow:work:clean` | Sweep merged worktrees, dead branches, unarchived folders. Never deletes on a guess |
175
175
  | `/flow:work:abandon` | Close a work item without shipping |
176
+ | `/flow:work:handoff` | Hand a finished study to whoever builds it elsewhere — pushes only its folder to a branch of its own, offers a summary for the ticket |
176
177
  | `/flow:news` | What changed in the plugin since the version you last saw |
177
178
 
178
179
  ## What a work looks like on disk
@@ -17,7 +17,7 @@ Every `$flow-*` command assumes these rules. They are stated once, here, so a co
17
17
  carries what is specific to its phase. Read this once per session; a command that says "load
18
18
  `flow-core`" means this file.
19
19
 
20
- **This file belongs to flow `0.86.0`.** Compare it once, at the start of the session, against
20
+ **This file belongs to flow `0.87.0`.** Compare it once, at the start of the session, against
21
21
  `version` in `~/.claude/flow/.claude-plugin/plugin.json`. The two differing means the session
22
22
  is running a **mixture** — the commands from one copy of the plugin, these shared rules from another
23
23
  — which is exactly what happens when a branch or a release candidate is loaded over an installed
@@ -249,6 +249,7 @@ Two signals arrive on an open MR/PR, each with its own cross-cutting loop (feat
249
249
  - `$flow-work-daily [question]` — the **work assistant** (daily standup), read-only. **Local** (`.claude/work/` + git) + **forge** (open MRs/PRs, reviews awaiting you, red CI, unmergeable, unresolved threads via `git.cli`) + **tracker** (assigned tickets, priority changes via `tracker.tool`); where they cross, concrete commands (`$flow-feat-start`, `$flow-work-green`, `$flow-work-respond`). No argument → *yesterday · today · blockers*; a question → just that. External sources best-effort, never blocking; the only write is a "last seen" marker, like `$flow-news`.
250
250
  - `$flow-work-status` — all works in `.claude/work/`, current phase, divergence with git.
251
251
  - `$flow-work-run [ticket]` — the **entry an orchestrator launches** for an `unattended` run: asks it `why` (start · answer · merged · review · pipeline), routes to the phase that answers it, and makes no decision of its own. Refuses to run in any other mode.
252
+ - `$flow-work-handoff` — hands a **finished study** to whoever builds it elsewhere (an unattended server, another machine): refuses an unfinished one, pushes only this work's folder as a parentless commit to `git.handoff_branch` without touching the checkout, asks before the push, then offers a marked summary for the ticket body (`tracker.edit_cmd`).
252
253
  - `$flow-work-resume` — detects the branch, reads `meta.json`, recaps, suggests the next step. **Re-reads the ticket's comment thread** (§2.5), reports only what is new since `01-context.md`, appends it to the artifact; never amends the design on its own.
253
254
  - `$flow-work-try <branch>` / `$flow-work-try --back` — point the main checkout at a branch to test it here, then return; re-syncs per `git.worktree_resync`. Resolves the work behind the branch and, once the environment is up, **prints the manual test plan already written** (the `needs-manual`/`unproven` criteria, or the acceptance criteria when `validate` has not run) and offers to collect Pass/Fail/Blocked into the validation artifact. No work behind the branch → the plain switch, as before.
254
255
  - `$flow-work-clean [--dry-run]` — **periodic sweep** of finished work's residue: worktree, local branch, `.claude/work/` folder (intermediate train MR/PRs never set `phase: done`, so the `ship`/`abandon` prompt never fires). Verdict per branch from **the forge** with local fallbacks, including patch-equivalence for **squash merges** (`git branch --merged` misses them); acts only on `merged`/`empty`. `unknown` never a candidate; dirty and unpushed protected; work folders **archived, never deleted**; remote never touched. **`autonomy.mode` does not authorize deletion**: `auto` confirms the list too. `--purge-archive <N>d`: separate opt-in pass over `_archive/`, only folders already committed to git.
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: flow-work-handoff
3
+ description: "Hand a finished study to whoever builds it elsewhere — push only this work's folder to a branch of its own, then offer a summary for the ticket"
4
+ ---
5
+
6
+ <!-- GENERATED by script/adapter-build.py from plugins/flow/commands/work/handoff.md — do not edit; change the plugin file and run the script. -->
7
+
8
+ # `$flow-work-handoff`
9
+
10
+ > **codex adapter — how the Claude Code primitives named below map here.**
11
+ > - `AskUserQuestion` → ask in plain text with numbered options and wait for the reply.
12
+ > - `Agent <role>` / `Agent general-purpose` / subagents → the subagent declared under `[agents.<name>]` in `~/.codex/config.toml`; the name comes from `agents.<role>` in `FLOW.md`, empty → do it in this context.
13
+ > - Parallel fan-out → several subagents in one response, capped at `agents.fanout_max` (empty → 4); `agents.fanout_tool` is Claude Code-only, ignore it.
14
+ > - `ScheduleWakeup` / `Monitor` / `/loop` → not available in-session: run one cycle, persist state in `monitor.md`, let the user schedule `codex exec "<command>"` with cron or Codex automations.
15
+ > - `TaskCreate` → a markdown checklist in the phase artifact.
16
+ > - `Skill commit-commands:commit-push-pr` → `git add` · `git commit` · `git push -u origin HEAD` · the `git.cli` CLI (`gh pr create` / `glab mr create`).
17
+ > - `/model <value>` → the `--model` flag at launch (or `/model` if your Codex version has it).
18
+ > - `knowledge.*` roles → whatever tools `FLOW.md` names there; an MCP tool keeps its name, its server is declared under `[mcp_servers.<name>]` in `config.toml` (see `config.snippet.toml`).
19
+ > - `$ARGUMENTS` → whatever the user typed after the skill name, empty if nothing — Codex substitutes nothing, so read it off their message.
20
+
21
+ Read `~/.claude/flow/CORE.codex.md` first (shared rules: `FLOW.md` step 0, autonomy, how a stop reads, the live panel, `00-summary.md`) — skip if you already read it in this session.
22
+
23
+ **Panel words are closed** — `mark`: `done` · `current` · `pending` · `wait` · `block` · `info`; `style`: `normal` · `dim` · `title` · `accent` · `ok` · `warn` · `error`. Anything else is dropped by the reader in silence: the panel still paints, and nobody is told.
24
+
25
+ For a study made here and built somewhere else — a server running `$flow-work-run` unattended,
26
+ another person, another machine. The builder gets the work folder at the same path and nothing
27
+ else: no conversation, maybe a less capable model. So this command checks the study can stand on
28
+ its own, pushes the folder to a branch that holds only it, and offers to put a summary in the
29
+ ticket for the people who review it.
30
+
31
+ ## 1. Pre-flight
32
+
33
+ - Effective FLOW configuration per flow-core §0. `autonomy.mode: unattended` → refuse in one line:
34
+ an unattended run never pushes (flow-core §2.1); handing off is a person's step.
35
+ - Locate the active `meta.json` by current branch; not found → ask the user for the ticket.
36
+ - `git.handoff_branch` empty → refuse, naming the key and an example
37
+ (`git.handoff_branch: study/{TICKET}`): where the study goes is the receiver's convention, never
38
+ flow's guess. `{TICKET}` = `meta.json.ticket`.
39
+ - `origin` not configured → refuse; there is nowhere to push.
40
+
41
+ ## 2. Is the study finished?
42
+
43
+ The builder can ask no one. Refuse — pushing nothing, naming every reason that holds — when:
44
+
45
+ - **The study's last phase is not in `phases_done`.** A feature: `context` on XS (what `start`
46
+ records), `design` on S, `plan` on M/L. A bug: `context` on XS, `investigate` otherwise.
47
+ - **`meta.json.pending` is set** — a question is still waiting for its answer.
48
+ - **A clarification is still open** — a question under "Decisions clarified at start" in
49
+ `01-context.md` recorded without its answer.
50
+
51
+ Then, **before anything is pushed**: decisions this session settled while talking that no artifact
52
+ records yet → write them where they belong now (`01-context.md` "Decisions clarified at start",
53
+ `03-design.md` "Decisions (ADR-light)", `04-mr-plan.md`), and say which. What was decided only in
54
+ the chat does not travel.
55
+
56
+ ## 3. Build the commit, without touching the checkout
57
+
58
+ A throwaway index holds only this folder, so the current branch, `HEAD`, the real index (staged
59
+ work included) and the working tree are never read into it nor written to, and a git-ignored
60
+ `.claude/work/` is no obstacle.
61
+
62
+ **Each block below is one shell call, run as it stands** — a harness need not keep variables from
63
+ one call to the next, so every block sets its own. Replace each `@…@` with its literal value first:
64
+ `@WORK@` the work folder relative to the repository root (`.claude/work/<work-dir>`, no trailing
65
+ slash), `@BRANCH@` the resolved `git.handoff_branch`, `@TICKET@` `meta.json.ticket`, `@COMMIT@` the
66
+ sha the build block printed. A value holding a `'` cannot go inside those quotes: refuse it and
67
+ say which.
68
+
69
+ First, the branch must not exist on `origin` — a second push would need a force, and this command
70
+ never forces:
71
+
72
+ ```bash handoff-check
73
+ BRANCH='@BRANCH@'
74
+ git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null; echo "rc=$?"
75
+ ```
76
+
77
+ `rc=0` → the branch exists: refuse, and say what the person does next — delete it on the forge
78
+ (only when nothing still reads it), or set another name in `git.handoff_branch`. `rc=2` → absent,
79
+ continue. Anything else → stop and say why (no network, no access). Then build the commit:
80
+
81
+ ```bash handoff-build
82
+ WORK='@WORK@'; TICKET='@TICKET@'
83
+ cd "$(git rev-parse --show-toplevel)" || exit 1
84
+ [ -d "$WORK" ] || { echo "no such folder: $WORK"; exit 1; }
85
+ tmp=$(mktemp -d) || exit 1
86
+ idx="$tmp/index" # does not exist yet: git starts an empty index there
87
+ GIT_INDEX_FILE="$idx" git add -f -- "$WORK" ":(exclude)$WORK/panel.json" &&
88
+ files=$(GIT_INDEX_FILE="$idx" git ls-files) &&
89
+ tree=$(GIT_INDEX_FILE="$idx" git write-tree) &&
90
+ commit=$(git commit-tree "$tree" -m "study $TICKET")
91
+ rc=$?; rm -rf "$tmp"
92
+ [ "$rc" -eq 0 ] && [ -n "$files" ] || { echo "nothing was built (rc=$rc)"; exit 1; }
93
+ printf '%s\n' "$files"; echo "commit=$commit"
94
+ ```
95
+
96
+ `GIT_INDEX_FILE` is set per command, never exported, so a failure leaves no session pointing git
97
+ at a deleted index. `panel.json` stays out: it is this machine's terminal panel, and on another one
98
+ it would show this session's last state as if it were theirs. The block failing → its message, and
99
+ stop: `commit-tree` with no git identity configured is the usual cause.
100
+
101
+ ## 4. Confirm, then push
102
+
103
+ Show the branch, the remote and the file list the build printed — every path, one per line — then
104
+ ask with `AskUserQuestion` **in every `autonomy.mode`**: a push leaves the machine (flow-core §2).
105
+ **Push** · **Cancel**. Nothing between the list and the question (flow-core §3).
106
+
107
+ ```bash handoff-push
108
+ BRANCH='@BRANCH@'; COMMIT='@COMMIT@'
109
+ case "$COMMIT" in ''|*[!0-9a-f]*) echo "not a commit sha: $COMMIT"; exit 1 ;; esac
110
+ git cat-file -e "$COMMIT^{commit}" || exit 1
111
+ git push origin "$COMMIT:refs/heads/$BRANCH"
112
+ ```
113
+
114
+ The sha is checked first because `git push origin :refs/heads/<branch>` — an empty source —
115
+ deletes the branch on the remote. A rejected push → its message, verbatim, and stop. Never retried
116
+ with `--force`.
117
+
118
+ ## 5. The ticket summary (offer)
119
+
120
+ Only after a successful push, and only in ticket mode with a real tracker id. The people reviewing
121
+ the ticket read it there, never in the work folder; the receiver does not parse it.
122
+
123
+ - **Read the body back** — the summary goes inside the ticket's own text, never over it:
124
+ `gh` → `gh issue view {TICKET} --json body -q .body` · `glab` → `glab issue view {TICKET} -F json`,
125
+ its `description`. Any other tracker → the offer is not made: one line saying flow cannot read that
126
+ body back to keep its text.
127
+ - **The write command** — `tracker.edit_cmd`, `{TICKET}` and `{BODY_FILE}` substituted. Empty →
128
+ `gh issue edit {TICKET} --body-file {BODY_FILE}` for `gh`,
129
+ `glab issue update {TICKET} --description-file {BODY_FILE}` for `glab`.
130
+ - **The block**, short, in the language of the ticket. A feature takes it from `01-context.md`,
131
+ `03-design.md` and `04-mr-plan.md`; a bug from `01-context.md` and `03-investigation.md` — the
132
+ expected behaviour as its criteria, the root cause and the intended fix as its design, "Areas with
133
+ similar risk" as its risks. A section whose source does not exist (an XS study, an S feature with
134
+ no plan) says `none`:
135
+
136
+ ```
137
+ <!-- flow-study -->
138
+ ## What and why
139
+ ## Acceptance criteria
140
+ ## Design in brief
141
+ ## MR/PR plan
142
+ ## Risks
143
+ <!-- end flow-study -->
144
+ ```
145
+
146
+ Criteria keep their literal values. The plan lists each MR/PR with what it covers and what it
147
+ waits on; a bug's plan is its single fix.
148
+ - **The new body**: the body read above with the block between the two markers replaced — or, with
149
+ no markers yet, the block appended after a blank line. Written to a `mktemp` file outside the
150
+ tree — `{BODY_FILE}` — so nothing of it can be committed later.
151
+ - **Show the block and ask** with `AskUserQuestion`, **in every mode**: it writes to a shared
152
+ tracker. **Write it** · **Skip**. Failure → its message in one line; the push stands.
153
+
154
+ ## 6. Close
155
+
156
+ Report following flow-core §3: the branch and the commit, the file count, whether the ticket
157
+ summary was written. Refresh the panel: a `Now` line saying the study was handed off, no `Decision`
158
+ line. `meta.json` gains nothing — the branch on `origin` is the record, and the §3 refusal reads it.
@@ -16,7 +16,7 @@ Every `/flow:*` command assumes these rules. They are stated once, here, so a co
16
16
  carries what is specific to its phase. Read this once per session; a command that says "load
17
17
  `flow-core`" means this file.
18
18
 
19
- **This file belongs to flow `0.86.0`.** Compare it once, at the start of the session, against
19
+ **This file belongs to flow `0.87.0`.** Compare it once, at the start of the session, against
20
20
  `version` in `~/.claude/flow/.claude-plugin/plugin.json`. The two differing means the session
21
21
  is running a **mixture** — the commands from one copy of the plugin, these shared rules from another
22
22
  — which is exactly what happens when a branch or a release candidate is loaded over an installed
@@ -245,6 +245,7 @@ Two signals arrive on an open MR/PR, each with its own cross-cutting loop (feat
245
245
  - `/flow:work:daily [question]` — the **work assistant** (daily standup), read-only. **Local** (`.claude/work/` + git) + **forge** (open MRs/PRs, reviews awaiting you, red CI, unmergeable, unresolved threads via `git.cli`) + **tracker** (assigned tickets, priority changes via `tracker.tool`); where they cross, concrete commands (`/flow:feat:start`, `/flow:work:green`, `/flow:work:respond`). No argument → *yesterday · today · blockers*; a question → just that. External sources best-effort, never blocking; the only write is a "last seen" marker, like `/flow:news`.
246
246
  - `/flow:work:status` — all works in `.claude/work/`, current phase, divergence with git.
247
247
  - `/flow:work:run [ticket]` — the **entry an orchestrator launches** for an `unattended` run: asks it `why` (start · answer · merged · review · pipeline), routes to the phase that answers it, and makes no decision of its own. Refuses to run in any other mode.
248
+ - `/flow:work:handoff` — hands a **finished study** to whoever builds it elsewhere (an unattended server, another machine): refuses an unfinished one, pushes only this work's folder as a parentless commit to `git.handoff_branch` without touching the checkout, asks before the push, then offers a marked summary for the ticket body (`tracker.edit_cmd`).
248
249
  - `/flow:work:resume` — detects the branch, reads `meta.json`, recaps, suggests the next step. **Re-reads the ticket's comment thread** (§2.5), reports only what is new since `01-context.md`, appends it to the artifact; never amends the design on its own.
249
250
  - `/flow:work:try <branch>` / `/flow:work:try --back` — point the main checkout at a branch to test it here, then return; re-syncs per `git.worktree_resync`. Resolves the work behind the branch and, once the environment is up, **prints the manual test plan already written** (the `needs-manual`/`unproven` criteria, or the acceptance criteria when `validate` has not run) and offers to collect Pass/Fail/Blocked into the validation artifact. No work behind the branch → the plain switch, as before.
250
251
  - `/flow:work:clean [--dry-run]` — **periodic sweep** of finished work's residue: worktree, local branch, `.claude/work/` folder (intermediate train MR/PRs never set `phase: done`, so the `ship`/`abandon` prompt never fires). Verdict per branch from **the forge** with local fallbacks, including patch-equivalence for **squash merges** (`git branch --merged` misses them); acts only on `merged`/`empty`. `unknown` never a candidate; dirty and unpushed protected; work folders **archived, never deleted**; remote never touched. **`autonomy.mode` does not authorize deletion**: `auto` confirms the list too. `--purge-archive <N>d`: separate opt-in pass over `_archive/`, only folders already committed to git.
@@ -0,0 +1,155 @@
1
+ # GENERATED by script/adapter-build.py from plugins/flow/commands/work/handoff.md — do not edit; change the plugin file and run the script.
2
+ description = "Hand a finished study to whoever builds it elsewhere — push only this work's folder to a branch of its own, then offer a summary for the ticket"
3
+
4
+ prompt = """
5
+ # `/flow:work:handoff`
6
+
7
+ > **gemini adapter — how the Claude Code primitives named below map here.**
8
+ > - `AskUserQuestion` → ask in plain text with numbered options and wait for the reply.
9
+ > - `Agent <role>` / `Agent general-purpose` / subagents → `@<name>` from `.gemini/agents/`; the name comes from `agents.<role>` in `FLOW.md`, empty or absent → do it in this context.
10
+ > - Parallel fan-out → several `@name` in one turn (sequential in this context if none are configured), capped at `agents.fanout_max` (empty → 4); `agents.fanout_tool` is Claude Code-only, ignore it.
11
+ > - `ScheduleWakeup` / `Monitor` / `/loop` → not available in-session: run one cycle, persist state in `monitor.md`, let the user schedule `gemini -p "<command>"` with cron.
12
+ > - `TaskCreate` → a markdown checklist in the phase artifact.
13
+ > - `Skill commit-commands:commit-push-pr` → `git add` · `git commit` · `git push -u origin HEAD` · the `git.cli` CLI (`gh pr create` / `glab mr create`).
14
+ > - `/model <value>` → the `--model` flag at launch.
15
+ > - `knowledge.*` roles → whatever tools `FLOW.md` names there; an MCP tool keeps its name, its server is declared under `mcpServers` in `settings.json` (see `settings.snippet.json`).
16
+
17
+ Read `~/.claude/flow/CORE.gemini.md` first (shared rules: `FLOW.md` step 0, autonomy, how a stop reads, the live panel, `00-summary.md`) — skip if you already read it in this session.
18
+
19
+ **Panel words are closed** — `mark`: `done` · `current` · `pending` · `wait` · `block` · `info`; `style`: `normal` · `dim` · `title` · `accent` · `ok` · `warn` · `error`. Anything else is dropped by the reader in silence: the panel still paints, and nobody is told.
20
+
21
+ For a study made here and built somewhere else — a server running `/flow:work:run` unattended,
22
+ another person, another machine. The builder gets the work folder at the same path and nothing
23
+ else: no conversation, maybe a less capable model. So this command checks the study can stand on
24
+ its own, pushes the folder to a branch that holds only it, and offers to put a summary in the
25
+ ticket for the people who review it.
26
+
27
+ ## 1. Pre-flight
28
+
29
+ - Effective FLOW configuration per flow-core §0. `autonomy.mode: unattended` → refuse in one line:
30
+ an unattended run never pushes (flow-core §2.1); handing off is a person's step.
31
+ - Locate the active `meta.json` by current branch; not found → ask the user for the ticket.
32
+ - `git.handoff_branch` empty → refuse, naming the key and an example
33
+ (`git.handoff_branch: study/{TICKET}`): where the study goes is the receiver's convention, never
34
+ flow's guess. `{TICKET}` = `meta.json.ticket`.
35
+ - `origin` not configured → refuse; there is nowhere to push.
36
+
37
+ ## 2. Is the study finished?
38
+
39
+ The builder can ask no one. Refuse — pushing nothing, naming every reason that holds — when:
40
+
41
+ - **The study's last phase is not in `phases_done`.** A feature: `context` on XS (what `start`
42
+ records), `design` on S, `plan` on M/L. A bug: `context` on XS, `investigate` otherwise.
43
+ - **`meta.json.pending` is set** — a question is still waiting for its answer.
44
+ - **A clarification is still open** — a question under "Decisions clarified at start" in
45
+ `01-context.md` recorded without its answer.
46
+
47
+ Then, **before anything is pushed**: decisions this session settled while talking that no artifact
48
+ records yet → write them where they belong now (`01-context.md` "Decisions clarified at start",
49
+ `03-design.md` "Decisions (ADR-light)", `04-mr-plan.md`), and say which. What was decided only in
50
+ the chat does not travel.
51
+
52
+ ## 3. Build the commit, without touching the checkout
53
+
54
+ A throwaway index holds only this folder, so the current branch, `HEAD`, the real index (staged
55
+ work included) and the working tree are never read into it nor written to, and a git-ignored
56
+ `.claude/work/` is no obstacle.
57
+
58
+ **Each block below is one shell call, run as it stands** — a harness need not keep variables from
59
+ one call to the next, so every block sets its own. Replace each `@…@` with its literal value first:
60
+ `@WORK@` the work folder relative to the repository root (`.claude/work/<work-dir>`, no trailing
61
+ slash), `@BRANCH@` the resolved `git.handoff_branch`, `@TICKET@` `meta.json.ticket`, `@COMMIT@` the
62
+ sha the build block printed. A value holding a `'` cannot go inside those quotes: refuse it and
63
+ say which.
64
+
65
+ First, the branch must not exist on `origin` — a second push would need a force, and this command
66
+ never forces:
67
+
68
+ ```bash handoff-check
69
+ BRANCH='@BRANCH@'
70
+ git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null; echo "rc=$?"
71
+ ```
72
+
73
+ `rc=0` → the branch exists: refuse, and say what the person does next — delete it on the forge
74
+ (only when nothing still reads it), or set another name in `git.handoff_branch`. `rc=2` → absent,
75
+ continue. Anything else → stop and say why (no network, no access). Then build the commit:
76
+
77
+ ```bash handoff-build
78
+ WORK='@WORK@'; TICKET='@TICKET@'
79
+ cd "$(git rev-parse --show-toplevel)" || exit 1
80
+ [ -d "$WORK" ] || { echo "no such folder: $WORK"; exit 1; }
81
+ tmp=$(mktemp -d) || exit 1
82
+ idx="$tmp/index" # does not exist yet: git starts an empty index there
83
+ GIT_INDEX_FILE="$idx" git add -f -- "$WORK" ":(exclude)$WORK/panel.json" &&
84
+ files=$(GIT_INDEX_FILE="$idx" git ls-files) &&
85
+ tree=$(GIT_INDEX_FILE="$idx" git write-tree) &&
86
+ commit=$(git commit-tree "$tree" -m "study $TICKET")
87
+ rc=$?; rm -rf "$tmp"
88
+ [ "$rc" -eq 0 ] && [ -n "$files" ] || { echo "nothing was built (rc=$rc)"; exit 1; }
89
+ printf '%s\\n' "$files"; echo "commit=$commit"
90
+ ```
91
+
92
+ `GIT_INDEX_FILE` is set per command, never exported, so a failure leaves no session pointing git
93
+ at a deleted index. `panel.json` stays out: it is this machine's terminal panel, and on another one
94
+ it would show this session's last state as if it were theirs. The block failing → its message, and
95
+ stop: `commit-tree` with no git identity configured is the usual cause.
96
+
97
+ ## 4. Confirm, then push
98
+
99
+ Show the branch, the remote and the file list the build printed — every path, one per line — then
100
+ ask with `AskUserQuestion` **in every `autonomy.mode`**: a push leaves the machine (flow-core §2).
101
+ **Push** · **Cancel**. Nothing between the list and the question (flow-core §3).
102
+
103
+ ```bash handoff-push
104
+ BRANCH='@BRANCH@'; COMMIT='@COMMIT@'
105
+ case "$COMMIT" in ''|*[!0-9a-f]*) echo "not a commit sha: $COMMIT"; exit 1 ;; esac
106
+ git cat-file -e "$COMMIT^{commit}" || exit 1
107
+ git push origin "$COMMIT:refs/heads/$BRANCH"
108
+ ```
109
+
110
+ The sha is checked first because `git push origin :refs/heads/<branch>` — an empty source —
111
+ deletes the branch on the remote. A rejected push → its message, verbatim, and stop. Never retried
112
+ with `--force`.
113
+
114
+ ## 5. The ticket summary (offer)
115
+
116
+ Only after a successful push, and only in ticket mode with a real tracker id. The people reviewing
117
+ the ticket read it there, never in the work folder; the receiver does not parse it.
118
+
119
+ - **Read the body back** — the summary goes inside the ticket's own text, never over it:
120
+ `gh` → `gh issue view {TICKET} --json body -q .body` · `glab` → `glab issue view {TICKET} -F json`,
121
+ its `description`. Any other tracker → the offer is not made: one line saying flow cannot read that
122
+ body back to keep its text.
123
+ - **The write command** — `tracker.edit_cmd`, `{TICKET}` and `{BODY_FILE}` substituted. Empty →
124
+ `gh issue edit {TICKET} --body-file {BODY_FILE}` for `gh`,
125
+ `glab issue update {TICKET} --description-file {BODY_FILE}` for `glab`.
126
+ - **The block**, short, in the language of the ticket. A feature takes it from `01-context.md`,
127
+ `03-design.md` and `04-mr-plan.md`; a bug from `01-context.md` and `03-investigation.md` — the
128
+ expected behaviour as its criteria, the root cause and the intended fix as its design, "Areas with
129
+ similar risk" as its risks. A section whose source does not exist (an XS study, an S feature with
130
+ no plan) says `none`:
131
+
132
+ ```
133
+ <!-- flow-study -->
134
+ ## What and why
135
+ ## Acceptance criteria
136
+ ## Design in brief
137
+ ## MR/PR plan
138
+ ## Risks
139
+ <!-- end flow-study -->
140
+ ```
141
+
142
+ Criteria keep their literal values. The plan lists each MR/PR with what it covers and what it
143
+ waits on; a bug's plan is its single fix.
144
+ - **The new body**: the body read above with the block between the two markers replaced — or, with
145
+ no markers yet, the block appended after a blank line. Written to a `mktemp` file outside the
146
+ tree — `{BODY_FILE}` — so nothing of it can be committed later.
147
+ - **Show the block and ask** with `AskUserQuestion`, **in every mode**: it writes to a shared
148
+ tracker. **Write it** · **Skip**. Failure → its message in one line; the push stands.
149
+
150
+ ## 6. Close
151
+
152
+ Report following flow-core §3: the branch and the commit, the file count, whether the ticket
153
+ summary was written. Refresh the panel: a `Now` line saying the study was handed off, no `Decision`
154
+ line. `meta.json` gains nothing — the branch on `origin` is the record, and the §3 refusal reads it.
155
+ """
@@ -17,7 +17,7 @@ Every `/flow-*` command assumes these rules. They are stated once, here, so a co
17
17
  carries what is specific to its phase. Read this once per session; a command that says "load
18
18
  `flow-core`" means this file.
19
19
 
20
- **This file belongs to flow `0.86.0`.** Compare it once, at the start of the session, against
20
+ **This file belongs to flow `0.87.0`.** Compare it once, at the start of the session, against
21
21
  `version` in `~/.claude/flow/.claude-plugin/plugin.json`. The two differing means the session
22
22
  is running a **mixture** — the commands from one copy of the plugin, these shared rules from another
23
23
  — which is exactly what happens when a branch or a release candidate is loaded over an installed
@@ -249,6 +249,7 @@ Two signals arrive on an open MR/PR, each with its own cross-cutting loop (feat
249
249
  - `/flow-work-daily [question]` — the **work assistant** (daily standup), read-only. **Local** (`.claude/work/` + git) + **forge** (open MRs/PRs, reviews awaiting you, red CI, unmergeable, unresolved threads via `git.cli`) + **tracker** (assigned tickets, priority changes via `tracker.tool`); where they cross, concrete commands (`/flow-feat-start`, `/flow-work-green`, `/flow-work-respond`). No argument → *yesterday · today · blockers*; a question → just that. External sources best-effort, never blocking; the only write is a "last seen" marker, like `/flow-news`.
250
250
  - `/flow-work-status` — all works in `.claude/work/`, current phase, divergence with git.
251
251
  - `/flow-work-run [ticket]` — the **entry an orchestrator launches** for an `unattended` run: asks it `why` (start · answer · merged · review · pipeline), routes to the phase that answers it, and makes no decision of its own. Refuses to run in any other mode.
252
+ - `/flow-work-handoff` — hands a **finished study** to whoever builds it elsewhere (an unattended server, another machine): refuses an unfinished one, pushes only this work's folder as a parentless commit to `git.handoff_branch` without touching the checkout, asks before the push, then offers a marked summary for the ticket body (`tracker.edit_cmd`).
252
253
  - `/flow-work-resume` — detects the branch, reads `meta.json`, recaps, suggests the next step. **Re-reads the ticket's comment thread** (§2.5), reports only what is new since `01-context.md`, appends it to the artifact; never amends the design on its own.
253
254
  - `/flow-work-try <branch>` / `/flow-work-try --back` — point the main checkout at a branch to test it here, then return; re-syncs per `git.worktree_resync`. Resolves the work behind the branch and, once the environment is up, **prints the manual test plan already written** (the `needs-manual`/`unproven` criteria, or the acceptance criteria when `validate` has not run) and offers to collect Pass/Fail/Blocked into the validation artifact. No work behind the branch → the plain switch, as before.
254
255
  - `/flow-work-clean [--dry-run]` — **periodic sweep** of finished work's residue: worktree, local branch, `.claude/work/` folder (intermediate train MR/PRs never set `phase: done`, so the `ship`/`abandon` prompt never fires). Verdict per branch from **the forge** with local fallbacks, including patch-equivalence for **squash merges** (`git branch --merged` misses them); acts only on `merged`/`empty`. `unknown` never a candidate; dirty and unpushed protected; work folders **archived, never deleted**; remote never touched. **`autonomy.mode` does not authorize deletion**: `auto` confirms the list too. `--purge-archive <N>d`: separate opt-in pass over `_archive/`, only folders already committed to git.
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: flow-work-handoff
3
+ description: "Hand a finished study to whoever builds it elsewhere — push only this work's folder to a branch of its own, then offer a summary for the ticket"
4
+ ---
5
+
6
+ <!-- GENERATED by script/adapter-build.py from plugins/flow/commands/work/handoff.md — do not edit; change the plugin file and run the script. -->
7
+
8
+ # `/flow-work-handoff`
9
+
10
+ > **hermes adapter — how the Claude Code primitives named below map here.**
11
+ > - `AskUserQuestion` → ask in plain text with numbered options and wait for the reply.
12
+ > - `Agent <role>` / `Agent general-purpose` / subagents → `delegate_task`, which creates the child on the spot from the `goal` and `context` you give it: the name in `agents.<role>` in `FLOW.md` is a role to state in that prompt, not an agent to declare anywhere; empty → do it in this context.
13
+ > - Parallel fan-out → several `delegate_task` calls in one response, capped at `agents.fanout_max` (empty → 4) and, above that, by `delegation.max_concurrent_children` in `config.yaml` (default 10); `agents.fanout_tool` is Claude Code-only, ignore it.
14
+ > - `ScheduleWakeup` / `Monitor` / `/loop` → Hermes schedules itself: the `cronjob` tool, or `/cron add "every 30m" "<this command>"`. Each firing is a fresh session with no history, so the state still lives in `monitor.md` and the scheduled text must stand alone; retiring the job from inside a firing needs `cron.allow_agent_scheduling: true`.
15
+ > - `TaskCreate` → a markdown checklist in the phase artifact.
16
+ > - `Skill commit-commands:commit-push-pr` → `git add` · `git commit` · `git push -u origin HEAD` · the `git.cli` CLI (`gh pr create` / `glab mr create`).
17
+ > - `/model <value>` → Hermes's own `/model`; a subagent's model is `delegation.model` in `config.yaml`, one value for every child of a round.
18
+ > - `knowledge.*` roles → whatever tools `FLOW.md` names there; an MCP tool keeps its name, its server is declared under `mcp_servers` in `~/.hermes/config.yaml` (see `config.snippet.yaml`).
19
+ > - `$ARGUMENTS` → what the user typed after the skill name: Hermes takes everything from the first non-skill token on as the instruction.
20
+
21
+ Read `~/.claude/flow/CORE.hermes.md` first (shared rules: `FLOW.md` step 0, autonomy, how a stop reads, the live panel, `00-summary.md`) — skip if you already read it in this session.
22
+
23
+ **Panel words are closed** — `mark`: `done` · `current` · `pending` · `wait` · `block` · `info`; `style`: `normal` · `dim` · `title` · `accent` · `ok` · `warn` · `error`. Anything else is dropped by the reader in silence: the panel still paints, and nobody is told.
24
+
25
+ For a study made here and built somewhere else — a server running `/flow-work-run` unattended,
26
+ another person, another machine. The builder gets the work folder at the same path and nothing
27
+ else: no conversation, maybe a less capable model. So this command checks the study can stand on
28
+ its own, pushes the folder to a branch that holds only it, and offers to put a summary in the
29
+ ticket for the people who review it.
30
+
31
+ ## 1. Pre-flight
32
+
33
+ - Effective FLOW configuration per flow-core §0. `autonomy.mode: unattended` → refuse in one line:
34
+ an unattended run never pushes (flow-core §2.1); handing off is a person's step.
35
+ - Locate the active `meta.json` by current branch; not found → ask the user for the ticket.
36
+ - `git.handoff_branch` empty → refuse, naming the key and an example
37
+ (`git.handoff_branch: study/{TICKET}`): where the study goes is the receiver's convention, never
38
+ flow's guess. `{TICKET}` = `meta.json.ticket`.
39
+ - `origin` not configured → refuse; there is nowhere to push.
40
+
41
+ ## 2. Is the study finished?
42
+
43
+ The builder can ask no one. Refuse — pushing nothing, naming every reason that holds — when:
44
+
45
+ - **The study's last phase is not in `phases_done`.** A feature: `context` on XS (what `start`
46
+ records), `design` on S, `plan` on M/L. A bug: `context` on XS, `investigate` otherwise.
47
+ - **`meta.json.pending` is set** — a question is still waiting for its answer.
48
+ - **A clarification is still open** — a question under "Decisions clarified at start" in
49
+ `01-context.md` recorded without its answer.
50
+
51
+ Then, **before anything is pushed**: decisions this session settled while talking that no artifact
52
+ records yet → write them where they belong now (`01-context.md` "Decisions clarified at start",
53
+ `03-design.md` "Decisions (ADR-light)", `04-mr-plan.md`), and say which. What was decided only in
54
+ the chat does not travel.
55
+
56
+ ## 3. Build the commit, without touching the checkout
57
+
58
+ A throwaway index holds only this folder, so the current branch, `HEAD`, the real index (staged
59
+ work included) and the working tree are never read into it nor written to, and a git-ignored
60
+ `.claude/work/` is no obstacle.
61
+
62
+ **Each block below is one shell call, run as it stands** — a harness need not keep variables from
63
+ one call to the next, so every block sets its own. Replace each `@…@` with its literal value first:
64
+ `@WORK@` the work folder relative to the repository root (`.claude/work/<work-dir>`, no trailing
65
+ slash), `@BRANCH@` the resolved `git.handoff_branch`, `@TICKET@` `meta.json.ticket`, `@COMMIT@` the
66
+ sha the build block printed. A value holding a `'` cannot go inside those quotes: refuse it and
67
+ say which.
68
+
69
+ First, the branch must not exist on `origin` — a second push would need a force, and this command
70
+ never forces:
71
+
72
+ ```bash handoff-check
73
+ BRANCH='@BRANCH@'
74
+ git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null; echo "rc=$?"
75
+ ```
76
+
77
+ `rc=0` → the branch exists: refuse, and say what the person does next — delete it on the forge
78
+ (only when nothing still reads it), or set another name in `git.handoff_branch`. `rc=2` → absent,
79
+ continue. Anything else → stop and say why (no network, no access). Then build the commit:
80
+
81
+ ```bash handoff-build
82
+ WORK='@WORK@'; TICKET='@TICKET@'
83
+ cd "$(git rev-parse --show-toplevel)" || exit 1
84
+ [ -d "$WORK" ] || { echo "no such folder: $WORK"; exit 1; }
85
+ tmp=$(mktemp -d) || exit 1
86
+ idx="$tmp/index" # does not exist yet: git starts an empty index there
87
+ GIT_INDEX_FILE="$idx" git add -f -- "$WORK" ":(exclude)$WORK/panel.json" &&
88
+ files=$(GIT_INDEX_FILE="$idx" git ls-files) &&
89
+ tree=$(GIT_INDEX_FILE="$idx" git write-tree) &&
90
+ commit=$(git commit-tree "$tree" -m "study $TICKET")
91
+ rc=$?; rm -rf "$tmp"
92
+ [ "$rc" -eq 0 ] && [ -n "$files" ] || { echo "nothing was built (rc=$rc)"; exit 1; }
93
+ printf '%s\n' "$files"; echo "commit=$commit"
94
+ ```
95
+
96
+ `GIT_INDEX_FILE` is set per command, never exported, so a failure leaves no session pointing git
97
+ at a deleted index. `panel.json` stays out: it is this machine's terminal panel, and on another one
98
+ it would show this session's last state as if it were theirs. The block failing → its message, and
99
+ stop: `commit-tree` with no git identity configured is the usual cause.
100
+
101
+ ## 4. Confirm, then push
102
+
103
+ Show the branch, the remote and the file list the build printed — every path, one per line — then
104
+ ask with `AskUserQuestion` **in every `autonomy.mode`**: a push leaves the machine (flow-core §2).
105
+ **Push** · **Cancel**. Nothing between the list and the question (flow-core §3).
106
+
107
+ ```bash handoff-push
108
+ BRANCH='@BRANCH@'; COMMIT='@COMMIT@'
109
+ case "$COMMIT" in ''|*[!0-9a-f]*) echo "not a commit sha: $COMMIT"; exit 1 ;; esac
110
+ git cat-file -e "$COMMIT^{commit}" || exit 1
111
+ git push origin "$COMMIT:refs/heads/$BRANCH"
112
+ ```
113
+
114
+ The sha is checked first because `git push origin :refs/heads/<branch>` — an empty source —
115
+ deletes the branch on the remote. A rejected push → its message, verbatim, and stop. Never retried
116
+ with `--force`.
117
+
118
+ ## 5. The ticket summary (offer)
119
+
120
+ Only after a successful push, and only in ticket mode with a real tracker id. The people reviewing
121
+ the ticket read it there, never in the work folder; the receiver does not parse it.
122
+
123
+ - **Read the body back** — the summary goes inside the ticket's own text, never over it:
124
+ `gh` → `gh issue view {TICKET} --json body -q .body` · `glab` → `glab issue view {TICKET} -F json`,
125
+ its `description`. Any other tracker → the offer is not made: one line saying flow cannot read that
126
+ body back to keep its text.
127
+ - **The write command** — `tracker.edit_cmd`, `{TICKET}` and `{BODY_FILE}` substituted. Empty →
128
+ `gh issue edit {TICKET} --body-file {BODY_FILE}` for `gh`,
129
+ `glab issue update {TICKET} --description-file {BODY_FILE}` for `glab`.
130
+ - **The block**, short, in the language of the ticket. A feature takes it from `01-context.md`,
131
+ `03-design.md` and `04-mr-plan.md`; a bug from `01-context.md` and `03-investigation.md` — the
132
+ expected behaviour as its criteria, the root cause and the intended fix as its design, "Areas with
133
+ similar risk" as its risks. A section whose source does not exist (an XS study, an S feature with
134
+ no plan) says `none`:
135
+
136
+ ```
137
+ <!-- flow-study -->
138
+ ## What and why
139
+ ## Acceptance criteria
140
+ ## Design in brief
141
+ ## MR/PR plan
142
+ ## Risks
143
+ <!-- end flow-study -->
144
+ ```
145
+
146
+ Criteria keep their literal values. The plan lists each MR/PR with what it covers and what it
147
+ waits on; a bug's plan is its single fix.
148
+ - **The new body**: the body read above with the block between the two markers replaced — or, with
149
+ no markers yet, the block appended after a blank line. Written to a `mktemp` file outside the
150
+ tree — `{BODY_FILE}` — so nothing of it can be committed later.
151
+ - **Show the block and ask** with `AskUserQuestion`, **in every mode**: it writes to a shared
152
+ tracker. **Write it** · **Skip**. Failure → its message in one line; the push stands.
153
+
154
+ ## 6. Close
155
+
156
+ Report following flow-core §3: the branch and the commit, the file count, whether the ticket
157
+ summary was written. Refresh the panel: a `Now` line saying the study was handed off, no `Decision`
158
+ line. `meta.json` gains nothing — the branch on `origin` is the record, and the §3 refusal reads it.
@@ -16,7 +16,7 @@ Every `/flow-*` command assumes these rules. They are stated once, here, so a co
16
16
  carries what is specific to its phase. Read this once per session; a command that says "load
17
17
  `flow-core`" means this file.
18
18
 
19
- **This file belongs to flow `0.86.0`.** Compare it once, at the start of the session, against
19
+ **This file belongs to flow `0.87.0`.** Compare it once, at the start of the session, against
20
20
  `version` in `~/.claude/flow/.claude-plugin/plugin.json`. The two differing means the session
21
21
  is running a **mixture** — the commands from one copy of the plugin, these shared rules from another
22
22
  — which is exactly what happens when a branch or a release candidate is loaded over an installed
@@ -247,6 +247,7 @@ Two signals arrive on an open MR/PR, each with its own cross-cutting loop (feat
247
247
  - `/flow-work-daily [question]` — the **work assistant** (daily standup), read-only. **Local** (`.claude/work/` + git) + **forge** (open MRs/PRs, reviews awaiting you, red CI, unmergeable, unresolved threads via `git.cli`) + **tracker** (assigned tickets, priority changes via `tracker.tool`); where they cross, concrete commands (`/flow-feat-start`, `/flow-work-green`, `/flow-work-respond`). No argument → *yesterday · today · blockers*; a question → just that. External sources best-effort, never blocking; the only write is a "last seen" marker, like `/flow-news`.
248
248
  - `/flow-work-status` — all works in `.claude/work/`, current phase, divergence with git.
249
249
  - `/flow-work-run [ticket]` — the **entry an orchestrator launches** for an `unattended` run: asks it `why` (start · answer · merged · review · pipeline), routes to the phase that answers it, and makes no decision of its own. Refuses to run in any other mode.
250
+ - `/flow-work-handoff` — hands a **finished study** to whoever builds it elsewhere (an unattended server, another machine): refuses an unfinished one, pushes only this work's folder as a parentless commit to `git.handoff_branch` without touching the checkout, asks before the push, then offers a marked summary for the ticket body (`tracker.edit_cmd`).
250
251
  - `/flow-work-resume` — detects the branch, reads `meta.json`, recaps, suggests the next step. **Re-reads the ticket's comment thread** (§2.5), reports only what is new since `01-context.md`, appends it to the artifact; never amends the design on its own.
251
252
  - `/flow-work-try <branch>` / `/flow-work-try --back` — point the main checkout at a branch to test it here, then return; re-syncs per `git.worktree_resync`. Resolves the work behind the branch and, once the environment is up, **prints the manual test plan already written** (the `needs-manual`/`unproven` criteria, or the acceptance criteria when `validate` has not run) and offers to collect Pass/Fail/Blocked into the validation artifact. No work behind the branch → the plain switch, as before.
252
253
  - `/flow-work-clean [--dry-run]` — **periodic sweep** of finished work's residue: worktree, local branch, `.claude/work/` folder (intermediate train MR/PRs never set `phase: done`, so the `ship`/`abandon` prompt never fires). Verdict per branch from **the forge** with local fallbacks, including patch-equivalence for **squash merges** (`git branch --merged` misses them); acts only on `merged`/`empty`. `unknown` never a candidate; dirty and unpushed protected; work folders **archived, never deleted**; remote never touched. **`autonomy.mode` does not authorize deletion**: `auto` confirms the list too. `--purge-archive <N>d`: separate opt-in pass over `_archive/`, only folders already committed to git.
@@ -0,0 +1,156 @@
1
+ ---
2
+ description: Hand a finished study to whoever builds it elsewhere — push only this work's folder to a branch of its own, then offer a summary for the ticket
3
+ ---
4
+
5
+ <!-- GENERATED by script/adapter-build.py from plugins/flow/commands/work/handoff.md — do not edit; change the plugin file and run the script. -->
6
+
7
+ # `/flow-work-handoff`
8
+
9
+ > **opencode adapter — how the Claude Code primitives named below map here.**
10
+ > - `AskUserQuestion` → ask in plain text with numbered options and wait for the reply.
11
+ > - `Agent <role>` / `Agent general-purpose` / subagents → `@<name>` declared in `agents/<name>.md` (`mode: subagent`); the name comes from `agents.<role>` in `FLOW.md`, empty → do it in this context.
12
+ > - Parallel fan-out → several `@name` in one prompt, capped at `agents.fanout_max` (empty → 4); `agents.fanout_tool` is Claude Code-only, ignore it.
13
+ > - `ScheduleWakeup` / `Monitor` / `/loop` → not available in-session: run one cycle, persist state in `monitor.md`, let the user schedule `opencode run -p "<command>"` with cron.
14
+ > - `TaskCreate` → a markdown checklist in the phase artifact.
15
+ > - `Skill commit-commands:commit-push-pr` → `git add` · `git commit` · `git push -u origin HEAD` · the `git.cli` CLI (`gh pr create` / `glab mr create`).
16
+ > - `/model <value>` → opencode's model picker (`/models`).
17
+ > - `knowledge.*` roles → whatever tools `FLOW.md` names there; an MCP tool keeps its name, its server is declared in `opencode.json` (see this adapter's `opencode.json` for the domain-memory example).
18
+
19
+ Read `~/.claude/flow/CORE.opencode.md` first (shared rules: `FLOW.md` step 0, autonomy, how a stop reads, the live panel, `00-summary.md`) — skip if you already read it in this session.
20
+
21
+ **Panel words are closed** — `mark`: `done` · `current` · `pending` · `wait` · `block` · `info`; `style`: `normal` · `dim` · `title` · `accent` · `ok` · `warn` · `error`. Anything else is dropped by the reader in silence: the panel still paints, and nobody is told.
22
+
23
+ For a study made here and built somewhere else — a server running `/flow-work-run` unattended,
24
+ another person, another machine. The builder gets the work folder at the same path and nothing
25
+ else: no conversation, maybe a less capable model. So this command checks the study can stand on
26
+ its own, pushes the folder to a branch that holds only it, and offers to put a summary in the
27
+ ticket for the people who review it.
28
+
29
+ ## 1. Pre-flight
30
+
31
+ - Effective FLOW configuration per flow-core §0. `autonomy.mode: unattended` → refuse in one line:
32
+ an unattended run never pushes (flow-core §2.1); handing off is a person's step.
33
+ - Locate the active `meta.json` by current branch; not found → ask the user for the ticket.
34
+ - `git.handoff_branch` empty → refuse, naming the key and an example
35
+ (`git.handoff_branch: study/{TICKET}`): where the study goes is the receiver's convention, never
36
+ flow's guess. `{TICKET}` = `meta.json.ticket`.
37
+ - `origin` not configured → refuse; there is nowhere to push.
38
+
39
+ ## 2. Is the study finished?
40
+
41
+ The builder can ask no one. Refuse — pushing nothing, naming every reason that holds — when:
42
+
43
+ - **The study's last phase is not in `phases_done`.** A feature: `context` on XS (what `start`
44
+ records), `design` on S, `plan` on M/L. A bug: `context` on XS, `investigate` otherwise.
45
+ - **`meta.json.pending` is set** — a question is still waiting for its answer.
46
+ - **A clarification is still open** — a question under "Decisions clarified at start" in
47
+ `01-context.md` recorded without its answer.
48
+
49
+ Then, **before anything is pushed**: decisions this session settled while talking that no artifact
50
+ records yet → write them where they belong now (`01-context.md` "Decisions clarified at start",
51
+ `03-design.md` "Decisions (ADR-light)", `04-mr-plan.md`), and say which. What was decided only in
52
+ the chat does not travel.
53
+
54
+ ## 3. Build the commit, without touching the checkout
55
+
56
+ A throwaway index holds only this folder, so the current branch, `HEAD`, the real index (staged
57
+ work included) and the working tree are never read into it nor written to, and a git-ignored
58
+ `.claude/work/` is no obstacle.
59
+
60
+ **Each block below is one shell call, run as it stands** — a harness need not keep variables from
61
+ one call to the next, so every block sets its own. Replace each `@…@` with its literal value first:
62
+ `@WORK@` the work folder relative to the repository root (`.claude/work/<work-dir>`, no trailing
63
+ slash), `@BRANCH@` the resolved `git.handoff_branch`, `@TICKET@` `meta.json.ticket`, `@COMMIT@` the
64
+ sha the build block printed. A value holding a `'` cannot go inside those quotes: refuse it and
65
+ say which.
66
+
67
+ First, the branch must not exist on `origin` — a second push would need a force, and this command
68
+ never forces:
69
+
70
+ ```bash handoff-check
71
+ BRANCH='@BRANCH@'
72
+ git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null; echo "rc=$?"
73
+ ```
74
+
75
+ `rc=0` → the branch exists: refuse, and say what the person does next — delete it on the forge
76
+ (only when nothing still reads it), or set another name in `git.handoff_branch`. `rc=2` → absent,
77
+ continue. Anything else → stop and say why (no network, no access). Then build the commit:
78
+
79
+ ```bash handoff-build
80
+ WORK='@WORK@'; TICKET='@TICKET@'
81
+ cd "$(git rev-parse --show-toplevel)" || exit 1
82
+ [ -d "$WORK" ] || { echo "no such folder: $WORK"; exit 1; }
83
+ tmp=$(mktemp -d) || exit 1
84
+ idx="$tmp/index" # does not exist yet: git starts an empty index there
85
+ GIT_INDEX_FILE="$idx" git add -f -- "$WORK" ":(exclude)$WORK/panel.json" &&
86
+ files=$(GIT_INDEX_FILE="$idx" git ls-files) &&
87
+ tree=$(GIT_INDEX_FILE="$idx" git write-tree) &&
88
+ commit=$(git commit-tree "$tree" -m "study $TICKET")
89
+ rc=$?; rm -rf "$tmp"
90
+ [ "$rc" -eq 0 ] && [ -n "$files" ] || { echo "nothing was built (rc=$rc)"; exit 1; }
91
+ printf '%s\n' "$files"; echo "commit=$commit"
92
+ ```
93
+
94
+ `GIT_INDEX_FILE` is set per command, never exported, so a failure leaves no session pointing git
95
+ at a deleted index. `panel.json` stays out: it is this machine's terminal panel, and on another one
96
+ it would show this session's last state as if it were theirs. The block failing → its message, and
97
+ stop: `commit-tree` with no git identity configured is the usual cause.
98
+
99
+ ## 4. Confirm, then push
100
+
101
+ Show the branch, the remote and the file list the build printed — every path, one per line — then
102
+ ask with `AskUserQuestion` **in every `autonomy.mode`**: a push leaves the machine (flow-core §2).
103
+ **Push** · **Cancel**. Nothing between the list and the question (flow-core §3).
104
+
105
+ ```bash handoff-push
106
+ BRANCH='@BRANCH@'; COMMIT='@COMMIT@'
107
+ case "$COMMIT" in ''|*[!0-9a-f]*) echo "not a commit sha: $COMMIT"; exit 1 ;; esac
108
+ git cat-file -e "$COMMIT^{commit}" || exit 1
109
+ git push origin "$COMMIT:refs/heads/$BRANCH"
110
+ ```
111
+
112
+ The sha is checked first because `git push origin :refs/heads/<branch>` — an empty source —
113
+ deletes the branch on the remote. A rejected push → its message, verbatim, and stop. Never retried
114
+ with `--force`.
115
+
116
+ ## 5. The ticket summary (offer)
117
+
118
+ Only after a successful push, and only in ticket mode with a real tracker id. The people reviewing
119
+ the ticket read it there, never in the work folder; the receiver does not parse it.
120
+
121
+ - **Read the body back** — the summary goes inside the ticket's own text, never over it:
122
+ `gh` → `gh issue view {TICKET} --json body -q .body` · `glab` → `glab issue view {TICKET} -F json`,
123
+ its `description`. Any other tracker → the offer is not made: one line saying flow cannot read that
124
+ body back to keep its text.
125
+ - **The write command** — `tracker.edit_cmd`, `{TICKET}` and `{BODY_FILE}` substituted. Empty →
126
+ `gh issue edit {TICKET} --body-file {BODY_FILE}` for `gh`,
127
+ `glab issue update {TICKET} --description-file {BODY_FILE}` for `glab`.
128
+ - **The block**, short, in the language of the ticket. A feature takes it from `01-context.md`,
129
+ `03-design.md` and `04-mr-plan.md`; a bug from `01-context.md` and `03-investigation.md` — the
130
+ expected behaviour as its criteria, the root cause and the intended fix as its design, "Areas with
131
+ similar risk" as its risks. A section whose source does not exist (an XS study, an S feature with
132
+ no plan) says `none`:
133
+
134
+ ```
135
+ <!-- flow-study -->
136
+ ## What and why
137
+ ## Acceptance criteria
138
+ ## Design in brief
139
+ ## MR/PR plan
140
+ ## Risks
141
+ <!-- end flow-study -->
142
+ ```
143
+
144
+ Criteria keep their literal values. The plan lists each MR/PR with what it covers and what it
145
+ waits on; a bug's plan is its single fix.
146
+ - **The new body**: the body read above with the block between the two markers replaced — or, with
147
+ no markers yet, the block appended after a blank line. Written to a `mktemp` file outside the
148
+ tree — `{BODY_FILE}` — so nothing of it can be committed later.
149
+ - **Show the block and ask** with `AskUserQuestion`, **in every mode**: it writes to a shared
150
+ tracker. **Write it** · **Skip**. Failure → its message in one line; the push stands.
151
+
152
+ ## 6. Close
153
+
154
+ Report following flow-core §3: the branch and the commit, the file count, whether the ticket
155
+ summary was written. Refresh the panel: a `Now` line saying the study was handed off, no `Decision`
156
+ line. `meta.json` gains nothing — the branch on `origin` is the record, and the §3 refusal reads it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flow-workflows",
3
- "version": "0.86.0",
3
+ "version": "0.87.0",
4
4
  "description": "Guided feat/bug workflows (start→ship), post-deploy monitoring and multi-agent code review, stack-agnostic. Each repo uses a shared FLOW.md plus optional harness overlay.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,7 @@
3
3
  "name": "flow",
4
4
  "displayName": "Flow — guided dev workflows",
5
5
  "description": "Guided feat/bug workflows (start→ship), post-deploy monitoring and multi-agent code review, stack-agnostic. Each repo uses a shared FLOW.md plus optional harness overlay.",
6
- "version": "0.86.0",
6
+ "version": "0.87.0",
7
7
  "author": {
8
8
  "name": "Alberto Vioque",
9
9
  "email": "mashware@gmail.com"
@@ -5,6 +5,22 @@ plugin and is what `/flow:news` reads to show you what changed since your previo
5
5
 
6
6
  The canonical, richest notes live in the [GitHub Releases](https://github.com/mashware/flow-workflows/releases).
7
7
 
8
+ ## v0.87.0 — A finished study is handed off from the laptop, and summarised in its ticket · 2026-10-01
9
+
10
+ **In short**
11
+ - **`/flow:work:handoff` hands a study to whoever builds it elsewhere.** It pushes this work's
12
+ folder, and nothing else, as a commit with no history to the branch `git.handoff_branch` names
13
+ (e.g. `study/{TICKET}`). Your current branch, staged changes and other works are never touched,
14
+ even when `.claude/work/` is git-ignored.
15
+ - **It refuses a study that cannot stand on its own** — its design or plan not closed, a question
16
+ pending, a clarification open — and writes down first what this session decided but no file says.
17
+ - **It asks before the push, and never forces**: a branch that already exists is refused, with what
18
+ to do next.
19
+ - **A summary for the ticket**, offered after the push: what and why, criteria, design in brief,
20
+ MR/PR plan and risks, in a marked block that keeps the ticket's own text and is replaced on a
21
+ second run. GitHub and GitLab out of the box; new key `tracker.edit_cmd`.
22
+ - Completes #186.
23
+
8
24
  ## v0.86.0 — An unattended run builds a study made on another machine · 2026-10-01
9
25
 
10
26
  **In short**
@@ -48,6 +48,7 @@ How tickets are identified and read.
48
48
  # they are asked before running in `autonomy.mode: manual` and run automatically in `guided`/`auto`;
49
49
  # a failure or an already-in-state ticket warns and continues, never blocks. They run only in
50
50
  # ticket mode with a real tracker id (skipped for ticket-less / local-only works).
51
+ - `edit_cmd:` # optional, rewrites a ticket's BODY from a file: `/flow:work:handoff` writes its study summary with it. `{TICKET}` and `{BODY_FILE}` substituted. Empty = derived for `gh` (`gh issue edit {TICKET} --body-file {BODY_FILE}`) and `glab` (`glab issue update {TICKET} --description-file {BODY_FILE}`); other trackers get no summary — flow cannot read their body back to keep its text.
51
52
  - `followup_ask_max:` # how many parked findings one finished work may turn into questions at `ship`'s Close (flow-core §7). Empty = 2. `0` = never ask, log everything. A ticket where
52
53
  # seven phases each notice something is not seven problems: with 3+ candidates a skeptic tries to refute each one (burden of proof on the finding), the ones
53
54
  # without a named subject are dropped, and only the strongest survivors up to this number are put to you. Raise it if you would rather triage than be handed a log.
@@ -86,6 +87,8 @@ Branch and Pull/Merge Request conventions.
86
87
  # - make database-update
87
88
  # - make frontend
88
89
 
90
+ - `handoff_branch:` # branch `/flow:work:handoff` pushes a finished study to, holding only that work's folder. `{TICKET}` substituted. Empty = the command refuses: where a study goes is the receiver's convention. e.g. `study/{TICKET}`.
91
+
89
92
  ## autonomy
90
93
  How much the flow advances on its own vs. stopping to ask you.
91
94