@ulysses-ai/create-workspace 0.18.0-beta.0 → 0.20.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 (112) hide show
  1. package/lib/init.mjs +4 -1
  2. package/lib/payload.mjs +18 -1
  3. package/lib/payload.test.mjs +55 -0
  4. package/lib/scaffold.mjs +23 -6
  5. package/lib/scaffold.test.mjs +59 -0
  6. package/lib/upgrade.mjs +20 -0
  7. package/lib/upgrade.test.mjs +119 -0
  8. package/package.json +3 -3
  9. package/template/CLAUDE.md.tmpl +3 -0
  10. package/template/{.claude → _claude}/lib/freshness.mjs +20 -7
  11. package/template/_claude/lib/registry-check.mjs +172 -0
  12. package/template/{.claude → _claude}/rules/forge-operations.md +6 -0
  13. package/template/{.claude → _claude}/rules/memory-guidance.md +4 -0
  14. package/template/{.claude → _claude}/rules/task-list-mirroring.md +6 -0
  15. package/template/{.claude → _claude}/scripts/build-workspace-context.mjs +25 -14
  16. package/template/{.claude → _claude}/scripts/chat-record.mjs +62 -21
  17. package/template/_claude/scripts/classify-update.mjs +117 -0
  18. package/template/{.claude → _claude}/scripts/cleanup-work-session.mjs +4 -2
  19. package/template/{.claude → _claude}/scripts/context-footprint.mjs +139 -30
  20. package/template/{.claude → _claude}/scripts/forges/github.mjs +2 -1
  21. package/template/{.claude → _claude}/scripts/forges/interface.mjs +5 -4
  22. package/template/_claude/scripts/merge-mode.mjs +61 -0
  23. package/template/{.claude → _claude}/scripts/migrate-canonical-priority.mjs +7 -1
  24. package/template/_claude/scripts/migrate-claude-md-freshness-include.mjs +55 -0
  25. package/template/{.claude → _claude}/scripts/migrate-session-layout.mjs +19 -8
  26. package/template/{.claude → _claude}/scripts/migrate-sessions.mjs +444 -121
  27. package/template/_claude/scripts/task-pr.mjs +555 -0
  28. package/template/{.claude → _claude}/scripts/task-worktree.mjs +26 -16
  29. package/template/{.claude → _claude}/scripts/trackers/github-issues.mjs +11 -0
  30. package/template/{.claude → _claude}/scripts/trackers/interface.mjs +8 -0
  31. package/template/{.claude → _claude}/skills/braindump/SKILL.md +1 -0
  32. package/template/{.claude → _claude}/skills/complete-work/SKILL.md +25 -77
  33. package/template/{.claude → _claude}/skills/context-placement/SKILL.md +8 -5
  34. package/template/{.claude → _claude}/skills/goal-driven-work/SKILL.md +1 -1
  35. package/template/{.claude → _claude}/skills/handoff/SKILL.md +1 -0
  36. package/template/{.claude → _claude}/skills/maintenance/SKILL.md +49 -17
  37. package/template/{.claude → _claude}/skills/migrate-sessions/SKILL.md +17 -5
  38. package/template/{.claude → _claude}/skills/release/SKILL.md +6 -2
  39. package/template/{.claude → _claude}/skills/start-work/SKILL.md +5 -5
  40. package/template/{.claude → _claude}/skills/workspace-init/SKILL.md +20 -14
  41. package/template/_claude/skills/workspace-update/SKILL.md +177 -0
  42. package/template/_gitignore +3 -0
  43. package/template/workspace.json.tmpl +1 -1
  44. package/template/.claude/lib/registry-check.mjs +0 -106
  45. package/template/.claude/scripts/migrate-claude-md-freshness-include.mjs +0 -30
  46. package/template/.claude/skills/workspace-update/SKILL.md +0 -134
  47. /package/template/{.claude → _claude}/agents/aside-researcher.md +0 -0
  48. /package/template/{.claude → _claude}/agents/implementer.md +0 -0
  49. /package/template/{.claude → _claude}/agents/researcher.md +0 -0
  50. /package/template/{.claude → _claude}/agents/reviewer.md +0 -0
  51. /package/template/{.claude → _claude}/hooks/_utils.mjs +0 -0
  52. /package/template/{.claude → _claude}/hooks/bash-output-advisory.mjs +0 -0
  53. /package/template/{.claude → _claude}/hooks/post-compact.mjs +0 -0
  54. /package/template/{.claude → _claude}/hooks/pre-compact.mjs +0 -0
  55. /package/template/{.claude → _claude}/hooks/repo-write-detection.mjs +0 -0
  56. /package/template/{.claude → _claude}/hooks/session-end.mjs +0 -0
  57. /package/template/{.claude → _claude}/hooks/session-start.mjs +0 -0
  58. /package/template/{.claude → _claude}/hooks/subagent-start.mjs +0 -0
  59. /package/template/{.claude → _claude}/hooks/version-freshness-check.mjs +0 -0
  60. /package/template/{.claude → _claude}/hooks/workspace-update-check.mjs +0 -0
  61. /package/template/{.claude → _claude}/lib/require-node.mjs +0 -0
  62. /package/template/{.claude → _claude}/lib/session-frontmatter.mjs +0 -0
  63. /package/template/{.claude → _claude}/recipes/migrate-from-notion.md +0 -0
  64. /package/template/{.claude → _claude}/rules/agent-rules.md.skip +0 -0
  65. /package/template/{.claude → _claude}/rules/cloud-infrastructure.md.skip +0 -0
  66. /package/template/{.claude → _claude}/rules/coherent-revisions.md +0 -0
  67. /package/template/{.claude → _claude}/rules/config-review.md.skip +0 -0
  68. /package/template/{.claude → _claude}/rules/documentation.md.skip +0 -0
  69. /package/template/{.claude → _claude}/rules/git-conventions.md +0 -0
  70. /package/template/{.claude → _claude}/rules/goal-driven-work.md +0 -0
  71. /package/template/{.claude → _claude}/rules/honest-pushback.md +0 -0
  72. /package/template/{.claude → _claude}/rules/local-dev-environment.md.skip +0 -0
  73. /package/template/{.claude → _claude}/rules/product-integrity.md.skip +0 -0
  74. /package/template/{.claude → _claude}/rules/scope-guard.md.skip +0 -0
  75. /package/template/{.claude → _claude}/rules/superpowers-workflow.md.skip +0 -0
  76. /package/template/{.claude → _claude}/rules/token-economics.md.skip +0 -0
  77. /package/template/{.claude → _claude}/rules/work-item-tracking.md +0 -0
  78. /package/template/{.claude → _claude}/rules/workspace-structure.md +0 -0
  79. /package/template/{.claude → _claude}/scripts/add-repo-to-session.mjs +0 -0
  80. /package/template/{.claude → _claude}/scripts/capture-context.mjs +0 -0
  81. /package/template/{.claude → _claude}/scripts/create-work-session.mjs +0 -0
  82. /package/template/{.claude → _claude}/scripts/forges/gitlab.mjs +0 -0
  83. /package/template/{.claude → _claude}/scripts/generate-claude-local.mjs +0 -0
  84. /package/template/{.claude → _claude}/scripts/migrate-open-work.mjs +0 -0
  85. /package/template/{.claude → _claude}/scripts/migrate-to-workspace-context.mjs +0 -0
  86. /package/template/{.claude → _claude}/scripts/sweep-references.mjs +0 -0
  87. /package/template/{.claude → _claude}/scripts/sync-tasks.mjs +0 -0
  88. /package/template/{.claude → _claude}/scripts/workspace-diagnostics.mjs +0 -0
  89. /package/template/{.claude → _claude}/settings.json +0 -0
  90. /package/template/{.claude → _claude}/skills/aside/SKILL.md +0 -0
  91. /package/template/{.claude → _claude}/skills/build-docs-site/SKILL.md +0 -0
  92. /package/template/{.claude → _claude}/skills/build-docs-site/checklists/framing.md +0 -0
  93. /package/template/{.claude → _claude}/skills/build-docs-site/checklists/pitfalls.md +0 -0
  94. /package/template/{.claude → _claude}/skills/build-docs-site/checklists/review.md +0 -0
  95. /package/template/{.claude → _claude}/skills/build-docs-site/scripts/bulk-fill-migration.py +0 -0
  96. /package/template/{.claude → _claude}/skills/build-docs-site/scripts/forbidden-word-grep.mjs +0 -0
  97. /package/template/{.claude → _claude}/skills/build-docs-site/scripts/leak-grep.mjs +0 -0
  98. /package/template/{.claude → _claude}/skills/build-docs-site/templates/custom.css.tmpl +0 -0
  99. /package/template/{.claude → _claude}/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +0 -0
  100. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Arrow.tsx +0 -0
  101. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Box.tsx +0 -0
  102. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +0 -0
  103. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Region.tsx +0 -0
  104. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/SectionTitle.tsx +0 -0
  105. /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/tokens.ts +0 -0
  106. /package/template/{.claude → _claude}/skills/build-docs-site/templates/sidebars.ts.tmpl +0 -0
  107. /package/template/{.claude → _claude}/skills/build-docs-site/templates/spec.md.tmpl +0 -0
  108. /package/template/{.claude → _claude}/skills/pause-work/SKILL.md +0 -0
  109. /package/template/{.claude → _claude}/skills/promote/SKILL.md +0 -0
  110. /package/template/{.claude → _claude}/skills/setup-tracker/SKILL.md +0 -0
  111. /package/template/{.claude → _claude}/skills/sync-work/SKILL.md +0 -0
  112. /package/template/{.mcp.json → _mcp.json} +0 -0
