@ulysses-ai/create-workspace 0.17.0-beta.0 → 0.19.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 (117) hide show
  1. package/README.md +3 -3
  2. package/lib/init.mjs +4 -1
  3. package/lib/payload.mjs +18 -1
  4. package/lib/payload.test.mjs +55 -0
  5. package/lib/scaffold.mjs +23 -6
  6. package/lib/scaffold.test.mjs +59 -0
  7. package/package.json +1 -1
  8. package/template/CLAUDE.md.tmpl +19 -2
  9. package/template/{.claude → _claude}/hooks/_utils.mjs +1 -1
  10. package/template/_claude/hooks/repo-write-detection.mjs +204 -0
  11. package/template/{.claude → _claude}/hooks/session-start.mjs +35 -1
  12. package/template/_claude/hooks/subagent-start.mjs +111 -0
  13. package/template/{.claude → _claude}/lib/session-frontmatter.mjs +28 -0
  14. package/template/{.claude → _claude}/rules/coherent-revisions.md +1 -1
  15. package/template/_claude/rules/forge-operations.md +57 -0
  16. package/template/_claude/rules/git-conventions.md +39 -0
  17. package/template/_claude/rules/goal-driven-work.md +24 -0
  18. package/template/_claude/rules/honest-pushback.md +56 -0
  19. package/template/_claude/rules/memory-guidance.md +66 -0
  20. package/template/{.claude → _claude}/rules/superpowers-workflow.md.skip +1 -1
  21. package/template/{.claude → _claude}/rules/task-list-mirroring.md +6 -0
  22. package/template/_claude/rules/work-item-tracking.md +48 -0
  23. package/template/_claude/rules/workspace-structure.md +79 -0
  24. package/template/{.claude → _claude}/scripts/build-workspace-context.mjs +86 -30
  25. package/template/_claude/scripts/chat-record.mjs +315 -0
  26. package/template/_claude/scripts/cleanup-work-session.mjs +436 -0
  27. package/template/_claude/scripts/context-footprint.mjs +391 -0
  28. package/template/{.claude → _claude}/scripts/forges/github.mjs +46 -0
  29. package/template/{.claude → _claude}/scripts/forges/gitlab.mjs +3 -2
  30. package/template/{.claude → _claude}/scripts/forges/interface.mjs +13 -0
  31. package/template/{.claude → _claude}/scripts/generate-claude-local.mjs +21 -2
  32. package/template/_claude/scripts/migrate-sessions.mjs +1571 -0
  33. package/template/{.claude → _claude}/scripts/migrate-to-workspace-context.mjs +7 -2
  34. package/template/_claude/scripts/task-pr.mjs +447 -0
  35. package/template/_claude/scripts/task-worktree.mjs +525 -0
  36. package/template/{.claude → _claude}/scripts/trackers/github-issues.mjs +11 -0
  37. package/template/{.claude → _claude}/scripts/trackers/interface.mjs +8 -0
  38. package/template/_claude/scripts/workspace-diagnostics.mjs +654 -0
  39. package/template/{.claude → _claude}/skills/braindump/SKILL.md +12 -4
  40. package/template/{.claude → _claude}/skills/build-docs-site/SKILL.md +5 -5
  41. package/template/{.claude → _claude}/skills/build-docs-site/templates/spec.md.tmpl +1 -1
  42. package/template/_claude/skills/complete-work/SKILL.md +452 -0
  43. package/template/_claude/skills/context-placement/SKILL.md +202 -0
  44. package/template/{.claude/rules/goal-driven-work.md → _claude/skills/goal-driven-work/SKILL.md} +46 -19
  45. package/template/{.claude → _claude}/skills/handoff/SKILL.md +12 -4
  46. package/template/{.claude → _claude}/skills/maintenance/SKILL.md +56 -17
  47. package/template/_claude/skills/migrate-sessions/SKILL.md +70 -0
  48. package/template/{.claude → _claude}/skills/pause-work/SKILL.md +9 -1
  49. package/template/_claude/skills/release/SKILL.md +91 -0
  50. package/template/{.claude → _claude}/skills/start-work/SKILL.md +89 -7
  51. package/template/{.claude → _claude}/skills/workspace-init/SKILL.md +3 -1
  52. package/template/{.claude → _claude}/skills/workspace-update/SKILL.md +4 -0
  53. package/template/_gitignore +9 -0
  54. package/template/workspace.json.tmpl +4 -3
  55. package/template/.claude/hooks/repo-write-detection.mjs +0 -107
  56. package/template/.claude/hooks/subagent-start.mjs +0 -44
  57. package/template/.claude/rules/forge-operations.md +0 -107
  58. package/template/.claude/rules/git-conventions.md +0 -34
  59. package/template/.claude/rules/honest-pushback.md +0 -56
  60. package/template/.claude/rules/memory-guidance.md +0 -109
  61. package/template/.claude/rules/work-item-tracking.md +0 -90
  62. package/template/.claude/rules/workspace-structure.md +0 -137
  63. package/template/.claude/scripts/cleanup-work-session.mjs +0 -247
  64. package/template/.claude/skills/complete-work/SKILL.md +0 -498
  65. package/template/.claude/skills/release/SKILL.md +0 -151
  66. /package/template/{.claude → _claude}/agents/aside-researcher.md +0 -0
  67. /package/template/{.claude → _claude}/agents/implementer.md +0 -0
  68. /package/template/{.claude → _claude}/agents/researcher.md +0 -0
  69. /package/template/{.claude → _claude}/agents/reviewer.md +0 -0
  70. /package/template/{.claude → _claude}/hooks/bash-output-advisory.mjs +0 -0
  71. /package/template/{.claude → _claude}/hooks/post-compact.mjs +0 -0
  72. /package/template/{.claude → _claude}/hooks/pre-compact.mjs +0 -0
  73. /package/template/{.claude → _claude}/hooks/session-end.mjs +0 -0
  74. /package/template/{.claude → _claude}/hooks/version-freshness-check.mjs +0 -0
  75. /package/template/{.claude → _claude}/hooks/workspace-update-check.mjs +0 -0
  76. /package/template/{.claude → _claude}/lib/freshness.mjs +0 -0
  77. /package/template/{.claude → _claude}/lib/registry-check.mjs +0 -0
  78. /package/template/{.claude → _claude}/lib/require-node.mjs +0 -0
  79. /package/template/{.claude → _claude}/recipes/migrate-from-notion.md +0 -0
  80. /package/template/{.claude → _claude}/rules/agent-rules.md.skip +0 -0
  81. /package/template/{.claude → _claude}/rules/cloud-infrastructure.md.skip +0 -0
  82. /package/template/{.claude → _claude}/rules/config-review.md.skip +0 -0
  83. /package/template/{.claude → _claude}/rules/documentation.md.skip +0 -0
  84. /package/template/{.claude → _claude}/rules/local-dev-environment.md.skip +0 -0
  85. /package/template/{.claude → _claude}/rules/product-integrity.md.skip +0 -0
  86. /package/template/{.claude → _claude}/rules/scope-guard.md.skip +0 -0
  87. /package/template/{.claude → _claude}/rules/token-economics.md.skip +0 -0
  88. /package/template/{.claude → _claude}/scripts/add-repo-to-session.mjs +0 -0
  89. /package/template/{.claude → _claude}/scripts/capture-context.mjs +0 -0
  90. /package/template/{.claude → _claude}/scripts/create-work-session.mjs +0 -0
  91. /package/template/{.claude → _claude}/scripts/migrate-canonical-priority.mjs +0 -0
  92. /package/template/{.claude → _claude}/scripts/migrate-claude-md-freshness-include.mjs +0 -0
  93. /package/template/{.claude → _claude}/scripts/migrate-open-work.mjs +0 -0
  94. /package/template/{.claude → _claude}/scripts/migrate-session-layout.mjs +0 -0
  95. /package/template/{.claude → _claude}/scripts/sweep-references.mjs +0 -0
  96. /package/template/{.claude → _claude}/scripts/sync-tasks.mjs +0 -0
  97. /package/template/{.claude → _claude}/settings.json +0 -0
  98. /package/template/{.claude → _claude}/skills/aside/SKILL.md +0 -0
  99. /package/template/{.claude → _claude}/skills/build-docs-site/checklists/framing.md +0 -0
  100. /package/template/{.claude → _claude}/skills/build-docs-site/checklists/pitfalls.md +0 -0
  101. /package/template/{.claude → _claude}/skills/build-docs-site/checklists/review.md +0 -0
  102. /package/template/{.claude → _claude}/skills/build-docs-site/scripts/bulk-fill-migration.py +0 -0
  103. /package/template/{.claude → _claude}/skills/build-docs-site/scripts/forbidden-word-grep.mjs +0 -0
  104. /package/template/{.claude → _claude}/skills/build-docs-site/scripts/leak-grep.mjs +0 -0
  105. /package/template/{.claude → _claude}/skills/build-docs-site/templates/custom.css.tmpl +0 -0
  106. /package/template/{.claude → _claude}/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +0 -0
  107. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Arrow.tsx +0 -0
  108. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Box.tsx +0 -0
  109. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +0 -0
  110. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Region.tsx +0 -0
  111. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/SectionTitle.tsx +0 -0
  112. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/tokens.ts +0 -0
  113. /package/template/{.claude → _claude}/skills/build-docs-site/templates/sidebars.ts.tmpl +0 -0
  114. /package/template/{.claude → _claude}/skills/promote/SKILL.md +0 -0
  115. /package/template/{.claude → _claude}/skills/setup-tracker/SKILL.md +0 -0
  116. /package/template/{.claude → _claude}/skills/sync-work/SKILL.md +0 -0
  117. /package/template/{.mcp.json → _mcp.json} +0 -0
