@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.
- package/README.md +2 -2
- package/lib/init.mjs +19 -0
- package/package.json +1 -1
- package/template/.claude/hooks/session-end.mjs +68 -2
- package/template/.claude/rules/config-review.md.skip +29 -0
- package/template/.claude/rules/forge-operations.md +107 -0
- package/template/.claude/rules/goal-driven-work.md +48 -4
- package/template/.claude/rules/workspace-structure.md +38 -0
- package/template/.claude/scripts/cleanup-work-session.mjs +164 -26
- package/template/.claude/scripts/forges/github.mjs +210 -0
- package/template/.claude/scripts/forges/gitlab.mjs +19 -0
- package/template/.claude/scripts/forges/interface.mjs +113 -0
- package/template/.claude/settings.json +5 -13
- package/template/.claude/skills/complete-work/SKILL.md +73 -42
- package/template/.claude/skills/maintenance/SKILL.md +32 -6
- package/template/.claude/skills/pause-work/SKILL.md +24 -7
- package/template/.claude/skills/release/SKILL.md +7 -5
- package/template/.claude/skills/workspace-init/SKILL.md +32 -0
- package/template/.claudeignore +3 -0
- package/template/CLAUDE.md.tmpl +1 -0
- package/template/CODEBASE.md.tmpl +13 -0
- package/template/repo-claude.md.tmpl +10 -0
- package/template/workspace.json.tmpl +2 -1
- package/template/.claude/hooks/worktree-create.mjs +0 -53
|
@@ -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
|
|
90
|
+
mkdir -p {releaseNotesDir}/unreleased/{repo-name}
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
**File 1: `
|
|
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: `
|
|
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
|
|
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
|
-
```
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
|
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
|
-
```
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
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 `
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
138
|
+
### 10. Canonical budget triage
|
|
125
139
|
|
|
126
|
-
This step runs only when the post-regen `--check` from step
|
|
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
|
-
###
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
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
|
|
76
|
+
rm {releaseNotesDir}/unreleased/{repo}/branch-release-*
|
|
75
77
|
# If the directory is now empty, remove it too:
|
|
76
|
-
rmdir
|
|
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
|
|
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 `
|
|
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.
|
package/template/CLAUDE.md.tmpl
CHANGED
|
@@ -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
|
-
}
|