@ulysses-ai/create-workspace 0.17.0-beta.0 → 0.18.0-beta.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 +3 -3
- package/package.json +1 -1
- package/template/.claude/hooks/_utils.mjs +1 -1
- package/template/.claude/hooks/repo-write-detection.mjs +161 -64
- package/template/.claude/hooks/session-start.mjs +35 -1
- package/template/.claude/hooks/subagent-start.mjs +89 -22
- package/template/.claude/lib/session-frontmatter.mjs +28 -0
- package/template/.claude/rules/coherent-revisions.md +1 -1
- package/template/.claude/rules/forge-operations.md +37 -93
- package/template/.claude/rules/git-conventions.md +16 -11
- package/template/.claude/rules/goal-driven-work.md +8 -416
- package/template/.claude/rules/honest-pushback.md +37 -37
- package/template/.claude/rules/memory-guidance.md +43 -90
- package/template/.claude/rules/superpowers-workflow.md.skip +1 -1
- package/template/.claude/rules/work-item-tracking.md +30 -72
- package/template/.claude/rules/workspace-structure.md +36 -94
- package/template/.claude/scripts/build-workspace-context.mjs +61 -16
- package/template/.claude/scripts/chat-record.mjs +282 -0
- package/template/.claude/scripts/cleanup-work-session.mjs +257 -68
- package/template/.claude/scripts/context-footprint.mjs +282 -0
- package/template/.claude/scripts/forges/github.mjs +45 -0
- package/template/.claude/scripts/forges/gitlab.mjs +3 -2
- package/template/.claude/scripts/forges/interface.mjs +12 -0
- package/template/.claude/scripts/generate-claude-local.mjs +21 -2
- package/template/.claude/scripts/migrate-sessions.mjs +1571 -0
- package/template/.claude/scripts/migrate-to-workspace-context.mjs +7 -2
- package/template/.claude/scripts/task-worktree.mjs +525 -0
- package/template/.claude/scripts/workspace-diagnostics.mjs +654 -0
- package/template/.claude/skills/braindump/SKILL.md +11 -4
- package/template/.claude/skills/build-docs-site/SKILL.md +5 -5
- package/template/.claude/skills/build-docs-site/templates/spec.md.tmpl +1 -1
- package/template/.claude/skills/complete-work/SKILL.md +229 -219
- package/template/.claude/skills/context-placement/SKILL.md +199 -0
- package/template/.claude/skills/goal-driven-work/SKILL.md +459 -0
- package/template/.claude/skills/handoff/SKILL.md +11 -4
- package/template/.claude/skills/maintenance/SKILL.md +7 -0
- package/template/.claude/skills/migrate-sessions/SKILL.md +70 -0
- package/template/.claude/skills/pause-work/SKILL.md +9 -1
- package/template/.claude/skills/release/SKILL.md +44 -108
- package/template/.claude/skills/start-work/SKILL.md +89 -7
- package/template/.claude/skills/workspace-init/SKILL.md +3 -1
- package/template/.claude/skills/workspace-update/SKILL.md +4 -0
- package/template/CLAUDE.md.tmpl +19 -2
- package/template/_gitignore +9 -0
- package/template/workspace.json.tmpl +3 -2
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: complete-work
|
|
3
|
-
description: Finalize a work session — rebase,
|
|
3
|
+
description: Finalize a work session — rebase, PR, merge, and close the linked issue. Handles all project repos and the workspace repo. Use when work on a session is done.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Complete Work
|
|
7
7
|
|
|
8
|
-
Finalize the active work session. Handles all project repos (code changes,
|
|
8
|
+
Finalize the active work session. Handles all project repos (code changes, PRs) and the workspace repo (context processing, PR). Presents a unified summary with a single merge approval, then tears down the session folder.
|
|
9
9
|
|
|
10
10
|
## Flow
|
|
11
11
|
|
|
12
12
|
### Step 1: Detect context
|
|
13
13
|
|
|
14
|
-
Read the active-session pointer from `.claude/.active-session.json` in the current worktree.
|
|
15
|
-
|
|
14
|
+
Read the active-session pointer from `.claude/.active-session.json` in the current worktree. If it is present, this chat runs inside a session worktree: continue with this flow unchanged.
|
|
15
|
+
|
|
16
|
+
If no pointer is present, run work-model detection — this covers the task model, whose chats run at the workspace root (the launcher), not inside a worktree:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node "{launcher-root}/.claude/scripts/task-worktree.mjs" --root "{launcher-root}" --detect --chat "{chat}"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `{launcher-root}` is the absolute path on the `Workspace root:` line the SessionStart hook injects. If that line is absent, derive it from git: run `git rev-parse --git-common-dir` (when it prints a relative path, resolve it against the cwd) and take its parent directory. That derivation lands on the source clone `…/repos/{repo}` when run from inside a **project** task worktree — there the launcher is two levels up; from inside a `.` worktree (`.claude/worktrees/{slug}`) the parent already is the launcher.
|
|
23
|
+
- `{chat}` is the name from the `Chat record:` line the SessionStart hook injects. If that line is absent, omit `--chat` — detection then relies on cwd alone.
|
|
24
|
+
- `model: session` → continue with this flow (read the session tracker as below), taking `{session-name}` from the detect result's `sessionName`.
|
|
25
|
+
- `model: task` → go to **Task completion (session model v2)**. The result's `tasks` come from the chat record; if several are open, ask the user which one to complete — group by branch, a multi-repo task is several entries sharing a branch.
|
|
26
|
+
- `model: none` → "No active work session. Nothing to complete."
|
|
16
27
|
|
|
17
28
|
Read the full session tracker at `work-sessions/{session-name}/workspace/session.md` (use the frontmatter helper in `.claude/lib/session-frontmatter.mjs` — scripts and hooks use `_utils.mjs` which wraps it).
|
|
18
29
|
|
|
@@ -21,16 +32,15 @@ Determine paths:
|
|
|
21
32
|
- Workspace worktree: `work-sessions/{session-name}/workspace/`
|
|
22
33
|
- Project worktrees: `work-sessions/{session-name}/workspace/repos/{repo}/` for each repo in the tracker's `repos:` list
|
|
23
34
|
- Read each repo's default branch from workspace.json (`repos.{repo}.branch`)
|
|
24
|
-
- **Release-notes base directory.** Read `workspace.releaseNotesDir` from `workspace.json` (default `workspace-context/release-notes` if the field is absent). Throughout this skill `{releaseNotesDir}` refers to that resolved value; every branch-note path is `{releaseNotesDir}/unreleased/{repo}/…`. Never use a bare `release-notes/`.
|
|
25
35
|
|
|
26
36
|
### Step 2: Rebase project repos
|
|
27
37
|
|
|
28
38
|
For each repo in the tracker's `repos:`:
|
|
29
39
|
```bash
|
|
30
40
|
# {repo-branch} = repos.{repo}.branch from workspace.json
|
|
31
|
-
cd work-sessions/{session-name}/workspace/repos/{repo}
|
|
41
|
+
cd "work-sessions/{session-name}/workspace/repos/{repo}"
|
|
32
42
|
git fetch origin
|
|
33
|
-
git rebase origin/{repo-branch}
|
|
43
|
+
git rebase "origin/{repo-branch}"
|
|
34
44
|
```
|
|
35
45
|
If conflicts arise in any repo, STOP and present them to the user. Do not auto-resolve.
|
|
36
46
|
|
|
@@ -41,10 +51,10 @@ If the user declines or there's nothing to capture, skip.
|
|
|
41
51
|
|
|
42
52
|
### Step 4: Flush task list to session.md
|
|
43
53
|
|
|
44
|
-
Before reading sources for
|
|
54
|
+
Before reading sources for the PR body, flush current `TodoWrite` state to `## Tasks` per the `task-list-mirroring` rule. This ensures the body written in Step 9 sees the final state:
|
|
45
55
|
|
|
46
56
|
```bash
|
|
47
|
-
cd work-sessions/{session-name}/workspace
|
|
57
|
+
cd "work-sessions/{session-name}/workspace"
|
|
48
58
|
echo '<JSON-of-current-todos>' | node .claude/scripts/sync-tasks.mjs --write session.md
|
|
49
59
|
```
|
|
50
60
|
|
|
@@ -52,7 +62,7 @@ Mark `Complete work` as `in_progress` in the JSON before flushing — the rest o
|
|
|
52
62
|
|
|
53
63
|
### Step 5: Gather source material
|
|
54
64
|
|
|
55
|
-
Formally read ALL sources before
|
|
65
|
+
Formally read ALL sources before writing the PR body — do not summarize from memory alone:
|
|
56
66
|
|
|
57
67
|
1. **Session tracker** at `work-sessions/{session-name}/workspace/session.md` — read the full body (frontmatter is machine state, body is human content)
|
|
58
68
|
|
|
@@ -73,72 +83,22 @@ Formally read ALL sources before synthesizing — do not write release notes fro
|
|
|
73
83
|
4. **Branch commit logs** (per repo):
|
|
74
84
|
```bash
|
|
75
85
|
# For each repo in the tracker's repos list:
|
|
76
|
-
cd work-sessions/{session-name}/workspace/repos/{repo}
|
|
77
|
-
git log origin/{repo-branch}..HEAD --oneline
|
|
86
|
+
cd "work-sessions/{session-name}/workspace/repos/{repo}"
|
|
87
|
+
git log "origin/{repo-branch}..HEAD" --oneline
|
|
78
88
|
```
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Branch notes are written to the **workspace** repo, not the project repo. They are an internal retrospection artifact consumed by `/release` at release time; the project repo only ever receives a `CHANGELOG.md` entry. This separation keeps dogfood content out of public project repos between feature merge and the next release cut.
|
|
83
|
-
|
|
84
|
-
For each repo in the tracker's `repos:` list that has commits beyond the base branch:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
cd work-sessions/{session-name}/workspace/repos/{repo}
|
|
88
|
-
COMMIT_ID=$(git rev-parse --short HEAD)
|
|
89
|
-
cd ../.. # back to the workspace worktree
|
|
90
|
-
mkdir -p {releaseNotesDir}/unreleased/{repo-name}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
**File 1: `{releaseNotesDir}/unreleased/{repo-name}/branch-release-notes-{COMMIT_ID}.md`** (relative to the workspace worktree)
|
|
94
|
-
```markdown
|
|
95
|
-
---
|
|
96
|
-
branch: {branch}
|
|
97
|
-
repo: {repo-name}
|
|
98
|
-
type: {feature|fix|chore}
|
|
99
|
-
author: {user}
|
|
100
|
-
date: {YYYY-MM-DD}
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## {Human-readable title}
|
|
90
|
+
5. **The linked issue** — if the tracker has a `workItem:` field and `workspace.tracker` is configured, read the issue through the tracker adapter so the PR body can speak to what was asked.
|
|
104
91
|
|
|
105
|
-
|
|
106
|
-
Written from scratch per coherent-revisions rule.}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
**File 2: `{releaseNotesDir}/unreleased/{repo-name}/branch-release-questions-{COMMIT_ID}.md`**
|
|
110
|
-
```markdown
|
|
111
|
-
---
|
|
112
|
-
branch: {branch}
|
|
113
|
-
repo: {repo-name}
|
|
114
|
-
author: {user}
|
|
115
|
-
date: {YYYY-MM-DD}
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## Open Questions
|
|
119
|
-
|
|
120
|
-
{Only genuinely open questions — not things resolved during implementation.}
|
|
121
|
-
```
|
|
92
|
+
These sources feed the PR bodies in Step 9: a short summary of what changed and why, plus a Verification section.
|
|
122
93
|
|
|
123
|
-
|
|
94
|
+
### Step 6: Remove session artifacts from the workspace branch
|
|
124
95
|
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
cd work-sessions/{session-name}/workspace
|
|
128
|
-
git add {releaseNotesDir}/unreleased/
|
|
129
|
-
git commit -m "docs: add release notes for {branch}"
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
If a repo has no commits beyond the base, skip release notes for it.
|
|
133
|
-
|
|
134
|
-
### Step 7: Remove session artifacts from the workspace branch
|
|
135
|
-
|
|
136
|
-
The entire `work-sessions/{session-name}/` folder is removed by the cleanup script in Step 12. Before that happens, make sure everything worth preserving has landed in release notes (Step 6) — once Step 6 has run, the tracker, specs, plans, and goal artifacts have served their purpose.
|
|
96
|
+
The entire `work-sessions/{session-name}/` folder is removed by the cleanup script in Step 11. Before that happens, decide what deserves to survive: the tracker, specs, plans, and goal artifacts hold the session's reasoning, and the commands below delete them from the branch.
|
|
137
97
|
|
|
138
98
|
**Goal sub-branch pre-flight (only when a `goal-*.md` artifact is present).** A `/goal`-driven session can produce per-phase sub-branches for code phases (any phase declaring `integration.strategy: sub-branch` in the goal artifact). Those sub-branches must be merged into the session branch before completion, or their work is lost when the session folder is torn down. Before stripping anything, check each repo in the session — the workspace worktree itself and every `repos/{repo}/` project worktree:
|
|
139
99
|
|
|
140
100
|
```bash
|
|
141
|
-
cd work-sessions/{session-name}/workspace/repos/{repo} # repeat for the workspace worktree too
|
|
101
|
+
cd "work-sessions/{session-name}/workspace/repos/{repo}" # repeat for the workspace worktree too
|
|
142
102
|
session_branch=$(git rev-parse --abbrev-ref HEAD)
|
|
143
103
|
for sub in $(git branch --format='%(refname:short)' --list "${session_branch}-*"); do
|
|
144
104
|
git merge-base --is-ancestor "$sub" "$session_branch" || echo "UNMERGED: $sub"
|
|
@@ -147,10 +107,25 @@ done
|
|
|
147
107
|
|
|
148
108
|
If any `UNMERGED:` lines print, abort completion and show the list. The user merges the intended sub-branches into the session branch, or closes abandoned ones, then re-runs `/complete-work`. When no `goal-*.md` artifact is present, this check is a no-op and completion proceeds normally.
|
|
149
109
|
|
|
150
|
-
|
|
110
|
+
**Decide the fate of the artifacts — runs after the pre-flight, never before it.** Before stripping, list the session artifacts present at the top of the workspace worktree — `session.md` plus every `design-*.md`, `plan-*.md`, `goal-*.md`, `research-*.md`, and `crossref-*.md` — and offer two choices:
|
|
111
|
+
|
|
112
|
+
1. **Promote first (default).** Run `/promote` on the artifacts so they land in
|
|
113
|
+
`workspace-context/` as durable team knowledge, then strip and continue. This is the
|
|
114
|
+
right answer in every case: reasoning that is worth keeping belongs in workspace-context,
|
|
115
|
+
which is where it will actually be read.
|
|
116
|
+
2. **Discard.** Strip anyway, with an explicit confirmation that names each file.
|
|
117
|
+
|
|
118
|
+
Never choose on the user's behalf. There is deliberately no "keep them on the branch"
|
|
119
|
+
option: `session.md` would merge to the workspace repo root, which is exactly the path the
|
|
120
|
+
next `/start-work` worktree writes its own tracker to, and `createSessionTracker` overwrites
|
|
121
|
+
unconditionally. Keeping artifacts on the branch does not preserve them — it contaminates
|
|
122
|
+
`main` and then loses them anyway on the next session.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
Session content lives at the top of the workspace worktree on the session branch. Once the pre-flight passes and the artifacts' fate is decided, remove these files from the branch before the final push so main's top level stays free of session artifacts:
|
|
151
126
|
|
|
152
127
|
```bash
|
|
153
|
-
cd work-sessions/{session-name}/workspace
|
|
128
|
+
cd "work-sessions/{session-name}/workspace"
|
|
154
129
|
git rm -f session.md 2>/dev/null || true
|
|
155
130
|
git rm -f design-*.md 2>/dev/null || true
|
|
156
131
|
git rm -f plan-*.md 2>/dev/null || true
|
|
@@ -164,62 +139,64 @@ The `|| true` guards keep this idempotent — if a file is already gone (e.g., a
|
|
|
164
139
|
|
|
165
140
|
This commit persists in the branch's history. On squash merge or rebase merge, branch history collapses to one clean commit on main with no session artifacts. On merge commits, branch history is reachable but the final tree on main shows no session content.
|
|
166
141
|
|
|
167
|
-
> **No version bump here.** Versions are bumped at release time by `/release`, which
|
|
142
|
+
> **No version bump here.** Versions are bumped at release time by `/release`, which tags the merge and cuts a forge release whose notes are generated from merged PRs. `/complete-work` does not modify any project repo's `package.json`.
|
|
168
143
|
|
|
169
|
-
### Step
|
|
144
|
+
### Step 7: Detect remote type per repo
|
|
170
145
|
|
|
171
|
-
For each repo in the tracker's `repos:` plus the workspace repo, determine the remote type. This drives how Step
|
|
146
|
+
For each repo in the tracker's `repos:` plus the workspace repo, determine the remote type. This drives how Step 8 and Step 9 push and merge.
|
|
172
147
|
|
|
173
148
|
```bash
|
|
174
|
-
cd work-sessions/{session-name}/workspace/repos/{repo}
|
|
149
|
+
cd "work-sessions/{session-name}/workspace/repos/{repo}"
|
|
175
150
|
git remote get-url origin 2>&1
|
|
176
151
|
```
|
|
177
152
|
|
|
178
153
|
Classify the result:
|
|
179
154
|
|
|
180
|
-
- **GitHub remote** — URL contains `github.com` or `gh repo view` succeeds against origin → use the PR flow (Step
|
|
181
|
-
- **Local / bare remote** — URL is a filesystem path (starts with `/`, `./`, `file://`, or points at a `.git` bare mirror) → use the local merge flow (Step
|
|
182
|
-
- **Other remote** (e.g., GitLab, Bitbucket, self-hosted) — no `gh` support → fall back to the local merge flow (Step
|
|
155
|
+
- **GitHub remote** — URL contains `github.com` or `gh repo view` succeeds against origin → use the PR flow (Step 8a, Step 9a).
|
|
156
|
+
- **Local / bare remote** — URL is a filesystem path (starts with `/`, `./`, `file://`, or points at a `.git` bare mirror) → use the local merge flow (Step 8b, Step 9b).
|
|
157
|
+
- **Other remote** (e.g., GitLab, Bitbucket, self-hosted) — no `gh` support → fall back to the local merge flow (Step 8b, Step 9b), and mention it in the final summary.
|
|
183
158
|
- **No remote at all** — "No remote configured for {repo}. Want me to create one on GitHub, add an existing URL, or keep the session local (push/merge inside the local clone only)?" Act on the user's choice. Never silently skip push.
|
|
184
159
|
|
|
185
|
-
### Step
|
|
160
|
+
### Step 8: Push all repos
|
|
186
161
|
|
|
187
|
-
#### Step
|
|
162
|
+
#### Step 8a: GitHub remotes
|
|
188
163
|
|
|
189
164
|
```bash
|
|
190
165
|
# Each project repo with a GitHub remote
|
|
191
|
-
cd work-sessions/{session-name}/workspace/repos/{repo}
|
|
192
|
-
git push -u origin {branch}
|
|
166
|
+
cd "work-sessions/{session-name}/workspace/repos/{repo}"
|
|
167
|
+
git push -u origin "{branch}"
|
|
193
168
|
|
|
194
169
|
# Workspace repo — from the workspace worktree
|
|
195
|
-
cd work-sessions/{session-name}/workspace
|
|
170
|
+
cd "work-sessions/{session-name}/workspace"
|
|
196
171
|
git add .
|
|
197
172
|
git commit -m "chore: finalize context for {session-name}"
|
|
198
|
-
git push -u origin {branch}
|
|
173
|
+
git push -u origin "{branch}"
|
|
199
174
|
```
|
|
200
175
|
|
|
201
|
-
#### Step
|
|
176
|
+
#### Step 8b: Local/bare remotes
|
|
202
177
|
|
|
203
178
|
```bash
|
|
204
179
|
# Push the feature branch to the bare remote so it exists there
|
|
205
|
-
cd work-sessions/{session-name}/workspace/repos/{repo}
|
|
206
|
-
git push -u origin {branch}
|
|
180
|
+
cd "work-sessions/{session-name}/workspace/repos/{repo}"
|
|
181
|
+
git push -u origin "{branch}"
|
|
207
182
|
|
|
208
183
|
# Workspace repo — same commit + push pattern
|
|
209
|
-
cd work-sessions/{session-name}/workspace
|
|
184
|
+
cd "work-sessions/{session-name}/workspace"
|
|
210
185
|
git add .
|
|
211
186
|
git commit -m "chore: finalize context for {session-name}"
|
|
212
|
-
git push -u origin {branch}
|
|
187
|
+
git push -u origin "{branch}"
|
|
213
188
|
```
|
|
214
189
|
|
|
215
|
-
The push shape is the same as
|
|
190
|
+
The push shape is the same as 8a — what differs is the merge mechanics in Step 9b.
|
|
216
191
|
|
|
217
|
-
### Step
|
|
192
|
+
### Step 9: Merge and present unified summary
|
|
218
193
|
|
|
219
|
-
#### Step
|
|
194
|
+
#### Step 9a: GitHub remotes — create PRs, unified summary, merge
|
|
220
195
|
|
|
221
196
|
Create one PR per project repo plus one workspace PR. PR operations go through the forge adapter (`.claude/scripts/forges/interface.mjs`), not `gh` directly — see `.claude/rules/forge-operations.md` for the contract. The adapter resolves the target repo from `workspace.forge.repo` or the local git remote.
|
|
222
197
|
|
|
198
|
+
Each PR body is built from the material gathered in Step 5 — the session tracker body, the linked issue, and the commits: a short summary of what changed and why, then a **Verification** section stating how the change was checked (tests run, commands executed, results).
|
|
199
|
+
|
|
223
200
|
```javascript
|
|
224
201
|
import { createForge } from './.claude/scripts/forges/interface.mjs';
|
|
225
202
|
import { readFileSync } from 'node:fs';
|
|
@@ -231,7 +208,7 @@ const forge = createForge(ws.workspace?.forge);
|
|
|
231
208
|
// work-sessions/{session-name}/workspace/repos/{repo}:
|
|
232
209
|
const projectPr = await forge.prCreate({
|
|
233
210
|
title: `${type}: ${description}`,
|
|
234
|
-
body: prBody, //
|
|
211
|
+
body: prBody, // short summary + Verification section
|
|
235
212
|
});
|
|
236
213
|
|
|
237
214
|
// Workspace PR — from the workspace worktree:
|
|
@@ -249,14 +226,13 @@ PROJECT: {repo-1}
|
|
|
249
226
|
PR #{n}: {type}: {description}
|
|
250
227
|
Branch: {branch} → {repo-1-branch}
|
|
251
228
|
Changes:
|
|
252
|
-
- {bullet points from
|
|
253
|
-
Release notes: branch-release-notes-{COMMIT_ID}.md
|
|
229
|
+
- {bullet points from the PR body}
|
|
254
230
|
|
|
255
231
|
PROJECT: {repo-2}
|
|
256
232
|
PR #{m}: {type}: {description}
|
|
257
233
|
Branch: {branch} → {repo-2-branch}
|
|
258
234
|
Changes:
|
|
259
|
-
- {bullet points from
|
|
235
|
+
- {bullet points from the PR body}
|
|
260
236
|
|
|
261
237
|
WORKSPACE: {workspace-name}
|
|
262
238
|
PR #{p}: context: {session-name} work session
|
|
@@ -268,7 +244,7 @@ Merge all? [Y/n]
|
|
|
268
244
|
If yes — merge all PRs atomically through the forge adapter:
|
|
269
245
|
|
|
270
246
|
```javascript
|
|
271
|
-
// For each project PR returned from Step
|
|
247
|
+
// For each project PR returned from Step 9a's prCreate calls:
|
|
272
248
|
await forge.prMerge({ id: projectPr.id, strategy: 'squash', deleteBranch: true });
|
|
273
249
|
|
|
274
250
|
// Workspace PR:
|
|
@@ -281,112 +257,11 @@ Then pull all repos to their default branches (still plain git):
|
|
|
281
257
|
|
|
282
258
|
```bash
|
|
283
259
|
# For each repo in the tracker's repos:
|
|
284
|
-
cd repos/{repo} && git pull origin {repo-branch}
|
|
285
|
-
cd {main-workspace-root} && git pull origin main
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
**Step 10a.1: Tag the merge commit (release sessions only, project repos with `package.json`)**
|
|
289
|
-
|
|
290
|
-
The next three sub-substeps run only when the session branch starts with `release/` — the convention for release sessions (e.g., `release/v0.15.0-beta.0`). For feature, bugfix, and chore sessions, skip 10a.1, 10a.2, and 10a.3 entirely; non-release sessions don't trigger publishes. Detection is purely by branch prefix.
|
|
291
|
-
|
|
292
|
-
Derive the version tag from the branch name by stripping the `release/` prefix (so `release/v0.15.0-beta.0` yields `v0.15.0-beta.0`). For each project repo whose `package.json` declares a `version` field, verify that version matches the derived tag. The workspace repo is **never** tagged — only project repos with publishable `package.json` files get tagged, since the tag triggers `.github/workflows/publish.yml` in that project repo. If a project repo's `package.json` version doesn't match the release tag, skip that repo with a warning rather than failing the whole completion flow — the mismatch usually means `/release` was run against a different version than the branch name suggests, and the user needs to investigate before publishing.
|
|
293
|
-
|
|
294
|
-
Before tagging, preflight against origin: if `v{version}` already exists remotely, surface the conflict to the user with three explicit recovery options — **Reuse** (skip to 10a.2 if the existing tag points at the right commit), **Replace** (`git push origin --delete v{version}` then re-run 10a.1), or **Investigate** (`forge.releaseView({ tag: 'v{version}', repo: '{org}/{repo}' })` — or `gh release view v{version}` as a manual fallback — to see what shipped). Do **not** silently force-push the tag; an existing tag means a published artifact, and overwriting it without confirmation can corrupt the npm registry's view of the release history.
|
|
295
|
-
|
|
296
|
-
If the tag is absent on origin, tag the merge commit (HEAD on `{default-branch}` after the prior `git pull origin {default-branch}`) and push the tag. The tag push triggers `.github/workflows/publish.yml`.
|
|
297
|
-
|
|
298
|
-
```bash
|
|
299
|
-
# Detect: only run for release sessions.
|
|
300
|
-
if [[ ! "$branch" =~ ^release/ ]]; then
|
|
301
|
-
# Not a release session — skip 10a.1, 10a.2, 10a.3.
|
|
302
|
-
return
|
|
303
|
-
fi
|
|
304
|
-
|
|
305
|
-
# Extract the version from the branch name (release/v{X} → v{X}).
|
|
306
|
-
version_tag="${branch#release/}" # e.g. "v0.15.0-beta.0"
|
|
307
|
-
|
|
308
|
-
# For each project repo with a package.json containing a version field:
|
|
309
|
-
for repo in {project-repos-with-package-json}; do
|
|
310
|
-
cd repos/{repo}
|
|
311
|
-
|
|
312
|
-
# Verify package.json version matches the tag.
|
|
313
|
-
pkg_version=$(node -p "require('./package.json').version")
|
|
314
|
-
expected_version="${version_tag#v}"
|
|
315
|
-
if [ "$pkg_version" != "$expected_version" ]; then
|
|
316
|
-
echo "Skipping {repo}: package.json version ($pkg_version) does not match release tag ($expected_version)."
|
|
317
|
-
continue
|
|
318
|
-
fi
|
|
319
|
-
|
|
320
|
-
# Preflight: does the tag already exist on origin?
|
|
321
|
-
if git ls-remote --exit-code origin "refs/tags/$version_tag" >/dev/null 2>&1; then
|
|
322
|
-
# Tag exists. Surface to user with three options:
|
|
323
|
-
# 1. Reuse — skip to 10a.2 if the existing tag points at the right commit.
|
|
324
|
-
# 2. Replace — `git push origin --delete $version_tag` then re-run 10a.1.
|
|
325
|
-
# 3. Investigate — call forge.releaseView({ tag: '$version_tag' }) — or
|
|
326
|
-
# `gh release view $version_tag` as a manual fallback — to see what shipped.
|
|
327
|
-
# Do NOT silently force-push.
|
|
328
|
-
echo "Tag $version_tag already exists on origin. Aborting with recovery options."
|
|
329
|
-
return 1
|
|
330
|
-
fi
|
|
331
|
-
|
|
332
|
-
# Tag the merge commit (HEAD on default branch after the prior `git pull`).
|
|
333
|
-
git tag "$version_tag"
|
|
334
|
-
git push origin "$version_tag" # Triggers .github/workflows/publish.yml
|
|
335
|
-
done
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
**Step 10a.2: Watch the publish workflow (release sessions only)**
|
|
339
|
-
|
|
340
|
-
For each project repo tagged in 10a.1, find and follow the `publish.yml` workflow run on GitHub. The workflow takes a moment to register against the new tag — poll up to 5 times with a 3-second backoff before giving up. Once the run is found, attach with `workflowRunWatch` so the maintainer sees progress live alongside the unified summary. The adapter's `exitStatus: true` makes the underlying `gh run watch --exit-status` exit non-zero on workflow failure; the adapter returns the exit code via `res.exitCode` instead of throwing, so a failure does **not** abort the rest of `/complete-work` — the maintainer still needs to see the unified summary, including the failure URL, to decide whether to rerun, redo the release, or roll the tag back. If no run registers within the retry window, log a warning with the manual investigation command and continue.
|
|
341
|
-
|
|
342
|
-
```javascript
|
|
343
|
-
import { createForge } from './.claude/scripts/forges/interface.mjs';
|
|
344
|
-
import { readFileSync } from 'node:fs';
|
|
345
|
-
|
|
346
|
-
const ws = JSON.parse(readFileSync('workspace.json', 'utf-8'));
|
|
347
|
-
const forge = createForge(ws.workspace?.forge);
|
|
348
|
-
|
|
349
|
-
// Retry up to 5 times with 3-second backoff — the run takes a moment to register.
|
|
350
|
-
let run = null;
|
|
351
|
-
for (let i = 0; i < 5; i++) {
|
|
352
|
-
run = await forge.workflowRunFind({
|
|
353
|
-
workflow: 'publish.yml',
|
|
354
|
-
branch: versionTag, // e.g. 'v0.15.0-beta.0'
|
|
355
|
-
repo: `${org}/${repo}`,
|
|
356
|
-
limit: 1,
|
|
357
|
-
});
|
|
358
|
-
if (run) break;
|
|
359
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (!run) {
|
|
363
|
-
console.warn(`Warning: no publish workflow run found for ${versionTag} after 15s. Investigate via 'gh run list --workflow publish.yml --branch ${versionTag}'.`);
|
|
364
|
-
} else {
|
|
365
|
-
const result = await forge.workflowRunWatch({
|
|
366
|
-
runId: run.runId,
|
|
367
|
-
repo: `${org}/${repo}`,
|
|
368
|
-
exitStatus: true,
|
|
369
|
-
});
|
|
370
|
-
// result.exitCode === 0 on success; non-zero on workflow failure (NOT thrown).
|
|
371
|
-
// result.exitCode and run.url feed into the unified summary in Step 10a.3.
|
|
372
|
-
}
|
|
260
|
+
cd "repos/{repo}" && git pull origin "{repo-branch}"
|
|
261
|
+
cd "{main-workspace-root}" && git pull origin main
|
|
373
262
|
```
|
|
374
263
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
The unified summary block presented earlier in Step 10a already has a section per project repo. For release sessions, append a `PUBLISH` section per tagged project repo to the same summary — this goes inside the existing summary, not in a new location, so the maintainer sees one consolidated report covering merges, tags, and npm publishes:
|
|
378
|
-
|
|
379
|
-
```
|
|
380
|
-
PUBLISH ({repo}):
|
|
381
|
-
Tag: v{version}
|
|
382
|
-
Workflow: {run-url}
|
|
383
|
-
Status: success | failure
|
|
384
|
-
Published: {dist-tag}@{version} on npm
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
Pull `Status` from the watch result's `exitCode` (success when `result.exitCode === 0`, failure otherwise). Pull `Workflow` from `run.url` captured in 10a.2. Pull `Published: {dist-tag}@{version}` from the workflow's published-package output if available; if the workflow failed before publishing, omit the `Published:` line and rely on `Status: failure` plus the workflow URL to point the maintainer at the failure.
|
|
388
|
-
|
|
389
|
-
#### Step 10b: Local / bare / other remotes — local merge flow
|
|
264
|
+
#### Step 9b: Local / bare / other remotes — local merge flow
|
|
390
265
|
|
|
391
266
|
No PRs are created — these remotes don't have a PR concept (or we don't have a client wired up for them). Present an adjusted summary:
|
|
392
267
|
|
|
@@ -396,13 +271,12 @@ Work session complete:
|
|
|
396
271
|
PROJECT: {repo-1} (local remote)
|
|
397
272
|
Branch: {branch} → {repo-1-branch}
|
|
398
273
|
Changes:
|
|
399
|
-
- {bullet points from
|
|
400
|
-
Release notes: branch-release-notes-{COMMIT_ID}.md
|
|
274
|
+
- {bullet points from the PR-body material}
|
|
401
275
|
|
|
402
276
|
PROJECT: {repo-2} (local remote)
|
|
403
277
|
Branch: {branch} → {repo-2-branch}
|
|
404
278
|
Changes:
|
|
405
|
-
- {bullet points from
|
|
279
|
+
- {bullet points from the PR-body material}
|
|
406
280
|
|
|
407
281
|
WORKSPACE: {workspace-name} (local remote)
|
|
408
282
|
Branch: {branch} → main
|
|
@@ -413,26 +287,26 @@ Merge all locally? [Y/n]
|
|
|
413
287
|
If yes — fast-forward merge on each remote, delete the feature branch, pull the source clone:
|
|
414
288
|
```bash
|
|
415
289
|
# For each repo in the tracker's repos with a local/bare remote:
|
|
416
|
-
cd work-sessions/{session-name}/workspace/repos/{repo}
|
|
417
|
-
git push origin HEAD:{repo-branch} # fast-forward the default branch
|
|
418
|
-
git push origin --delete {branch} # remove the feature branch from the remote
|
|
419
|
-
cd repos/{repo} && git checkout {repo-branch} && git pull origin {repo-branch}
|
|
290
|
+
cd "work-sessions/{session-name}/workspace/repos/{repo}"
|
|
291
|
+
git push origin "HEAD:{repo-branch}" # fast-forward the default branch
|
|
292
|
+
git push origin --delete "{branch}" # remove the feature branch from the remote
|
|
293
|
+
cd "repos/{repo}" && git checkout "{repo-branch}" && git pull origin "{repo-branch}"
|
|
420
294
|
|
|
421
295
|
# Workspace repo — same pattern from the workspace worktree
|
|
422
|
-
cd work-sessions/{session-name}/workspace
|
|
296
|
+
cd "work-sessions/{session-name}/workspace"
|
|
423
297
|
git push origin HEAD:main
|
|
424
|
-
git push origin --delete {branch}
|
|
425
|
-
cd {main-workspace-root} && git pull origin main
|
|
298
|
+
git push origin --delete "{branch}"
|
|
299
|
+
cd "{main-workspace-root}" && git pull origin main
|
|
426
300
|
```
|
|
427
301
|
|
|
428
302
|
If the fast-forward push fails because the remote's default branch has moved ahead, STOP and present the divergence — the user decides whether to rebase and retry or handle it another way. Do not auto-resolve.
|
|
429
303
|
|
|
430
304
|
For repos with no remote at all (user chose "keep local"): skip push entirely. The branch lives only in the source clone after cleanup merges it:
|
|
431
305
|
```bash
|
|
432
|
-
cd repos/{repo} && git merge --ff-only {branch}
|
|
306
|
+
cd "repos/{repo}" && git merge --ff-only "{branch}"
|
|
433
307
|
```
|
|
434
308
|
|
|
435
|
-
### Step
|
|
309
|
+
### Step 10: Close the linked issue on the tracker
|
|
436
310
|
|
|
437
311
|
If the session tracker has a `workItem:` field AND `workspace.tracker` is configured, close the linked issue via the adapter after all PRs have merged:
|
|
438
312
|
|
|
@@ -448,7 +322,7 @@ if (ws.workspace?.tracker) {
|
|
|
448
322
|
'Merged PRs:',
|
|
449
323
|
...mergedPrs.map(p => `- ${p.repo}: ${p.url}`),
|
|
450
324
|
'',
|
|
451
|
-
releaseSummary, // 1-3 sentence synthesis of what shipped, drawn from
|
|
325
|
+
releaseSummary, // 1-3 sentence synthesis of what shipped, drawn from the session tracker
|
|
452
326
|
].join('\n');
|
|
453
327
|
await tracker.closeIssue(workItem, { comment });
|
|
454
328
|
}
|
|
@@ -456,9 +330,9 @@ if (ws.workspace?.tracker) {
|
|
|
456
330
|
|
|
457
331
|
If `workItem:` is unset, skip the close — this was a blank session.
|
|
458
332
|
|
|
459
|
-
If the close call fails (tracker unreachable, auth expired), report the error in the unified summary but do not block Step
|
|
333
|
+
If the close call fails (tracker unreachable, auth expired), report the error in the unified summary but do not block Step 11 cleanup. The issue can be closed manually via the GitHub UI; no data is at risk.
|
|
460
334
|
|
|
461
|
-
### Step
|
|
335
|
+
### Step 11: Cleanup
|
|
462
336
|
|
|
463
337
|
Run the cleanup helper script from the main workspace root:
|
|
464
338
|
```bash
|
|
@@ -470,7 +344,7 @@ The script tears down in the **mandatory** order:
|
|
|
470
344
|
2. Remove the workspace worktree from the workspace repo
|
|
471
345
|
3. `git worktree prune` on each project repo (belt-and-suspenders for orphan records)
|
|
472
346
|
4. Delete local branches in all repos
|
|
473
|
-
5. `rm -rf work-sessions/{session-name}/` — the tracker, specs, plans, and any local-only artifacts vanish.
|
|
347
|
+
5. `rm -rf work-sessions/{session-name}/` — the tracker, specs, plans, and any local-only artifacts vanish. Anything worth preserving was promoted into `workspace-context/` in Step 6.
|
|
474
348
|
|
|
475
349
|
Workspace-first removal silently deletes the nested project worktrees' `.git` files and leaves orphan worktree records in the project repos. The script enforces the safe order.
|
|
476
350
|
|
|
@@ -489,10 +363,146 @@ Ask: "These changes weren't part of a formal work session. What do you want to d
|
|
|
489
363
|
- **Hand off to someone** — create a team-visible handoff at root workspace-context/ for another member to pick up
|
|
490
364
|
- **Revert** — undo the changes (with confirmation)
|
|
491
365
|
|
|
366
|
+
## Task completion (session model v2)
|
|
367
|
+
|
|
368
|
+
Reached from Step 1 when detection says `model: task`. The state is the branch, the chat record's task entries, and the linked issue — there is no session folder and no `session.md`. This chat normally runs at the workspace root (the launcher) but may be running inside one of the worktrees; either way, every input comes from the detect result, never from cwd. Run `cd "{launcher-root}"` first — steps 2–5 and every relative path in them are anchored there:
|
|
369
|
+
|
|
370
|
+
- `{chat}` — the `Chat record:` line injected by the SessionStart hook (the same value Step 1 passed as `--chat`)
|
|
371
|
+
- `{tasks}` — the detect result's task entries from the chat record, each carrying `{workItem}`, `{branch}`, `{repo}`; `repo: "."` is the workspace repo itself. When detection came from cwd alone (`source: 'worktree'`, no chat record entry — e.g. a no-tracker task) there are no task entries: take `{repo}` and `{branch}` from the detect result itself and treat `{workItem}` as absent
|
|
372
|
+
- `{worktree}` — `{launcher-root}/repos/{repo}/.claude/worktrees/{slug}` for a project repo, or `{launcher-root}/.claude/worktrees/{slug}` for the workspace repo (`.`), where `{slug}` is the branch with `/` replaced by `-`
|
|
373
|
+
- `{workspace-worktree}` — `{launcher-root}/.claude/worktrees/{slug}`: the workspace repo's own task worktree, created in step 2 only when drawer items are promoted (Claude Code's native worktree location — the two converge on it)
|
|
374
|
+
- `{defaultBranch}` — `workspace.json` → `repos.{repo}.branch`, default `main`; for `.` it is the workspace origin's HEAD with `main` as the fallback — exactly what `task-worktree.mjs`'s `defaultBranchFor` resolves
|
|
375
|
+
|
|
376
|
+
If several tasks are open, ask the user which one to complete — group by branch; a multi-repo task is several entries sharing a branch — and complete one branch at a time.
|
|
377
|
+
|
|
378
|
+
0. **Pre-flight: the worktrees must be clean.** For each `{worktree}` of the chosen task, `git -C "{worktree}" status --porcelain` must be empty. If it is not, stop here — before the rebase — and ask the user to commit or discard: rebasing over uncommitted work silently invalidates it.
|
|
379
|
+
|
|
380
|
+
1. **Rebase each task worktree onto `origin/{defaultBranch}`** (`git -C "{worktree}" fetch origin`, then `git -C "{worktree}" rebase "origin/{defaultBranch}"`). Freshness first: the PR in step 3 must describe the branch as it will merge. If conflicts arise, STOP and present them — do not auto-resolve.
|
|
381
|
+
|
|
382
|
+
2. **Route durable thinking.** List anything in the chat drawer `{launcher-root}/workspace-scratchpad/chats/{chat}/` and ask which items should graduate into `workspace-context/`. If nothing is chosen, skip this step — no `.` worktree is needed yet. Do NOT invoke `/promote`: it writes relative to cwd and commits per item, which at the launcher lands on the default branch — exactly the hole this flow closes, and it does not know about drawer items. For the chosen items, first create the workspace worktree on the task's branch and record it alongside the project entries:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
node "{launcher-root}/.claude/scripts/task-worktree.mjs" --root "{launcher-root}" --create --repo "." --branch "{branch}"
|
|
386
|
+
node "{launcher-root}/.claude/scripts/chat-record.mjs" --root "{launcher-root}" --add-task --chat "{chat}" --work-item "{workItem}" --branch "{branch}" --repo "."
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Skip the record line when there is no `{workItem}`. Then copy each chosen item to `{workspace-worktree}/workspace-context/shared/{item}` or `{workspace-worktree}/workspace-context/team-member/{user}/{item}` — ask which level; `shared/locked/` only if explicitly requested — keeping the filename and making sure the file carries a `description:` frontmatter (add one if the drawer item has none). Never write under `{launcher-root}/workspace-context/` at the launcher. Rebuild the indexes and commit once, and only when something is staged:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
node "{launcher-root}/.claude/scripts/build-workspace-context.mjs" --write --root "{workspace-worktree}"
|
|
393
|
+
git -C "{workspace-worktree}" add workspace-context/
|
|
394
|
+
git -C "{workspace-worktree}" diff --cached --quiet || git -C "{workspace-worktree}" commit -m "context: promote durable thinking for {branch}"
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The drawer is per-chat, so it survives task teardown — but it is machine-local and backed up nowhere, and this review, with the work fresh in mind, is the moment to decide what graduates. Items left behind are not lost, only unreviewed.
|
|
398
|
+
|
|
399
|
+
3. **Check each origin, push, then open one PR per repo through the forge adapter** — never `gh pr` directly. The workspace repo (`.`) is handled exactly like a project repo: same origin check and push in `{workspace-worktree}`, same PR, with `wsForge` — the per-repo forge constructed from the workspace worktree's own origin — yielding `wsPr`. First, drop empty branches: for every repo of the task, `.` included when its worktree exists, run
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
git -C "{worktree}" rev-list --count "origin/{defaultBranch}..{branch}"
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
and if the count is `0`, skip push and PR for that repo entirely — it is torn down in step 5 like any other. A workspace branch that collected no promotions gets no PR. Then, in order: the origin decides whether this path can proceed at all (nothing is pushed to a repo this path cannot finish), and the forge is constructed per repo so it aims at the worktree's own remote, never the launcher's.
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
git -C "{worktree}" remote get-url origin # → parse {owner}/{name} FIRST
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
If the URL does not parse into `{owner}/{name}` (a local/bare remote) or is a forge the adapter does not support, STOP before pushing anything: the task path supports forge-hosted repos only in this stage — complete that repo under the session model.
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
git -C "{worktree}" push -u origin "{branch}"
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
If the push is rejected as non-fast-forward — the branch already existed on origin and step 1 rebased it — ask the user before retrying with `git -C "{worktree}" push --force-with-lease`. Never force without asking.
|
|
418
|
+
|
|
419
|
+
If `workspace.forge` is `false` in `workspace.json`, STOP here: forge operations are disabled in this workspace — the push above is done, the PR is opened by hand.
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
// Run from {launcher-root} (see above). Imports stay relative: an absolute
|
|
423
|
+
// path is not a valid ESM specifier on Windows.
|
|
424
|
+
import { createForge } from './.claude/scripts/forges/interface.mjs';
|
|
425
|
+
import { createTracker } from './.claude/scripts/trackers/interface.mjs';
|
|
426
|
+
import { readFileSync } from 'node:fs';
|
|
427
|
+
const ws = JSON.parse(readFileSync('{launcher-root}/workspace.json', 'utf-8'));
|
|
428
|
+
|
|
429
|
+
// Constructed per repo, so the adapter never resolves the launcher's own remote.
|
|
430
|
+
const forge = createForge({ ...ws.workspace?.forge, repo: '{owner}/{name}' });
|
|
431
|
+
// For the workspace repo (.): from the workspace worktree's own origin.
|
|
432
|
+
const wsForge = createForge({ ...ws.workspace?.forge, repo: '{ws-owner}/{ws-name}' });
|
|
433
|
+
|
|
434
|
+
// PR title: the linked issue's title — tracker.getIssue(workItem).title —
|
|
435
|
+
// or, with no workItem, the subject of the branch's first commit beyond
|
|
436
|
+
// the base: git log "origin/{defaultBranch}..HEAD" --reverse --format=%s
|
|
437
|
+
// (run in "{worktree}").
|
|
438
|
+
const title = workItem
|
|
439
|
+
? (await createTracker(ws.workspace.tracker).getIssue(workItem)).title
|
|
440
|
+
: firstCommitSubject;
|
|
441
|
+
|
|
442
|
+
// PR body: one line per commit from
|
|
443
|
+
// git -C "{worktree}" log "origin/{defaultBranch}..HEAD" --oneline,
|
|
444
|
+
// then a blank line and `Closes {workItem}` when a workItem exists.
|
|
445
|
+
const pr = await forge.prCreate({ title, body, head: '{branch}', base: '{defaultBranch}' });
|
|
446
|
+
// The workspace PR, when step 3 did not skip "." as empty:
|
|
447
|
+
const wsPr = await wsForge.prCreate({ title: `context: {branch} task`, body: workspacePrBody, head: '{branch}', base: '{defaultBranch}' });
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
4. **Ask before merging, then merge, then close the linked issue.** Present a summary per repo that got a PR — the workspace repo included — and ask once:
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
Task complete:
|
|
454
|
+
|
|
455
|
+
PROJECT: {owner}/{name}
|
|
456
|
+
PR: {pr.url}
|
|
457
|
+
Branch: {branch} → {defaultBranch}
|
|
458
|
+
Commits: {n} # git -C "{worktree}" rev-list --count "origin/{defaultBranch}..{branch}"
|
|
459
|
+
|
|
460
|
+
WORKSPACE: {ws-owner}/{ws-name} # from the workspace worktree's origin
|
|
461
|
+
PR: {ws-pr.url}
|
|
462
|
+
Branch: {branch} → {defaultBranch}
|
|
463
|
+
|
|
464
|
+
Merge all? [Y/n]
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
On "n", stop: the PRs stay open and the worktrees, branches, and record entries stay in place — say so.
|
|
468
|
+
|
|
469
|
+
On "y", merge the project PRs first, each through the same per-repo forge. **If any project merge fails, do NOT merge the workspace PR** — stop, report the failure, and leave every unmerged PR open for retry: the workspace branch's promoted context describes the project merges and must never merge ahead of them. Merge must also precede close, because an issue closed before its PR merges points at work that never landed.
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
for (const pr of projectPrs) {
|
|
473
|
+
await forge.prMerge({ id: pr.id, strategy: 'squash', deleteBranch: true });
|
|
474
|
+
}
|
|
475
|
+
if (wsPr) await wsForge.prMerge({ id: wsPr.id, strategy: 'squash', deleteBranch: true }); // workspace PR, last
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Pull the launcher only after the workspace PR actually merged — it is still on its default branch, waiting on that merge; when `.` had no PR, pull after the project merges instead:
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
git -C "{launcher-root}" pull --ff-only
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Then close the linked issue — only if a `{workItem}` exists — with a one-line comment naming the merged PR URL(s):
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
const tracker = createTracker(ws.workspace.tracker);
|
|
488
|
+
const comment = `Merged: ${prUrls.join(' ')}`; // prUrls = the .url of each PR merged above
|
|
489
|
+
await tracker.closeIssue(workItem, { comment });
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Without a `{workItem}` (no tracker, or the task was never recorded), skip the close and say so.
|
|
493
|
+
|
|
494
|
+
5. **Tear down only what finished — worktree first, then the record entry**, and only for a repo whose PR merged in step 4, or whose branch was empty and never pushed in step 3; the workspace repo included (`--repo "."`). If a repo's push, PR, or merge failed, or the user declined the merge, leave that repo's worktree, branch, and record entry exactly in place and say so: `--delete-branch` would otherwise `branch -D` commits that exist nowhere but the local worktree. For `.` the script never deletes the branch checked out at the launcher root.
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
node "{launcher-root}/.claude/scripts/task-worktree.mjs" --root "{launcher-root}" --remove --repo "{repo}" --branch "{branch}" --delete-branch
|
|
498
|
+
node "{launcher-root}/.claude/scripts/chat-record.mjs" --root "{launcher-root}" --remove-task --chat "{chat}" --work-item "{workItem}" --repo "{repo}"
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Skip the `--remove-task` line when there is no record entry (no `{workItem}`). Worktree first because a refusal (dirty worktree, slug collision) then leaves both the worktree and its record entry in place — nothing orphaned, safe to retry. `--delete-branch` also removes the local branch: the forge's `deleteBranch` removed only the remote one, so post-merge teardown passes it to clean the local clone; this is the only step that ever passes it. Never pass `--force` without asking the user.
|
|
502
|
+
|
|
492
503
|
## Notes
|
|
493
|
-
-
|
|
494
|
-
- The session tracker's body is the primary source for release note synthesis — it captures the full session history alongside specs and plans
|
|
504
|
+
- The session tracker's body is the primary source for PR-body synthesis — it captures the full session history alongside specs and plans
|
|
495
505
|
- All repos get PRed and merged together — one approval for all
|
|
496
|
-
- Version bumps happen in `/release`, not `/complete-work` — this avoids version drift when multiple feature branches land between releases
|
|
506
|
+
- Version bumps, tags, and publish happen in `/release`, not `/complete-work` — this avoids version drift when multiple feature branches land between releases
|
|
497
507
|
- The teardown order is mandatory: project worktrees first, then workspace worktree, then prune, then delete the session folder
|
|
498
|
-
- Context
|
|
508
|
+
- Context promotion and cleanup are intentional workflow behavior — they bypass normal commit conventions by design
|