@ulysses-ai/create-workspace 0.16.0-beta.0 → 0.17.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.
@@ -21,6 +21,7 @@ Determine paths:
21
21
  - Workspace worktree: `work-sessions/{session-name}/workspace/`
22
22
  - Project worktrees: `work-sessions/{session-name}/workspace/repos/{repo}/` for each repo in the tracker's `repos:` list
23
23
  - 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/`.
24
25
 
25
26
  ### Step 2: Rebase project repos
26
27
 
@@ -86,10 +87,10 @@ For each repo in the tracker's `repos:` list that has commits beyond the base br
86
87
  cd work-sessions/{session-name}/workspace/repos/{repo}
87
88
  COMMIT_ID=$(git rev-parse --short HEAD)
88
89
  cd ../.. # back to the workspace worktree
89
- mkdir -p release-notes/unreleased/{repo-name}
90
+ mkdir -p {releaseNotesDir}/unreleased/{repo-name}
90
91
  ```
91
92
 
92
- **File 1: `release-notes/unreleased/{repo-name}/branch-release-notes-{COMMIT_ID}.md`** (relative to the workspace worktree)
93
+ **File 1: `{releaseNotesDir}/unreleased/{repo-name}/branch-release-notes-{COMMIT_ID}.md`** (relative to the workspace worktree)
93
94
  ```markdown
94
95
  ---
95
96
  branch: {branch}
@@ -105,7 +106,7 @@ date: {YYYY-MM-DD}
105
106
  Written from scratch per coherent-revisions rule.}
106
107
  ```
107
108
 
108
- **File 2: `release-notes/unreleased/{repo-name}/branch-release-questions-{COMMIT_ID}.md`**
109
+ **File 2: `{releaseNotesDir}/unreleased/{repo-name}/branch-release-questions-{COMMIT_ID}.md`**
109
110
  ```markdown
110
111
  ---
111
112
  branch: {branch}
@@ -124,7 +125,7 @@ The `repo:` frontmatter field is what `/release` uses to know which project repo
124
125
  After all repos are processed, commit once on the workspace branch:
125
126
  ```bash
126
127
  cd work-sessions/{session-name}/workspace
127
- git add release-notes/unreleased/
128
+ git add {releaseNotesDir}/unreleased/
128
129
  git commit -m "docs: add release notes for {branch}"
129
130
  ```
130
131
 
@@ -217,16 +218,27 @@ The push shape is the same as 9a — what differs is the merge mechanics in Step
217
218
 
218
219
  #### Step 10a: GitHub remotes — create PRs, unified summary, merge
219
220
 
220
- Create one PR per project repo plus one workspace PR:
221
+ 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.
221
222
 
222
- ```bash
223
- # For each repo in the tracker's repos with a GitHub remote:
224
- cd work-sessions/{session-name}/workspace/repos/{repo}
225
- gh pr create --title "{type}: {description}" --body "..."
223
+ ```javascript
224
+ import { createForge } from './.claude/scripts/forges/interface.mjs';
225
+ import { readFileSync } from 'node:fs';
226
226
 
227
- # Workspace PR — from the workspace worktree
228
- cd work-sessions/{session-name}/workspace
229
- gh pr create --title "context: {session-name} work session" --body "..."
227
+ const ws = JSON.parse(readFileSync('workspace.json', 'utf-8'));
228
+ const forge = createForge(ws.workspace?.forge);
229
+
230
+ // For each repo in the tracker's repos with a GitHub remote, from
231
+ // work-sessions/{session-name}/workspace/repos/{repo}:
232
+ const projectPr = await forge.prCreate({
233
+ title: `${type}: ${description}`,
234
+ body: prBody, // synthesized release notes + verification section
235
+ });
236
+
237
+ // Workspace PR — from the workspace worktree:
238
+ const workspacePr = await forge.prCreate({
239
+ title: `context: ${sessionName} work session`,
240
+ body: workspacePrBody,
241
+ });
230
242
  ```
231
243
 
232
244
  Present unified summary:
@@ -253,15 +265,21 @@ WORKSPACE: {workspace-name}
253
265
  Merge all? [Y/n]
