@ulysses-ai/create-workspace 0.20.0-beta.0 → 0.22.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/lib/init.mjs +9 -0
- package/lib/init.test.mjs +75 -0
- package/lib/scaffold.mjs +8 -0
- package/lib/scaffold.test.mjs +20 -0
- 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 +368 -17
- package/template/_claude/scripts/maintenance-audit.mjs +697 -0
- package/template/_claude/scripts/template-baseline.mjs +215 -0
- package/template/_claude/skills/maintenance/SKILL.md +48 -108
- package/template/_claude/skills/workspace-init/SKILL.md +6 -0
- package/template/_claude/skills/workspace-update/SKILL.md +71 -37
- package/template/workspace.json.tmpl +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Template baseline: the sha256 of every verbatim-installed file the template
|
|
3
|
+
// last shipped, recorded at <root>/.claude/.template-baseline.json. It is the
|
|
4
|
+
// third input of /workspace-update's three-way classification (workspace vs
|
|
5
|
+
// payload vs baseline), so a file the template changed since the installed
|
|
6
|
+
// version reads as an update to apply in batch — not a local edit to negotiate
|
|
7
|
+
// file by file (gh:183).
|
|
8
|
+
//
|
|
9
|
+
// Shape:
|
|
10
|
+
// { "templateVersion": "0.21.0",
|
|
11
|
+
// "files": { ".claude/skills/workspace-update/SKILL.md": "<sha256>", … } }
|
|
12
|
+
// Keys are root-relative posix paths covering the verbatim-installed roots
|
|
13
|
+
// (.claude/**, .mcp.json, .claudeignore). The file is committed with the
|
|
14
|
+
// workspace — every machine that pulls gets the same template files, so it
|
|
15
|
+
// gets the same baseline.
|
|
16
|
+
//
|
|
17
|
+
// Written by `--init` scaffolding (lib/init.mjs), by /workspace-init once the
|
|
18
|
+
// remaining components are installed, and at the end of every /workspace-update
|
|
19
|
+
// (classify-update.mjs --write-baseline). Interactive scaffolding writes it
|
|
20
|
+
// from the template tree (lib/scaffold.mjs).
|
|
21
|
+
//
|
|
22
|
+
// The rule every entry follows: record the hash of the payload's content —
|
|
23
|
+
// what the template last shipped — never the workspace's on-disk bytes. A
|
|
24
|
+
// file the user kept despite a template change therefore keeps the PAYLOAD
|
|
25
|
+
// hash: the next update sees workspace ≠ baseline with payload == baseline
|
|
26
|
+
// and reports the file as a purely local edit (informational, not re-asked
|
|
27
|
+
// per file) until the template touches it again, and a deliberate divergence
|
|
28
|
+
// is never silently adopted as the new baseline. One exception: an unapplied
|
|
29
|
+
// update — the workspace still holds the OLD baseline content while the
|
|
30
|
+
// payload ships something new (the user declined the `updated` batch) — keeps
|
|
31
|
+
// the old entry, so the file re-presents as `updated` next time instead of
|
|
32
|
+
// being filed away as a local edit. The invariant that matters either way: a
|
|
33
|
+
// file the user never touched (workspace == baseline) is never reported as a
|
|
34
|
+
// local edit.
|
|
35
|
+
//
|
|
36
|
+
// All hashes are CRLF-normalized for text files (see hashBytes): a Windows
|
|
37
|
+
// autocrlf checkout stores CRLF where the payload carries LF, and byte-exact
|
|
38
|
+
// hashing would read every such file as locally modified. Files containing
|
|
39
|
+
// NUL bytes hash byte-exact.
|
|
40
|
+
//
|
|
41
|
+
// Not covered: *.test.mjs (the tarball never ships them; a workspace's test
|
|
42
|
+
// files came from a dev checkout and age independently — classify-update
|
|
43
|
+
// reports them as staleTests instead), machine-local files
|
|
44
|
+
// (.claude/settings.local.json, .claude/.active-session.json), and anything
|
|
45
|
+
// under .claude/worktrees/.
|
|
46
|
+
|
|
47
|
+
import { createHash } from 'node:crypto';
|
|
48
|
+
import {
|
|
49
|
+
existsSync,
|
|
50
|
+
mkdirSync,
|
|
51
|
+
readFileSync,
|
|
52
|
+
readdirSync,
|
|
53
|
+
statSync,
|
|
54
|
+
writeFileSync,
|
|
55
|
+
} from 'node:fs';
|
|
56
|
+
import { dirname, join, resolve } from 'node:path';
|
|
57
|
+
|
|
58
|
+
export const BASELINE_PATH = '.claude/.template-baseline.json';
|
|
59
|
+
|
|
60
|
+
// [source name, installed name] pairs for the verbatim-installed roots. The
|
|
61
|
+
// staged payload carries the live names; the template tree stores .claude/ and
|
|
62
|
+
// .mcp.json under the inert names _claude/ and _mcp.json, so scaffold passes
|
|
63
|
+
// the inert pairs.
|
|
64
|
+
export const LIVE_PAIRS = [
|
|
65
|
+
['.claude', '.claude'],
|
|
66
|
+
['.mcp.json', '.mcp.json'],
|
|
67
|
+
['.claudeignore', '.claudeignore'],
|
|
68
|
+
];
|
|
69
|
+
export const INERT_PAIRS = [
|
|
70
|
+
['_claude', '.claude'],
|
|
71
|
+
['_mcp.json', '.mcp.json'],
|
|
72
|
+
['.claudeignore', '.claudeignore'],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Machine-local or per-workspace files that never belong in the baseline.
|
|
76
|
+
const OWNED_PATHS = new Set([
|
|
77
|
+
'.claude/settings.local.json',
|
|
78
|
+
'.claude/.active-session.json',
|
|
79
|
+
BASELINE_PATH,
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// Entire nested worktrees live under .claude/worktrees/ — never walked.
|
|
83
|
+
const SKIP_DIRS = new Set(['worktrees']);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The content hash used by the baseline and by classification: sha256 with
|
|
87
|
+
* CRLF normalized to LF for text files, byte-exact for binary (anything
|
|
88
|
+
* containing a NUL byte — git's own text/binary heuristic). Both the payload
|
|
89
|
+
* and the workspace side hash through this, so a git autocrlf checkout that
|
|
90
|
+
* stores CRLF where the payload ships LF classifies as identical instead of
|
|
91
|
+
* reading every file as locally modified.
|
|
92
|
+
*/
|
|
93
|
+
export function hashBytes(bytes) {
|
|
94
|
+
let body = bytes;
|
|
95
|
+
if (!bytes.includes(0)) {
|
|
96
|
+
// latin1 round-trips bytes 1:1 — safe on text that isn't valid UTF-8 too.
|
|
97
|
+
body = Buffer.from(bytes.toString('latin1').replace(/\r\n/g, '\n'), 'latin1');
|
|
98
|
+
}
|
|
99
|
+
return createHash('sha256').update(body).digest('hex');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function* walkFiles(dir, prefix = '') {
|
|
103
|
+
let entries;
|
|
104
|
+
try {
|
|
105
|
+
entries = readdirSync(dir).sort();
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
for (const name of entries) {
|
|
110
|
+
if (prefix === '.claude' && SKIP_DIRS.has(name)) continue;
|
|
111
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
112
|
+
const full = join(dir, name);
|
|
113
|
+
let st;
|
|
114
|
+
try { st = statSync(full); } catch { continue; }
|
|
115
|
+
if (st.isDirectory()) yield* walkFiles(full, rel);
|
|
116
|
+
else if (st.isFile()) yield rel;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read the baseline at <root>/.claude/.template-baseline.json. Returns
|
|
122
|
+
* { templateVersion, files } or null when absent (workspaces older than the
|
|
123
|
+
* baseline's introduction) or unparseable — classification then falls back to
|
|
124
|
+
* the two-way behavior rather than failing the update.
|
|
125
|
+
*/
|
|
126
|
+
export function readBaseline(root) {
|
|
127
|
+
const path = join(resolve(root), BASELINE_PATH);
|
|
128
|
+
if (!existsSync(path)) return null;
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
131
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.files !== 'object' || parsed.files === null) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return { templateVersion: typeof parsed.templateVersion === 'string' ? parsed.templateVersion : null, files: parsed.files };
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build a baseline from a template source directory (a staged payload with
|
|
142
|
+
* live names, or the template tree via INERT_PAIRS). `version` overrides the
|
|
143
|
+
* payload manifest's templateVersion — pass it when the source has no
|
|
144
|
+
* .manifest.json (the template tree).
|
|
145
|
+
*/
|
|
146
|
+
export function buildBaseline(sourceDir, { pairs = LIVE_PAIRS, version = null } = {}) {
|
|
147
|
+
const absSource = resolve(sourceDir);
|
|
148
|
+
const files = {};
|
|
149
|
+
for (const [sourceName, installedName] of pairs) {
|
|
150
|
+
const src = join(absSource, sourceName);
|
|
151
|
+
if (!existsSync(src)) continue;
|
|
152
|
+
let st;
|
|
153
|
+
try { st = statSync(src); } catch { continue; }
|
|
154
|
+
if (st.isFile()) {
|
|
155
|
+
if (!OWNED_PATHS.has(installedName) && !installedName.endsWith('.test.mjs')) {
|
|
156
|
+
files[installedName] = hashBytes(readFileSync(src));
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const rel of walkFiles(src, installedName)) {
|
|
161
|
+
if (OWNED_PATHS.has(rel) || rel.endsWith('.test.mjs')) continue;
|
|
162
|
+
files[rel] = hashBytes(readFileSync(join(absSource, sourceName, rel.slice(installedName.length + 1))));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
let templateVersion = version;
|
|
166
|
+
if (templateVersion === null) {
|
|
167
|
+
try {
|
|
168
|
+
const manifest = JSON.parse(readFileSync(join(absSource, '.manifest.json'), 'utf8'));
|
|
169
|
+
if (typeof manifest.templateVersion === 'string') templateVersion = manifest.templateVersion;
|
|
170
|
+
} catch {
|
|
171
|
+
// No manifest (or unreadable) — caller should pass `version`.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { templateVersion, files: Object.fromEntries(Object.keys(files).sort().map((k) => [k, files[k]])) };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Write the baseline for the workspace at `root`. Returns the written object.
|
|
179
|
+
*
|
|
180
|
+
* Refuses to write an empty baseline: a missing source directory or one that
|
|
181
|
+
* yields zero verbatim files throws rather than clobbering an existing good
|
|
182
|
+
* baseline with `{files:{}}` (which would make the next update classify every
|
|
183
|
+
* template change as a local edit).
|
|
184
|
+
*
|
|
185
|
+
* Before writing, entries carried over from the previous baseline are kept as
|
|
186
|
+
* they were for unapplied updates: a file whose workspace content still
|
|
187
|
+
* matches the old baseline while the payload ships something new (a declined
|
|
188
|
+
* `updated` batch) keeps the OLD entry, so the next update still offers the
|
|
189
|
+
* change instead of filing the file away as a local edit.
|
|
190
|
+
*/
|
|
191
|
+
export function writeBaseline(root, sourceDir, opts = {}) {
|
|
192
|
+
const absRoot = resolve(root);
|
|
193
|
+
const baseline = buildBaseline(sourceDir, opts);
|
|
194
|
+
if (Object.keys(baseline.files).length === 0) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`No verbatim template files found under ${resolve(sourceDir)} — refusing to write an empty baseline`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const previous = readBaseline(absRoot);
|
|
200
|
+
if (previous) {
|
|
201
|
+
for (const rel of Object.keys(baseline.files)) {
|
|
202
|
+
const oldHash = previous.files[rel];
|
|
203
|
+
if (typeof oldHash !== 'string' || oldHash === baseline.files[rel]) continue;
|
|
204
|
+
const installed = join(absRoot, rel);
|
|
205
|
+
if (existsSync(installed) && hashBytes(readFileSync(installed)) === oldHash) {
|
|
206
|
+
// Unapplied update: the workspace never took the payload's change.
|
|
207
|
+
baseline.files[rel] = oldHash;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const dest = join(absRoot, BASELINE_PATH);
|
|
212
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
213
|
+
writeFileSync(dest, JSON.stringify(baseline, null, 2) + '\n');
|
|
214
|
+
return baseline;
|
|
215
|
+
}
|
|
@@ -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
|
|
@@ -126,6 +126,12 @@ Also install these top-level files from the payload:
|
|
|
126
126
|
|
|
127
127
|
**Per-repo CLAUDE.md stubs:** For each repo in `workspace.json`, check if `repos/{repo}/CLAUDE.md` exists. If not, ask "Scaffold a CLAUDE.md for {repo}? [Y/n]". If yes, write a blank stub from `.workspace-update/repo-claude.md.tmpl`, substituting `{{repo-name}}` with the repo name. The stub body is comment text only — no workspace-specific content — and its `## Commands` section is where you will add repo-specific test, lint, and build commands.
|
|
128
128
|
|
|
129
|
+
**Write the template baseline** now that the components are installed, so future `/workspace-update` runs can tell template changes from local edits (three-way classification):
|
|
130
|
+
```bash
|
|
131
|
+
node .claude/scripts/classify-update.mjs --root . --write-baseline --payload .workspace-update
|
|
132
|
+
```
|
|
133
|
+
It hashes the payload's verbatim files — run it before Step 15 deletes the payload.
|
|
134
|
+
|
|
129
135
|
**Commit:** `git commit -m "feat: install template components from payload"`
|
|
130
136
|
|
|
131
137
|
### Step 6: Activate optional rules
|