@ulysses-ai/create-workspace 0.21.0-beta.0 → 0.23.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/lib/init.mjs +9 -0
- package/lib/init.test.mjs +75 -0
- package/lib/payload.mjs +170 -2
- package/lib/payload.test.mjs +158 -3
- package/lib/scaffold.mjs +8 -0
- package/lib/scaffold.test.mjs +20 -0
- package/lib/upgrade.mjs +148 -6
- package/lib/upgrade.test.mjs +319 -15
- package/package.json +1 -1
- package/template/_claude/rules/forge-operations.md +27 -6
- package/template/_claude/scripts/chat-record.mjs +51 -4
- package/template/_claude/scripts/classify-update.mjs +474 -38
- package/template/_claude/scripts/cleanup-work-session.mjs +64 -3
- package/template/_claude/scripts/forges/gitlab.mjs +450 -18
- package/template/_claude/scripts/forges/interface.mjs +39 -6
- package/template/_claude/scripts/maintenance-audit.mjs +0 -0
- package/template/_claude/scripts/merge-mode.mjs +96 -12
- package/template/_claude/scripts/migrate-sessions.mjs +232 -24
- package/template/_claude/scripts/task-pr.mjs +52 -13
- package/template/_claude/scripts/task-worktree.mjs +79 -10
- package/template/_claude/scripts/template-baseline.mjs +239 -0
- package/template/_claude/scripts/trackers/gitlab-issues.mjs +276 -0
- package/template/_claude/scripts/trackers/interface.mjs +3 -0
- package/template/_claude/skills/complete-work/SKILL.md +7 -4
- package/template/_claude/skills/migrate-sessions/SKILL.md +20 -4
- package/template/_claude/skills/release/SKILL.md +24 -8
- package/template/_claude/skills/setup-tracker/SKILL.md +46 -12
- package/template/_claude/skills/start-work/SKILL.md +15 -2
- package/template/_claude/skills/workspace-init/SKILL.md +6 -0
- package/template/_claude/skills/workspace-update/SKILL.md +63 -27
|
@@ -24,41 +24,56 @@ Apply a staged template update to an initialized workspace. The CLI (`npx @ulyss
|
|
|
24
24
|
|
|
25
25
|
### Step 1: Decide where the update lands
|
|
26
26
|
|
|
27
|
-
Check the workspace repo for a remote
|
|
27
|
+
Check the workspace repo for a remote:
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
```bash
|
|
30
|
+
git remote
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
ANY remote — even one you cannot push to — routes the update through a worktree: a commit made directly on the launcher's local default branch diverges from `origin/<default>` (the push is refused on a protected default branch, and later task worktrees based on `origin/<default>` cannot fast-forward past it). Never commit or push the launcher's default branch directly.
|
|
34
|
+
|
|
35
|
+
- **A remote exists (the normal case)** — create a task worktree up front and treat it as the workspace root for Steps 2–6:
|
|
31
36
|
```bash
|
|
32
37
|
node .claude/scripts/task-worktree.mjs --root . --create --repo . --branch chore/template-update-{version}
|
|
33
38
|
```
|
|
34
|
-
The payload is untracked, so it does not appear inside the worktree — keep referencing it at the launcher's absolute path (`{launcher}/.workspace-update`). Step 7 commits, pushes, and
|
|
39
|
+
The payload is untracked, so it does not appear inside the worktree — keep referencing it at the launcher's absolute path (`{launcher}/.workspace-update`). Everything the update needs travels with the payload, including any `.template-baseline.reconstructed.json` the CLI staged for a pre-baseline workspace. Step 7 commits, pushes, and opens the PR/MR from the worktree.
|
|
40
|
+
- **No remote** — apply in place. The Step 7 commit lands on the launcher's default branch: the one sanctioned launcher commit, because a repo with no remote has nowhere else for a template update to go. The payload path is `.workspace-update/`.
|
|
35
41
|
|
|
36
42
|
In the commands below, `{payload}` is `.workspace-update` in the no-remote flow and `{launcher}/.workspace-update` in the worktree flow.
|
|
37
43
|
|
|
38
44
|
### Step 2: Classify the payload
|
|
39
45
|
|
|
40
|
-
Run the classifier — it compares every verbatim-installed payload file (`.claude/**`, `.mcp.json`, `.claudeignore
|
|
46
|
+
Run the classifier — it compares every verbatim-installed payload file (`.claude/**`, `.mcp.json`, `.claudeignore`, minus the two JSON configs that get their own list below) against the workspace and the template baseline (`.claude/.template-baseline.json`, the hashes of what the template last shipped here), and detects files the template no longer ships:
|
|
41
47
|
|
|
42
48
|
```bash
|
|
43
|
-
node {payload}/.claude/scripts/classify-update.mjs --root . --payload {payload}
|
|
49
|
+
node {payload}/.claude/scripts/classify-update.mjs --root . --payload {payload} --baseline {baseline}
|
|
44
50
|
```
|
|
45
51
|
|
|
46
|
-
It runs from the payload precisely so workspaces that don't have it installed yet can use it; once installed, `node .claude/scripts/classify-update.mjs --root .` is equivalent.
|
|
52
|
+
It runs from the payload precisely so workspaces that don't have it installed yet can use it; once installed, `node .claude/scripts/classify-update.mjs --root .` is equivalent. `{baseline}` is whichever baseline the classification root has — `.claude/.template-baseline.json` when it exists there, else `{payload}/.template-baseline.reconstructed.json`, the file `--upgrade` stages for a pre-baseline workspace. Pass the flag explicitly in the worktree flow: the worktree cannot see launcher-only files, and without a baseline it classifies two-way and asks about every changed file. An explicit path is authoritative, so never point it at a file that may not exist. In the no-remote flow the flag can be omitted — the default resolves the same order itself (root baseline first, then the payload's reconstructed one, a corrupt root file counting as absent). Output is JSON:
|
|
47
53
|
|
|
48
|
-
- `new` — no installed counterpart; safe to batch-apply (Step 3) behind one confirmation
|
|
54
|
+
- `new` — no installed counterpart and no baseline entry; safe to batch-apply (Step 3) behind one confirmation
|
|
49
55
|
- `identical` — installed file already equals the payload; skip silently
|
|
50
|
-
- `
|
|
56
|
+
- `updated` — installed file still holds the baseline content while the payload ships something new: a pure template change the user never touched. Batched with `new` behind one confirmation
|
|
57
|
+
- `differs` — installed file matches neither the payload nor the baseline, and the template changed it since the baseline too: a local edit that meets a template change. Needs a per-file decision
|
|
58
|
+
- `config` — `.mcp.json` and `.claude/settings.json`: JSON the workspace owns jointly with the template — never compared by content, never batch-copied. Each entry carries a key-level diff (`added` keys the template ships, `workspaceOnly` keys only the workspace has, `changed` keys with different values; nested paths like `mcpServers/{server}`), element diffs for array-valued keys (`arrays`: `{ path, added, workspaceOnly }` element lists for `hooks/{event}`, `permissions/allow`/`deny`), or a `notInstalled` / `unparseable` flag. Merged key by key in Step 3.
|
|
59
|
+
- `localOnly` — installed file differs from the payload, but the payload equals the baseline: these are local edits to files the template didn't touch. Informational only — never asked about, never applied
|
|
60
|
+
- `deletedLocally` — the baseline records the file and the payload still ships it, but it is missing from the workspace (deleted locally, or declined at install time). Step 3 asks once whether to restore the list
|
|
51
61
|
- `activated` — the payload ships `rules/{name}.md.skip` while the workspace keeps `{name}.md` active: the rule was deliberately activated. Nothing to install — the active rule stays.
|
|
52
|
-
- `removed` — installed file with no counterpart in the payload.
|
|
62
|
+
- `removed` — installed file with no counterpart in the payload. The config files above never appear here (the template dropping one hands it to the workspace). Gitignored paths, `.claude/worktrees/`, and files listed in `workspace.json` → `workspace.localFiles` (an array of `.claude/`-relative paths or globs the workspace owns) are excluded automatically, so only real template removals are listed.
|
|
63
|
+
- `staleTests` — `*.test.mjs` files under `.claude/` the payload doesn't carry. The package never ships tests, so these came from a dev checkout and no update refreshes them (Step 3 offers removal).
|
|
64
|
+
|
|
65
|
+
Content is compared with line endings normalized (CRLF ≡ LF; binary files byte-exact), so a Windows autocrlf checkout does not read as locally modified.
|
|
66
|
+
|
|
67
|
+
If `hasBaseline` is false (no workspace baseline and the payload carries no reconstructed one — current `--upgrade` stages `.template-baseline.reconstructed.json` inside the payload whenever reconstruction succeeds), tell the user: "No template baseline — this first update asks about every changed file individually; once it finishes and writes the baseline (Step 4), later updates won't." Template changes then land in `differs`. `baselineSource` names which file was used and `baselineReconstructed` flags a reconstructed one.
|
|
53
68
|
|
|
54
69
|
Templates (`*.tmpl`, which install with `{{project-name}}` substitution), `_gitignore` (merged line-by-line), and `.manifest.json` (payload metadata) are not classified — each is handled by its own sub-step in Step 3.
|
|
55
70
|
|
|
56
71
|
Report with version info from the manifest:
|
|
57
72
|
```
|
|
58
|
-
"Template update: v{fromVersion} → v{templateVersion}. {N} new files, {M} locally modified, {A} activated rules, {R} removed files, {K} unchanged."
|
|
73
|
+
"Template update: v{fromVersion} → v{templateVersion}. {N} new files, {U} template-updated, {M} locally modified, {C} config files to merge, {L} local-only edits, {D} deleted locally, {A} activated rules, {R} removed files, {K} unchanged."
|
|
59
74
|
```
|
|
60
75
|
|
|
61
|
-
If `new`, `differs`, `activated`, and `removed` are all empty, report: "Workspace is up to date (template v{templateVersion}). No changes needed."
|
|
76
|
+
If `new`, `updated`, `differs`, `config` (an entry with empty `added`/`workspaceOnly`/`changed` lists and no array elements to merge counts as empty), `deletedLocally`, `activated`, and `removed` are all empty, report: "Workspace is up to date (template v{templateVersion}). No changes needed." (`localOnly` files are informational and `staleTests` may still be worth offering.)
|
|
62
77
|
|
|
63
78
|
### Step 2b: Historical .gitignore safety check
|
|
64
79
|
|
|
@@ -75,29 +90,44 @@ git ls-files | grep -E '^repos$'
|
|
|
75
90
|
```
|
|
76
91
|
If found, untrack it: `git rm --cached repos`.
|
|
77
92
|
|
|
78
|
-
Commit the fix **before** applying other template updates. This runs ahead of Step 3 because applying other updates while the bug is still present could itself trigger the destruction on workspaces that still have the old layout.
|
|
93
|
+
Commit the fix **before** applying other template updates — on the task branch in the worktree flow (Step 1), in place in the no-remote flow. This runs ahead of Step 3 because applying other updates while the bug is still present could itself trigger the destruction on workspaces that still have the old layout.
|
|
79
94
|
|
|
80
95
|
### Step 3: Selective update
|
|
81
96
|
|
|
82
|
-
Batch the safe
|
|
97
|
+
Batch the safe cases, ask on the rest:
|
|
83
98
|
|
|
84
|
-
- **New files (`new`):** present
|
|
85
|
-
- **Locally modified (`differs`):** ask per file — "
|
|
99
|
+
- **New and template-updated files (`new` + `updated`):** present both lists once — "Apply these {N} new and {U} template-updated files? [Y/n]" — and install them all on confirmation. No per-file prompting: `updated` means the file still holds exactly what the template last shipped here, so applying the new version loses nothing.
|
|
100
|
+
- **Locally modified (`differs`):** ask per file — "Your version of {file} differs from the template's. Show diff? [y/N]" — then apply, keep, or merge per the user's decision.
|
|
101
|
+
- **Config files (`config`):** `.mcp.json` and `.claude/settings.json` are never copied wholesale — a batch copy wipes the workspace's own MCP servers and settings. Merge each entry key by key (values from `{payload}/{path}` and the workspace's copy): add every `added` key, keep every `workspaceOnly` key untouched, and for each `changed` key ask — "Template changed `{key}` in `{path}`. Take the template's, keep yours, or inspect?" Array-valued keys merge as a union, no ask: keep the workspace's elements in place and append each `arrays` entry's `added` elements (`workspaceOnly` elements are already in place, listed for visibility). `notInstalled` — ask once: "Install {path} from the template? [Y/n]" (never install a config silently); `unparseable` means broken JSON on one side — show the file and ask, never merge blind.
|
|
102
|
+
- **Local-only edits (`localOnly`):** nothing to decide — these are your local edits to files the template hasn't changed since the last update. List them in the summary (so the edits are visible) and move on; do not ask about them.
|
|
103
|
+
- **Deleted locally (`deletedLocally`):** "These {N} files exist in the template and its baseline but not in your workspace — deleted locally (or never installed). Restore from the template? [Y/n]" — one confirmation for the whole list. Restoring installs the payload's version of each.
|
|
86
104
|
- **Removed in template (`removed`):** "Template removed {file}. Delete locally? [y/N]" — conservative default.
|
|
87
105
|
- **Activated rules (`activated`):** keep the active file, install nothing — report: "Rule {name} is active here and optional in the template; your activation is preserved."
|
|
106
|
+
- **Stale tests (`staleTests`):** "These {N} test files under .claude/ came from a dev checkout — the package never ships them, so updates can't refresh them (tests live in the template repo). Remove them? [Y/n]" — one confirmation for the whole list.
|
|
88
107
|
- **Hook migration (.sh to .mjs):** Detect old `.sh` hooks in `.claude/hooks/` that have `.mjs` replacements in the payload. Offer: "Hook {name}.sh has a .mjs replacement in the update. Replace and update settings.json commands? [Y/n]" — this is a one-time migration for workspaces upgrading from pre-0.2.0
|
|
89
108
|
|
|
90
109
|
Also handle these non-component files from the payload:
|
|
91
110
|
|
|
92
|
-
- **settings.json:** Merge payload values into existing `.claude/settings.json` — do not overwrite user customizations. Add new keys, update hook commands if hooks were migrated, preserve user-added entries.
|
|
93
111
|
- **workspace.json keys:** Compare the `workspace` object of the payload's `workspace.json.tmpl` with the installed `workspace.json`, key by key. For each key the template ships that the workspace lacks, show its template default and ask before adding. Never remove an existing key just because the template no longer ships it. `canonicalBudgetBytes` is opt-in since v0.19 — leave an existing value alone and mention it can be removed to turn the budget off.
|
|
94
|
-
- **CLAUDE.md:** If `{payload}/CLAUDE.md.tmpl` exists,
|
|
95
|
-
|
|
112
|
+
- **CLAUDE.md:** If `{payload}/CLAUDE.md.tmpl` exists, merge — never regenerate from scratch:
|
|
113
|
+
```bash
|
|
114
|
+
node {payload}/.claude/scripts/classify-update.mjs --root . --payload {payload} --merge-claude-md
|
|
115
|
+
```
|
|
116
|
+
The command prints the merged CLAUDE.md: template-owned lines take the template's new versions (skill-list entries match by their `/name`), while lines the template doesn't have — the workspace's own skill entries, custom bullets, whole sections — are kept in place. Two consequences to watch in the diff: an edit made directly to a template-owned skill line is replaced by the template's new wording, and a template prose line that was reworded locally survives alongside the new template line (it may appear twice). The merge keeps the current file's line endings. Show the user the diff against the current CLAUDE.md before writing the merged result. (The two JSON configs are the `config` list's, not this block's.)
|
|
117
|
+
- **.gitignore:** Merge new entries from the payload's `_gitignore` into the existing `.gitignore` — do not remove user-added lines. An ignore pattern does not untrack already-committed files: if the workspace still tracks the per-machine catalogs the template now ignores (`git ls-files -- 'workspace-context/team-member/*/index.md'`), untrack them (`git rm -r --cached 'workspace-context/team-member/*/index.md'`), or every machine's regenerations keep dirtying pulls.
|
|
96
118
|
|
|
97
|
-
### Step 4: Update version
|
|
119
|
+
### Step 4: Update version and write the baseline
|
|
98
120
|
|
|
99
121
|
Read `templateVersion` from `.workspace-update/.manifest.json` and update `templateVersion` in `workspace.json` to match.
|
|
100
122
|
|
|
123
|
+
Then write the template baseline so the NEXT update classifies three ways instead of asking per file:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
node {payload}/.claude/scripts/classify-update.mjs --root . --payload {payload} --write-baseline
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Run it after every Step 3 decision has been made (it runs from the payload because the workspace's own copy may predate this update). In the worktree flow this writes the baseline inside the worktree (`--root .`), so it is committed through the PR and reaches every clone — and it reads the same baseline the classification used (the worktree has none of its own yet, so the payload's reconstructed file supplies the old entries a declined update keeps). It records the hash of every verbatim payload file — what the template now ships — with one deliberate exception: a file whose update was declined (the workspace still holds the old baseline content while the payload ships something new) keeps the OLD entry, so the change is offered again as `updated` next time instead of being filed away. Everything else records the payload hash: a file the user kept in their own version reads as `localOnly` (informational) until the template changes it again, and a file nobody touched never reads as a local edit. The command refuses to write an empty baseline — if it errors, the payload path is wrong; do not force it.
|
|
130
|
+
|
|
101
131
|
### Step 4a: Run idempotent migrators
|
|
102
132
|
|
|
103
133
|
Two migrators run on **every** update — both idempotent, safe on already-migrated workspaces. Run each and surface its action in the upgrade summary.
|
|
@@ -138,7 +168,7 @@ node .claude/scripts/build-workspace-context.mjs --check --root .
|
|
|
138
168
|
|
|
139
169
|
`--check` must exit clean. If it reports drift, re-run `--write` and check again before continuing.
|
|
140
170
|
|
|
141
|
-
Then run the scripted audit — once, covering everything (gh:180). Build the changed list — every file this update added or changed: the applied `new` and `differs` files plus the merged ones (`CLAUDE.md`, `workspace.json`, `.gitignore`, `.claude/settings.json`, `.mcp.json` as touched) — one path per line into a temp file such as `workspace-scratchpad/update-changed.txt`, then:
|
|
171
|
+
Then run the scripted audit — once, covering everything (gh:180). Build the changed list — every file this update added or changed: the applied `new`, `updated`, and `differs` files, any restored `deletedLocally` files, plus the merged ones (`CLAUDE.md`, `workspace.json`, `.gitignore`, `.claude/settings.json`, `.mcp.json` as touched) — one path per line into a temp file such as `workspace-scratchpad/update-changed.txt`, then:
|
|
142
172
|
|
|
143
173
|
```bash
|
|
144
174
|
node {payload}/.claude/scripts/maintenance-audit.mjs --root . --changed workspace-scratchpad/update-changed.txt
|
|
@@ -160,14 +190,19 @@ Delete the `.workspace-update/` directory at the launcher — in the worktree fl
|
|
|
160
190
|
|
|
161
191
|
### Step 7: Commit
|
|
162
192
|
|
|
163
|
-
Where the commit lands was decided in Step 1.
|
|
193
|
+
Where the commit lands was decided in Step 1 — the launcher's default branch is never pushed directly.
|
|
164
194
|
|
|
165
195
|
- **No remote:** commit in place on the launcher's default branch — the one sanctioned launcher commit:
|
|
166
196
|
```bash
|
|
167
197
|
git add -A
|
|
168
198
|
git commit -m "chore: update workspace from template v{fromVersion} to v{templateVersion}"
|
|
169
199
|
```
|
|
170
|
-
- **Remote exists:** from the task worktree created in Step 1, commit the applied update, push the branch, and open
|
|
200
|
+
- **Remote exists:** from the task worktree created in Step 1, commit the applied update, push the branch, and open the PR/MR through `node .claude/scripts/task-pr.mjs` — it drives GitHub today and GitLab once gh:185 lands; if it reports the forge unsupported, open the MR with the forge's own CLI (for GitLab, `glab mr create`) from the worktree and say so in the report. After the PR/MR merges, restore the launcher's skill copy before pulling — `--upgrade` replaced `.claude/skills/workspace-update/` in the launcher (a tracked modification) and the merged PR delivers the same content, so a dirty launcher blocks the pull. If `.claude/skills/workspace-update/SKILL.md.local-backup` sits there (a customised skill the CLI backed up), move it somewhere safe first (e.g. `workspace-scratchpad/`), then:
|
|
201
|
+
```bash
|
|
202
|
+
git -C {launcher} checkout -- .claude/skills/workspace-update
|
|
203
|
+
git -C {launcher} clean -f -- .claude/skills/workspace-update
|
|
204
|
+
```
|
|
205
|
+
Then pull the launcher and delete the payload (Step 6).
|
|
171
206
|
|
|
172
207
|
Report: "Workspace updated to v{templateVersion}. Restart Claude Code if rules or hooks changed."
|
|
173
208
|
|
|
@@ -177,10 +212,11 @@ After the update is applied, if the sessions directory (`workspace.workSessionsD
|
|
|
177
212
|
|
|
178
213
|
## Notes
|
|
179
214
|
|
|
180
|
-
- The CLI (`npx @ulysses-ai/create-workspace --upgrade`) stages the payload
|
|
181
|
-
- Never overwrites without asking — `new` files are batched behind one confirmation; `differs` files are asked per file
|
|
182
|
-
- Preserves local modifications, custom content, existing `workspace.json` keys, and deliberately activated rules
|
|
183
|
-
- The
|
|
215
|
+
- The CLI (`npx @ulysses-ai/create-workspace --upgrade`) stages the payload, installs the current copy of this skill into `.claude/skills/workspace-update/` (so an outdated installed flow never processes a new payload; a locally customised SKILL.md is backed up as `SKILL.md.local-backup` first), and stages a reconstructed baseline inside the payload as `.template-baseline.reconstructed.json` when the workspace has none — the launcher itself gets no new untracked files, and Step 7 restores the launcher's skill copy before the post-merge pull
|
|
216
|
+
- Never overwrites without asking — `new` and `updated` files are batched behind one confirmation; `differs` files are asked per file; `config` files (`.mcp.json`, `.claude/settings.json`) are merged key by key (array-valued keys union-merged) and never copied wholesale; `localOnly` files are never asked about (local edits to files the template didn't touch)
|
|
217
|
+
- Preserves local modifications, custom content, the workspace's own MCP servers and settings, existing `workspace.json` keys, and deliberately activated rules
|
|
218
|
+
- The template baseline (`.claude/.template-baseline.json`) is what separates `updated`, `differs`, and `localOnly`: entries hold the payload hash of the last-shipped content (unapplied updates keep the older entry), so a deliberately kept local edit stays visible across updates while an untouched file never prompts
|
|
219
|
+
- The launcher's default branch takes a template-update commit only when the workspace repo has no remote; with ANY remote, the update lands through a task worktree, a branch, and a PR/MR — the default branch is never pushed directly
|
|
184
220
|
- Can be run multiple times safely (idempotent) — if `.workspace-update/` doesn't exist, it reports no payload and exits
|
|
185
221
|
- Initial setup is handled by `npx @ulysses-ai/create-workspace --init` + `/workspace-init` — this skill is for subsequent updates only
|
|
186
222
|
- The `.sh` to `.mjs` hook migration is a one-time transition for workspaces created before hooks moved to JavaScript
|