@ulysses-ai/create-workspace 0.15.0-beta.0 → 0.15.0-beta.2

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 (26) hide show
  1. package/lib/init.mjs +12 -25
  2. package/lib/scaffold.mjs +3 -2
  3. package/package.json +1 -1
  4. package/template/.claude/rules/memory-guidance.md +30 -0
  5. package/template/.claude/scripts/build-workspace-context.mjs +370 -23
  6. package/template/.claude/scripts/migrate-canonical-priority.mjs +108 -0
  7. package/template/.claude/skills/complete-work/SKILL.md +88 -0
  8. package/template/.claude/skills/maintenance/SKILL.md +79 -11
  9. package/template/.claude/skills/release/SKILL.md +3 -0
  10. package/template/.claude/skills/workspace-update/SKILL.md +7 -1
  11. package/template/workspace.json.tmpl +1 -0
  12. package/template/.claude/hooks/_bash-output-advisory.test.mjs +0 -88
  13. package/template/.claude/hooks/_utils.test.mjs +0 -99
  14. package/template/.claude/lib/freshness.test.mjs +0 -175
  15. package/template/.claude/lib/registry-check.test.mjs +0 -130
  16. package/template/.claude/lib/session-frontmatter.test.mjs +0 -242
  17. package/template/.claude/scripts/build-workspace-context.test.mjs +0 -633
  18. package/template/.claude/scripts/capture-context.test.mjs +0 -383
  19. package/template/.claude/scripts/generate-claude-local.test.mjs +0 -184
  20. package/template/.claude/scripts/migrate-claude-md-freshness-include.test.mjs +0 -54
  21. package/template/.claude/scripts/migrate-session-layout.test.mjs +0 -144
  22. package/template/.claude/scripts/migrate-to-workspace-context.test.mjs +0 -325
  23. package/template/.claude/scripts/sweep-references.test.mjs +0 -184
  24. package/template/.claude/scripts/sync-tasks.test.mjs +0 -350
  25. package/template/.claude/scripts/trackers/github-issues.test.mjs +0 -190
  26. package/template/.claude/scripts/trackers/interface.test.mjs +0 -40
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ // Idempotent migrator: back-fill `priority: critical` on locked workspace-context
3
+ // files that lack the field. Default-to-critical preserves existing behavior —
4
+ // no surprise drops on upgrade.
5
+ //
6
+ // Usage:
7
+ // node migrate-canonical-priority.mjs [--root <path>]
8
+ //
9
+ // Walks <root>/workspace-context/shared/locked/*.md. For each file:
10
+ // - Skip non-.md files and files without parseable frontmatter (warn to stderr).
11
+ // - If `priority` is already set (any value), leave the file untouched.
12
+ // - Otherwise add `priority: critical` losslessly via updateSessionContent.
13
+ //
14
+ // Returns { status, files: { applied, unchanged } } where status is 'applied'
15
+ // when at least one file was modified, else 'noop'. Always exits 0 — idempotent
16
+ // migrations don't fail.
17
+
18
+ import {
19
+ existsSync,
20
+ readFileSync,
21
+ writeFileSync,
22
+ readdirSync,
23
+ statSync,
24
+ realpathSync,
25
+ } from 'node:fs';
26
+ import { join, resolve } from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+ import { parseSessionContent, updateSessionContent } from '../lib/session-frontmatter.mjs';
29
+
30
+ function isMainModule(metaUrl) {
31
+ if (!process.argv[1]) return false;
32
+ try {
33
+ return realpathSync(fileURLToPath(metaUrl)) === realpathSync(process.argv[1]);
34
+ } catch { return false; }
35
+ }
36
+
37
+ function parseArgs(argv) {
38
+ const args = { root: process.cwd() };
39
+ for (let i = 2; i < argv.length; i++) {
40
+ const a = argv[i];
41
+ if (a === '--root') args.root = argv[++i];
42
+ else throw new Error(`Unknown arg: ${a}`);
43
+ }
44
+ return args;
45
+ }
46
+
47
+ export function migrateCanonicalPriority({ root }) {
48
+ const absRoot = resolve(root);
49
+ const lockedDir = join(absRoot, 'workspace-context', 'shared', 'locked');
50
+ const files = { applied: [], unchanged: [] };
51
+
52
+ if (!existsSync(lockedDir)) {
53
+ return { status: 'noop', files };
54
+ }
55
+
56
+ let entries;
57
+ try {
58
+ entries = readdirSync(lockedDir).sort();
59
+ } catch {
60
+ return { status: 'noop', files };
61
+ }
62
+
63
+ for (const name of entries) {
64
+ if (!name.endsWith('.md')) continue;
65
+ const full = join(lockedDir, name);
66
+ let st;
67
+ try { st = statSync(full); } catch { continue; }
68
+ if (!st.isFile()) continue;
69
+
70
+ const raw = readFileSync(full, 'utf-8');
71
+ let parsed;
72
+ try {
73
+ parsed = parseSessionContent(raw);
74
+ } catch {
75
+ console.error(`warning: skipping ${full}: no parseable frontmatter`);
76
+ continue;
77
+ }
78
+
79
+ if (parsed?.fields?.priority !== undefined) {
80
+ files.unchanged.push(name);
81
+ continue;
82
+ }
83
+
84
+ const updated = updateSessionContent(raw, { priority: 'critical' });
85
+ writeFileSync(full, updated);
86
+ files.applied.push(name);
87
+ }
88
+
89
+ return {
90
+ status: files.applied.length > 0 ? 'applied' : 'noop',
91
+ files,
92
+ };
93
+ }
94
+
95
+ function main() {
96
+ const args = parseArgs(process.argv);
97
+ const result = migrateCanonicalPriority({ root: args.root });
98
+ process.stdout.write(JSON.stringify(result) + '\n');
99
+ }
100
+
101
+ if (isMainModule(import.meta.url)) {
102
+ try {
103
+ main();
104
+ } catch (err) {
105
+ process.stderr.write(`migrate-canonical-priority: ${err.message}\n`);
106
+ process.exit(1);
107
+ }
108
+ }
@@ -249,6 +249,94 @@ cd repos/{repo} && git pull origin {repo-branch}
249
249
  cd {main-workspace-root} && git pull origin main