254
266
  ```
255
267
 
256
- If yes — merge all PRs atomically:
257
- ```bash
258
- # For each project PR:
259
- gh pr merge {pr-number} --merge
268
+ If yes — merge all PRs atomically through the forge adapter:
269
+
270
+ ```javascript
271
+ // For each project PR returned from Step 10a's prCreate calls:
272
+ await forge.prMerge({ id: projectPr.id, strategy: 'squash', deleteBranch: true });
273
+
274
+ // Workspace PR:
275
+ await forge.prMerge({ id: workspacePr.id, strategy: 'squash', deleteBranch: true });
276
+ ```
260
277
 
261
- # Workspace PR:
262
- gh pr merge {workspace-pr-number} --merge
278
+ `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.
263
279
 
264
- # Pull all repos to their default branches
280
+ Then pull all repos to their default branches (still plain git):
281
+
282
+ ```bash
265
283
  # For each repo in the tracker's repos:
266
284
  cd repos/{repo} && git pull origin {repo-branch}
267
285
  cd {main-workspace-root} && git pull origin main
@@ -273,7 +291,7 @@ The next three sub-substeps run only when the session branch starts with `releas
273
291
 
274
292
  Derive the version tag from the branch name by stripping the `release/` prefix (so `release/v0.15.0-beta.0` yields `v0.15.0-beta.0`). For each project repo whose `package.json` declares a `version` field, verify that version matches the derived tag. The workspace repo is **never** tagged — only project repos with publishable `package.json` files get tagged, since the tag triggers `.github/workflows/publish.yml` in that project repo. If a project repo's `package.json` version doesn't match the release tag, skip that repo with a warning rather than failing the whole completion flow — the mismatch usually means `/release` was run against a different version than the branch name suggests, and the user needs to investigate before publishing.
275
293
 
276
- 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.
294
+ Before tagging, preflight against origin: if `v{version}` already exists remotely, surface the conflict to the user with three explicit recovery options — **Reuse** (skip to 10a.2 if the existing tag points at the right commit), **Replace** (`git push origin --delete v{version}` then re-run 10a.1), or **Investigate** (`forge.releaseView({ tag: 'v{version}', repo: '{org}/{repo}' })` — or `gh release view v{version}` as a manual fallback — to see what shipped). Do **not** silently force-push the tag; an existing tag means a published artifact, and overwriting it without confirmation can corrupt the npm registry's view of the release history.
277
295
 
278
296
  If the tag is absent on origin, tag the merge commit (HEAD on `{default-branch}` after the prior `git pull origin {default-branch}`) and push the tag. The tag push triggers `.github/workflows/publish.yml`.
279
297
 
@@ -304,7 +322,8 @@ for repo in {project-repos-with-package-json}; do
304
322
  # Tag exists. Surface to user with three options:
305
323
  # 1. Reuse — skip to 10a.2 if the existing tag points at the right commit.
306
324
  # 2. Replace — `git push origin --delete $version_tag` then re-run 10a.1.
307
- # 3. Investigate — `gh release view $version_tag` to see what shipped.
325
+ # 3. Investigate — call forge.releaseView({ tag: '$version_tag' }) or
326
+ # `gh release view $version_tag` as a manual fallback — to see what shipped.
308
327
  # Do NOT silently force-push.
309
328
  echo "Tag $version_tag already exists on origin. Aborting with recovery options."
310
329
  return 1
@@ -318,27 +337,39 @@ done
318
337
 
319
338
  **Step 10a.2: Watch the publish workflow (release sessions only)**
320
339
 
321
- 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.
340
+ For each project repo tagged in 10a.1, find and follow the `publish.yml` workflow run on GitHub. The workflow takes a moment to register against the new tag — poll up to 5 times with a 3-second backoff before giving up. Once the run is found, attach with `workflowRunWatch` so the maintainer sees progress live alongside the unified summary. The adapter's `exitStatus: true` makes the underlying `gh run watch --exit-status` exit non-zero on workflow failure; the adapter returns the exit code via `res.exitCode` instead of throwing, so a failure does **not** abort the rest of `/complete-work` the maintainer still needs to see the unified summary, including the failure URL, to decide whether to rerun, redo the release, or roll the tag back. If no run registers within the retry window, log a warning with the manual investigation command and continue.
322
341
 
323
- ```bash
324
- # Retry up to 5 times with 3-second backoff — the run takes a moment to register.
325
- for i in 1 2 3 4 5; do
326
- run_id=$(gh run list \
327
- --repo {org}/{repo} \
328
- --workflow publish.yml \
329
- --branch "$version_tag" \
330
- --limit 1 \
331
- --json databaseId \
332
- --jq '.[0].databaseId')
333
- if [ -n "$run_id" ]; then break; fi
334
- sleep 3
335
- done
342
+ ```javascript
343
+ import { createForge } from './.claude/scripts/forges/interface.mjs';
344
+ import { readFileSync } from 'node:fs';
336
345
 
337
- if [ -z "$run_id" ]; then
338
- echo "Warning: no publish workflow run found for $version_tag after 15s. Investigate via 'gh run list'."
339
- else
340
- gh run watch "$run_id" --exit-status --repo {org}/{repo} || true
341
- fi
346
+ const ws = JSON.parse(readFileSync('workspace.json', 'utf-8'));
347
+ const forge = createForge(ws.workspace?.forge);
348
+
349
+ // Retry up to 5 times with 3-second backoff the run takes a moment to register.
350
+ let run = null;
351
+ for (let i = 0; i < 5; i++) {
352
+ run = await forge.workflowRunFind({
353
+ workflow: 'publish.yml',
354
+ branch: versionTag, // e.g. 'v0.15.0-beta.0'
355
+ repo: `${org}/${repo}`,
356
+ limit: 1,
357
+ });
358
+ if (run) break;
359
+ await new Promise(r => setTimeout(r, 3000));
360
+ }
361
+
362
+ if (!run) {
363
+ console.warn(`Warning: no publish workflow run found for ${versionTag} after 15s. Investigate via 'gh run list --workflow publish.yml --branch ${versionTag}'.`);
364
+ } else {
365
+ const result = await forge.workflowRunWatch({
366
+ runId: run.runId,
367
+ repo: `${org}/${repo}`,
368
+ exitStatus: true,
369
+ });
370
+ // result.exitCode === 0 on success; non-zero on workflow failure (NOT thrown).
371
+ // result.exitCode and run.url feed into the unified summary in Step 10a.3.
372
+ }
342
373
  ```
