@ulysses-ai/create-workspace 0.16.0-beta.1 → 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.
Files changed (53) hide show
  1. package/README.md +5 -5
  2. package/lib/init.mjs +19 -0
  3. package/package.json +1 -1
  4. package/template/.claude/hooks/_utils.mjs +1 -1
  5. package/template/.claude/hooks/repo-write-detection.mjs +161 -64
  6. package/template/.claude/hooks/session-end.mjs +68 -2
  7. package/template/.claude/hooks/session-start.mjs +35 -1
  8. package/template/.claude/hooks/subagent-start.mjs +89 -22
  9. package/template/.claude/lib/session-frontmatter.mjs +28 -0
  10. package/template/.claude/rules/coherent-revisions.md +1 -1
  11. package/template/.claude/rules/config-review.md.skip +29 -0
  12. package/template/.claude/rules/forge-operations.md +51 -0
  13. package/template/.claude/rules/git-conventions.md +16 -11
  14. package/template/.claude/rules/goal-driven-work.md +8 -403
  15. package/template/.claude/rules/honest-pushback.md +37 -37
  16. package/template/.claude/rules/memory-guidance.md +43 -90
  17. package/template/.claude/rules/superpowers-workflow.md.skip +1 -1
  18. package/template/.claude/rules/work-item-tracking.md +30 -72
  19. package/template/.claude/rules/workspace-structure.md +49 -69
  20. package/template/.claude/scripts/build-workspace-context.mjs +61 -16
  21. package/template/.claude/scripts/chat-record.mjs +282 -0
  22. package/template/.claude/scripts/cleanup-work-session.mjs +363 -36
  23. package/template/.claude/scripts/context-footprint.mjs +282 -0
  24. package/template/.claude/scripts/forges/github.mjs +255 -0
  25. package/template/.claude/scripts/forges/gitlab.mjs +20 -0
  26. package/template/.claude/scripts/forges/interface.mjs +125 -0
  27. package/template/.claude/scripts/generate-claude-local.mjs +21 -2
  28. package/template/.claude/scripts/migrate-sessions.mjs +1571 -0
  29. package/template/.claude/scripts/migrate-to-workspace-context.mjs +7 -2
  30. package/template/.claude/scripts/task-worktree.mjs +525 -0
  31. package/template/.claude/scripts/workspace-diagnostics.mjs +654 -0
  32. package/template/.claude/settings.json +5 -13
  33. package/template/.claude/skills/braindump/SKILL.md +11 -4
  34. package/template/.claude/skills/build-docs-site/SKILL.md +5 -5
  35. package/template/.claude/skills/build-docs-site/templates/spec.md.tmpl +1 -1
  36. package/template/.claude/skills/complete-work/SKILL.md +255 -215
  37. package/template/.claude/skills/context-placement/SKILL.md +199 -0
  38. package/template/.claude/skills/goal-driven-work/SKILL.md +459 -0
  39. package/template/.claude/skills/handoff/SKILL.md +11 -4
  40. package/template/.claude/skills/maintenance/SKILL.md +39 -6
  41. package/template/.claude/skills/migrate-sessions/SKILL.md +70 -0
  42. package/template/.claude/skills/pause-work/SKILL.md +33 -8
  43. package/template/.claude/skills/release/SKILL.md +44 -108
  44. package/template/.claude/skills/start-work/SKILL.md +89 -7
  45. package/template/.claude/skills/workspace-init/SKILL.md +34 -0
  46. package/template/.claude/skills/workspace-update/SKILL.md +4 -0
  47. package/template/.claudeignore +3 -0
  48. package/template/CLAUDE.md.tmpl +20 -2
  49. package/template/CODEBASE.md.tmpl +13 -0
  50. package/template/_gitignore +9 -0
  51. package/template/repo-claude.md.tmpl +10 -0
  52. package/template/workspace.json.tmpl +5 -3
  53. package/template/.claude/hooks/worktree-create.mjs +0 -53
@@ -1,18 +1,29 @@
1
1
  ---
2
2
  name: complete-work
3
- description: Finalize a work session — rebase, synthesize release notes from spec/plan/session tracker/commits, create PRs with unified presentation. Handles all project repos and workspace repo. Use when work on a session is done.
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, release notes, PRs) and the workspace repo (context processing, PR). Presents a unified summary with a single merge approval, then tears down the session folder.
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
- If no active session: "No active work session. Nothing to complete."
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 synthesis, flush current `TodoWrite` state to `## Tasks` per the `task-list-mirroring` rule. This ensures the synthesis in Step 6 sees the final state:
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 synthesizing — do not write release notes from memory alone:
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
- ### Step 6: Synthesize release notes
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:
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.
85
91
 
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
+ These sources feed the PR bodies in Step 9: a short summary of what changed and why, plus a Verification section.
92
93
 
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
- ---
94
+ ### Step 6: Remove session artifacts from the workspace branch
102
95
 
