@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 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 runs a maintenance audit before and after.
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/lib/init.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, readdirSync, statSync } from 'fs';
3
3
  import { join, basename } from 'path';
4
4
  import { stagePayload } from './payload.mjs';
5
+ import { writeBaseline } from '../template/_claude/scripts/template-baseline.mjs';
5
6
 
6
7
  function ensureDir(dir) {
7
8
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -127,6 +128,14 @@ export async function initWorkspace(targetDir) {
127
128
  }
128
129
  }
129
130
 
131
+ // Record the template baseline — the sha256 of every verbatim payload file —
132
+ // so the first /workspace-update can tell template changes from local edits
133
+ // three ways (gh:183). Entries always hold the payload's content, so the
134
+ // components /workspace-init installs verbatim are already covered;
135
+ // /workspace-init refreshes the baseline after the full install anyway.
136
+ writeBaseline(targetDir, payloadDir, { version: toVersion });
137
+ console.log(' Wrote template baseline (.claude/.template-baseline.json)');
138
+
130
139
  console.log(`
131
140
  Workspace initialized (v${toVersion}).
132
141
 
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // Test for init.mjs (--init scaffolding)
3
+ // Run: node lib/init.test.mjs
4
+ //
5
+ // --init stages the template payload, installs the bootstrap skills plus
6
+ // hooks/scripts/lib, and must leave a template baseline behind: the sha256 of
7
+ // every verbatim payload file, so the first /workspace-update classifies
8
+ // three ways instead of treating every template change as a local edit
9
+ // (gh:183).
10
+ import { initWorkspace } from './init.mjs';
11
+ import { createHash } from 'node:crypto';
12
+ import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
13
+ import { join } from 'path';
14
+ import { tmpdir } from 'os';
15
+ import { fileURLToPath } from 'url';
16
+ import { dirname } from 'path';
17
+
18
+ let failed = 0;
19
+ let passed = 0;
20
+ function check(label, ok) {
21
+ if (ok) { passed++; } else {
22
+ failed++;
23
+ console.error(` FAIL: ${label}`);
24
+ }
25
+ }
26
+
27
+ const here = dirname(fileURLToPath(import.meta.url));
28
+ const pkgVersion = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version;
29
+ const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex');
30
+
31
+ const root = mkdtempSync(join(tmpdir(), 'init-test-'));
32
+
33
+ try {
34
+ await initWorkspace(root);
35
+
36
+ const baselinePath = join(root, '.claude', '.template-baseline.json');
37
+ check('baseline written at .claude/.template-baseline.json', existsSync(baselinePath));
38
+ const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
39
+
40
+ check(`baseline records the payload version (${pkgVersion})`, baseline.templateVersion === pkgVersion);
41
+ check(
42
+ 'baseline covers the bootstrap skills --init installs',
43
+ typeof baseline.files['.claude/skills/workspace-update/SKILL.md'] === 'string'
44
+ && typeof baseline.files['.claude/skills/workspace-init/SKILL.md'] === 'string',
45
+ );
46
+ check(
47
+ 'baseline covers hooks and scripts',
48
+ typeof baseline.files['.claude/hooks/session-start.mjs'] === 'string'
49
+ && typeof baseline.files['.claude/scripts/classify-update.mjs'] === 'string',
50
+ );
51
+ check(
52
+ 'baseline covers the standalone verbatim files',
53
+ typeof baseline.files['.mcp.json'] === 'string' && typeof baseline.files['.claudeignore'] === 'string',
54
+ );
55
+ check(
56
+ 'entry hash matches the installed file byte-for-byte',
57
+ baseline.files['.claude/scripts/classify-update.mjs'] === sha(join(root, '.claude', 'scripts', 'classify-update.mjs')),
58
+ );
59
+ check(
60
+ 'baseline never covers tests (the tarball ships none)',
61
+ Object.keys(baseline.files).every((k) => !k.endsWith('.test.mjs')),
62
+ );
63
+ check(
64
+ 'baseline never covers machine-local files',
65
+ !('.claude/settings.local.json' in baseline.files) && !('.claude/.active-session.json' in baseline.files),
66
+ );
67
+ } finally {
68
+ rmSync(root, { recursive: true, force: true });
69
+ }
70
+
71
+ if (failed > 0) {
72
+ console.error(`${failed} check(s) failed, ${passed} passed`);
73
+ process.exit(1);
74
+ }
75
+ console.log(`${passed} checks passed`);
package/lib/scaffold.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { cpSync, mkdirSync, readFileSync, writeFileSync, renameSync, existsSync, rmSync, statSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { writeBaseline, INERT_PAIRS } from '../template/_claude/scripts/template-baseline.mjs';
4
5
 
5
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
7
  const TEMPLATE_DIR = join(__dirname, '..', 'template');
@@ -98,5 +99,12 @@ export async function scaffold(answers) {
98
99
  }
99
100
  }
100
101
 
102
+ // Record the template baseline — the sha256 of every verbatim-installed
103
+ // file — so /workspace-update can classify three ways (workspace vs payload
104
+ // vs baseline) instead of treating every template change as a local edit
105
+ // (gh:183). The template tree carries the inert names; entries always hold
106
+ // the template's content, so hashes come straight from the source of truth.
107
+ writeBaseline(directory, TEMPLATE_DIR, { pairs: INERT_PAIRS, version: pkgJson.version });
108
+
101
109
  return directory;
102
110
  }
@@ -48,6 +48,26 @@ try {
48
48
  check('no _claude/ leftover', !existsSync(join(directory, '_claude')));
49
49
  check('no _mcp.json leftover', !existsSync(join(directory, '_mcp.json')));
50
50
  check('no _gitignore leftover', !existsSync(join(directory, '_gitignore')));
51
+
52
+ // Template baseline: hashes of the verbatim-installed files, for three-way
53
+ // update classification (gh:183)
54
+ const baselinePath = join(directory, '.claude', '.template-baseline.json');
55
+ check('template baseline written', existsSync(baselinePath));
56
+ const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
57
+ const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
58
+ check(`baseline records the package version (${pkgVersion})`, baseline.templateVersion === pkgVersion);
59
+ check(
60
+ 'baseline covers rules installed verbatim from the template',
61
+ typeof baseline.files['.claude/rules/coherent-revisions.md'] === 'string',
62
+ );
63
+ check(
64
+ 'baseline covers .mcp.json via its inert source name',
65
+ typeof baseline.files['.mcp.json'] === 'string' && typeof baseline.files['.claudeignore'] === 'string',
66
+ );
67
+ check(
68
+ 'baseline never covers tests or machine-local files',
69
+ Object.keys(baseline.files).every((k) => !k.endsWith('.test.mjs') && k !== '.claude/settings.local.json'),
70
+ );
51
71
  } finally {
52
72
  rmSync(directory, { recursive: true, force: true });
53
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulysses-ai/create-workspace",
3
- "version": "0.20.0-beta.0",
3
+ "version": "0.22.0-beta.0",
4
4
  "description": "A workspace convention for Claude Code: sessions, handoffs, and shared context as files in git",
5
5
  "keywords": [
6
6
  "claude",
@@ -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 (runs maintenance before/after)
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]` — workspace health checks and 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 skipAudit = stale
30
- ? 'Skip the pre-update audit and proceed directly to comparing and applying changes. '
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
- ${skipAudit}Do not proceed with the user's request until initialization is complete.`);
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
- ${skipAudit}Do not proceed with the user's request until the update is complete.`);
41
+ ${staleHint}Do not proceed with the user's request until the update is complete.`);
42
42
  }
@@ -765,4 +765,6 @@ export {
765
765
  stripFrontmatter,
766
766
  gitIgnoredPaths,
767
767
  isLocalOnlyName,
768
+ readIgnorePrefixes,
769
+ isIgnored,
768
770
  };