343
374
 
344
375
  **Step 10a.3: Update the unified summary (release sessions only)**
@@ -353,7 +384,7 @@ PUBLISH ({repo}):
353
384
  Published: {dist-tag}@{version} on npm
354
385
  ```
355
386
 
356
- 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.
387
+ Pull `Status` from the watch result's `exitCode` (success when `result.exitCode === 0`, failure otherwise). Pull `Workflow` from `run.url` captured in 10a.2. Pull `Published: {dist-tag}@{version}` from the workflow's published-package output if available; if the workflow failed before publishing, omit the `Published:` line and rely on `Status: failure` plus the workflow URL to point the maintainer at the failure.
357
388
 
358
389
  #### Step 10b: Local / bare / other remotes — local merge flow
359
390
 
@@ -459,7 +490,7 @@ Ask: "These changes weren't part of a formal work session. What do you want to d
459
490
  - **Revert** — undo the changes (with confirmation)
460
491
 
461
492
  ## Notes
462
- - Branch release notes live in the WORKSPACE repo at `release-notes/unreleased/{repo-name}/` — never in project repos. Project repos only ever see code commits and (at release time) `CHANGELOG.md` entries written by `/release`.
493
+ - 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`.
463
494
  - The session tracker's body is the primary source for release note synthesis — it captures the full session history alongside specs and plans
464
495
  - All repos get PRed and merged together — one approval for all
465
496
  - Version bumps happen in `/release`, not `/complete-work` — this avoids version drift when multiple feature branches land between releases
@@ -108,22 +108,36 @@ Report one of:
108
108
 
