@ulysses-ai/create-workspace 0.20.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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/template/CLAUDE.md.tmpl +2 -2
- package/template/_claude/hooks/workspace-update-check.mjs +4 -4
- package/template/_claude/scripts/build-workspace-context.mjs +2 -0
- package/template/_claude/scripts/classify-update.mjs +101 -6
- package/template/_claude/scripts/maintenance-audit.mjs +0 -0
- package/template/_claude/skills/maintenance/SKILL.md +48 -108
- package/template/_claude/skills/workspace-update/SKILL.md +39 -29
- package/template/workspace.json.tmpl +0 -1
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ cd my-workspace
|
|
|
62
62
|
npx @ulysses-ai/create-workspace@beta --upgrade
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
This stages the new template payload to `.workspace-update/` without changing anything yet. Open Claude Code and run `/workspace-update` — the skill applies each change interactively (asks how to resolve any file you've customized) and
|
|
65
|
+
This stages the new template payload to `.workspace-update/` without changing anything yet. Open Claude Code and run `/workspace-update` — the skill applies each change interactively (asks how to resolve any file you've customized) and verifies the result with a scripted maintenance audit.
|
|
66
66
|
|
|
67
67
|
## Why "Ulysses"?
|
|
68
68
|
|
package/package.json
CHANGED
package/template/CLAUDE.md.tmpl
CHANGED
|
@@ -30,9 +30,9 @@ This is a claude-workspace. All conventions are defined in .claude/rules/.
|
|
|
30
30
|
- `/promote` — move personal memory to shared context
|
|
31
31
|
- `/release [version]` — cut a versioned release: bump, tag, forge release with generated notes
|
|
32
32
|
- `/sync-work` — push branches without ceremony
|
|
33
|
-
- `/workspace-update` — apply template updates
|
|
33
|
+
- `/workspace-update` — apply template updates, verified by a post-update audit
|
|
34
34
|
- `/setup-tracker` — wire this workspace to an issue tracker (GitHub Issues shipped; others pluggable)
|
|
35
|
-
- `/maintenance [audit|cleanup]` —
|
|
35
|
+
- `/maintenance [audit|cleanup]` — scripted health audit and cleanup
|
|
36
36
|
- `/context-placement` — decide where durable content belongs and what each destination costs before writing it
|
|
37
37
|
- `/goal-driven-work` — run multi-phase autonomous work under `/goal` with phase artifacts and agent-team dispatch
|
|
38
38
|
- `/migrate-sessions` — drain session-model work sessions and switch the workspace to the task lifecycle
|
|
@@ -26,17 +26,17 @@ const stale = manifest?.timestamp
|
|
|
26
26
|
const urgency = stale
|
|
27
27
|
? `URGENT: This update payload has been pending since ${manifest.timestamp}. It was not completed in a previous session. `
|
|
28
28
|
: '';
|
|
29
|
-
const
|
|
30
|
-
? '
|
|
29
|
+
const staleHint = stale
|
|
30
|
+
? 'This payload survived a previous session — classify and apply the changes directly, and still run the post-update verification the skill ends with. '
|
|
31
31
|
: '';
|
|
32
32
|
|
|
33
33
|
if (action === 'init' || !initialized) {
|
|
34
34
|
respond(`MANDATORY: ${urgency}A workspace init payload (template v${version}) is pending at .workspace-update/.
|
|
35
35
|
Read .workspace-update/.claude/skills/workspace-init/SKILL.md and follow it before doing anything else.
|
|
36
|
-
${
|
|
36
|
+
${staleHint}Do not proceed with the user's request until initialization is complete.`);
|
|
37
37
|
} else {
|
|
38
38
|
const from = manifest?.fromVersion || 'unknown';
|
|
39
39
|
respond(`MANDATORY: ${urgency}A workspace upgrade payload (v${from} → v${version}) is pending at .workspace-update/.
|
|
40
40
|
Read .workspace-update/.claude/skills/workspace-update/SKILL.md and follow it before doing anything else.
|
|
41
|
-
${
|
|
41
|
+
${staleHint}Do not proceed with the user's request until the update is complete.`);
|
|
42
42
|
}
|
|
@@ -10,10 +10,21 @@
|
|
|
10
10
|
// this file from <workspace>/.workspace-update/.claude/scripts/)
|
|
11
11
|
// --payload the staged payload; defaults to <root>/.workspace-update
|
|
12
12
|
//
|
|
13
|
-
// Prints JSON
|
|
13
|
+
// Prints JSON with five lists:
|
|
14
14
|
// new — no installed counterpart; safe to batch-apply after one confirm
|
|
15
15
|
// identical — installed file already equals the payload byte-for-byte
|
|
16
16
|
// differs — installed file differs; needs a per-file decision
|
|
17
|
+
// activated — the payload ships rules/{name}.md.skip while the workspace
|
|
18
|
+
// deliberately keeps {name}.md active; nothing to install, the
|
|
19
|
+
// active rule stays (gh:180)
|
|
20
|
+
// removed — installed file with no payload counterpart: the template
|
|
21
|
+
// stopped shipping it. Excludes what the workspace owns:
|
|
22
|
+
// *.test.mjs (the npm tarball does not ship tests, dev-checkout
|
|
23
|
+
// installs do — every test file would otherwise read as
|
|
24
|
+
// removed), anything gitignored (machine-local), paths under
|
|
25
|
+
// .claude/worktrees/, and entries of workspace.json →
|
|
26
|
+
// workspace.localFiles (array of .claude/-relative paths or
|
|
27
|
+
// globs for files this workspace owns) (gh:180)
|
|
17
28
|
//
|
|
18
29
|
// Only verbatim-installed files are classified: everything under .claude/,
|
|
19
30
|
// plus .mcp.json and .claudeignore. The payload's templates (*.tmpl, which
|
|
@@ -30,6 +41,7 @@ import {
|
|
|
30
41
|
} from 'node:fs';
|
|
31
42
|
import { join, resolve } from 'node:path';
|
|
32
43
|
import { fileURLToPath } from 'node:url';
|
|
44
|
+
import { gitIgnoredPaths } from './build-workspace-context.mjs';
|
|
33
45
|
|
|
34
46
|
function isMainModule(metaUrl) {
|
|
35
47
|
if (!process.argv[1]) return false;
|
|
@@ -58,7 +70,12 @@ function isClassified(payloadRelPath) {
|
|
|
58
70
|
return VERBATIM_ROOTS.includes(first);
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
// Directories never walked when looking for removed files. .claude/worktrees/
|
|
74
|
+
// holds entire nested worktrees — walking them is slow and every file inside
|
|
75
|
+
// is unmanaged by the template.
|
|
76
|
+
const SKIPPED_DIRS = new Set(['worktrees']);
|
|
77
|
+
|
|
78
|
+
function* walkFiles(dir, prefix = '', skipDirs = null) {
|
|
62
79
|
let entries;
|
|
63
80
|
try {
|
|
64
81
|
entries = readdirSync(dir).sort();
|
|
@@ -66,15 +83,67 @@ function* walkFiles(dir, prefix = '') {
|
|
|
66
83
|
return;
|
|
67
84
|
}
|
|
68
85
|
for (const name of entries) {
|
|
86
|
+
if (skipDirs && skipDirs.has(name)) continue;
|
|
69
87
|
const rel = prefix ? `${prefix}/${name}` : name;
|
|
70
88
|
const full = join(dir, name);
|
|
71
89
|
let st;
|
|
72
90
|
try { st = statSync(full); } catch { continue; }
|
|
73
|
-
if (st.isDirectory()) yield* walkFiles(full, rel);
|
|
91
|
+
if (st.isDirectory()) yield* walkFiles(full, rel, skipDirs);
|
|
74
92
|
else if (st.isFile()) yield rel;
|
|
75
93
|
}
|
|
76
94
|
}
|
|
77
95
|
|
|
96
|
+
// Installed files under the verbatim-managed roots: the .claude/ tree (minus
|
|
97
|
+
// skipped directories) plus the two standalone files. Nothing else in the
|
|
98
|
+
// workspace root is walked — repos/ and work-sessions/ hold entire worktrees
|
|
99
|
+
// the template never manages.
|
|
100
|
+
function* walkInstalledFiles(absRoot) {
|
|
101
|
+
yield* walkFiles(join(absRoot, '.claude'), '.claude', SKIPPED_DIRS);
|
|
102
|
+
for (const name of ['.mcp.json', '.claudeignore']) {
|
|
103
|
+
if (existsSync(join(absRoot, name))) yield name;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** workspace.json → workspace.localFiles, normalized to .claude/-relative globs. */
|
|
108
|
+
function readLocalFiles(absRoot) {
|
|
109
|
+
const configPath = join(absRoot, 'workspace.json');
|
|
110
|
+
try {
|
|
111
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
112
|
+
const entries = config?.workspace?.localFiles;
|
|
113
|
+
if (!Array.isArray(entries)) return [];
|
|
114
|
+
return entries
|
|
115
|
+
.filter((e) => typeof e === 'string' && e.length > 0)
|
|
116
|
+
.map((e) => e.replace(/^(\.claude\/)+/, ''));
|
|
117
|
+
} catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Match `rel` (a .claude/-relative posix path) against a localFiles entry —
|
|
124
|
+
* an exact path or a glob where `**` spans separators and `*` does not.
|
|
125
|
+
* No glob library: the shapes localFiles needs are these two stars.
|
|
126
|
+
*/
|
|
127
|
+
function globMatches(pattern, rel) {
|
|
128
|
+
if (pattern === rel) return true;
|
|
129
|
+
if (!pattern.includes('*')) return false;
|
|
130
|
+
const re = new RegExp(
|
|
131
|
+
`^${pattern.split('**').map(
|
|
132
|
+
(part) => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*'),
|
|
133
|
+
).join('.*')}$`,
|
|
134
|
+
);
|
|
135
|
+
return re.test(rel);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isOwnedByWorkspace(rel, localFiles) {
|
|
139
|
+
if (rel.endsWith('.test.mjs')) return true;
|
|
140
|
+
// The template's own .gitignore declares these machine-local.
|
|
141
|
+
if (rel === '.claude/settings.local.json' || rel === '.claude/.active-session.json') return true;
|
|
142
|
+
if (!rel.startsWith('.claude/')) return false;
|
|
143
|
+
const claudeRel = rel.slice('.claude/'.length);
|
|
144
|
+
return localFiles.some((pattern) => globMatches(pattern, claudeRel));
|
|
145
|
+
}
|
|
146
|
+
|
|
78
147
|
export function classifyUpdate({ root, payload }) {
|
|
79
148
|
const absRoot = resolve(root);
|
|
80
149
|
const absPayload = resolve(payload ?? join(absRoot, '.workspace-update'));
|
|
@@ -82,9 +151,20 @@ export function classifyUpdate({ root, payload }) {
|
|
|
82
151
|
throw new Error(`No payload found at ${absPayload} — run npx @ulysses-ai/create-workspace --upgrade first`);
|
|
83
152
|
}
|
|
84
153
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
154
|
+
const payloadFiles = [...walkFiles(absPayload)].filter(isClassified);
|
|
155
|
+
const payloadSet = new Set(payloadFiles);
|
|
156
|
+
|
|
157
|
+
const result = { new: [], identical: [], differs: [], activated: [], removed: [] };
|
|
158
|
+
for (const rel of payloadFiles) {
|
|
159
|
+
// A .skip rule whose active counterpart is installed was deliberately
|
|
160
|
+
// activated by this workspace: report it as activated, not new.
|
|
161
|
+
if (rel.startsWith('.claude/rules/') && rel.endsWith('.md.skip')) {
|
|
162
|
+
const active = rel.replace(/\.skip$/, '');
|
|
163
|
+
if (existsSync(join(absRoot, active)) && !existsSync(join(absRoot, rel))) {
|
|
164
|
+
result.activated.push({ skip: rel, active });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
88
168
|
const installed = join(absRoot, rel);
|
|
89
169
|
if (!existsSync(installed)) {
|
|
90
170
|
result.new.push(rel);
|
|
@@ -98,6 +178,21 @@ export function classifyUpdate({ root, payload }) {
|
|
|
98
178
|
result.differs.push(rel);
|
|
99
179
|
}
|
|
100
180
|
}
|
|
181
|
+
|
|
182
|
+
// Removed: installed verbatim-managed files with no payload counterpart.
|
|
183
|
+
const skipSet = new Set(payloadFiles);
|
|
184
|
+
const localFiles = readLocalFiles(absRoot);
|
|
185
|
+
const installedFiles = [...walkInstalledFiles(absRoot)];
|
|
186
|
+
const gitignored = gitIgnoredPaths(absRoot, installedFiles);
|
|
187
|
+
for (const rel of installedFiles) {
|
|
188
|
+
if (skipSet.has(rel)) continue;
|
|
189
|
+
// An active rule whose .skip twin is in the payload is an activated rule,
|
|
190
|
+
// not a removed one.
|
|
191
|
+
if (rel.startsWith('.claude/rules/') && rel.endsWith('.md') && skipSet.has(`${rel}.skip`)) continue;
|
|
192
|
+
if (gitignored.has(rel)) continue;
|
|
193
|
+
if (isOwnedByWorkspace(rel, localFiles)) continue;
|
|
194
|
+
result.removed.push(rel);
|
|
195
|
+
}
|
|
101
196
|
return result;
|
|
102
197
|
}
|
|
103
198
|
|
|
Binary file
|
|
@@ -16,33 +16,41 @@ Keep the workspace healthy. Combines integrity auditing with active cleanup reco
|
|
|
16
16
|
|
|
17
17
|
Read-only integrity checks. Reports problems but never modifies files.
|
|
18
18
|
|
|
19
|
+
The mechanical checks are scripted (gh:180) — run the audit and present its report:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node .claude/scripts/maintenance-audit.mjs --root .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The report prints one block per section — ✓ ok lines, ✗ issues, ⚠ warnings, ℹ infos — then a result summary. Severity drives the exit code: any issue-severity finding → exit `1`, otherwise `0` (a non-zero exit is the report above it, not a crash). `--offline` skips section 7, the only network user; `--json` emits `{issues, summary}` instead of the human report when another step needs the machine-readable numbers.
|
|
26
|
+
|
|
27
|
+
Sections 1–7 below explain what the script checks, so its findings can be interpreted. Each also names what it does not carry — those residual checks Claude performs alongside the script run.
|
|
28
|
+
|
|
19
29
|
### 1. Cross-reference consistency
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- **Contradictions** — file A says "user-scoped is default" but file B says "root is default"
|
|
30
|
+
The script checks the CLAUDE.md skill list against `.claude/skills/` in both directions, and walks CLAUDE.md's `@`-import graph for dangling imports. A missing `local-only-*` file or the optional `CODEBASE.md` stub is info, not an issue — expected states on machines that never generated them.
|
|
31
|
+
|
|
32
|
+
Semantic drift between context files — one saying "2 mandatory rules" when there are now 4, or two files contradicting each other — is not mechanically decidable; cleanup step 10's reconciliation covers it.
|
|
24
33
|
|
|
25
34
|
### 2. Frontmatter integrity
|
|
26
|
-
For each workspace-context `.md`
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
- `lifecycle:
|
|
32
|
-
-
|
|
33
|
-
- `confidence
|
|
35
|
+
For each non-gitignored workspace-context `.md` and each `work-sessions/*/workspace/session.md`, the script checks:
|
|
36
|
+
- Frontmatter parses
|
|
37
|
+
- Session trackers carry `name`, `status`, `branch`
|
|
38
|
+
- `branch` references a branch that still exists
|
|
39
|
+
- `repo`/`repos` reference repos in workspace.json
|
|
40
|
+
- `lifecycle: active` untouched in 7+ days (stale candidate)
|
|
41
|
+
- `lifecycle: resolved` (info — confirm /complete-work has processed it)
|
|
42
|
+
- `confidence`, when set, is one of `high`, `medium`, `low`
|
|
34
43
|
|
|
35
44
|
### 3. Workspace structure
|
|
36
|
-
|
|
37
|
-
- CLAUDE.md references skills and rules that actually exist?
|
|
38
|
-
- Orphaned rules or skills not referenced anywhere?
|
|
39
|
-
- workspace.json repos all present in `repos/`?
|
|
45
|
+
The script checks that workspace.json and CLAUDE.md are present and parseable, that `workspace-context/` and `.claude/rules`, `skills`, `scripts` directories exist, and that every manifest repo is cloned under `repos/`.
|
|
40
46
|
|
|
41
47
|
### 4. Git state
|
|
48
|
+
The script covers the launcher itself: on its default branch, tracked tree clean. Untracked paths are info — gitignored content is not counted.
|
|
49
|
+
|
|
50
|
+
The worktree-level checks are not carried by the script; perform them alongside it:
|
|
42
51
|
- Worktrees with no recent commits? (orphaned)
|
|
43
52
|
- Local branches with no remote tracking? (unpushed work)
|
|
44
53
|
- Worktrees whose branch has already been merged? (cleanup candidates)
|
|
45
|
-
- Workspace repo on expected branch?
|
|
46
54
|
- Orphan worktree records in project repos — run `git -C repos/{repo} worktree list` for each repo and flag any `prunable` markers. These usually come from a workspace-first teardown (the unsafe order) leaving stale admin records behind. Suggest `git worktree prune` on the affected repo.
|
|
47
55
|
- Task-model state (gh:146), three checks:
|
|
48
56
|
- **Unrecorded task worktrees** — list `repos/*/.claude/worktrees/*` and `.claude/worktrees/*`, read each candidate's branch (`git -C "{path}" rev-parse --abbrev-ref HEAD`), and keep only those on a task-prefixed branch (`feature/`, `bugfix/`, `chore/`) — Claude Code's own worktrees carry other branch names, so the prefix filter skips them without guessing a name convention. Cross-reference the chat records (`node .claude/scripts/chat-record.mjs --root . --list`): a task-prefixed worktree no record entry claims is *unrecorded* — it may be a legitimate no-tracker task (those are never recorded), so present it and ask before suggesting `node .claude/scripts/task-worktree.mjs --root . --remove --repo "{repo}" --branch "{branch}"`.
|
|
@@ -50,92 +58,29 @@ For each workspace-context `.md` file and each `work-sessions/*/workspace/sessio
|
|
|
50
58
|
- **Merged but never completed** — a recorded task branch that already merged. Judge merged-ness by the forge's merged PRs for that repo, matching on head branch — never `git branch --merged`, which a squash merge (never an ancestor) silently misses. `/complete-work` never ran. Suggest running `/complete-work` for that branch (detection from the chat record finds it).
|
|
51
59
|
|
|
52
60
|
### 5. Workspace-context auto-file integrity
|
|
61
|
+
The script regenerates `workspace-context/index.md`, `canonical.md`, and each `workspace-context/team-member/{user}/index.md` in memory and compares fingerprints — the same semantics as `build-workspace-context.mjs --check`:
|
|
62
|
+
- Missing or stale artifact → issue; regenerate with `node .claude/scripts/build-workspace-context.mjs --write --root .`
|
|
63
|
+
- A missing gitignored per-user index → info (regenerated per machine, the normal fresh-checkout state)
|
|
64
|
+
- Canonical body over budget after trim and stub → warning, deferred to cleanup triage
|
|
65
|
+
- Trimmed or stubbed but within budget → info
|
|
53
66
|
|
|
54
|
-
`workspace
|
|
67
|
+
The canonical budget is opt-in: `workspace.canonicalBudgetBytes` off (absent or `null`) means canonical ships every locked file in full and over-budget cannot occur. When set, selection walks `ok` → `trimmed` → `stubbed` → `over-budget` as the generator gives up progressively more reference content to fit: `trimmedFiles` are reference files whose `<!-- canonical:trim --> ... <!-- canonical:end-trim -->` spans were dropped, `stubbedFiles` are reference files reduced to a one-line breadcrumb. `over-budget` means the budget is still exceeded after stubbing — regeneration cannot fix it; the locked content itself needs triage via `/maintenance cleanup` (step 11). Stale wins over over-budget: a stale canonical is reported as stale, hiding any over-budget condition until it is regenerated.
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
node .claude/scripts/build-workspace-context.mjs --check --root .
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
The script reports per-artifact status as JSON and uses three exit codes to distinguish what's wrong:
|
|
61
|
-
|
|
62
|
-
- `0` — all artifacts current and, when a canonical budget is set, the rendered canonical fits inside `workspace.canonicalBudgetBytes`.
|
|
63
|
-
- `1` — at least one artifact is `missing` or `stale`. Run `--write` to regenerate. `missing` means the artifact does not exist yet; `stale` means it exists but no longer matches its sources (a file was added or deleted, a `description:` changed, a `shared/locked/` file was edited, an `.indexignore` rule was added).
|
|
64
|
-
- `2` — artifacts are current but canonical body bytes exceed the budget after the trim and stub stages have already run. Only reachable when a budget is set. Regeneration cannot fix this; the locked content itself needs triage. Stale wins over over-budget when both apply, so a `1` can hide an over-budget condition until you regen.
|
|
65
|
-
|
|
66
|
-
The JSON payload always includes a `canonical` block summarizing the budget outcome:
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"status": "current",
|
|
71
|
-
"missing": [],
|
|
72
|
-
"stale": [],
|
|
73
|
-
"canonical": {
|
|
74
|
-
"budget": 40960,
|
|
75
|
-
"current": 47802,
|
|
76
|
-
"overBy": 6842,
|
|
77
|
-
"selectionStatus": "stubbed",
|
|
78
|
-
"trimmedFiles": ["post-release-discipline"],
|
|
79
|
-
"stubbedFiles": ["project-status", "release-flow-recipes"]
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
`selectionStatus` walks `ok` → `trimmed` → `stubbed` → `over-budget` as the script gives up progressively more reference content trying to fit the budget. `trimmedFiles` lists reference files whose `<!-- canonical:trim --> ... <!-- canonical:end-trim -->` spans were dropped; `stubbedFiles` lists reference files whose entire body was replaced with a one-line breadcrumb. `overBy` is present only when `selectionStatus === 'over-budget'` and reports the bytes still over after stubbing.
|
|
85
|
-
|
|
86
|
-
The canonical budget is opt-in. `workspace.canonicalBudgetBytes` is off unless workspace.json sets it — absent or `null` means no budget. When off, `canonical.md` ships every locked file in full, the `canonical` block reports `"budget": null` with `selectionStatus: "ok"`, exit `2` cannot occur, and the audit reports one informational line in place of the budget OK/warning line:
|
|
87
|
-
|
|
88
|
-
```
|
|
89
|
-
• Canonical budget: off (alwaysLoadedBudgetBytes covers the total)
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
No warning accompanies it. To turn the budget back on, set a byte count in workspace.json (e.g. `"canonicalBudgetBytes": 40960`) and regenerate.
|
|
93
|
-
|
|
94
|
-
Audit mode reports the status verbatim. When a budget is set and `selectionStatus` is `over-budget`, audit emits the budget violation and recommends `/maintenance cleanup` to triage — regeneration will not resolve it. Cleanup mode runs `--write` when `missing` or `stale`, re-checks, and then enters the budget triage flow described in cleanup step 11 if the post-regen check still reports `over-budget`.
|
|
95
|
-
|
|
96
|
-
While the indexes are being read, also flag entries with weak fallbacks: filename-slug-only descriptions (e.g., "project status" with no period) usually indicate the underlying file is missing a `description:` or has no usable opening sentence. Suggest adding `description:` to those source files — the index will pick it up on the next regeneration.
|
|
69
|
+
Residual, not in the script: while reading the indexes, flag filename-slug-only descriptions (e.g., "project status" with no period) — usually the source file is missing a `description:` or has no usable opening sentence. Suggest adding `description:`; the index picks it up on the next regeneration.
|
|
97
70
|
|
|
98
71
|
### 6. Always-loaded context budget
|
|
72
|
+
The script measures everything Claude reads at launch — CLAUDE.md, its @-imports, and the active rules — against `workspace.alwaysLoadedBudgetBytes`. Rules with `paths:` frontmatter are conditional (they load only when a matching file is touched) and count separately; with no budget in workspace.json the check passes trivially.
|
|
99
73
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
node .claude/scripts/context-footprint.mjs --root .
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Rules carrying `paths:` frontmatter are conditional (they load only when a matching file is touched); the script lists them in a separate conditional section and excludes them from the total. With no `alwaysLoadedBudgetBytes` in workspace.json there is no budget and this check passes trivially.
|
|
107
|
-
|
|
108
|
-
Within budget → an OK line: `✓ Always-loaded context: 43 KB / 64 KB`. Over budget → a Warning (the workspace still functions; this is drift, not breakage) naming the top contributors and the fixes:
|
|
109
|
-
|
|
110
|
-
```
|
|
111
|
-
⚠ Always-loaded context exceeds budget: 78 KB / 64 KB. Top contributors:
|
|
112
|
-
.claude/rules/git-conventions.md (12 KB), CLAUDE.md (9 KB),
|
|
113
|
-
.claude/rules/workspace-structure.md (8 KB). Scope situational rules with
|
|
114
|
-
paths: frontmatter, or move reference content to shared/.
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
The script itself exits `1` when over budget; `/maintenance` reports that as the warning above, not as a failed run.
|
|
74
|
+
Within budget → an OK line (`✓ Always-loaded context: 43 KB / 64 KB`). Over → a warning — the workspace still functions; this is drift, not breakage — naming the top contributors. Fixes: scope situational rules with `paths:` frontmatter, or move reference content to `shared/`.
|
|
118
75
|
|
|
119
76
|
### 7. Template freshness
|
|
77
|
+
The script invokes `refreshIfStale` with a 24h TTL regardless of `workspace.versionCheck.ambient` — the user explicitly ran `/maintenance` — and reports:
|
|
78
|
+
- `outdated` → warning: `Template v{current} → v{latest} available. Run npx @ulysses-ai/create-workspace --upgrade.`
|
|
79
|
+
- `current` → ✓ Template is up to date (v{latest})
|
|
80
|
+
- `unknown` → warning: could not reach the npm registry
|
|
81
|
+
- `skipped: 'uninitialized'` → info: workspace not initialized, freshness check unavailable
|
|
120
82
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Always invoke `refreshIfStale` from the audit (regardless of `workspace.versionCheck.ambient` — the user explicitly ran `/maintenance`):
|
|
124
|
-
|
|
125
|
-
```javascript
|
|
126
|
-
import { refreshIfStale } from './.claude/lib/freshness.mjs';
|
|
127
|
-
const result = await refreshIfStale({
|
|
128
|
-
workspaceRoot: process.cwd(),
|
|
129
|
-
ttlMs: 24 * 60 * 60 * 1000,
|
|
130
|
-
});
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Report one of:
|
|
134
|
-
- `outdated` → `✗ Template v{current} → v{latest} available. Run npx @ulysses-ai/create-workspace --upgrade.`
|
|
135
|
-
- `current` → `✓ Template is up to date (v{latest}).`
|
|
136
|
-
- `unknown` (with cache) → `⚠ Could not reach npm registry; last cached latest was v{latest} as of {checkedAt}.`
|
|
137
|
-
- `unknown` (no cache) → `⚠ Could not reach npm registry; no cached version on file. Try again when online.`
|
|
138
|
-
- `skipped: 'uninitialized'` → `⚠ Workspace not initialized; freshness check unavailable.`
|
|
83
|
+
An `outdated` result also rewrites the `local-only-template-freshness.md` banner and the version cache, as this check always has.
|
|
139
84
|
|
|
140
85
|
## Cleanup
|
|
141
86
|
|
|
@@ -173,7 +118,7 @@ When stale candidates are found, surface them as warnings in the output format a
|
|
|
173
118
|
|
|
174
119
|
### 11. Canonical budget triage
|
|
175
120
|
|
|
176
|
-
This step runs only when a canonical budget is set (`workspace.canonicalBudgetBytes` holds a number) and the post-regen `--check` from the cleanup regen pass (Flow step
|
|
121
|
+
This step runs only when a canonical budget is set (`workspace.canonicalBudgetBytes` holds a number) and the post-regen `--check` from the cleanup regen pass (Flow step 4) still reports `selectionStatus: 'over-budget'`. With the budget off — absent or `null` in workspace.json — `--check` can never report over-budget, so this step is unreachable. Skip it too if the regular regen pass cleared the budget, or if `--check` was already `ok`, `trimmed`, or `stubbed` after that pass.
|
|
177
122
|
|
|
178
123
|
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.
|
|
179
124
|
|
|
@@ -231,8 +176,8 @@ Read `workspace.json`. If `workspace.tracker?.type === 'github-issues'` and `wor
|
|
|
231
176
|
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.
|
|
232
177
|
|
|
233
178
|
### 13. Health metrics
|
|
234
|
-
- Canonical budget — read from the same
|
|
235
|
-
- Always-loaded context — read from the
|
|
179
|
+
- Canonical budget — read from the audit script's section 5 outcome (the same regenerate-and-compare it already ran). When a budget is set, 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. When off, report the step 5 one-liner: `• Canonical budget: off (alwaysLoadedBudgetBytes covers the total)`.
|
|
180
|
+
- Always-loaded context — read from the audit script's section 6 outcome, reported the same way (`current / budget` bytes); over-budget is already surfaced as a warning there.
|
|
236
181
|
- Number of ephemeral files — flag if accumulating without resolution
|
|
237
182
|
- Session log stats (if `workspace-scratchpad/session-log.jsonl` exists):
|
|
238
183
|
- Sessions without capture
|
|
@@ -272,16 +217,11 @@ OK (6):
|
|
|
272
217
|
|
|
273
218
|
## Flow
|
|
274
219
|
|
|
275
|
-
1.
|
|
276
|
-
2.
|
|
277
|
-
3. Read
|
|
278
|
-
4.
|
|
279
|
-
5.
|
|
280
|
-
6. Run `node .claude/scripts/build-workspace-context.mjs --check --root .` — capture status. Exit `0` = clean (and within budget when one is set), `1` = artifact missing or stale, `2` = artifacts current but canonical body over budget — only possible with a budget set. The `canonical` block in the JSON output drives both the audit budget line and the cleanup triage decision; `"budget": null` means the canonical budget is off.
|
|
281
|
-
7. Run `node .claude/scripts/context-footprint.mjs --root .` — capture the total and the `BUDGET` line. Exit `0` = within budget or no budget set; exit `1` = over budget, reported as a warning with the top contributors (audit step 6).
|
|
282
|
-
8. Read session-log.jsonl if it exists
|
|
283
|
-
9. 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 11.
|
|
284
|
-
10. Compile and present findings grouped by severity
|
|
220
|
+
1. Run `node .claude/scripts/maintenance-audit.mjs --root .` and present its report — it carries audit sections 1–7. Add `--offline` when there is no network (section 7 is the only network user), or `--json` when step 13's health metrics want the machine-readable `summary.canonical` / `summary.alwaysLoaded` numbers.
|
|
221
|
+
2. Perform the residual checks the script does not carry — the worktree-level git checks from audit section 4 (orphaned worktrees, unpushed branches, merged worktrees, prunable worktree records, the task-model checks against `chat-record.mjs --list`).
|
|
222
|
+
3. Read session-log.jsonl if it exists (feeds step 13's session log stats)
|
|
223
|
+
4. If cleanup mode: run `node .claude/scripts/build-workspace-context.mjs --check --root .` — exit `0` = clean (and within budget when one is set), `1` = artifact missing or stale → regenerate with `--write`, `2` = artifacts current but canonical body over budget (only possible with a budget set). The `canonical` block in the JSON drives the triage decision; `"budget": null` means the canonical budget is off. Then compare context files pairwise for overlap and scan for stale cross-references; if the post-regen `--check` reports `over-budget`, enter the canonical-budget triage flow described in cleanup step 11.
|
|
224
|
+
5. Compile and present findings grouped by severity (Output Format above): the script's findings plus the residual checks, with cleanup suggestions from steps 3–4 folded in.
|
|
285
225
|
|
|
286
226
|
## Notes
|
|
287
227
|
- Audit mode is always read-only — never modifies files
|
|
@@ -1,11 +1,11 @@
|
|
|
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
|
|
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
|
|
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
|
|
|
@@ -22,11 +22,7 @@ Apply a staged template update to an initialized workspace. The CLI (`npx @ulyss
|
|
|
22
22
|
|
|
23
23
|
## Flow
|
|
24
24
|
|
|
25
|
-
### Step 1:
|
|
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
|
|
25
|
+
### Step 1: Decide where the update lands
|
|
30
26
|
|
|
31
27
|
Check the workspace repo for a remote (`git remote`). This decides where every later step works:
|
|
32
28
|
|
|
@@ -41,28 +37,28 @@ In the commands below, `{payload}` is `.workspace-update` in the no-remote flow
|
|
|
41
37
|
|
|
42
38
|
### Step 2: Classify the payload
|
|
43
39
|
|
|
44
|
-
Run the classifier — it compares every verbatim-installed payload file (`.claude/**`, `.mcp.json`, `.claudeignore`) against the workspace by content:
|
|
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:
|
|
45
41
|
|
|
46
42
|
```bash
|
|
47
43
|
node {payload}/.claude/scripts/classify-update.mjs --root . --payload {payload}
|
|
48
44
|
```
|
|
49
45
|
|
|
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
|
|
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:
|
|
51
47
|
|
|
52
48
|
- `new` — no installed counterpart; safe to batch-apply (Step 3) behind one confirmation
|
|
53
49
|
- `identical` — installed file already equals the payload; skip silently
|
|
54
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.
|
|
55
53
|
|
|
56
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.
|
|
57
55
|
|
|
58
|
-
Also list **removed files**: files present in the local `.claude/{component}/` with no counterpart in `{payload}/.claude/{component}/`.
|
|
59
|
-
|
|
60
56
|
Report with version info from the manifest:
|
|
61
57
|
```
|
|
62
|
-
"Template update: v{fromVersion} → v{templateVersion}. {N} new files, {M} locally modified, {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."
|
|
63
59
|
```
|
|
64
60
|
|
|
65
|
-
If `new`, `differs`, and
|
|
61
|
+
If `new`, `differs`, `activated`, and `removed` are all empty, report: "Workspace is up to date (template v{templateVersion}). No changes needed."
|
|
66
62
|
|
|
67
63
|
### Step 2b: Historical .gitignore safety check
|
|
68
64
|
|
|
@@ -87,14 +83,14 @@ Batch the safe case, ask on the rest:
|
|
|
87
83
|
|
|
88
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.
|
|
89
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.
|
|
90
|
-
- **Removed in template:** "Template removed {file}. Delete locally? [y/N]" — conservative default.
|
|
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."
|
|
91
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
|
|
92
89
|
|
|
93
90
|
Also handle these non-component files from the payload:
|
|
94
91
|
|
|
95
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.
|
|
96
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.
|
|
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
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.
|
|
99
95
|
- **.gitignore:** Merge new entries from the payload's `_gitignore` into the existing `.gitignore` — do not remove user-added lines.
|
|
100
96
|
|
|
@@ -104,7 +100,7 @@ Read `templateVersion` from `.workspace-update/.manifest.json` and update `templ
|
|
|
104
100
|
|
|
105
101
|
### Step 4a: Run idempotent migrators
|
|
106
102
|
|
|
107
|
-
|
|
103
|
+
Two migrators run on **every** update — both idempotent, safe on already-migrated workspaces. Run each and surface its action in the upgrade summary.
|
|
108
104
|
|
|
109
105
|
```bash
|
|
110
106
|
node {payload}/.claude/scripts/migrate-claude-md-freshness-include.mjs --root .
|
|
@@ -120,20 +116,20 @@ Output is JSON: `{"action":"appended"|"unchanged"|"skipped"}`.
|
|
|
120
116
|
node {payload}/.claude/scripts/migrate-canonical-priority.mjs --root .
|
|
121
117
|
```
|
|
122
118
|
|
|
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.
|
|
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.
|
|
124
120
|
|
|
125
|
-
|
|
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.
|
|
126
122
|
|
|
127
|
-
|
|
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.
|
|
128
127
|
|
|
129
|
-
|
|
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/`.
|
|
130
129
|
|
|
131
|
-
|
|
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
|
|
130
|
+
### Step 5: Post-update verification
|
|
135
131
|
|
|
136
|
-
|
|
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:
|
|
137
133
|
|
|
138
134
|
```bash
|
|
139
135
|
node .claude/scripts/build-workspace-context.mjs --write --root .
|
|
@@ -142,22 +138,36 @@ node .claude/scripts/build-workspace-context.mjs --check --root .
|
|
|
142
138
|
|
|
143
139
|
`--check` must exit clean. If it reports drift, re-run `--write` and check again before continuing.
|
|
144
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.
|
|
154
|
+
|
|
145
155
|
Report: "Post-update verification: {N} issues found" or "Post-update verification clean."
|
|
146
156
|
|
|
147
157
|
### Step 6: Cleanup
|
|
148
158
|
|
|
149
|
-
Delete the `.workspace-update/` directory at the launcher — in the worktree flow (Step
|
|
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.
|
|
150
160
|
|
|
151
161
|
### Step 7: Commit
|
|
152
162
|
|
|
153
|
-
Where the commit lands was decided in Step
|
|
163
|
+
Where the commit lands was decided in Step 1.
|
|
154
164
|
|
|
155
165
|
- **No remote:** commit in place on the launcher's default branch — the one sanctioned launcher commit:
|
|
156
166
|
```bash
|
|
157
167
|
git add -A
|
|
158
168
|
git commit -m "chore: update workspace from template v{fromVersion} to v{templateVersion}"
|
|
159
169
|
```
|
|
160
|
-
- **Remote exists:** from the task worktree created in Step
|
|
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).
|
|
161
171
|
|
|
162
172
|
Report: "Workspace updated to v{templateVersion}. Restart Claude Code if rules or hooks changed."
|
|
163
173
|
|
|
@@ -174,4 +184,4 @@ After the update is applied, if the sessions directory (`workspace.workSessionsD
|
|
|
174
184
|
- Can be run multiple times safely (idempotent) — if `.workspace-update/` doesn't exist, it reports no payload and exits
|
|
175
185
|
- Initial setup is handled by `npx @ulysses-ai/create-workspace --init` + `/workspace-init` — this skill is for subsequent updates only
|
|
176
186
|
- The `.sh` to `.mjs` hook migration is a one-time transition for workspaces created before hooks moved to JavaScript
|
|
177
|
-
- The
|
|
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
|