@@ -96,11 +96,11 @@ For the codebase:
96
96
  - Read actual implementations, not just existing docs about them
97
97
 
98
98
  For shared context:
99
- - Walk `workspace-context/` for handoffs, braindumps, locked team knowledge, release notes
99
+ - Walk `workspace-context/` for handoffs, braindumps, and locked team knowledge
100
100
 
101
101
  For work-session history:
102
- - Walk `work-sessions/*/workspace/session.md` for any currently-active session trackers — their bodies may contain decisions not yet consumed into release notes
103
- - Check git history for previously-completed session trackers that were synthesized into release notes by `/complete-work`
102
+ - Walk `work-sessions/*/workspace/session.md` for any currently-active session trackers — their bodies may contain decisions not yet captured anywhere durable
103
+ - Walk the chat drawers `workspace-scratchpad/chats/*/` for in-progress task-model material — designs, plans, braindumps, research not yet promoted into `workspace-context/` — same rationale: it is thinking the site may need that no merged artifact carries
104
104
 
105
105
  For existing project documentation (from Phase 1 Q2):
106
106
  - Read every file the user pointed at
@@ -312,11 +312,11 @@ Produce a final report:
312
312
  - **Files needing user decision.** Any old docs with content that didn't migrate cleanly — coverage check found gaps the user needs to resolve.