109
109
  Active recommendations. Flags problems and suggests fixes, but asks before acting.
110
110
 
111
- ### 7. Stale context
111
+ ### 7. Component age check
112
+
113
+ Scan the following file sets for a YAML frontmatter `updated:` field:
114
+ - `.claude/rules/*.md` (active rules only — `.md.skip` files are included too, since the rule content can still drift)
115
+ - `.claude/skills/*/SKILL.md`
116
+ - `.claude/agents/*.md`
117
+ - `.claude/hooks/*.mjs`
118
+
119
+ For each file that has an `updated:` field, compute the age in days from today. If the age exceeds 180 days, flag the file as a stale component candidate. Print the file name, the `updated:` date, and the age in days so the contributor knows how far the file has drifted.
120
+
121
+ Files without an `updated:` field are skipped — the check is opt-in and activates the discipline incrementally as contributors add frontmatter to the files they own. To start tracking a file, add `updated: <today>` to its frontmatter; the check will surface it if it goes stale.
122
+
123
+ When stale candidates are found, surface them as warnings in the output format and link to `config-review.md.skip` (in `.claude/rules/`) as the opt-in rule that documents the review cadence and rationale.
124
+
125
+ ### 8. Stale context
112
126
  - Ephemeral files not updated in 7+ days — suggest resolve, update, or archive
113
127
  - `work-sessions/{name}/` folders whose worktrees are gone — suggest cleanup
114
128
  - Session trackers whose branches have been merged — suggest `/complete-work` post-flight cleanup
115
129
  - Braindumps that overlap significantly — suggest merging (e.g., "workspace-branching.md and persistent-work-sessions.md cover the same topic")
116
130
  - Handoffs referencing deleted branches — suggest resolve or remove
117
131
 
118
- ### 8. Context reconciliation
132
+ ### 9. Context reconciliation
119
133
  - Read recent workspace-context writes (last session or last N files by updated date)
120
134
  - For each, scan other workspace-context files for references that are now stale
121
135
  - Surface: "{file} says X but {newer-file} now says Y. Update {file}?"
122
136
  - This is the capture-time cross-check, run retroactively instead of inline
123
137
 
124
- ### 9. Canonical budget triage
138
+ ### 10. Canonical budget triage
125
139
 
126
- This step runs only when the post-regen `--check` from step 8 still reports `selectionStatus: 'over-budget'`. If the regular regen pass cleared the budget — or if `--check` was already `ok`, `trimmed`, or `stubbed` after step 8 — skip this step entirely.
140
+ This step runs only when the post-regen `--check` from step 9 still reports `selectionStatus: 'over-budget'`. If the regular regen pass cleared the budget — or if `--check` was already `ok`, `trimmed`, or `stubbed` after step 9 — skip this step entirely.
127
141
 
128
142
  The rest of cleanup is suggestion-list-with-confirmation: surface a candidate, ask before applying, move on. Triage is the one meaningfully more interactive surface in `/maintenance`. It runs as a small REPL: present the budget state and a triage menu, take one action, re-run `--check`, present the menu again with the new state. No suggestion is auto-applied; every action is the user's choice.
129
143
 
@@ -168,7 +182,19 @@ For each chosen action:
168
182
 
169
183
  Trim markers and demotions only matter for `priority: reference` files — `<!-- canonical:trim -->` spans on a `priority: critical` file are inert until the file is demoted. The triage flow never auto-decides which file to demote or which section to wrap; it surfaces the data, presents options, and waits.
170
184
 
171
- ### 10. Health metrics
185
+ ### 11. Forge configuration
186
+
187
+ Read `workspace.json`. If `workspace.tracker?.type === 'github-issues'` and `workspace.forge` is unset, emit a notice (not an error):
188
+
189
+ ```
190
+ ℹ workspace.json has tracker.type='github-issues' but no workspace.forge field.
191
+ Skills default to GitHub forge operations; add `"forge": {"type": "github"}`
192
+ to workspace.json to make the choice explicit. See .claude/rules/forge-operations.md.
193
+ ```
194
+
195
+ This is migration guidance for workspaces created before the `forge` field landed — the field is back-compat with a sensible default, so the unset case is not a bug, just an opportunity to make the implicit explicit. If `workspace.forge.type` is set to a value with no adapter at `.claude/scripts/forges/{type}.mjs`, that IS an error and goes in the Issues section.
196
+
197
+ ### 12. Health metrics
172
198
  - Canonical budget — read from the same `--check` invocation as step 5. Reported as `current / budget` bytes with the selection status (e.g., `full`, `2 reference files trimmed`). Over-budget cases are deferred to the cleanup triage flow rather than re-reported here.
