@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
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/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
|
}
|
package/lib/scaffold.test.mjs
CHANGED
|
@@ -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
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
|
}
|