103
- ## {Human-readable title}
104
-
105
- {Coherent narrative synthesized from tracker + spec + plan + commits.
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
- ```
122
-
123
- The `repo:` frontmatter field is what `/release` uses to know which project repo's `CHANGELOG.md` should consume each note. The directory name is the same as the field for redundancy.
124
-
125
- After all repos are processed, commit once on the workspace branch:
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
- Session content lives at the top of the workspace worktree on the session branch. Once the pre-flight passes, remove these files from the branch before the final push so main's top level stays free of session artifacts:
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,70 +139,83 @@ 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 consumes accumulated unreleased branch notes into a single `CHANGELOG.md` entry per project repo. `/complete-work` only writes branch notes; it does not modify any project repo's `package.json`. This avoids version drift when multiple feature branches land between releases.
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 8: Detect remote type per repo
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 9 and Step 10 push and merge.
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 9a, Step 10a).
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 9b, Step 10b).
182
- - **Other remote** (e.g., GitLab, Bitbucket, self-hosted) — no `gh` support → fall back to the local merge flow (Step 9b, Step 10b), and mention it in the final summary.
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 9: Push all repos
160
+ ### Step 8: Push all repos
186
161
 
187
- #### Step 9a: GitHub remotes
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 9b: Local/bare remotes
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 9a — what differs is the merge mechanics in Step 10b.
190
+ The push shape is the same as 8a — what differs is the merge mechanics in Step 9b.
216
191
 
217
- ### Step 10: Merge and present unified summary
192
+ ### Step 9: Merge and present unified summary
218
193
 
219
- #### Step 10a: GitHub remotes — create PRs, unified summary, merge
194
+ #### Step 9a: GitHub remotes — create PRs, unified summary, merge
220
195
 
221
- Create one PR per project repo plus one workspace PR:
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
 
223
- ```bash
224
- # For each repo in the tracker's repos with a GitHub remote:
225
- cd work-sessions/{session-name}/workspace/repos/{repo}
226
- gh pr create --title "{type}: {description}" --body "..."
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
+
200
+ ```javascript
201
+ import { createForge } from './.claude/scripts/forges/interface.mjs';
202
+ import { readFileSync } from 'node:fs';
227
203
 
228
- # Workspace PR — from the workspace worktree
229
- cd work-sessions/{session-name}/workspace
230
- gh pr create --title "context: {session-name} work session" --body "..."
204
+ const ws = JSON.parse(readFileSync('workspace.json', 'utf-8'));
205
+ const forge = createForge(ws.workspace?.forge);
206
+
207
+ // For each repo in the tracker's repos with a GitHub remote, from
208
+ // work-sessions/{session-name}/workspace/repos/{repo}:
209
+ const projectPr = await forge.prCreate({
210
+ title: `${type}: ${description}`,
211
+ body: prBody, // short summary + Verification section
212
+ });
213
+
214
+ // Workspace PR — from the workspace worktree:
215
+ const workspacePr = await forge.prCreate({
216
+ title: `context: ${sessionName} work session`,
217
+ body: workspacePrBody,
218
+ });
231
219
  ```
232
220
 
233
221
  Present unified summary:
@@ -238,14 +226,13 @@ PROJECT: {repo-1}
238
226
  PR #{n}: {type}: {description}
239
227
  Branch: {branch} → {repo-1-branch}
240
228
  Changes:
241
- - {bullet points from release notes}
242
- Release notes: branch-release-notes-{COMMIT_ID}.md
229
+ - {bullet points from the PR body}
243
230
 
244
231
  PROJECT: {repo-2}
245
232
  PR #{m}: {type}: {description}
246
233
  Branch: {branch} → {repo-2-branch}
247
234
  Changes:
248
- - {bullet points from release notes}
235
+ - {bullet points from the PR body}
249
236
 
250
237
  WORKSPACE: {workspace-name}
251
238
  PR #{p}: context: {session-name} work session
@@ -254,109 +241,27 @@ WORKSPACE: {workspace-name}
254
241
  Merge all? [Y/n]
255
242
  ```
256
243
 
257
- If yes — merge all PRs atomically:
258
- ```bash
259
- # For each project PR:
260
- gh pr merge {pr-number} --merge
261
-
262
- # Workspace PR:
263
- gh pr merge {workspace-pr-number} --merge
264
-
265
- # Pull all repos to their default branches
266
- # For each repo in the tracker's repos:
267
- cd repos/{repo} && git pull origin {repo-branch}
268
- cd {main-workspace-root} && git pull origin main
269
- ```
270
-
271
- **Step 10a.1: Tag the merge commit (release sessions only, project repos with `package.json`)**
244
+ If yes — merge all PRs atomically through the forge adapter:
272
245
 
273
- 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.
274
-
275
- 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.
276
-
277
- 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** (`gh release view v{version}` 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.
278
-
279
- 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`.
246
+ ```javascript
247
+ // For each project PR returned from Step 9a's prCreate calls:
248
+ await forge.prMerge({ id: projectPr.id, strategy: 'squash', deleteBranch: true });
280
249
 
281
- ```bash
282
- # Detect: only run for release sessions.
283
- if [[ ! "$branch" =~ ^release/ ]]; then
284
- # Not a release session — skip 10a.1, 10a.2, 10a.3.
285
- return
286
- fi
287
-
288
- # Extract the version from the branch name (release/v{X} → v{X}).
289
- version_tag="${branch#release/}" # e.g. "v0.15.0-beta.0"
290
-
291
- # For each project repo with a package.json containing a version field:
292
- for repo in {project-repos-with-package-json}; do
293
- cd repos/{repo}
294
-
295
- # Verify package.json version matches the tag.
296
- pkg_version=$(node -p "require('./package.json').version")
297
- expected_version="${version_tag#v}"
298
- if [ "$pkg_version" != "$expected_version" ]; then
299
- echo "Skipping {repo}: package.json version ($pkg_version) does not match release tag ($expected_version)."
300
- continue
301
- fi
302
-
303
- # Preflight: does the tag already exist on origin?
304
- if git ls-remote --exit-code origin "refs/tags/$version_tag" >/dev/null 2>&1; then
305
- # Tag exists. Surface to user with three options:
306
- # 1. Reuse — skip to 10a.2 if the existing tag points at the right commit.
307
- # 2. Replace — `git push origin --delete $version_tag` then re-run 10a.1.
308
- # 3. Investigate — `gh release view $version_tag` to see what shipped.
309
- # Do NOT silently force-push.
310
- echo "Tag $version_tag already exists on origin. Aborting with recovery options."
311
- return 1
312
- fi
313
-
314
- # Tag the merge commit (HEAD on default branch after the prior `git pull`).
315
- git tag "$version_tag"
316
- git push origin "$version_tag" # Triggers .github/workflows/publish.yml
317
- done
250
+ // Workspace PR:
251
+ await forge.prMerge({ id: workspacePr.id, strategy: 'squash', deleteBranch: true });
318
252
  ```
319
253
 
320
- **Step 10a.2: Watch the publish workflow (release sessions only)**
254
+ `strategy: 'squash'` matches the workspace convention from `post-release-discipline` (`create-ulysses-workspace` requires linear history, so squash is the only strategy that merges cleanly; squash also lifts the PR body into the commit message). `deleteBranch: true` cleans the remote feature branch on success.
321
255
 
322
- 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 `gh run list` up to 5 times with a 3-second backoff before giving up. Once the run is found, attach with `gh run watch` so the maintainer sees progress live alongside the unified summary. Append `|| true` to the watch command so a workflow 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.
256
+ Then pull all repos to their default branches (still plain git):
323
257
 
324
258
  ```bash
325
- # Retry up to 5 times with 3-second backoff — the run takes a moment to register.
326
- for i in 1 2 3 4 5; do
327
- run_id=$(gh run list \
328
- --repo {org}/{repo} \
329
- --workflow publish.yml \
330
- --branch "$version_tag" \
331
- --limit 1 \
332
- --json databaseId \
333
- --jq '.[0].databaseId')
334
- if [ -n "$run_id" ]; then break; fi
335
- sleep 3
336
- done
337
-
338
- if [ -z "$run_id" ]; then
339
- echo "Warning: no publish workflow run found for $version_tag after 15s. Investigate via 'gh run list'."
340
- else
341
- gh run watch "$run_id" --exit-status --repo {org}/{repo} || true
342
- fi
343
- ```
344
-
345
- **Step 10a.3: Update the unified summary (release sessions only)**
346
-
347
- 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:
348
-
349
- ```
350
- PUBLISH ({repo}):
351
- Tag: v{version}
352
- Workflow: {run-url}
353
- Status: success | failure
354
- Published: {dist-tag}@{version} on npm
259
+ # For each repo in the tracker's repos:
260
+ cd "repos/{repo}" && git pull origin "{repo-branch}"
261
+ cd "{main-workspace-root}" && git pull origin main
355
262
  ```
356
263
 
357
- Pull `Status` from the `gh run watch` exit code (success when the watch returned 0, failure otherwise). 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.
358
-
359
- #### Step 10b: Local / bare / other remotes — local merge flow
264
+ #### Step 9b: Local / bare / other remotes — local merge flow
360
265
 
361
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:
362
267
 
@@ -366,13 +271,12 @@ Work session complete:
366
271
  PROJECT: {repo-1} (local remote)
367
272
  Branch: {branch} → {repo-1-branch}
368
273
  Changes:
369
- - {bullet points from release notes}
370
- Release notes: branch-release-notes-{COMMIT_ID}.md
274
+ - {bullet points from the PR-body material}
371
275
 
372
276
  PROJECT: {repo-2} (local remote)
373
277
  Branch: {branch} → {repo-2-branch}
374
278
  Changes:
375
- - {bullet points from release notes}
279
+ - {bullet points from the PR-body material}
376
280
 
377
281
  WORKSPACE: {workspace-name} (local remote)
378
282
  Branch: {branch} → main
@@ -383,26 +287,26 @@ Merge all locally? [Y/n]
383
287
  If yes — fast-forward merge on each remote, delete the feature branch, pull the source clone:
384
288
  ```bash
385
289
  # For each repo in the tracker's repos with a local/bare remote:
386
- cd work-sessions/{session-name}/workspace/repos/{repo}
387
- git push origin HEAD:{repo-branch} # fast-forward the default branch
388
- git push origin --delete {branch} # remove the feature branch from the remote
389
- 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}"
390
294
 