313
313
  - **URLs that may need redirects.** If the old docs had live URLs, list them. The skill flags but does not set up redirects.
314
314
 
315
- Update the session tracker with the final state. The skill's work ends here. `/complete-work` handles the merge, release notes, and cleanup.
315
+ Update the session tracker with the final state. The skill's work ends here. `/complete-work` handles the merge, the PR, and cleanup.
316
316
 
317
317
  ## Notes
318
318
 
319
- - Specs and plans live at the project worktree root, not inflight (per workspace-structure rule). They are consumed by `/complete-work` into release notes.
319
+ - Specs and plans live at the project worktree root, not inflight (per workspace-structure rule). They are consumed by `/complete-work` when it builds the PR body, or promoted into `workspace-context/`.
320
320
  - Diagrams primitives are deliberately fixed (no project customization) because the class-based fill pattern is load-bearing for theme support.
321
321
  - The leak grep is project-accurate because it derives the list from the project's own dependency manifests. Do not maintain a hardcoded master list — it would over-trigger for projects whose docs legitimately discuss their own dependencies.
322
322
  - The forbidden-word grep is per-project — each project supplies its own list from Phase 1 Q5. There is no default.
@@ -116,4 +116,4 @@ Terms that came from brainstorming sessions and need to be translated to descrip
116
116
 