173
199
  - Number of ephemeral files — flag if accumulating without resolution
174
200
  - Session log stats (if `workspace-scratchpad/session-log.jsonl` exists):
@@ -215,7 +241,7 @@ OK (5):
215
241
  5. Check git state (worktrees, branches, remotes)
216
242
  6. Run `node .claude/scripts/build-workspace-context.mjs --check --root .` — capture status. Exit `0` = clean and within budget, `1` = artifact missing or stale, `2` = artifacts current but canonical body over budget. The `canonical` block in the JSON output drives both the audit budget line and the cleanup triage decision.
217
243
  7. Read session-log.jsonl if it exists
218
- 8. If cleanup mode: regenerate the workspace-context auto-files if stale (index.md, canonical.md, per-user team-member indexes); compare files pairwise for overlap; scan for stale cross-references. If post-regen `--check` reports `over-budget`, enter the canonical-budget triage flow described in cleanup step 9.
244
+ 8. If cleanup mode: regenerate the workspace-context auto-files if stale (index.md, canonical.md, per-user team-member indexes); compare files pairwise for overlap; scan for stale cross-references. If post-regen `--check` reports `over-budget`, enter the canonical-budget triage flow described in cleanup step 10.
219
245
  9. Compile and present findings grouped by severity
220
246
 
221
247
  ## Notes
@@ -95,16 +95,33 @@ git push -u origin {branch}
95
95
 
96
96
  ### Step 7: Create draft PRs
97
97
 
98
- ```bash
99
- # For each repo in the tracker's repos:
100
- cd work-sessions/{session-name}/workspace/repos/{repo}
101
- gh pr create --draft --title "WIP: {description}" --body "Work in progress. Session paused."
98
+ PR creation goes through the forge adapter (`.claude/scripts/forges/interface.mjs`), not directly through `gh` — see `.claude/rules/forge-operations.md` for the contract and why. The adapter resolves the target repo from `workspace.forge.repo` or the local git remote.
102
99
 
103
- # Workspace repo — from the workspace worktree
104
- gh pr create --draft --title "context: {session-name} (paused)" --body "Workspace context for paused session."
100
+ ```javascript
101
+ import { createForge } from './.claude/scripts/forges/interface.mjs';
102
+ import { readFileSync } from 'node:fs';
103
+
104
+ const ws = JSON.parse(readFileSync('workspace.json', 'utf-8'));
105
+ const forge = createForge(ws.workspace?.forge);
106
+
107
+ // For each repo in the tracker's repos, from work-sessions/{session-name}/workspace/repos/{repo}:
108
+ const projectPr = await forge.prCreate({
109
+ title: `WIP: ${description}`,
110
+ body: 'Work in progress. Session paused.',
111
+ draft: true,
112
+ });
113
+ console.log(projectPr.url);
114
+
115
+ // Workspace repo — from the workspace worktree:
116
+ const workspacePr = await forge.prCreate({
117
+ title: `context: ${sessionName} (paused)`,
118
+ body: 'Workspace context for paused session.',
119
+ draft: true,
120
+ });
121
+ console.log(workspacePr.url);
105
122
  ```
106
123
 
107
- If PRs already exist, update them to draft status if needed.
124
+ If PRs already exist, update them to draft status if needed (use `gh pr ready --undo` directly until a `forge.prSetDraft` method lands — that's tracked as a future forge adapter extension, not blocking here).
108
125
 
109
126
  ### Step 8: Confirm
110
127
 
@@ -27,10 +27,12 @@ Check `workspace.json` for `releaseMode`:
27
27
  - **workspace**: process all repos together
28
28
  - **ask**: "Process all repos together or individually?"
29
29
 
30
+ **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/`.
31
+
30
32
  **Step 2: Read unreleased notes**