391
295
  # Workspace repo — same pattern from the workspace worktree
392
- cd work-sessions/{session-name}/workspace
296
+ cd "work-sessions/{session-name}/workspace"
393
297
  git push origin HEAD:main
394
- git push origin --delete {branch}
395
- cd {main-workspace-root} && git pull origin main
298
+ git push origin --delete "{branch}"
299
+ cd "{main-workspace-root}" && git pull origin main
396
300
  ```
397
301
 
398
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.
399
303
 
400
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:
401
305
  ```bash
402
- cd repos/{repo} && git merge --ff-only {branch}
306
+ cd "repos/{repo}" && git merge --ff-only "{branch}"
403
307
  ```
404
308
 
405
- ### Step 11: Close the linked issue on the tracker
309
+ ### Step 10: Close the linked issue on the tracker
406
310
 
407
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:
408
312
 
@@ -418,7 +322,7 @@ if (ws.workspace?.tracker) {
418
322
  'Merged PRs:',
419
323
  ...mergedPrs.map(p => `- ${p.repo}: ${p.url}`),
420
324
  '',
421
- releaseSummary, // 1-3 sentence synthesis of what shipped, drawn from release notes
325
+ releaseSummary, // 1-3 sentence synthesis of what shipped, drawn from the session tracker
422
326
  ].join('\n');
423
327
  await tracker.closeIssue(workItem, { comment });
424
328
  }
@@ -426,9 +330,9 @@ if (ws.workspace?.tracker) {
426
330
 
427
331
  If `workItem:` is unset, skip the close — this was a blank session.
428
332
 
429
- If the close call fails (tracker unreachable, auth expired), report the error in the unified summary but do not block Step 12 cleanup. The issue can be closed manually via the GitHub UI; no data is at risk.
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.
430
334
 
431
- ### Step 12: Cleanup
335
+ ### Step 11: Cleanup
432
336
 
433
337
  Run the cleanup helper script from the main workspace root:
434
338
  ```bash