117
117
  ## Notes
118
118
 
119
- This spec is consumed by `/complete-work` into release notes when the session finishes. Update it as decisions change during writing.
119
+ This spec feeds the PR body when `/complete-work` finishes the session (or is promoted into `workspace-context/`). Update it as decisions change during writing.
@@ -0,0 +1,452 @@
1
+ ---
2
+ name: complete-work
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
+ ---
5
+
6
+ # Complete Work
7
+
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
+
10
+ ## Flow
11
+
12
+ ### Step 1: Detect context
13
+
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, run `node .claude/scripts/chat-record.mjs --whoami --root "{launcher-root}"` first — compaction can drop the hook line, and this recovers the name by matching the chat's session id against the records. When that too prints nothing (exit 1), 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."
27
+
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).
29
+
30
+ Determine paths:
31
+ - Session folder: `work-sessions/{session-name}/`
32
+ - Workspace worktree: `work-sessions/{session-name}/workspace/`
33
+ - Project worktrees: `work-sessions/{session-name}/workspace/repos/{repo}/` for each repo in the tracker's `repos:` list
34
+ - Read each repo's default branch from workspace.json (`repos.{repo}.branch`)
35
+
36
+ ### Step 2: Rebase project repos
37
+
38
+ For each repo in the tracker's `repos:`:
39
+ ```bash
40
+ # {repo-branch} = repos.{repo}.branch from workspace.json
41
+ cd "work-sessions/{session-name}/workspace/repos/{repo}"
42
+ git fetch origin
43
+ git rebase "origin/{repo-branch}"
44
+ ```
45
+ If conflicts arise in any repo, STOP and present them to the user. Do not auto-resolve.
46
+
47
+ ### Step 3: Capture final discussion state
48
+
49
+ Run `/braindump` to capture any final discussion/reasoning to the session tracker body.
50
+ If the user declines or there's nothing to capture, skip.
51
+
52
+ ### Step 4: Flush task list to session.md
53
+
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:
55
+
56
+ ```bash
57
+ cd "work-sessions/{session-name}/workspace"
58
+ echo '<JSON-of-current-todos>' | node .claude/scripts/sync-tasks.mjs --write session.md
59
+ ```
60
+
61
+ Mark `Complete work` as `in_progress` in the JSON before flushing — the rest of this skill IS the act of completing.
62
+
63
+ ### Step 5: Gather source material
64
+
65
+ Formally read ALL sources before writing the PR body — do not summarize from memory alone:
66
+
67
+ 1. **Session tracker** at `work-sessions/{session-name}/workspace/session.md` — read the full body (frontmatter is machine state, body is human content)
68
+
69
+ 2. **Session-scoped specs/plans/goal artifacts** at the top of the session worktree:
70
+ - `work-sessions/{session-name}/workspace/design-*.md` files
71
+ - `work-sessions/{session-name}/workspace/plan-*.md` files
72
+ - `work-sessions/{session-name}/workspace/goal-*.md` files
73
+ - `work-sessions/{session-name}/workspace/research-*.md` files
74
+ - `work-sessions/{session-name}/workspace/crossref-*.md` files
75
+ - Read each one fully
76
+
77
+ 3. **Handoffs** — any workspace-context entries referencing this branch:
78
+ ```bash
79
+ grep -rl "branch: {branch}" workspace-context/
80
+ ```
81
+ Read each matching file.
82
+
83
+ 4. **Branch commit logs** (per repo):
84
+ ```bash
85
+ # For each repo in the tracker's repos list:
86
+ cd "work-sessions/{session-name}/workspace/repos/{repo}"
87
+ git log "origin/{repo-branch}..HEAD" --oneline
88
+ ```
89
+
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.
91
+
92
+ These sources feed the PR bodies in Step 9: a short summary of what changed and why, plus a Verification section.
93
+
94
+ ### Step 6: Remove session artifacts from the workspace branch
95
+
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.
97
+
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:
99
+
100
+ ```bash
101
+ cd "work-sessions/{session-name}/workspace/repos/{repo}" # repeat for the workspace worktree too
102
+ session_branch=$(git rev-parse --abbrev-ref HEAD)
103
+ for sub in $(git branch --format='%(refname:short)' --list "${session_branch}-*"); do
104
+ git merge-base --is-ancestor "$sub" "$session_branch" || echo "UNMERGED: $sub"
105
+ done
106
+ ```
107
+
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.
109
+
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:
126
+
127
+ ```bash
128
+ cd "work-sessions/{session-name}/workspace"
129
+ git rm -f session.md 2>/dev/null || true
130
+ git rm -f design-*.md 2>/dev/null || true
131
+ git rm -f plan-*.md 2>/dev/null || true
132
+ git rm -f goal-*.md 2>/dev/null || true
133
+ git rm -f research-*.md 2>/dev/null || true
134
+ git rm -f crossref-*.md 2>/dev/null || true
135
+ git commit -m "chore: remove session artifacts before PR" 2>/dev/null || true
136
+ ```
137
+
138
+ The `|| true` guards keep this idempotent — if a file is already gone (e.g., a session without specs or goals), the step is a no-op. The commit is skipped when there's nothing staged.
139
+
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.
141
+
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`.
143
+
144
+ ### Step 7: Detect remote type per repo
145
+
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.
147
+
148
+ ```bash
149
+ cd "work-sessions/{session-name}/workspace/repos/{repo}"
150
+ git remote get-url origin 2>&1
151
+ ```
152
+
153
+ Classify the result:
154
+
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.
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.
159
+
160
+ ### Step 8: Push all repos
161
+
162
+ #### Step 8a: GitHub remotes
163
+
164
+ ```bash
165
+ # Each project repo with a GitHub remote
166
+ cd "work-sessions/{session-name}/workspace/repos/{repo}"
167
+ git push -u origin "{branch}"
168
+
169
+ # Workspace repo — from the workspace worktree
170
+ cd "work-sessions/{session-name}/workspace"
171
+ git add .
172
+ git commit -m "chore: finalize context for {session-name}"
173
+ git push -u origin "{branch}"
174
+ ```
175
+
176
+ #### Step 8b: Local/bare remotes
177
+
178
+ ```bash
179
+ # Push the feature branch to the bare remote so it exists there
180
+ cd "work-sessions/{session-name}/workspace/repos/{repo}"
181
+ git push -u origin "{branch}"
182
+
183
+ # Workspace repo — same commit + push pattern
184
+ cd "work-sessions/{session-name}/workspace"
185
+ git add .
186
+ git commit -m "chore: finalize context for {session-name}"
187
+ git push -u origin "{branch}"
188
+ ```
189
+
190
+ The push shape is the same as 8a — what differs is the merge mechanics in Step 9b.
191
+
192
+ ### Step 9: Merge and present unified summary
193
+
194
+ #### Step 9a: GitHub remotes — create PRs, unified summary, merge
195
+
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.
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
+
200
+ ```javascript
201
+ import { createForge } from './.claude/scripts/forges/interface.mjs';
202
+ import { readFileSync } from 'node:fs';
203
+
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
+ });
219
+ ```
220
+
221
+ Present unified summary:
222
+ ```
223
+ Work session complete:
224
+
225
+ PROJECT: {repo-1}
226
+ PR #{n}: {type}: {description}
227
+ Branch: {branch} → {repo-1-branch}
228
+ Changes:
229
+ - {bullet points from the PR body}
230
+
231
+ PROJECT: {repo-2}
232
+ PR #{m}: {type}: {description}
233
+ Branch: {branch} → {repo-2-branch}
234
+ Changes:
235
+ - {bullet points from the PR body}
236
+
237
+ WORKSPACE: {workspace-name}
238
+ PR #{p}: context: {session-name} work session
239
+ Branch: {branch} → main
240
+
241
+ Merge all? [Y/n]
242
+ ```
243
+
244
+ If yes — merge all PRs atomically through the forge adapter:
245
+
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 });
249
+
250
+ // Workspace PR:
251
+ await forge.prMerge({ id: workspacePr.id, strategy: 'squash', deleteBranch: true });
252
+ ```
253
+
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.
255
+
256
+ Then pull all repos to their default branches (still plain git):
257
+
258
+ ```bash
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
262
+ ```
263
+
264
+ #### Step 9b: Local / bare / other remotes — local merge flow
265
+
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:
267
+
268
+ ```
269
+ Work session complete:
270
+
271
+ PROJECT: {repo-1} (local remote)
272
+ Branch: {branch} → {repo-1-branch}
273
+ Changes:
274
+ - {bullet points from the PR-body material}
275
+
276
+ PROJECT: {repo-2} (local remote)
277
+ Branch: {branch} → {repo-2-branch}
278
+ Changes:
279
+ - {bullet points from the PR-body material}
280
+
281
+ WORKSPACE: {workspace-name} (local remote)
282
+ Branch: {branch} → main
283
+
284
+ Merge all locally? [Y/n]
285
+ ```
286
+
287
+ If yes — fast-forward merge on each remote, delete the feature branch, pull the source clone:
288
+ ```bash
289
+ # For each repo in the tracker's repos with a local/bare remote:
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}"
294
+
295
+ # Workspace repo — same pattern from the workspace worktree
296
+ cd "work-sessions/{session-name}/workspace"
297
+ git push origin HEAD:main
298
+ git push origin --delete "{branch}"
299
+ cd "{main-workspace-root}" && git pull origin main
300
+ ```
301
+
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.
303
+
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:
305
+ ```bash
306
+ cd "repos/{repo}" && git merge --ff-only "{branch}"
307
+ ```
308
+
309
+ ### Step 10: Close the linked issue on the tracker
310
+
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:
312
+
313
+ ```javascript
314
+ import { createTracker } from './.claude/scripts/trackers/interface.mjs';
315
+ import { readFileSync } from 'node:fs';
316
+ const ws = JSON.parse(readFileSync('workspace.json', 'utf-8'));
317
+ if (ws.workspace?.tracker) {
318
+ const tracker = createTracker(ws.workspace.tracker);
319
+ const comment = [
320
+ `**Completed by @${currentUser}**`,
321
+ '',
322
+ 'Merged PRs:',
323
+ ...mergedPrs.map(p => `- ${p.repo}: ${p.url}`),
324
+ '',
325
+ releaseSummary, // 1-3 sentence synthesis of what shipped, drawn from the session tracker
326
+ ].join('\n');
327
+ await tracker.closeIssue(workItem, { comment });
328
+ }
329
+ ```
330
+
331
+ If `workItem:` is unset, skip the close — this was a blank session.
332
+
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.
334
+
335
+ ### Step 11: Cleanup
336
+
337
+ Run the cleanup helper script from the main workspace root:
338
+ ```bash
339
+ node .claude/scripts/cleanup-work-session.mjs --session-name "{session-name}"
340
+ ```
341
+
342
+ The script tears down in the **mandatory** order:
343
+ 1. Remove each nested project worktree from its project repo
344
+ 2. Remove the workspace worktree from the workspace repo
345
+ 3. `git worktree prune` on each project repo (belt-and-suspenders for orphan records)
346
+ 4. Delete local branches in all repos
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.
348
+
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.
350
+
351
+ Verify workspace root is still on main:
352
+ ```bash
353
+ git branch --show-current # should be "main"
354
+ ```
355
+
356
+ ## Handling Unformal Work Sessions
357
+
358
+ If /complete-work is called but changes were made without a formal work session (no branch, changes on default branch):
359
+
360
+ Ask: "These changes weren't part of a formal work session. What do you want to do?"
361
+ - **Accept as work** — create a session retroactively, proceed with normal completion
362
+ - **Stash for later** — create a user-scoped handoff describing what was done, stash the changes
363
+ - **Hand off to someone** — create a team-visible handoff at root workspace-context/ for another member to pick up
364
+ - **Revert** — undo the changes (with confirmation)
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 the chat drawer `{launcher-root}/workspace-scratchpad/chats/{chat}/` and ask which items should graduate into `workspace-context/`. The offer is filtered by task: an item whose frontmatter carries a `workItem:` is listed only when it names this task's `{workItem}`, an item with no `workItem:` is always listed, and an item tagged for another open task is not this completion's to promote — it belongs to that task's completion. Leftovers of an earlier completion attempt are never offered: the `pr-{slug}-*.md` bodies and the `prs-{slug}.json` state file are step 3–4 machinery, not thinking. Drafting skills (`/braindump`, `/handoff`, designs and plans) tag their drawer writes with `workItem:` while a task is active so this filter has something to filter on. 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. **Write one PR body per repo into the drawer, then create every PR with `task-pr.mjs --create`** — never `gh pr` directly. For each repo of the task, `{worktree}` included when it exists, write `{launcher-root}/workspace-scratchpad/chats/{chat}/pr-{slug}-{repo}.md` (with `{repo}` rendered as `workspace` for `.`). Each body carries a short summary of what changed and why, then a `## Verification` section stating how the change was checked — the commands run and their results. The drawer is machine-local and gitignored, so these files never touch a branch, and `--create` requires one for every repo whose branch has commits. Then one command does the rest:
400
+
401
+ ```bash
402
+ node "{launcher-root}/.claude/scripts/task-pr.mjs" --create --root "{launcher-root}" --branch "{branch}" \
403
+ --work-item "{workItem}" --chat "{chat}" \
404
+ --body-file "{repo}={launcher-root}/workspace-scratchpad/chats/{chat}/pr-{slug}-{repo}.md" \
405
+ --out "{launcher-root}/workspace-scratchpad/chats/{chat}/prs-{slug}.json"
406
+ ```
407
+
408
+ Omit `--work-item` when the task has none, and repeat `--body-file` once per repo. `--chat` resolves the task's repos from this chat's record entries for the branch; when detection came from cwd alone, pass `--repo "{repo}"` once per repo instead. The script skips repos whose branch has no commits over `origin/{defaultBranch}` (reporting them as `empty` — no push, no PR, no body file needed, torn down in step 5 like any other; a workspace branch that collected no promotions gets no PR), parses each worktree's own origin into `{owner}/{name}` and stops **before pushing anything** if any origin is not forge-hosted — such a repo is completed under the session model — then pushes `-u origin {branch}` and opens one PR per remaining repo through a per-repo forge aimed at that worktree's own remote, never the launcher's. The workspace repo (`.`) is handled exactly like a project repo. The PR title is the linked issue's title, or the branch's first commit subject without a `{workItem}`; the body is the drawer file with `Closes <ref>` appended, where `<ref>` is `#N` when the PR's repo is the tracker's repo and `{tracker-repo}#N` otherwise — only the first form closes an issue in the PR's own repo. If the push is rejected as non-fast-forward — the branch already existed on origin and step 1's rebase rewrote it — the script stops and says so; ask the user, and only on explicit confirmation re-run the same command with `--force-with-lease` (the script never forces on its own). If `workspace.forge` is `false` in `workspace.json`, the script refuses before pushing anything: forge operations are disabled in this workspace and the PR is opened by hand. `--out` writes the run's JSON to `prs-{slug}.json` for step 4 — `{ prs: [{ repo, owner, name, number, id, url, isWorkspace }], empty: [repo…], pushed: [repo…] }`, printed to stdout as well — instead of shell redirection, so the command is identical on every shell. The run is idempotent: a repo whose branch already has an open PR against `{defaultBranch}` gets that PR reused, never duplicated, and if the run fails partway the file still records what was pushed and opened so far — fix the cause and re-run the same command; it completes the set.
409
+
410
+ 4. **Ask before merging, then run `task-pr.mjs --merge`, which also closes the linked issue.** When step 3 reported `prs: []` — every repo was empty — there is nothing to merge: skip this step, say so, and leave the linked issue open for the user to decide (close it by hand, or keep the task going); `--merge` itself refuses an empty PRs file for exactly that reason. Otherwise present a summary per repo that got a PR — the workspace repo included — built from `--create`'s output, and ask once:
411
+
412
+ ```
413
+ Task complete:
414
+
415
+ PROJECT: {owner}/{name}
416
+ PR: {pr.url}
417
+ Branch: {branch} → {defaultBranch}
418
+ Commits: {n} # git -C "{worktree}" rev-list --count "origin/{defaultBranch}..{branch}"
419
+
420
+ WORKSPACE: {ws-owner}/{ws-name} # from the workspace worktree's origin
421
+ PR: {ws-pr.url}
422
+ Branch: {branch} → {defaultBranch}
423
+
424
+ Merge all? [Y/n]
425
+ ```
426
+
427
+ On "n", stop: the PRs stay open and the worktrees, branches, and record entries stay in place — say so.
428
+
429
+ On "y":
430
+
431
+ ```bash
432
+ node "{launcher-root}/.claude/scripts/task-pr.mjs" --merge --root "{launcher-root}" \
433
+ --prs "{launcher-root}/workspace-scratchpad/chats/{chat}/prs-{slug}.json" --work-item "{workItem}"
434
+ ```
435
+
436
+ The script checks each PR's state first, then merges the project PRs (squash, delete branch), then the workspace PR only when every project merge succeeded — the workspace branch's promoted context describes the project merges and must never merge ahead of them. A PR the forge already reports as merged counts as done and is never merged twice, which is what makes a re-run safe: the run that finally gets every PR merged also does the pull and the close. The launcher is pulled `--ff-only` only when it sits on the workspace default branch — it waited there on the workspace merge, and a project-only task pulls after the project merges; when it sits on some other branch the JSON reports `pullSkipped: "launcher on <branch>"`, and a failed pull is reported as `pullFailed: true` rather than an error, because the merges stand and the issue still closes — in both cases tell the user to pull the launcher by hand before step 5. The close comes last, with a one-line comment naming the merged PR URL(s): merge precedes close because an issue closed before its PR merges points at work that never landed; without a `{workItem}` (no tracker, or the task was never recorded) the close is skipped and the script says so. On any merge failure the script stops, names the PRs still open, and exits non-zero — nothing is torn down, and re-running `--merge` once the failure is fixed is safe. After a successful merge, delete the drawer's `pr-{slug}-*.md` body files and the spent `prs-{slug}.json`.
437
+
438
+ 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.
439
+
440
+ ```bash
441
+ node "{launcher-root}/.claude/scripts/task-worktree.mjs" --root "{launcher-root}" --remove --repo "{repo}" --branch "{branch}" --delete-branch
442
+ node "{launcher-root}/.claude/scripts/chat-record.mjs" --root "{launcher-root}" --remove-task --chat "{chat}" --work-item "{workItem}" --repo "{repo}"
443
+ ```
444
+
445
+ 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.
446
+
447
+ ## Notes
448
+ - The session tracker's body is the primary source for PR-body synthesis — it captures the full session history alongside specs and plans
449
+ - All repos get PRed and merged together — one approval for all
450
+ - Version bumps, tags, and publish happen in `/release`, not `/complete-work` — this avoids version drift when multiple feature branches land between releases
451
+ - The teardown order is mandatory: project worktrees first, then workspace worktree, then prune, then delete the session folder
452
+ - Context promotion and cleanup are intentional workflow behavior — they bypass normal commit conventions by design