31
33
  Branch notes live in the **workspace** repo, written there by `/complete-work`. For each target repo, list the workspace's unreleased subdirectory for that project:
32
34
  ```bash
33
- ls release-notes/unreleased/{repo}/
35
+ ls {releaseNotesDir}/unreleased/{repo}/
34
36
  ```
35
37
  Read all `branch-release-notes-*.md` and `branch-release-questions-*.md` files.
36
38
 
@@ -71,9 +73,9 @@ The entry stays short. If a change needs more detail, reference the repo's docs
71
73
 
72
74
  **Step 6: Delete consumed branch notes from the workspace**
73
75
  ```bash
74
- rm release-notes/unreleased/{repo}/branch-release-*
76
+ rm {releaseNotesDir}/unreleased/{repo}/branch-release-*
75
77
  # If the directory is now empty, remove it too:
76
- rmdir release-notes/unreleased/{repo} 2>/dev/null || true
78
+ rmdir {releaseNotesDir}/unreleased/{repo} 2>/dev/null || true
77
79
  ```
78
80
  The branch notes were an intermediate capture; their content is now in the CHANGELOG entry and their raw form in git history. They do not survive into the project repo.
79
81
 
@@ -98,7 +100,7 @@ Skip this step if the repo has no package.json or no version field.
98
100
  **Step 7c: Commit the consumed-notes deletion in the workspace**
99
101
  ```bash
100
102
  # From the workspace root
101
- git add release-notes/unreleased/
103
+ git add {releaseNotesDir}/unreleased/
102
104
  git commit -m "release: consume {repo} branch notes for v{version}"
103
105
  ```
104
106
  Workspace and project repos have separate commits — they are separate git histories.
@@ -138,7 +140,7 @@ Process ephemeral workspace-context entries:
138
140
  ## Notes
139
141
 
140
142
  - Release entries live in `CHANGELOG.md` at the project repo root — one file, one concise entry per version. No `release-notes/v*.md`, no `release-notes/archive/`.
141
- - Branch notes live in the WORKSPACE at `release-notes/unreleased/{repo}/`. `/complete-work` writes them; `/release` consumes and deletes them. They never reach project repos.
143
+ - Branch notes live in the WORKSPACE at `{releaseNotesDir}/unreleased/{repo}/` (resolved from `workspace.json` → `workspace.releaseNotesDir`, default `workspace-context/release-notes`). `/complete-work` writes them; `/release` consumes and deletes them. They never reach project repos.
142
144
  - Versions are bumped here, not in `/complete-work`. This keeps the version semantics aligned with what actually shipped (accumulated changes since last release).
143
145
  - The public repo stays lean. Detailed per-branch retrospection exists in workspace git history (the consumed-notes commit) but is not surfaced as standalone files in either repo.
144
146
  - Context synthesis happens in the WORKSPACE repo — Step 7c (consumed-notes) and Step 9 (workspace-context synthesis) are separate workspace commits.
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: workspace-init
3
3
  description: First-time workspace initialization. Clones repos, installs template components, extracts team knowledge from documentation sources and Claude chat history, activates rules, configures user identity. Run once after scaffolding with --init.
4
+
5
+ # scope example (remove # to activate path-scoped loading):
6
+ # scope: repos/my-api/
4
7
  ---
5
8
 
6
9
  # Workspace Init
@@ -117,6 +120,9 @@ Also install these top-level files from the payload:
117
120
  - **`.claude/settings.json`** — Merge payload settings into existing file. Preserve user-added settings, add missing entries.
118
121
  - **`.gitignore`** — Merge: add lines from payload not already present.
119
122
  - **`CLAUDE.md`** — Generate from `.workspace-update/CLAUDE.md.tmpl`, substituting `{{project-name}}` with the workspace name. If the existing CLAUDE.md has user-added content beyond the bootstrap template, preserve it.