@@ -440,7 +344,7 @@ The script tears down in the **mandatory** order:
440
344
  2. Remove the workspace worktree from the workspace repo
441
345
  3. `git worktree prune` on each project repo (belt-and-suspenders for orphan records)
442
346
  4. Delete local branches in all repos
443
- 5. `rm -rf work-sessions/{session-name}/` — the tracker, specs, plans, and any local-only artifacts vanish. Their content was already archived into release notes in Step 6.
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.
444
348
 
445
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.
446
350
 
@@ -459,10 +363,146 @@ Ask: "These changes weren't part of a formal work session. What do you want to d
459
363
  - **Hand off to someone** — create a team-visible handoff at root workspace-context/ for another member to pick up
460
364
  - **Revert** — undo the changes (with confirmation)
461
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
+
462
503
  ## Notes
463
- - Branch release notes live in the WORKSPACE repo at `{releaseNotesDir}/unreleased/{repo-name}/` (resolved from `workspace.json` → `workspace.releaseNotesDir`, default `workspace-context/release-notes`) — never in project repos. Project repos only ever see code commits and (at release time) `CHANGELOG.md` entries written by `/release`.
464
- - 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
465
505
  - All repos get PRed and merged together — one approval for all
466
- - 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
467
507
  - The teardown order is mandatory: project worktrees first, then workspace worktree, then prune, then delete the session folder
468
- - Context consumption, cleanup, and auto-committing release notes are intentional workflow behavior — these bypass normal commit conventions by design
508
+ - Context promotion and cleanup are intentional workflow behavior — they bypass normal commit conventions by design