@ulysses-ai/create-workspace 0.19.0-beta.0 → 0.21.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/lib/upgrade.mjs +20 -0
  3. package/lib/upgrade.test.mjs +119 -0
  4. package/package.json +3 -3
  5. package/template/CLAUDE.md.tmpl +5 -2
  6. package/template/_claude/hooks/workspace-update-check.mjs +4 -4
  7. package/template/_claude/lib/freshness.mjs +20 -7
  8. package/template/_claude/lib/registry-check.mjs +80 -14
  9. package/template/_claude/scripts/build-workspace-context.mjs +2 -0
  10. package/template/_claude/scripts/chat-record.mjs +27 -19
  11. package/template/_claude/scripts/classify-update.mjs +212 -0
  12. package/template/_claude/scripts/cleanup-work-session.mjs +4 -2
  13. package/template/_claude/scripts/maintenance-audit.mjs +0 -0
  14. package/template/_claude/scripts/merge-mode.mjs +61 -0
  15. package/template/_claude/scripts/migrate-canonical-priority.mjs +7 -1
  16. package/template/_claude/scripts/migrate-claude-md-freshness-include.mjs +35 -10
  17. package/template/_claude/scripts/migrate-session-layout.mjs +19 -8
  18. package/template/_claude/scripts/migrate-sessions.mjs +444 -121
  19. package/template/_claude/scripts/task-pr.mjs +213 -105
  20. package/template/_claude/scripts/task-worktree.mjs +26 -16
  21. package/template/_claude/skills/complete-work/SKILL.md +16 -12
  22. package/template/_claude/skills/maintenance/SKILL.md +48 -108
  23. package/template/_claude/skills/migrate-sessions/SKILL.md +17 -5
  24. package/template/_claude/skills/start-work/SKILL.md +4 -4
  25. package/template/_claude/skills/workspace-init/SKILL.md +20 -14
  26. package/template/_claude/skills/workspace-update/SKILL.md +93 -40
  27. package/template/_gitignore +3 -0
  28. package/template/workspace.json.tmpl +0 -1
@@ -1,42 +1,64 @@
1
1
  ---
2
2
  name: workspace-update
3
- description: Apply a staged template update to an initialized workspace. The CLI stages a payload in .workspace-update/; this skill processes it. Runs maintenance audit before and after.
3
+ description: Apply a staged template update to an initialized workspace. The CLI stages a payload in .workspace-update/; this skill processes it and verifies the result with a scripted audit.
4
4
  ---
5
5
 
6
6
  # Workspace Update
7
7
 
8
- Apply a staged template update to an initialized workspace. The CLI (`npx @ulysses-ai/create-workspace --upgrade`) stages the payload in `.workspace-update/`. This skill reads and applies it. Runs a maintenance audit before updating and verifies integrity after.
8
+ Apply a staged template update to an initialized workspace. The CLI (`npx @ulysses-ai/create-workspace --upgrade`) stages the payload in `.workspace-update/`. This skill reads and applies it, then verifies the result with the scripted maintenance audit — one audit, at the end, when there is something to verify (gh:180).
9
9
 
10
10
  ## Prerequisites
11
11
 
12
12
  - `workspace.json` must have `initialized: true`
13
- - If not initialized, report: "Workspace not initialized. Run /workspace-init first."
13
+ - If not initialized, check whether initialization was committed but never merged — the workspace-init flow ends with its commits merged to the default branch, so an unmerged init branch explains a missing flag:
14
+ ```bash
15
+ git log --all --format=%H -S'"initialized": true' -- workspace.json
16
+ ```
17
+ If there are hits, name the branch holding the newest commit (`git branch --all --contains {sha}`) and report: "This workspace was initialized on branch `{branch}`, but that branch was never merged. Merge it first (`git merge {branch}`), then re-run /workspace-update." Only if there are no hits, report: "Workspace not initialized. Run /workspace-init first."
14
18
  - `.workspace-update/` payload directory must exist (staged by `npx @ulysses-ai/create-workspace --upgrade`)
15
19
  - If no `.workspace-update/` payload exists, report: "No update payload found. Run `npx @ulysses-ai/create-workspace --upgrade` to stage the template."
16
- - Read `.workspace-update/.manifest.json` for `fromVersion`, `toVersion`, and `action`
20
+ - Read `.workspace-update/.manifest.json` for `fromVersion`, `templateVersion` (the target version), and `action`
17
21
  - If `action` is `"init"`, report: "This payload is for initial setup. Run /workspace-init instead."
18
22
 
19
23
  ## Flow
20
24
 