123
+ - **`CODEBASE.md` (optional)** — Ask: "Generate CODEBASE.md? This produces a lightweight file-tree map that helps Claude navigate the codebase without exhaustive exploration. [Y/n]". If yes: scaffold `CODEBASE.md` from `.workspace-update/CODEBASE.md.tmpl`, substitute `{{project-name}}`, then pre-populate `## Top-level layout` by listing the top-level entries of each `repos/{repo}/` directory using `fs.readdirSync` (Node.js, no network calls). If no: skip — `CODEBASE.md` can always be created manually later by copying and filling the template.
124
+
125
+ **Per-repo CLAUDE.md stubs:** For each repo in `workspace.json`, check if `repos/{repo}/CLAUDE.md` exists. If not, ask "Scaffold a CLAUDE.md for {repo}? [Y/n]". If yes, write a blank stub from `.workspace-update/repo-claude.md.tmpl`, substituting `{{repo-name}}` with the repo name. The stub body is comment text only — no workspace-specific content — and its `## Commands` section is where you will add repo-specific test, lint, and build commands.
120
126
 
121
127
  **Commit:** `git commit -m "feat: install template components from payload"`
122
128
 
@@ -255,6 +261,29 @@ If you discovered candidate work items during earlier steps (bugs in braindumps,
255
261
 
256
262
  Do NOT batch-create issues automatically — the user should review and prune the list.
257
263
 
264
+ ### Step 12.5: Configure MCP servers
265
+
266
+ MCP (Model Context Protocol) servers extend what Claude can do inside the workspace. The template ships with a Playwright MCP server entry in `.mcp.json` for browser automation and visual testing. Additional servers open up two particularly useful capabilities:
267
+
268
+ - **LSP symbol navigation** — a Language Server Protocol server gives Claude go-to-definition, find-all-references, and call-graph queries without false positives from text search. The right server depends on your language stack:
269
+ - TypeScript / JavaScript: `typescript-language-server` (via `npx typescript-language-server --stdio`)
270
+ - Python: `pylsp` (via `pip install python-lsp-server`)
271
+ - Go, Rust, Java, and others: consult your language's LSP documentation.
272
+ - **Internal tool access** — if your team has internal APIs, databases, or services, an MCP server can expose them as tools Claude can call directly. The shape of an `mcpServers` entry in `.mcp.json`:
273
+
274
+ ```json
275
+ {
276
+ "mcpServers": {
277
+ "my-lsp": {
278
+ "command": "npx",
279
+ "args": ["typescript-language-server", "--stdio"]
280
+ }
281
+ }
282
+ }
283
+ ```
284
+
285
+ **This step is guidance only.** The right LSP server depends on your language; the right internal-tool server depends on what your team builds. Configure `.mcp.json` after init when you know what you need — there is no pressure to choose now. The Playwright entry already present in `.mcp.json` satisfies the template's audit assertion; additional servers are additive.
286
+
258
287
  ### Step 13: Configure user identity
259
288
 
260
289
  Ask: "What name should be used for your user-scoped context? [{system-username}]"
@@ -418,3 +447,6 @@ This session is done. Start a fresh Claude Code session and run /start-work to b
418
447
  - Documentation sources are first-class — always ask, always confirm access, always report failures
419
448
  - Chat history scanning uses a manifest to survive auto-compaction
420
449
  - Existing worktrees are formalized with session markers, trackers, and linked chat history
450
+ - **Subdirectory launch:** Once initialized, `claude` can be launched from `work-sessions/{name}/workspace/repos/{repo}/` instead of the workspace root. Claude walks up the filesystem loading every `CLAUDE.md` it finds, so starting from inside a project worktree loads both the per-repo conventions and the workspace conventions automatically. This is useful for repo-focused tasks — no configuration change needed, just a different launch point.
451
+ - **Skills are on-demand, not pre-loaded:** Skills are invoked explicitly by name (`/skill-name`) when needed; they are not loaded at session start. The `.skip` mechanism in `.claude/rules/` provides the analogous progressive-disclosure pattern for rules — a `.md.skip` file is present but inactive; rename it to `.md` to activate, rename it back to deactivate. Step 6 of this skill walks through the activation choices.
452
+ - **`scope:` for path-scoped skills:** The `scope:` frontmatter field (shown as a commented-out example at the top of this file) restricts a skill so it activates only when the working directory is inside the declared path. Removing the `#` prefix from the example line turns this skill into a path-scoped skill — useful when you want a repo-specific skill available only from inside that repo's worktree.
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ .build/
@@ -14,6 +14,7 @@ This is a claude-workspace. All conventions are defined in .claude/rules/.
14
14
  @local-only-template-freshness.md
15
15
 
16
16
  ## Team Knowledge
17
+ @CODEBASE.md
17
18
  @workspace-context/canonical.md
18
19
  @workspace-context/index.md
19
20
 
@@ -0,0 +1,13 @@
1
+ # {{project-name}} Codebase Map
2
+
3
+ ## Top-level layout
4
+ <!-- fill in -->
5
+
6
+ ## Key entry points
7
+ <!-- fill in -->
8
+
9
+ ## Conventions
10
+ <!-- fill in -->
11
+
12
+ ## External dependencies
13
+ <!-- fill in -->
@@ -0,0 +1,10 @@
1
+ # {{repo-name}}
2
+
3
+ ## Purpose
4
+ <!-- fill in -->
5
+
6
+ ## Conventions
7
+ <!-- fill in -->
8
+
9
+ ## Commands
10
+ <!-- fill in: test, lint, build commands for this repo -->
@@ -11,7 +11,8 @@
11
11
  "subagentContextMaxBytes": 10240,
12
12
  "greeting": "Welcome back to {{project-name}}.",
13
13
  "releaseMode": "per-repo",
14
- "tracker": null
14
+ "tracker": null,
15
+ "forge": { "type": "github" }
15
16
  },