@@ -26,11 +26,11 @@ If `workspace.json` has `"initialized": true` and no `.workspace-update/` payloa
26
26
 
27
27
  ## Branching
28
28
 
29
- Workspace-init creates a branch for all its work:
29
+ Workspace-init creates a branch for all its work. Record the default branch first (`git branch --show-current` before switching — usually `main`), then:
30
30
  ```bash
31
31
  git checkout -b chore/workspace-init
32
32
  ```
33
- All commits go on this branch. After completion, the user reviews and squash-merges to main.
33
+ All commits go on this branch. Step 18 merges the branch back into the default branch at the end — init must finish with every commit on the default branch, never stranded on the init branch (an unmerged init branch leaves `workspace.json` without `initialized: true` on the default branch, which blocks later `/workspace-update` runs). The granular per-step history stays on `chore/workspace-init` for review.
34
34
 
35
35
  ## Flow
36
36
 
@@ -373,9 +373,9 @@ git remote -v
373
373
  - Detect the org from project repo remotes in workspace.json
374
374
  - Ask: "Create workspace repo as `{org}/workspace-{project}`? Or provide a different name/URL."
375
375
  - Create via `gh repo create {org}/{name} --private` and add as remote
376
- - Do NOT push yet — user merges the branch first
376
+ - Do NOT push yet — Step 18 merges the init branch and pushes the default branch after the merge
377
377
 