250
250
  ```
251
251
 
252
+ **Step 10a.1: Tag the merge commit (release sessions only, project repos with `package.json`)**
253
+
254
+ The next three sub-substeps run only when the session branch starts with `release/` — the convention for release sessions (e.g., `release/v0.15.0-beta.0`). For feature, bugfix, and chore sessions, skip 10a.1, 10a.2, and 10a.3 entirely; non-release sessions don't trigger publishes. Detection is purely by branch prefix.
255
+
256
+ Derive the version tag from the branch name by stripping the `release/` prefix (so `release/v0.15.0-beta.0` yields `v0.15.0-beta.0`). For each project repo whose `package.json` declares a `version` field, verify that version matches the derived tag. The workspace repo is **never** tagged — only project repos with publishable `package.json` files get tagged, since the tag triggers `.github/workflows/publish.yml` in that project repo. If a project repo's `package.json` version doesn't match the release tag, skip that repo with a warning rather than failing the whole completion flow — the mismatch usually means `/release` was run against a different version than the branch name suggests, and the user needs to investigate before publishing.
257
+
258
+ Before tagging, preflight against origin: if `v{version}` already exists remotely, surface the conflict to the user with three explicit recovery options — **Reuse** (skip to 10a.2 if the existing tag points at the right commit), **Replace** (`git push origin --delete v{version}` then re-run 10a.1), or **Investigate** (`gh release view v{version}` to see what shipped). Do **not** silently force-push the tag; an existing tag means a published artifact, and overwriting it without confirmation can corrupt the npm registry's view of the release history.
259
+
260
+ If the tag is absent on origin, tag the merge commit (HEAD on `{default-branch}` after the prior `git pull origin {default-branch}`) and push the tag. The tag push triggers `.github/workflows/publish.yml`.
261
+
262
+ ```bash
263
+ # Detect: only run for release sessions.
264
+ if [[ ! "$branch" =~ ^release/ ]]; then
265
+ # Not a release session — skip 10a.1, 10a.2, 10a.3.
266
+ return
267
+ fi
268
+
269
+ # Extract the version from the branch name (release/v{X} → v{X}).
270
+ version_tag="${branch#release/}" # e.g. "v0.15.0-beta.0"
271
+
272
+ # For each project repo with a package.json containing a version field:
273
+ for repo in {project-repos-with-package-json}; do
274
+ cd repos/{repo}
275
+
276
+ # Verify package.json version matches the tag.
277
+ pkg_version=$(node -p "require('./package.json').version")
278
+ expected_version="${version_tag#v}"
279
+ if [ "$pkg_version" != "$expected_version" ]; then
280
+ echo "Skipping {repo}: package.json version ($pkg_version) does not match release tag ($expected_version)."
281
+ continue
282
+ fi
283
+
284
+ # Preflight: does the tag already exist on origin?
285
+ if git ls-remote --exit-code origin "refs/tags/$version_tag" >/dev/null 2>&1; then
286
+ # Tag exists. Surface to user with three options:
287
+ # 1. Reuse — skip to 10a.2 if the existing tag points at the right commit.
288
+ # 2. Replace — `git push origin --delete $version_tag` then re-run 10a.1.
289
+ # 3. Investigate — `gh release view $version_tag` to see what shipped.
290
+ # Do NOT silently force-push.
291
+ echo "Tag $version_tag already exists on origin. Aborting with recovery options."
292
+ return 1
293
+ fi
294
+
295
+ # Tag the merge commit (HEAD on default branch after the prior `git pull`).
296
+ git tag "$version_tag"
297
+ git push origin "$version_tag" # Triggers .github/workflows/publish.yml
298
+ done
299
+ ```
300
+
301
+ **Step 10a.2: Watch the publish workflow (release sessions only)**
302
+
303
+ For each project repo tagged in 10a.1, find and follow the `publish.yml` workflow run on GitHub. The workflow takes a moment to register against the new tag — poll `gh run list` up to 5 times with a 3-second backoff before giving up. Once the run is found, attach with `gh run watch` so the maintainer sees progress live alongside the unified summary. Append `|| true` to the watch command so a workflow failure does **not** abort the rest of `/complete-work`: the maintainer still needs to see the unified summary, including the failure URL, to decide whether to rerun, redo the release, or roll the tag back. If no run registers within the retry window, log a warning with the manual investigation command and continue.
304
+
305
+ ```bash
306
+ # Retry up to 5 times with 3-second backoff — the run takes a moment to register.
307
+ for i in 1 2 3 4 5; do
308
+ run_id=$(gh run list \
309
+ --repo {org}/{repo} \
310
+ --workflow publish.yml \
311
+ --branch "$version_tag" \
312
+ --limit 1 \
313
+ --json databaseId \
314
+ --jq '.[0].databaseId')
315
+ if [ -n "$run_id" ]; then break; fi
316
+ sleep 3
317
+ done
318
+
319
+ if [ -z "$run_id" ]; then
320
+ echo "Warning: no publish workflow run found for $version_tag after 15s. Investigate via 'gh run list'."
321
+ else
322
+ gh run watch "$run_id" --exit-status --repo {org}/{repo} || true
323
+ fi
324
+ ```
325
+
326
+ **Step 10a.3: Update the unified summary (release sessions only)**
327
+
328
+ The unified summary block presented earlier in Step 10a already has a section per project repo. For release sessions, append a `PUBLISH` section per tagged project repo to the same summary — this goes inside the existing summary, not in a new location, so the maintainer sees one consolidated report covering merges, tags, and npm publishes:
329
+
330
+ ```
331
+ PUBLISH ({repo}):
332
+ Tag: v{version}
333
+ Workflow: {run-url}
334
+ Status: success | failure
335
+ Published: {dist-tag}@{version} on npm
336
+ ```
337
+
338
+ Pull `Status` from the `gh run watch` exit code (success when the watch returned 0, failure otherwise). Pull `Published: {dist-tag}@{version}` from the workflow's published-package output if available; if the workflow failed before publishing, omit the `Published:` line and rely on `Status: failure` plus the workflow URL to point the maintainer at the failure.
339
+
252
340
  #### Step 10b: Local / bare / other remotes — local merge flow
253
341
 
254
342
  No PRs are created — these remotes don't have a PR concept (or we don't have a client wired up for them). Present an adjusted summary:
@@ -53,13 +53,33 @@ For each workspace-context `.md` file and each `work-sessions/*/workspace/sessio
53
53
  node .claude/scripts/build-workspace-context.mjs --check --root .
54
54
  ```
55
55
 
56
- The script exits 1 if any of the three artifact types is missing or stale, and reports per-file status as JSON:
56
+ The script reports per-artifact status as JSON and uses three exit codes to distinguish what's wrong:
57
+
58
+ - `0` — all artifacts current and the rendered canonical fits inside `workspace.canonicalBudgetBytes`.
59
+ - `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).
60
+ - `2` — artifacts are current but canonical body bytes exceed the budget after the trim and stub stages have already run. 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.
61
+
62
+ The JSON payload always includes a `canonical` block summarizing the budget outcome:
63
+
64
+ ```json
65
+ {
66
+ "status": "current",
67
+ "missing": [],
68
+ "stale": [],
69
+ "canonical": {
70
+ "budget": 40960,
71
+ "current": 47802,
72
+ "overBy": 6842,
73
+ "selectionStatus": "stubbed",
74
+ "trimmedFiles": ["post-release-discipline"],
75
+ "stubbedFiles": ["project-status", "release-flow-recipes"]
76
+ }
77
+ }
78
+ ```
57
79
 
58
- - `missing` `workspace-context/` exists but the artifact does not. Run `--write` to create.
59
- - `stale` — the artifact exists but no longer matches the filesystem. Causes: a file was added or deleted, a `description:` was changed, a `shared/locked/` file was edited, an `.indexignore` rule was added. Run `--write` to regenerate.
60
- - `current` — everything matches.
80
+ `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.
61
81
 
62
- Audit mode reports the status. Cleanup mode runs `--write` if stale or missing, then re-checks.
82
+ Audit mode reports the status verbatim. When `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 9 if the post-regen check still reports `over-budget`.
63
83
 
64
84
  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.
65
85
 
@@ -101,8 +121,55 @@ Active recommendations. Flags problems and suggests fixes, but asks before actin
101
121
  - Surface: "{file} says X but {newer-file} now says Y. Update {file}?"
102
122
  - This is the capture-time cross-check, run retroactively instead of inline
103
123
 
104
- ### 9. Health metrics
105
- - Size of `workspace-context/shared/locked/` relative to the active model's context window — flag if over 5% (yellow) or 15% (red). Absolute byte count is a weak proxy; contradictions, stale references, and duplicated coverage across files matter more than total size.
124
+ ### 9. Canonical budget triage
125
+
126
+ This step runs only when the post-regen `--check` from step 8 still reports `selectionStatus: 'over-budget'`. If the regular regen pass cleared the budget — or if `--check` was already `ok`, `trimmed`, or `stubbed` after step 8 — skip this step entirely.
127
+
128
+ 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.
129
+
130
+ Inputs to gather before the first menu render:
131
+
132
+ - The `canonical` block from the `--check` JSON: `budget`, `current`, `overBy`, `selectionStatus`, `trimmedFiles`, `stubbedFiles`.
133
+ - Each `workspace-context/shared/locked/*.md` file with its on-disk byte size and frontmatter `priority`.
134
+ - Per file, a list of `## Section heading` spans with byte sizes — use a simple `^## ` boundary scan, not a full markdown AST. Locked files are short and shallow enough that the naive split is sufficient; if a file ever has nested headings that confuse it, fall back to opening the file in an editor (option `[c]` below).
135
+
136
+ Render the state and present this menu:
137
+
138
+ ```
139
+ Canonical budget: 40960 bytes. Current: 47802 bytes. Over by 6842 bytes.
140
+
141
+ Locked files by size:
142
+ 1. project-status.md (priority: reference, 18432 bytes) ← stubbed in canonical
143
+ 2. post-release-discipline.md (priority: critical, 12104 bytes)
144
+ 3. naming-conventions.md (priority: critical, 4218 bytes)
145
+ 4. cross-platform.md (priority: critical, 702 bytes)
146
+ 5. product-bias-risk.md (priority: critical, 1346 bytes)
147
+
148
+ Largest sections in priority:critical files (eligible for promotion to reference or trim markers):
149
+ - project-status.md > "What's Built" (5120 bytes)
150
+ - post-release-discipline.md > "Backstop: branch protection" (3892 bytes)
151
+ - post-release-discipline.md > "Why" (2104 bytes)
152
+
153
+ Triage (one at a time):
154
+ [a] Demote a file from critical to reference
155
+ [b] Add canonical:trim markers around a specific section
156
+ [c] Open a locked file in the editor for manual edits
157
+ [d] Skip — accept the over-budget warning
158
+ [q] Done
159
+ ```
160
+
161
+ For each chosen action:
162
+
163
+ - **`[a]` Demote.** Ask which file. Rewrite its frontmatter `priority: critical` → `priority: reference`. No body changes. Re-run `--check`, re-render the menu with the new state.
164
+ - **`[b]` Add trim markers.** Ask which `file > section`. Wrap the section by inserting `<!-- canonical:trim -->` on its own line just before the section heading and `<!-- canonical:end-trim -->` on its own line just after the section's last line (the line before the next `^## ` heading or EOF). Re-run `--check`, re-render.
165
+ - **`[c]` Open in editor.** Print the file path and pause. The user edits manually, returns, and confirms — then re-run `--check` and re-render.
166
+ - **`[d]` Skip.** Accept the over-budget state for this run; record the acknowledgement in the run summary. Audit will continue to surface the warning on subsequent runs.
167
+ - **`[q]` Done.** Exit triage. Report the final `--check` status as the run result.
168
+
169
+ Trim markers and demotions only matter for `priority: reference` files — `<!-- canonical:trim -->` spans on a `priority: critical` file are inert until the file is demoted. The triage flow never auto-decides which file to demote or which section to wrap; it surfaces the data, presents options, and waits.
170
+
171
+ ### 10. Health metrics
172
+ - Canonical budget — read from the same `--check` invocation as step 5. 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.
106
173
  - Number of ephemeral files — flag if accumulating without resolution
107
174
  - Session log stats (if `workspace-scratchpad/session-log.jsonl` exists):
108
175
  - Sessions without capture
@@ -118,7 +185,8 @@ Issues (3):
118
185
  ✗ workspace-context/team-member/alice/old-handoff.md references branch feature/old
119
186
  but that branch was deleted
120
187
  ✗ 2 inflight files exist but no active work session (orphaned?)
121
- Locked context is 18% of model context window (red threshold: 15%)
188
+ Canonical exceeds budget: 47 KB / 40 KB. 2 reference files were stubbed;
189
+ canonical is still 6.8 KB over. Run /maintenance cleanup to triage.
122
190
 
123
191
  Warnings (2):
124
192
  ⚠ workspace-context/team-member/alice/workspace-analytics.md not updated in 8 days
@@ -134,7 +202,7 @@ OK (5):
134
202
  ✓ All CLAUDE.md skill references valid
135
203
  ✓ Workspace structure matches rule
136
204
  ✓ workspace.json repos all present
137
- No frontmatter errors
205
+ Canonical: 17 KB / 40 KB (full)
138
206
  ✓ Template is up to date (v0.14.0)
139
207
  ```
140
208
 
@@ -145,9 +213,9 @@ OK (5):
145
213
  3. Read workspace.json — extract repo manifest
146
214
  4. Check `.claude/rules/`, `.claude/skills/`, `.claude/agents/` against references
147
215
  5. Check git state (worktrees, branches, remotes)
148
- 6. Run `node .claude/scripts/build-workspace-context.mjs --check --root .` — capture status
216
+ 6. Run `node .claude/scripts/build-workspace-context.mjs --check --root .` — capture status. Exit `0` = clean and within budget, `1` = artifact missing or stale, `2` = artifacts current but canonical body over budget. The `canonical` block in the JSON output drives both the audit budget line and the cleanup triage decision.
149
217
  7. Read session-log.jsonl if it exists
150
- 8. 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
218
+ 8. 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 9.
151
219
  9. Compile and present findings grouped by severity
152
220
 
153
221
  ## Notes
@@ -144,3 +144,6 @@ Process ephemeral workspace-context entries:
144
144
  - Context synthesis happens in the WORKSPACE repo — Step 7c (consumed-notes) and Step 9 (workspace-context synthesis) are separate workspace commits.
145
145
  - Per-repo is the default — each project repo has its own release cadence.
146
146
  - The coherent-revisions rule applies: write the CHANGELOG entry from scratch, don't concatenate branch notes.
147
+ - Tagging happens in `/complete-work`, not here. When the session branch starts with `release/`, `/complete-work` tags the merge commit on the project repo's default branch and pushes the tag, which triggers `.github/workflows/publish.yml` to publish to npm. `/release` produces the synthesis (CHANGELOG entry + version bump + consumed-notes deletion); `/complete-work` does the push, PR, merge, and tag.
148
+ - Do not run `npm publish` locally. The publish workflow is the only path that exercises OIDC trusted publishing — local publish requires 2FA OTP and bypasses that. If the workflow fails, investigate via `gh run view`; do not fall back to local publish.
149
+ - Recovery from a failed publish. Transient failure: rerun via `gh run rerun {run_id}`. Content failure: delete the tag (`git push origin --delete v{version} && git tag -d v{version}`), then redo the release in a new release session — `/start-work`, then `/release v{version}`, then `/complete-work`. Once a version is published to npm, that version is committed on the registry; bump and start a new release.
@@ -77,7 +77,7 @@ Read `toVersion` from `.workspace-update/.manifest.json` and update `templateVer
77
77
 
78
78
  ### Step 4a: Run idempotent migrators
79
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 and surface its action in the upgrade summary.
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
81
 
82
82
  ```bash
83
83
  node .workspace-update/.claude/scripts/migrate-claude-md-freshness-include.mjs
@@ -89,6 +89,12 @@ Output is JSON: `{"action":"appended"|"unchanged"|"skipped"}`.
89
89
  - `unchanged` — the line was already present.
90
90
  - `skipped` — no `CLAUDE.md` exists at the workspace root (rare; surface to the user).
91
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
+
92
98
  Add other migrators here as the template ships them.
93
99
 
94
100
  ### Step 5: Post-update verification
@@ -7,6 +7,7 @@
7
7
  "workSessionsDir": "work-sessions",
8
8
  "workspaceContextDir": "workspace-context",
9
9
  "releaseNotesDir": "workspace-context/release-notes",
10
+ "canonicalBudgetBytes": 40960,
10
11
  "subagentContextMaxBytes": 10240,
11
12
  "greeting": "Welcome back to {{project-name}}.",
12
13
  "releaseMode": "per-repo",
@@ -1,88 +0,0 @@
1
- #!/usr/bin/env node
2
- // Unit tests for bash-output-advisory.mjs pattern detection.
3
- // Run: node .claude/hooks/_bash-output-advisory.test.mjs
4
- import { detectNoisyPattern } from './bash-output-advisory.mjs';
5
-
6
- let failed = 0;
7
- let passed = 0;
8
-
9
- function shouldWarn(command, label) {
10
- const result = detectNoisyPattern(command);
11
- if (result) {
12
- passed++;
13
- } else {
14
- failed++;
15
- console.error(` FAIL: ${label}\n command: ${command}\n expected an advisory, got null`);
16
- }
17
- }
18
-
19
- function shouldNotWarn(command, label) {
20
- const result = detectNoisyPattern(command);
21
- if (!result) {
22
- passed++;
23
- } else {
24
- failed++;
25
- console.error(` FAIL: ${label}\n command: ${command}\n expected null, got: ${result}`);
26
- }
27
- }
28
-
29
- // === Test runners ===
30
- shouldWarn('npm test', 'bare npm test');
31
- shouldWarn('npm run test', 'bare npm run test');
32
- shouldWarn('yarn test', 'bare yarn test');
33
- shouldWarn('pnpm test', 'bare pnpm test');
34
- shouldWarn('bun test', 'bare bun test');
35
- shouldWarn('cargo test', 'bare cargo test');
36
- shouldWarn('npm test --coverage', 'npm test with non-scoping flag');
37
-
38
- shouldNotWarn('npm test path/to/file.test.mjs', 'npm test with file path');
39
- shouldNotWarn('npm test src/', 'npm test with directory');
40
- shouldNotWarn('npm test -- --grep auth', 'npm test with -- args');
41
- shouldNotWarn('npm test myFile.test.js', 'npm test with bare filename');
42
- shouldNotWarn('npm test | grep FAIL', 'npm test piped to grep');
43
- shouldNotWarn('npm test | head -50', 'npm test piped to head');
44
- shouldNotWarn('cargo test auth_module', 'cargo test with module filter');
45
-
46
- // === grep -r ===
47
- shouldWarn('grep -r "pattern" .', 'bare grep -r');
48
- shouldWarn('grep --recursive "pattern" src/', 'grep --recursive long form');
49
- shouldWarn('grep -rn "TODO" .', 'grep -rn (recursive + line numbers)');
50
-
51
- shouldNotWarn('grep -r --include="*.js" pattern .', 'grep -r with --include');
52
- shouldNotWarn('grep -r pattern . --exclude="*.log"', 'grep -r with --exclude');
53
- shouldNotWarn('grep "pattern" file.txt', 'non-recursive grep');
54
-
55
- // === find on broad anchors ===
56
- shouldWarn('find /', 'find on root');
57
- shouldWarn('find ~', 'find on home tilde');
58
- shouldWarn('find $HOME', 'find on $HOME');
59
-
60
- shouldNotWarn('find / -name "*.log"', 'find / with -name');
61
- shouldNotWarn('find ~ -path "*node_modules*" -prune', 'find ~ with -path');
62
- shouldNotWarn('find . -type f', 'find on cwd');
63
- shouldNotWarn('find ./src -name "*.ts"', 'find on relative subdir');
64
-
65
- // === cat on log-shaped files ===
66
- shouldWarn('cat server.log', 'cat .log file');
67
- shouldWarn('cat events.jsonl', 'cat .jsonl file');
68
- shouldWarn('cat trace.ndjson', 'cat .ndjson file');
69
- shouldWarn('cat path/to/big.log', 'cat .log in subdir');
70
-
71
- shouldNotWarn('cat package.json', 'cat package.json');
72
- shouldNotWarn('cat README.md', 'cat README');
73
- shouldNotWarn('cat src/index.ts', 'cat source file');
74
- shouldNotWarn('cat server.log | tail -50', 'cat .log piped to tail');
75
-
76
- // === redirected output (always allowed) ===
77
- shouldNotWarn('npm test > /tmp/test-output.txt', 'npm test redirected to file');
78
- shouldNotWarn('grep -r foo . > out.txt', 'grep -r redirected to file');
79
- shouldNotWarn('find / 2>&1 | tee /tmp/find.txt', 'find piped to tee');
80
-
81
- // === unrelated commands ===
82
- shouldNotWarn('git status', 'git status');
83
- shouldNotWarn('ls -la', 'ls');
84
- shouldNotWarn('echo hello', 'echo');
85
- shouldNotWarn('node --version', 'node version');
86
-
87
- console.log(`Passed: ${passed}, Failed: ${failed}`);
88
- if (failed > 0) process.exit(1);
@@ -1,99 +0,0 @@
1
- #!/usr/bin/env node
2
- // Unit tests for _utils.mjs tracker path helpers.
3
- // Run: node .claude/hooks/_utils.test.mjs
4
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
5
- import { tmpdir } from 'os';
6
- import { join } from 'path';
7
- import {
8
- sessionFilePath,
9
- sessionWorktreePath,
10
- sessionFolderPath,
11
- getSessionTrackers,
12
- } from './_utils.mjs';
13
-
14
- let failed = 0;
15
- let passed = 0;
16
-
17
- function assertEq(actual, expected, msg) {
18
- const a = JSON.stringify(actual);
19
- const e = JSON.stringify(expected);
20
- if (a === e) {
21
- passed++;
22
- } else {
23
- failed++;
24
- console.error(` FAIL: ${msg}\n expected: ${e}\n actual: ${a}`);
25
- }
26
- }
27
-
28
- function fixture() {
29
- const root = mkdtempSync(join(tmpdir(), 'utils-test-'));
30
- writeFileSync(
31
- join(root, 'workspace.json'),
32
- JSON.stringify({
33
- workspace: { workSessionsDir: 'work-sessions', scratchpadDir: 'workspace-scratchpad' },
34
- })
35
- );
36
- mkdirSync(join(root, 'work-sessions', 'demo', 'workspace'), { recursive: true });
37
- writeFileSync(
38
- join(root, 'work-sessions', 'demo', 'workspace', 'session.md'),
39
- '---\ntype: session-tracker\nname: demo\nstatus: active\n---\n\nbody\n'
40
- );
41
- return root;
42
- }
43
-
44
- // sessionFilePath points inside the worktree
45
- {
46
- const root = fixture();
47
- try {
48
- assertEq(
49
- sessionFilePath(root, 'demo'),
50
- join(root, 'work-sessions', 'demo', 'workspace', 'session.md'),
51
- 'sessionFilePath resolves to in-worktree tracker'
52
- );
53
- } finally {
54
- rmSync(root, { recursive: true, force: true });
55
- }
56
- }
57
-
58
- // sessionWorktreePath returns the workspace worktree root
59
- {
60
- const root = fixture();
61
- try {
62
- assertEq(
63
- sessionWorktreePath(root, 'demo'),
64
- join(root, 'work-sessions', 'demo', 'workspace'),
65
- 'sessionWorktreePath returns worktree root'
66
- );
67
- } finally {
68
- rmSync(root, { recursive: true, force: true });
69
- }
70
- }
71
-
72
- // sessionFolderPath still returns the session parent folder
73
- {
74
- const root = fixture();
75
- try {
76
- assertEq(
77
- sessionFolderPath(root, 'demo'),
78
- join(root, 'work-sessions', 'demo'),
79
- 'sessionFolderPath unchanged'
80
- );
81
- } finally {
82
- rmSync(root, { recursive: true, force: true });
83
- }
84
- }
85
-
86
- // getSessionTrackers walks the in-worktree path
87
- {
88
- const root = fixture();
89
- try {
90
- const trackers = getSessionTrackers(root);
91
- assertEq(trackers.length, 1, 'getSessionTrackers finds demo session');
92
- assertEq(trackers[0].name, 'demo', 'tracker name parsed');
93
- } finally {
94
- rmSync(root, { recursive: true, force: true });
95
- }
96
- }
97
-
98
- console.log(`Passed: ${passed}, Failed: ${failed}`);
99
- if (failed > 0) process.exit(1);