16
17
  "repos": {}
17
18
  }
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env node
2
- // WorktreeCreate hook — scan for stale worktrees across project repos and
3
- // flag them. Project worktrees live inside work-sessions/{name}/workspace/repos/
4
- // in the new layout. The scan walks each project repo's worktree admin list
5
- // rather than a filesystem pattern, so it keeps working regardless of where
6
- // worktrees are physically located.
7
- import { readdirSync, existsSync } from 'fs';
8
- import { join, basename } from 'path';
9
- import { execSync } from 'child_process';
10
- import { getWorkspaceRoot, respond } from './_utils.mjs';
11
-
12
- const root = getWorkspaceRoot(import.meta.url);
13
- const reposDir = join(root, 'repos');
14
- const stale = [];
15
-
16
- if (!existsSync(reposDir)) {
17
- respond();
18
- process.exit(0);
19
- }
20
-
21
- for (const entry of readdirSync(reposDir)) {
22
- const repoPath = join(reposDir, entry);
23
- if (!existsSync(join(repoPath, '.git'))) continue;
24
-
25
- let worktreeOutput;
26
- try {
27
- worktreeOutput = execSync('git worktree list', { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
28
- } catch { continue; }
29
-
30
- for (const line of worktreeOutput.trim().split('\n')) {
31
- const parts = line.trim().split(/\s+/);
32
- const wtPath = parts[0];
33
- if (!wtPath || wtPath === repoPath) continue;
34
- if (!existsSync(wtPath)) continue;
35
-
36
- const branchMatch = line.match(/\[(.+?)\]/);
37
- const branch = branchMatch ? branchMatch[1] : 'unknown';
38
-
39
- try {
40
- const lastCommit = execSync('git log -1 --format=%ci', { cwd: wtPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
41
- const daysAgo = Math.floor((Date.now() - new Date(lastCommit).getTime()) / 86400000);
42
- if (daysAgo > 3) {
43
- stale.push(`- ${basename(wtPath)} (${branch}): no commits in ${daysAgo} days`);
44
- }
45
- } catch {}
46
- }
47
- }
48
-
49
- if (stale.length > 0) {
50
- respond(`Stale worktrees found:\n${stale.join('\n')}\n\nConsider cleaning up with: git worktree remove {path}`);
51
- } else {
52
- respond();
53
- }