378
- ### Step 18: Mark initialized and report
378
+ ### Step 18: Mark initialized, merge to the default branch, report
379
379
 
380
380
  Update workspace.json:
381
381
  - Set `initialized: true`
@@ -383,12 +383,20 @@ Update workspace.json:
383
383
 
384
384
  **Commit:** `git commit -m "chore: mark workspace as initialized"`
385
385
 
386
+ **Merge to the default branch.** Init is not finished while its commits live only on `chore/workspace-init` — an unmerged init branch leaves the default branch without `initialized: true`, which blocks later `/workspace-update` runs. Ask: "Merge chore/workspace-init into {default-branch} now? [Y/n]" and on yes:
387
+ ```bash
388
+ git checkout {default-branch}
389
+ git merge --squash chore/workspace-init
390
+ git commit -m "chore: workspace initialization"
391
+ ```
392
+ If a remote is configured, push the default branch (`git push origin {default-branch}`; use `--force-with-lease` only if the operator explicitly accepts rewritten history after a Step 17 rebase). Keep `chore/workspace-init` — it carries the granular per-step history — unless the user asks for it to be deleted. If the user declines the merge, the final report must tell them exactly how to finish it themselves.
393
+
386
394
  **Final report:**
387
395
 
388
396
  ```
389
397
  "Workspace initialized. Restart Claude Code for all rules and hooks to take effect. Then run /start-work to begin.
390
398
 
391
- Branch: chore/workspace-init
399
+ Branch: chore/workspace-init (merged to {default-branch} as "chore: workspace initialization")
392
400
 
393
401
  Summary:
394
402
  - {N} repos cloned
@@ -402,6 +410,7 @@ Summary:
402
410
  - {V} self-contradictions found and fixed
403
411
  - Template version: {version}
404
412
  - Remote: {status}
413
+ - All init commits are on {default-branch}
405
414
 
406
415
  Issues encountered:
407
416
  - {list every expected behavior that failed}
@@ -414,15 +423,12 @@ Active work sessions (formalized from existing worktrees):
414
423
  Items in workspace-scratchpad/unmigrated/:
415
424
  - {list each item with a one-line description}
416
425
 
417
- Review the branch:
418
- git log --oneline chore/workspace-init
419
- git diff main..chore/workspace-init
420
-
421
- Then merge:
422
- git checkout main
423
- git merge --squash chore/workspace-init
424
- git commit -m 'chore: workspace initialization'
425
- git push origin main
426
+ If the merge was declined, replace the branch line with:
427
+ Not merged yet — finish with:
428
+ git checkout {default-branch}
429
+ git merge --squash chore/workspace-init
430
+ git commit -m 'chore: workspace initialization'
431
+ git push origin {default-branch}
426
432
 
427
433
  This session is done. Start a fresh Claude Code session and run /start-work to begin."
428
434
  ```