21
- ### Step 1: Pre-update health check
25
+ ### Step 1: Decide where the update lands
22
26
 
23
- Run `/maintenance audit` (read-only) to surface existing issues. Report findings briefly but **always continue to Step 2 immediately** — do not stop to ask about audit results. The audit is informational, not a gate. Any issues found will be included in the post-update report (Step 5) alongside the update results.
27
+ Check the workspace repo for a remote (`git remote`). This decides where every later step works:
24
28
 
25
- ### Step 2: Compare current vs payload
29
+ - **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/`.
30
+ - **A remote exists** — the launcher never commits to its default branch. Create a task worktree up front and treat it as the workspace root for Steps 2–6:
31
+ ```bash
32
+ node .claude/scripts/task-worktree.mjs --root . --create --repo . --branch chore/template-update-{version}
33
+ ```
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 PRs from the worktree.
26
35
 
27
- For each component directory in `.workspace-update/.claude/` (skills, hooks, agents, rules, recipes), compare files against the corresponding `.claude/{component}/` directory locally:
36
+ In the commands below, `{payload}` is `.workspace-update` in the no-remote flow and `{launcher}/.workspace-update` in the worktree flow.
28
37
 
29
- - **New files:** present in `.workspace-update/.claude/{component}/` but not in `.claude/{component}/`
30
- - **Updated files:** present in both but contents differ
31
- - **Unchanged files:** present in both with identical contents
32
- - **Removed files:** present in `.claude/{component}/` locally but not in `.workspace-update/.claude/{component}/`
38
+ ### Step 2: Classify the payload
39
+
40
+ Run the classifier — it compares every verbatim-installed payload file (`.claude/**`, `.mcp.json`, `.claudeignore`) against the workspace by content, and detects files the template no longer ships:
41
+
42
+ ```bash
43
+ node {payload}/.claude/scripts/classify-update.mjs --root . --payload {payload}
44
+ ```
45
+
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. Output is JSON with five lists:
47
+
48
+ - `new` — no installed counterpart; safe to batch-apply (Step 3) behind one confirmation
49
+ - `identical` — installed file already equals the payload; skip silently
50
+ - `differs` — installed file was locally modified; needs a per-file decision
51
+ - `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. Tests (`*.test.mjs`), 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.
53
+
54
+ 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.
33
55
 
34
56
  Report with version info from the manifest:
35
57
  ```
36
- "Template update: v{fromVersion} → v{toVersion}. {N} new files, {M} updated files, {R} removed files, {K} unchanged."
58
+ "Template update: v{fromVersion} → v{templateVersion}. {N} new files, {M} locally modified, {A} activated rules, {R} removed files, {K} unchanged."
37
59
  ```
38
60
 
39
- If everything is unchanged and there are no new or removed files, report: "Workspace is up to date (template v{toVersion}). No changes needed."
61
+ If `new`, `differs`, `activated`, and `removed` are all empty, report: "Workspace is up to date (template v{templateVersion}). No changes needed."
40
62
 
41
63
  ### Step 2b: Historical .gitignore safety check
42
64
 
@@ -57,30 +79,31 @@ Commit the fix **before** applying other template updates. This runs ahead of St
57
79
 
58
80
  ### Step 3: Selective update
59
81
 
60
- For each change, ask before applying:
82
+ Batch the safe case, ask on the rest:
61
83
 
62
- - **New file:** "Add {file}? [Y/n]"
63
- - **Updated file (no local mods):** "Update {file} to latest template? [Y/n]"
64
- - **Updated file (locally modified):** "Template updated {file} but you have local changes. Show diff? [y/N]" — let user decide
65
- - **Removed in template:** "Template removed {file}. Delete locally? [y/N]" — conservative default
84
+ - **New files (`new`):** present the list once — "Apply these {N} new files? [Y/n]" — and install them all on confirmation. No per-file prompting.
85
+ - **Locally modified (`differs`):** ask per file — "Template updated {file} but you have local changes. Show diff? [y/N]" — then apply, keep, or merge per the user's decision.
86
+ - **Removed in template (`removed`):** "Template removed {file}. Delete locally? [y/N]" — conservative default.
87
+ - **Activated rules (`activated`):** keep the active file, install nothing — report: "Rule {name} is active here and optional in the template; your activation is preserved."
66
88
  - **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
67
89
 
68
90
  Also handle these non-component files from the payload:
69
91
 
70
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.
71
- - **CLAUDE.md:** If `.workspace-update/CLAUDE.md.tmpl` exists, regenerate `CLAUDE.md` from the template. Preserve any user-added sections not present in the template.
72
- - **.gitignore:** Merge new entries from the payload into the existing `.gitignore` — do not remove user-added lines.
93
+ - **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, regenerate `CLAUDE.md` from the template. Preserve any user-added sections not present in the template.
95
+ - **.gitignore:** Merge new entries from the payload's `_gitignore` into the existing `.gitignore` — do not remove user-added lines.
73
96
 
74
97
  ### Step 4: Update version
75
98
 
76
- Read `toVersion` from `.workspace-update/.manifest.json` and update `templateVersion` in `workspace.json` to match.
99
+ Read `templateVersion` from `.workspace-update/.manifest.json` and update `templateVersion` in `workspace.json` to match.
77
100
 
78
101
  ### Step 4a: Run idempotent migrators
79
102
 
80
- The payload may include migrator scripts at `.workspace-update/.claude/scripts/migrate-*.mjs` that bring older workspaces forward in shape. They are idempotent — safe to re-run on already-migrated workspaces. Run each one in document order and surface its action in the upgrade summary.
103
+ Two migrators run on **every** update — both idempotent, safe on already-migrated workspaces. Run each and surface its action in the upgrade summary.
81
104
 
82
105
  ```bash
83
- node .workspace-update/.claude/scripts/migrate-claude-md-freshness-include.mjs
106
+ node {payload}/.claude/scripts/migrate-claude-md-freshness-include.mjs --root .
84
107
  ```
85
108
 
86
109
  Output is JSON: `{"action":"appended"|"unchanged"|"skipped"}`.
@@ -90,34 +113,63 @@ Output is JSON: `{"action":"appended"|"unchanged"|"skipped"}`.
90
113
  - `skipped` — no `CLAUDE.md` exists at the workspace root (rare; surface to the user).
91
114
 
92
115
  ```bash
93
- node .workspace-update/.claude/scripts/migrate-canonical-priority.mjs --root .
116
+ node {payload}/.claude/scripts/migrate-canonical-priority.mjs --root .
94
117
  ```
95
118
 
96
- Output is JSON: `{"status":"applied"|"noop","files":[...]}`. Back-fills `priority: critical` on every `workspace-context/shared/locked/*.md` that lacks the field, preserving today's full-load behavior until the user explicitly demotes a file. Idempotent — safe to re-run on already-migrated workspaces.
119
+ Output is JSON: `{"status":"applied"|"noop","files":[...]}`. Back-fills `priority: critical` on every `workspace-context/shared/locked/*.md` that lacks the field, preserving today's full-load behavior until the user explicitly demotes a file. Skips `local-only-*` files — they are machine-local, never canonical.
120
+
121
+ The other `migrate-*.mjs` in the payload are **one-shot, version-gated migrations**: each applies to a specific step of the template's history and runs only when the manifest's `fromVersion` is older than the version that introduced it. Never run them unconditionally.
122
+
123
+ - `migrate-session-layout.mjs` — pre-v0.10.0 → v0.10.0: moves session content from launcher-side paths into each session's workspace worktree.
124
+ - `migrate-to-workspace-context.mjs` — pre-v0.15.0 → v0.15.0: renames `shared-context/` to `workspace-context/` and rebuilds its structure.
125
+ - `migrate-sessions.mjs` — pre-v0.18.0: drains session-model work sessions onto the task lifecycle. Not run inline — it has interactive inventory/backup/archive modes; `/migrate-sessions` drives it (Step 8 nudges when it applies).
126
+ - `migrate-open-work.mjs` — manual, any version: converts the deprecated `open-work.md` into tracker issues. Takes the file path as an argument and needs a configured tracker; run it only if the workspace still carries an `open-work.md` and the user asks.
97
127
 
98
- Add other migrators here as the template ships them.
128
+ Always run migrators with `--root .`. They resolve the workspace root from `--root` (default: the cwd) and never from their own location — a migrator invoked from the payload without `--root` would look for the workspace inside `.workspace-update/`.
99
129
 
100
130
  ### Step 5: Post-update verification
101
131
 
102
- Run `/maintenance audit` again to verify the update didn't introduce:
103
- - Broken references (new skills not in CLAUDE.md, removed rules still referenced)
104
- - Contradictions between updated rules and existing shared context
105
- - Structural mismatches
132
+ First regenerate the context catalogs — an update that adds or renames files under `.claude/` or `workspace-context/` leaves `index.md`/`canonical.md` stale until they are rebuilt:
133
+
134
+ ```bash
135
+ node .claude/scripts/build-workspace-context.mjs --write --root .
136
+ node .claude/scripts/build-workspace-context.mjs --check --root .
137
+ ```
138
+
139
+ `--check` must exit clean. If it reports drift, re-run `--write` and check again before continuing.
140
+
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:
142
+
143
+ ```bash
144
+ node {payload}/.claude/scripts/maintenance-audit.mjs --root . --changed workspace-scratchpad/update-changed.txt
145
+ ```
146
+
147
+ Run it from the payload for the same reason as the classifier in Step 2: the workspace's own copy may predate this update. It reuses the sections of `/maintenance` audit that a script can decide (cross-references, frontmatter, structure, git state, catalog integrity, budgets, freshness) and marks findings on changed files `(from this update)`.
148
+
149
+ Present its report as the verification result. Git-state warnings are expected here — the update's commit hasn't landed yet (Step 7). Delete the temp changed list afterwards.
150
+
151
+ - Findings labeled `(from this update)` — caused by this update; fix before committing (usually a new skill missing from CLAUDE.md's list, or a stale catalog).
152
+ - Other findings — pre-existing; mention briefly.
153
+ - If the audit found issue-severity findings, offer to walk through them with the full `/maintenance` skill. Warnings and infos alone need no follow-up.
106
154
 
107
155
  Report: "Post-update verification: {N} issues found" or "Post-update verification clean."
108
156
 
109
157
  ### Step 6: Cleanup
110
158
 
111
- Delete the `.workspace-update/` directory entirely. The payload has been fully processed and is no longer needed.
159
+ Delete the `.workspace-update/` directory at the launcher — in the worktree flow (Step 1) that happens after the merge and pull in Step 7. The payload has been fully processed and is no longer needed.
112
160
 
113
161
  ### Step 7: Commit
114
162
 
115
- ```bash
116
- git add -A
117
- git commit -m "chore: update workspace from template v{fromVersion} to v{toVersion}"
118
- ```
163
+ Where the commit lands was decided in Step 1.
164
+
165
+ - **No remote:** commit in place on the launcher's default branch — the one sanctioned launcher commit:
166
+ ```bash
167
+ git add -A
168
+ git commit -m "chore: update workspace from template v{fromVersion} to v{templateVersion}"
169
+ ```
170
+ - **Remote exists:** from the task worktree created in Step 1, commit the applied update, push the branch, and open a PR through the forge adapter — `node .claude/scripts/task-pr.mjs` when the workspace has it, otherwise the adapter under `.claude/scripts/forges/`. After the PR merges, pull the launcher, then delete the payload (Step 6).
119
171
 
120
- Report: "Workspace updated to v{toVersion}. Restart Claude Code if rules or hooks changed."
172
+ Report: "Workspace updated to v{templateVersion}. Restart Claude Code if rules or hooks changed."
121
173
 
122
174
  ### Step 8: Session-model migration nudge
123
175
 
@@ -126,9 +178,10 @@ After the update is applied, if the sessions directory (`workspace.workSessionsD
126
178
  ## Notes
127
179
 
128
180
  - The CLI (`npx @ulysses-ai/create-workspace --upgrade`) stages the payload. This skill processes it.
129
- - Never overwrites without asking
130
- - Preserves local modifications and custom content
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 launcher's default branch takes a template-update commit only when the workspace has no remote; with a remote, the update lands through a task worktree and a PR
131
184
  - Can be run multiple times safely (idempotent) — if `.workspace-update/` doesn't exist, it reports no payload and exits
132
185
  - Initial setup is handled by `npx @ulysses-ai/create-workspace --init` + `/workspace-init` — this skill is for subsequent updates only
133
186
  - The `.sh` to `.mjs` hook migration is a one-time transition for workspaces created before hooks moved to JavaScript
134
- - The maintenance audits are read-only and non-blocking — they surface issues but don't prevent the update
187
+ - The post-update audit is read-only and non-blocking — it surfaces issues, labels the ones this update caused, and never prevents the update itself
@@ -12,6 +12,9 @@ work-sessions/
12
12
  # Disposable workspace-scoped scratchpad (session log, hook debug output)
13
13
  workspace-scratchpad/
14
14
 
15
+ # Staged template payload — transient input for /workspace-update, never tracked
16
+ .workspace-update/
17
+
15
18
  # Personal overrides
16
19
  .claude/settings.local.json
17
20
  .claude/.active-session.json
@@ -10,7 +10,6 @@
10
10
  "subagentContextMaxBytes": 32768,
11
11
  "subagentInlineMaxBytes": 8192,
12
12
  "greeting": "Welcome back to {{project-name}}.",
13
- "releaseMode": "per-repo",
14
13
  "sessionModel": "session",
15
14
  "tracker": null,
16
15
  "forge": { "type": "github" }