@@ -0,0 +1,177 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Workspace Update
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.
9
+
10
+ ## Prerequisites
11
+
12
+ - `workspace.json` must have `initialized: true`
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."
18
+ - `.workspace-update/` payload directory must exist (staged by `npx @ulysses-ai/create-workspace --upgrade`)
19
+ - If no `.workspace-update/` payload exists, report: "No update payload found. Run `npx @ulysses-ai/create-workspace --upgrade` to stage the template."
20
+ - Read `.workspace-update/.manifest.json` for `fromVersion`, `templateVersion` (the target version), and `action`
21
+ - If `action` is `"init"`, report: "This payload is for initial setup. Run /workspace-init instead."
22
+
23
+ ## Flow
24
+
25
+ ### Step 1: Pre-update health check
26
+
27
+ 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.
28
+
29
+ ### Step 1b: Decide where the update lands
30
+
31
+ Check the workspace repo for a remote (`git remote`). This decides where every later step works:
32
+
33
+ - **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/`.
34
+ - **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:
35
+ ```bash
36
+ node .claude/scripts/task-worktree.mjs --root . --create --repo . --branch chore/template-update-{version}
37
+ ```
38
+ 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.
39
+
40
+ In the commands below, `{payload}` is `.workspace-update` in the no-remote flow and `{launcher}/.workspace-update` in the worktree flow.
41
+
42
+ ### Step 2: Classify the payload
43
+
44
+ Run the classifier — it compares every verbatim-installed payload file (`.claude/**`, `.mcp.json`, `.claudeignore`) against the workspace by content:
45
+
46
+ ```bash
47
+ node {payload}/.claude/scripts/classify-update.mjs --root . --payload {payload}
48
+ ```
49
+
50
+ 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 three lists:
51
+
52
+ - `new` — no installed counterpart; safe to batch-apply (Step 3) behind one confirmation
53
+ - `identical` — installed file already equals the payload; skip silently
54
+ - `differs` — installed file was locally modified; needs a per-file decision
55
+
56
+ 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.
57
+
58
+ Also list **removed files**: files present in the local `.claude/{component}/` with no counterpart in `{payload}/.claude/{component}/`.
59
+
60
+ Report with version info from the manifest:
61
+ ```
62
+ "Template update: v{fromVersion} → v{templateVersion}. {N} new files, {M} locally modified, {R} removed files, {K} unchanged."
63
+ ```
64
+
65
+ If `new`, `differs`, and the removed list are all empty, report: "Workspace is up to date (template v{templateVersion}). No changes needed."
66
+
67
+ ### Step 2b: Historical .gitignore safety check
68
+
69
+ Workspaces created before v0.5.1 are vulnerable to a destructive symlink bug in the old layout. The v0.8.0 layout removes the symlink entirely, so new workspaces are not vulnerable — but a workspace being upgraded from a pre-v0.8.0 version may still have the bad `.gitignore` pattern left over.
70
+
71
+ Check the workspace `.gitignore` for the `repos/` trailing-slash pattern:
72
+ ```bash
73
+ grep -E '^repos/$' .gitignore
74
+ ```
75
+
76
+ If found, rewrite it in place to `repos` (no trailing slash). Also check for any tracked `repos` symlink that was already committed:
77
+ ```bash
78
+ git ls-files | grep -E '^repos$'
79
+ ```
80
+ If found, untrack it: `git rm --cached repos`.
81
+
82
+ 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.
83
+
84
+ ### Step 3: Selective update
85
+
86
+ Batch the safe case, ask on the rest:
87
+
88
+ - **New files (`new`):** present the list once — "Apply these {N} new files? [Y/n]" — and install them all on confirmation. No per-file prompting.
89
+ - **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.
90
+ - **Removed in template:** "Template removed {file}. Delete locally? [y/N]" — conservative default.
91
+ - **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
92
+
93
+ Also handle these non-component files from the payload:
94
+
95
+ - **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.
96
+ - **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.
97
+ - **Rules renamed to `.skip`:** For each active `.claude/rules/{name}.md` whose template counterpart now ships as `{name}.md.skip`, keep the active file — it was deliberately activated — and tell the user that's what happened.
98
+ - **CLAUDE.md:** If `{payload}/CLAUDE.md.tmpl` exists, regenerate `CLAUDE.md` from the template. Preserve any user-added sections not present in the template.
99
+ - **.gitignore:** Merge new entries from the payload's `_gitignore` into the existing `.gitignore` — do not remove user-added lines.
100
+
101
+ ### Step 4: Update version
102
+
103
+ Read `templateVersion` from `.workspace-update/.manifest.json` and update `templateVersion` in `workspace.json` to match.
104
+
105
+ ### Step 4a: Run idempotent migrators
106
+
107
+ The payload may include migrator scripts at `{payload}/.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.
108
+
109
+ ```bash
110
+ node {payload}/.claude/scripts/migrate-claude-md-freshness-include.mjs --root .
111
+ ```
112
+
113
+ Output is JSON: `{"action":"appended"|"unchanged"|"skipped"}`.
114
+
115
+ - `appended` — the workspace's `CLAUDE.md` got the `@local-only-template-freshness.md` include line added at the end.
116
+ - `unchanged` — the line was already present.
117
+ - `skipped` — no `CLAUDE.md` exists at the workspace root (rare; surface to the user).
118
+
119
+ ```bash
120
+ node {payload}/.claude/scripts/migrate-canonical-priority.mjs --root .
121
+ ```
122
+
123
+ 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. Idempotent.
124
+
125
+ 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/`.
126
+
127
+ Add other migrators here as the template ships them.
128
+
129
+ ### Step 5: Post-update verification
130
+
131
+ Run `/maintenance audit` again to verify the update didn't introduce:
132
+ - Broken references (new skills not in CLAUDE.md, removed rules still referenced)
133
+ - Contradictions between updated rules and existing shared context
134
+ - Structural mismatches
135
+
136
+ Then 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:
137
+
138
+ ```bash
139
+ node .claude/scripts/build-workspace-context.mjs --write --root .
140
+ node .claude/scripts/build-workspace-context.mjs --check --root .
141
+ ```
142
+
143
+ `--check` must exit clean. If it reports drift, re-run `--write` and check again before continuing.
144
+
145
+ Report: "Post-update verification: {N} issues found" or "Post-update verification clean."
146
+
147
+ ### Step 6: Cleanup
148
+
149
+ Delete the `.workspace-update/` directory at the launcher — in the worktree flow (Step 1b) that happens after the merge and pull in Step 7. The payload has been fully processed and is no longer needed.
150
+
151
+ ### Step 7: Commit
152
+
153
+ Where the commit lands was decided in Step 1b.
154
+
155
+ - **No remote:** commit in place on the launcher's default branch — the one sanctioned launcher commit:
156
+ ```bash
157
+ git add -A
158
+ git commit -m "chore: update workspace from template v{fromVersion} to v{templateVersion}"
159
+ ```
160
+ - **Remote exists:** from the task worktree created in Step 1b, 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).
161
+
162
+ Report: "Workspace updated to v{templateVersion}. Restart Claude Code if rules or hooks changed."
163
+
164
+ ### Step 8: Session-model migration nudge
165
+
166
+ After the update is applied, if the sessions directory (`workspace.workSessionsDir`, default `work-sessions/`) has entries and `workspace.sessionModel` is not `"task"`, append one line to the report: "This workspace still has {N} session(s) under the session model — `/migrate-sessions` can inventory and drain them and switch to the task model whenever you're ready." Suggest only; the operator decides whether and when.
167
+
168
+ ## Notes
169
+
170
+ - The CLI (`npx @ulysses-ai/create-workspace --upgrade`) stages the payload. This skill processes it.
171
+ - Never overwrites without asking — `new` files are batched behind one confirmation; `differs` files are asked per file
172
+ - Preserves local modifications, custom content, existing `workspace.json` keys, and deliberately activated rules
173
+ - 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
174
+ - Can be run multiple times safely (idempotent) — if `.workspace-update/` doesn't exist, it reports no payload and exits
175
+ - Initial setup is handled by `npx @ulysses-ai/create-workspace --init` + `/workspace-init` — this skill is for subsequent updates only
176
+ - The `.sh` to `.mjs` hook migration is a one-time transition for workspaces created before hooks moved to JavaScript
177
+ - The maintenance audits are read-only and non-blocking — they surface issues but don't prevent the update
@@ -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
@@ -6,7 +6,7 @@
6
6
  "scratchpadDir": "workspace-scratchpad",
7
7
  "workSessionsDir": "work-sessions",
8
8
  "workspaceContextDir": "workspace-context",
9
- "canonicalBudgetBytes": 40960,
9
+ "alwaysLoadedBudgetBytes": 65536,
10
10
  "subagentContextMaxBytes": 32768,
11
11
  "subagentInlineMaxBytes": 8192,
12
12
  "greeting": "Welcome back to {{project-name}}.",
@@ -1,106 +0,0 @@
1
- import './require-node.mjs';
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
3
- import { dirname } from 'path';
4
-
5
- /**
6
- * SemVer 2.0 comparison limited to the formats this scaffolder publishes:
7
- * `x.y.z` and `x.y.z-prerelease.N`. Returns -1, 0, or 1.
8
- *
9
- * Rules:
10
- * - Compare major, minor, patch numerically.
11
- * - A pre-release version is older than the same x.y.z without a tag.
12
- * - Pre-release identifiers compare per-identifier; numeric identifiers
13
- * compare numerically (so `beta.10 > beta.2`), non-numeric lexically.
14
- */
15
- export function compareVersions(a, b) {
16
- if (a === b) return 0;
17
- const [aBase, aPre] = a.split('-', 2);
18
- const [bBase, bPre] = b.split('-', 2);
19
- const aParts = aBase.split('.').map(Number);
20
- const bParts = bBase.split('.').map(Number);
21
- for (let i = 0; i < 3; i++) {
22
- if ((aParts[i] || 0) < (bParts[i] || 0)) return -1;
23
- if ((aParts[i] || 0) > (bParts[i] || 0)) return 1;
24
- }
25
- if (!aPre && !bPre) return 0;
26
- if (!aPre && bPre) return 1;
27
- if (aPre && !bPre) return -1;
28
- const aIds = aPre.split('.');
29
- const bIds = bPre.split('.');
30
- const len = Math.max(aIds.length, bIds.length);
31
- for (let i = 0; i < len; i++) {
32
- const ai = aIds[i];
33
- const bi = bIds[i];
34
- if (ai === undefined) return -1;
35
- if (bi === undefined) return 1;
36
- const aNum = /^\d+$/.test(ai);
37
- const bNum = /^\d+$/.test(bi);
38
- if (aNum && bNum) {
39
- const an = Number(ai), bn = Number(bi);
40
- if (an < bn) return -1;
41
- if (an > bn) return 1;
42
- } else if (aNum && !bNum) {
43
- return -1;
44
- } else if (!aNum && bNum) {
45
- return 1;
46
- } else {
47
- if (ai < bi) return -1;
48
- if (ai > bi) return 1;
49
- }
50
- }
51
- return 0;
52
- }
53
-
54
- const REGISTRY_URL = 'https://registry.npmjs.org/@ulysses-ai/create-workspace/latest';
55
- const DEFAULT_TIMEOUT_MS = 3000;
56
-
57
- /**
58
- * Fetch the latest version of the scaffolder from the npm registry.
59
- * Returns { version, error } — exactly one of them is non-null.
60
- *
61
- * Caller injects fetchFn for testing. Default uses global fetch (Node 18+).
62
- */
63
- export async function getLatestVersion({ fetchFn = fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
64
- const controller = new AbortController();
65
- const timer = setTimeout(() => controller.abort(), timeoutMs);
66
- try {
67
- const res = await fetchFn(REGISTRY_URL, { signal: controller.signal });
68
- if (!res.ok) {
69
- return { version: null, error: `registry returned ${res.status} ${res.statusText || ''}`.trim() };
70
- }
71
- const body = await res.json();
72
- if (typeof body?.version !== 'string') {
73
- return { version: null, error: 'registry response missing version field' };
74
- }
75
- return { version: body.version, error: null };
76
- } catch (err) {
77
- return { version: null, error: err?.message || String(err) };
78
- } finally {
79
- clearTimeout(timer);
80
- }
81
- }
82
-
83
- /**
84
- * Read the version cache file. Returns the parsed object if it has a string
85
- * `latestVersion` field; otherwise null. Treats missing file, malformed JSON,
86
- * and shape mismatches all as "no cache".
87
- */
88
- export function readCache(path) {
89
- if (!existsSync(path)) return null;
90
- try {
91
- const data = JSON.parse(readFileSync(path, 'utf-8'));
92
- if (typeof data?.latestVersion !== 'string') return null;
93
- return data;
94
- } catch {
95
- return null;
96
- }
97
- }
98
-
99
- /**
100
- * Write the version cache file, creating parent directories as needed.
101
- */
102
- export function writeCache(path, data) {
103
- const dir = dirname(path);
104
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
105
- writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
106
- }
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env node
2
- // Idempotent migrator: ensures CLAUDE.md includes @local-only-template-freshness.md.
3
- // Appends one line at end if missing. Preserves the rest of the file byte-for-byte.
4
- //
5
- // Run standalone: node .claude/scripts/migrate-claude-md-freshness-include.mjs
6
- // Or import { runMigration } and call programmatically.
7
- import { existsSync, readFileSync, writeFileSync } from 'fs';
8
- import { join, dirname, resolve } from 'path';
9
- import { fileURLToPath } from 'url';
10
-
11
- const INCLUDE_LINE = '@local-only-template-freshness.md';
12
-
13
- export function runMigration({ workspaceRoot }) {
14
- const path = join(workspaceRoot, 'CLAUDE.md');
15
- if (!existsSync(path)) return { action: 'skipped', reason: 'no-claude-md' };
16
- const before = readFileSync(path, 'utf-8');
17
- if (before.includes(INCLUDE_LINE)) return { action: 'unchanged' };
18
- const after = before.endsWith('\n') ? before + INCLUDE_LINE + '\n' : before + '\n' + INCLUDE_LINE + '\n';
19
- writeFileSync(path, after);
20
- return { action: 'appended' };
21
- }
22
-
23
- // CLI entry point — workspace root is two levels up from this file
24
- // (.claude/scripts/migrate-... → workspace root).
25
- if (import.meta.url === `file://${process.argv[1]}`) {
26
- const here = dirname(fileURLToPath(import.meta.url));
27
- const root = resolve(here, '..', '..');
28
- const result = runMigration({ workspaceRoot: root });
29
- console.log(JSON.stringify(result));
30
- }
@@ -1,134 +0,0 @@
1
- ---
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.
4
- ---
5
-
6
- # Workspace Update
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.
9
-
10
- ## Prerequisites
11
-
12
- - `workspace.json` must have `initialized: true`
13
- - If not initialized, report: "Workspace not initialized. Run /workspace-init first."
14
- - `.workspace-update/` payload directory must exist (staged by `npx @ulysses-ai/create-workspace --upgrade`)
15
- - 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`
17
- - If `action` is `"init"`, report: "This payload is for initial setup. Run /workspace-init instead."
18
-
19
- ## Flow
20
-
21
- ### Step 1: Pre-update health check
22
-
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.
24
-
25
- ### Step 2: Compare current vs payload
26
-
27
- For each component directory in `.workspace-update/.claude/` (skills, hooks, agents, rules, recipes), compare files against the corresponding `.claude/{component}/` directory locally:
28
-
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}/`
33
-
34
- Report with version info from the manifest:
35
- ```
36
- "Template update: v{fromVersion} → v{toVersion}. {N} new files, {M} updated files, {R} removed files, {K} unchanged."
37
- ```
38
-
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."
40
-
41
- ### Step 2b: Historical .gitignore safety check
42
-
43
- Workspaces created before v0.5.1 are vulnerable to a destructive symlink bug in the old layout. The v0.8.0 layout removes the symlink entirely, so new workspaces are not vulnerable — but a workspace being upgraded from a pre-v0.8.0 version may still have the bad `.gitignore` pattern left over.
44
-
45
- Check the workspace `.gitignore` for the `repos/` trailing-slash pattern:
46
- ```bash
47
- grep -E '^repos/$' .gitignore
48
- ```
49
-
50
- If found, rewrite it in place to `repos` (no trailing slash). Also check for any tracked `repos` symlink that was already committed:
51
- ```bash
52
- git ls-files | grep -E '^repos$'
53
- ```
54
- If found, untrack it: `git rm --cached repos`.
55
-
56
- 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.
57
-
58
- ### Step 3: Selective update
59
-
60
- For each change, ask before applying:
61
-
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
66
- - **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
-
68
- Also handle these non-component files from the payload:
69
-
70
- - **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.
73
-
74
- ### Step 4: Update version
75
-
76
- Read `toVersion` from `.workspace-update/.manifest.json` and update `templateVersion` in `workspace.json` to match.
77
-
78
- ### Step 4a: Run idempotent migrators
79
-
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.
81
-
82
- ```bash
83
- node .workspace-update/.claude/scripts/migrate-claude-md-freshness-include.mjs
84
- ```
85
-
86
- Output is JSON: `{"action":"appended"|"unchanged"|"skipped"}`.
87
-
88
- - `appended` — the workspace's `CLAUDE.md` got the `@local-only-template-freshness.md` include line added at the end.
89
- - `unchanged` — the line was already present.
90
- - `skipped` — no `CLAUDE.md` exists at the workspace root (rare; surface to the user).
91
-
92
- ```bash
93
- node .workspace-update/.claude/scripts/migrate-canonical-priority.mjs --root .
94
- ```
95
-
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.
97
-
98
- Add other migrators here as the template ships them.
99
-
100
- ### Step 5: Post-update verification
101
-
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
106
-
107
- Report: "Post-update verification: {N} issues found" or "Post-update verification clean."
108
-
109
- ### Step 6: Cleanup
110
-
111
- Delete the `.workspace-update/` directory entirely. The payload has been fully processed and is no longer needed.
112
-
113
- ### Step 7: Commit
114
-
115
- ```bash
116
- git add -A
117
- git commit -m "chore: update workspace from template v{fromVersion} to v{toVersion}"
118
- ```
119
-
120
- Report: "Workspace updated to v{toVersion}. Restart Claude Code if rules or hooks changed."
121
-
122
- ### Step 8: Session-model migration nudge
123
-
124
- After the update is applied, if the sessions directory (`workspace.workSessionsDir`, default `work-sessions/`) has entries and `workspace.sessionModel` is not `"task"`, append one line to the report: "This workspace still has {N} session(s) under the session model — `/migrate-sessions` can inventory and drain them and switch to the task model whenever you're ready." Suggest only; the operator decides whether and when.
125
-
126
- ## Notes
127
-
128
- - 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
131
- - Can be run multiple times safely (idempotent) — if `.workspace-update/` doesn't exist, it reports no payload and exits
132
- - Initial setup is handled by `npx @ulysses-ai/create-workspace --init` + `/workspace-init` — this skill is for subsequent updates only
133
- - 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
File without changes