@ulysses-ai/create-workspace 0.18.0-beta.0 → 0.20.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/lib/init.mjs +4 -1
- package/lib/payload.mjs +18 -1
- package/lib/payload.test.mjs +55 -0
- package/lib/scaffold.mjs +23 -6
- package/lib/scaffold.test.mjs +59 -0
- package/lib/upgrade.mjs +20 -0
- package/lib/upgrade.test.mjs +119 -0
- package/package.json +3 -3
- package/template/CLAUDE.md.tmpl +3 -0
- package/template/{.claude → _claude}/lib/freshness.mjs +20 -7
- package/template/_claude/lib/registry-check.mjs +172 -0
- package/template/{.claude → _claude}/rules/forge-operations.md +6 -0
- package/template/{.claude → _claude}/rules/memory-guidance.md +4 -0
- package/template/{.claude → _claude}/rules/task-list-mirroring.md +6 -0
- package/template/{.claude → _claude}/scripts/build-workspace-context.mjs +25 -14
- package/template/{.claude → _claude}/scripts/chat-record.mjs +62 -21
- package/template/_claude/scripts/classify-update.mjs +117 -0
- package/template/{.claude → _claude}/scripts/cleanup-work-session.mjs +4 -2
- package/template/{.claude → _claude}/scripts/context-footprint.mjs +139 -30
- package/template/{.claude → _claude}/scripts/forges/github.mjs +2 -1
- package/template/{.claude → _claude}/scripts/forges/interface.mjs +5 -4
- package/template/_claude/scripts/merge-mode.mjs +61 -0
- package/template/{.claude → _claude}/scripts/migrate-canonical-priority.mjs +7 -1
- package/template/_claude/scripts/migrate-claude-md-freshness-include.mjs +55 -0
- package/template/{.claude → _claude}/scripts/migrate-session-layout.mjs +19 -8
- package/template/{.claude → _claude}/scripts/migrate-sessions.mjs +444 -121
- package/template/_claude/scripts/task-pr.mjs +555 -0
- package/template/{.claude → _claude}/scripts/task-worktree.mjs +26 -16
- package/template/{.claude → _claude}/scripts/trackers/github-issues.mjs +11 -0
- package/template/{.claude → _claude}/scripts/trackers/interface.mjs +8 -0
- package/template/{.claude → _claude}/skills/braindump/SKILL.md +1 -0
- package/template/{.claude → _claude}/skills/complete-work/SKILL.md +25 -77
- package/template/{.claude → _claude}/skills/context-placement/SKILL.md +8 -5
- package/template/{.claude → _claude}/skills/goal-driven-work/SKILL.md +1 -1
- package/template/{.claude → _claude}/skills/handoff/SKILL.md +1 -0
- package/template/{.claude → _claude}/skills/maintenance/SKILL.md +49 -17
- package/template/{.claude → _claude}/skills/migrate-sessions/SKILL.md +17 -5
- package/template/{.claude → _claude}/skills/release/SKILL.md +6 -2
- package/template/{.claude → _claude}/skills/start-work/SKILL.md +5 -5
- package/template/{.claude → _claude}/skills/workspace-init/SKILL.md +20 -14
- package/template/_claude/skills/workspace-update/SKILL.md +177 -0
- package/template/_gitignore +3 -0
- package/template/workspace.json.tmpl +1 -1
- package/template/.claude/lib/registry-check.mjs +0 -106
- package/template/.claude/scripts/migrate-claude-md-freshness-include.mjs +0 -30
- package/template/.claude/skills/workspace-update/SKILL.md +0 -134
- /package/template/{.claude → _claude}/agents/aside-researcher.md +0 -0
- /package/template/{.claude → _claude}/agents/implementer.md +0 -0
- /package/template/{.claude → _claude}/agents/researcher.md +0 -0
- /package/template/{.claude → _claude}/agents/reviewer.md +0 -0
- /package/template/{.claude → _claude}/hooks/_utils.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/bash-output-advisory.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/post-compact.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/pre-compact.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/repo-write-detection.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/session-end.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/session-start.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/subagent-start.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/version-freshness-check.mjs +0 -0
- /package/template/{.claude → _claude}/hooks/workspace-update-check.mjs +0 -0
- /package/template/{.claude → _claude}/lib/require-node.mjs +0 -0
- /package/template/{.claude → _claude}/lib/session-frontmatter.mjs +0 -0
- /package/template/{.claude → _claude}/recipes/migrate-from-notion.md +0 -0
- /package/template/{.claude → _claude}/rules/agent-rules.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/cloud-infrastructure.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/coherent-revisions.md +0 -0
- /package/template/{.claude → _claude}/rules/config-review.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/documentation.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/git-conventions.md +0 -0
- /package/template/{.claude → _claude}/rules/goal-driven-work.md +0 -0
- /package/template/{.claude → _claude}/rules/honest-pushback.md +0 -0
- /package/template/{.claude → _claude}/rules/local-dev-environment.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/product-integrity.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/scope-guard.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/superpowers-workflow.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/token-economics.md.skip +0 -0
- /package/template/{.claude → _claude}/rules/work-item-tracking.md +0 -0
- /package/template/{.claude → _claude}/rules/workspace-structure.md +0 -0
- /package/template/{.claude → _claude}/scripts/add-repo-to-session.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/capture-context.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/create-work-session.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/forges/gitlab.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/generate-claude-local.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-open-work.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/migrate-to-workspace-context.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/sweep-references.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/sync-tasks.mjs +0 -0
- /package/template/{.claude → _claude}/scripts/workspace-diagnostics.mjs +0 -0
- /package/template/{.claude → _claude}/settings.json +0 -0
- /package/template/{.claude → _claude}/skills/aside/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/framing.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/pitfalls.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/checklists/review.md +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/bulk-fill-migration.py +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/forbidden-word-grep.mjs +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/scripts/leak-grep.mjs +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/custom.css.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Arrow.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Box.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/Region.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/SectionTitle.tsx +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/primitives/tokens.ts +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/sidebars.ts.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/build-docs-site/templates/spec.md.tmpl +0 -0
- /package/template/{.claude → _claude}/skills/pause-work/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/promote/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/setup-tracker/SKILL.md +0 -0
- /package/template/{.claude → _claude}/skills/sync-work/SKILL.md +0 -0
- /package/template/{.mcp.json → _mcp.json} +0 -0
package/lib/init.mjs
CHANGED
|
@@ -20,7 +20,10 @@ export async function initWorkspace(targetDir) {
|
|
|
20
20
|
const { toVersion, payloadDir } = stagePayload(targetDir, { action: 'init' });
|
|
21
21
|
console.log(` Staged template payload (v${toVersion})`);
|
|
22
22
|
|
|
23
|
-
// Install only the bootstrap skills needed to complete initialization
|
|
23
|
+
// Install only the bootstrap skills needed to complete initialization.
|
|
24
|
+
// The payload keeps live names (.claude/, .mcp.json) — stagePayload()
|
|
25
|
+
// maps them back from the template's inert names so the staged layout
|
|
26
|
+
// stays readable by skills already installed in the workspace.
|
|
24
27
|
const bootstrapSkills = ['workspace-init', 'workspace-update'];
|
|
25
28
|
const payloadSkills = join(payloadDir, '.claude', 'skills');
|
|
26
29
|
const targetSkills = join(targetDir, '.claude', 'skills');
|
package/lib/payload.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// lib/payload.mjs
|
|
2
|
-
import { existsSync, cpSync, rmSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { existsSync, cpSync, rmSync, mkdirSync, writeFileSync, readFileSync, renameSync } from 'fs';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
|
|
@@ -22,6 +22,23 @@ export function stagePayload(targetDir, { action, fromVersion = null }) {
|
|
|
22
22
|
// Copy template to payload directory
|
|
23
23
|
cpSync(TEMPLATE_DIR, payloadDir, { recursive: true });
|
|
24
24
|
|
|
25
|
+
// The template stores .claude/ and .mcp.json under the inert names
|
|
26
|
+
// _claude/ and _mcp.json (Claude Code protects the live names from
|
|
27
|
+
// headless edits). The staged payload is an on-disk format shared with
|
|
28
|
+
// the /workspace-update skill ALREADY INSTALLED in the workspace, which
|
|
29
|
+
// reads .workspace-update/.claude/... — so it must keep the live names
|
|
30
|
+
// exactly as older package versions staged them. _gitignore stays inert:
|
|
31
|
+
// the skills have always merged it under that name.
|
|
32
|
+
for (const [from, to] of [
|
|
33
|
+
['_claude', '.claude'],
|
|
34
|
+
['_mcp.json', '.mcp.json'],
|
|
35
|
+
]) {
|
|
36
|
+
const src = join(payloadDir, from);
|
|
37
|
+
if (existsSync(src)) {
|
|
38
|
+
renameSync(src, join(payloadDir, to));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
// Write manifest
|
|
26
43
|
const manifest = {
|
|
27
44
|
action,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Unit tests for payload.mjs
|
|
3
|
+
// Run: node lib/payload.test.mjs
|
|
4
|
+
//
|
|
5
|
+
// The staged .workspace-update/ payload is an on-disk format shared with
|
|
6
|
+
// the /workspace-update skill already installed in existing workspaces,
|
|
7
|
+
// which reads .workspace-update/.claude/... and .workspace-update/.mcp.json.
|
|
8
|
+
// These tests pin that layout even though template/ itself stores the
|
|
9
|
+
// protected paths under the inert names _claude/ and _mcp.json.
|
|
10
|
+
import { stagePayload, cleanPayload } from './payload.mjs';
|
|
11
|
+
import { mkdtempSync, rmSync, existsSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { tmpdir } from 'os';
|
|
14
|
+
|
|
15
|
+
let failed = 0;
|
|
16
|
+
let passed = 0;
|
|
17
|
+
function check(label, ok) {
|
|
18
|
+
if (ok) { passed++; } else {
|
|
19
|
+
failed++;
|
|
20
|
+
console.error(` FAIL: ${label}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'payload-test-'));
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const { payloadDir, toVersion } = stagePayload(root, { action: 'init' });
|
|
28
|
+
|
|
29
|
+
check('payload staged at .workspace-update/', existsSync(payloadDir));
|
|
30
|
+
check('manifest written', existsSync(join(payloadDir, '.manifest.json')));
|
|
31
|
+
check('returns the package version', typeof toVersion === 'string' && toVersion.length > 0);
|
|
32
|
+
|
|
33
|
+
// The payload keeps the live names older workspaces' skills expect
|
|
34
|
+
check('.claude/ staged under live name', existsSync(join(payloadDir, '.claude')));
|
|
35
|
+
check('.claude/skills/workspace-update/SKILL.md staged', existsSync(join(payloadDir, '.claude', 'skills', 'workspace-update', 'SKILL.md')));
|
|
36
|
+
check('.claude/settings.json staged', existsSync(join(payloadDir, '.claude', 'settings.json')));
|
|
37
|
+
check('.mcp.json staged under live name', existsSync(join(payloadDir, '.mcp.json')));
|
|
38
|
+
check('_gitignore stays inert (skills merge it under that name)', existsSync(join(payloadDir, '_gitignore')));
|
|
39
|
+
|
|
40
|
+
// The template's inert names must not leak into the payload
|
|
41
|
+
check('no _claude/ in payload', !existsSync(join(payloadDir, '_claude')));
|
|
42
|
+
check('no _mcp.json in payload', !existsSync(join(payloadDir, '_mcp.json')));
|
|
43
|
+
|
|
44
|
+
// cleanPayload removes the staging directory
|
|
45
|
+
cleanPayload(root);
|
|
46
|
+
check('cleanPayload removes .workspace-update/', !existsSync(payloadDir));
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(root, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (failed > 0) {
|
|
52
|
+
console.error(`${failed} check(s) failed, ${passed} passed`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
console.log(`${passed} checks passed`);
|
package/lib/scaffold.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cpSync, mkdirSync, readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
|
|
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
4
|
|
|
@@ -26,11 +26,28 @@ export async function scaffold(answers) {
|
|
|
26
26
|
// scripts and hooks first need them — we do NOT pre-create them here.
|
|
27
27
|
mkdirSync(join(directory, 'workspace-context', 'locked'), { recursive: true });
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// The template stores some files under inert names — _gitignore,
|
|
30
|
+
// _claude/, _mcp.json — because Claude Code treats .claude/ directories
|
|
31
|
+
// and .mcp.json files as protected paths and would block automated edits
|
|
32
|
+
// to the template. Install each under its live name in the target.
|
|
33
|
+
const inertNames = [
|
|
34
|
+
{ from: '_gitignore', to: '.gitignore' },
|
|
35
|
+
{ from: '_claude', to: '.claude' },
|
|
36
|
+
{ from: '_mcp.json', to: '.mcp.json' },
|
|
37
|
+
];
|
|
38
|
+
for (const { from, to } of inertNames) {
|
|
39
|
+
const src = join(directory, from);
|
|
40
|
+
const dest = join(directory, to);
|
|
41
|
+
if (!existsSync(src)) continue;
|
|
42
|
+
if (existsSync(dest) && statSync(src).isDirectory()) {
|
|
43
|
+
// Pre-populated target: merge into the existing directory — template
|
|
44
|
+
// files overwrite same-named ones, extra local files survive — then
|
|
45
|
+
// drop the inert copy.
|
|
46
|
+
cpSync(src, dest, { recursive: true });
|
|
47
|
+
rmSync(src, { recursive: true });
|
|
48
|
+
} else {
|
|
49
|
+
renameSync(src, dest);
|
|
50
|
+
}
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
// Process CLAUDE.md template
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// End-to-end test for scaffold.mjs
|
|
3
|
+
// Run: node lib/scaffold.test.mjs
|
|
4
|
+
//
|
|
5
|
+
// The template stores .gitignore, .claude/, and .mcp.json under the inert
|
|
6
|
+
// names _gitignore, _claude/, and _mcp.json (Claude Code protects the live
|
|
7
|
+
// names from headless edits). scaffold() must install every one under its
|
|
8
|
+
// live name and leave no inert leftovers behind.
|
|
9
|
+
import { scaffold } from './scaffold.mjs';
|
|
10
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { tmpdir } from 'os';
|
|
13
|
+
|
|
14
|
+
let failed = 0;
|
|
15
|
+
let passed = 0;
|
|
16
|
+
function check(label, ok) {
|
|
17
|
+
if (ok) { passed++; } else {
|
|
18
|
+
failed++;
|
|
19
|
+
console.error(` FAIL: ${label}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const directory = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await scaffold({
|
|
27
|
+
name: 'demo',
|
|
28
|
+
directory,
|
|
29
|
+
repos: [],
|
|
30
|
+
userName: 'tester',
|
|
31
|
+
activateRules: [],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Live names installed
|
|
35
|
+
check('.claude/ directory installed', existsSync(join(directory, '.claude')));
|
|
36
|
+
check('.claude/settings.json installed', existsSync(join(directory, '.claude', 'settings.json')));
|
|
37
|
+
check('.claude/settings.local.json written', existsSync(join(directory, '.claude', 'settings.local.json')));
|
|
38
|
+
check('.claude/rules/ installed', existsSync(join(directory, '.claude', 'rules')));
|
|
39
|
+
check('.claude/rules/coherent-revisions.md installed', existsSync(join(directory, '.claude', 'rules', 'coherent-revisions.md')));
|
|
40
|
+
check('.mcp.json installed', existsSync(join(directory, '.mcp.json')));
|
|
41
|
+
check('.gitignore installed', existsSync(join(directory, '.gitignore')));
|
|
42
|
+
|
|
43
|
+
// .mcp.json is the template's payload, unmodified
|
|
44
|
+
const mcp = JSON.parse(readFileSync(join(directory, '.mcp.json'), 'utf8'));
|
|
45
|
+
check('.mcp.json parses with mcpServers object', typeof mcp.mcpServers === 'object' && mcp.mcpServers !== null);
|
|
46
|
+
|
|
47
|
+
// Inert names are gone
|
|
48
|
+
check('no _claude/ leftover', !existsSync(join(directory, '_claude')));
|
|
49
|
+
check('no _mcp.json leftover', !existsSync(join(directory, '_mcp.json')));
|
|
50
|
+
check('no _gitignore leftover', !existsSync(join(directory, '_gitignore')));
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(directory, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (failed > 0) {
|
|
56
|
+
console.error(`${failed} check(s) failed, ${passed} passed`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
console.log(`${passed} checks passed`);
|
package/lib/upgrade.mjs
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
// lib/upgrade.mjs
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
4
5
|
import { stagePayload } from './payload.mjs';
|
|
5
6
|
|
|
7
|
+
// .workspace-update/ is a transient staging area and is gitignored from
|
|
8
|
+
// v0.19.0 on — but workspaces upgraded from older templates may still track
|
|
9
|
+
// it from a previous run. A tracked payload gets committed, shared with
|
|
10
|
+
// teammates, and confuses /workspace-update, so surface it loudly.
|
|
11
|
+
function warnIfPayloadTracked(targetDir) {
|
|
12
|
+
const r = spawnSync('git', ['ls-files', '.workspace-update'], {
|
|
13
|
+
cwd: targetDir,
|
|
14
|
+
encoding: 'utf-8',
|
|
15
|
+
});
|
|
16
|
+
// Not a git repo (or git unavailable) — nothing can be tracked.
|
|
17
|
+
if (r.status !== 0) return;
|
|
18
|
+
const tracked = (r.stdout || '').split('\n').filter(Boolean);
|
|
19
|
+
if (tracked.length === 0) return;
|
|
20
|
+
console.error(` Warning: .workspace-update/ is tracked by git (${tracked.length} file(s)) — it is a transient staging area.`);
|
|
21
|
+
console.error(` Untrack it and commit:\n git rm -r --cached .workspace-update\n git commit -m "chore: untrack .workspace-update payload"\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
export async function upgradeWorkspace(targetDir) {
|
|
7
25
|
const workspaceJsonPath = join(targetDir, 'workspace.json');
|
|
8
26
|
|
|
@@ -24,6 +42,8 @@ export async function upgradeWorkspace(targetDir) {
|
|
|
24
42
|
process.exit(1);
|
|
25
43
|
}
|
|
26
44
|
|
|
45
|
+
warnIfPayloadTracked(targetDir);
|
|
46
|
+
|
|
27
47
|
const fromVersion = config.workspace?.templateVersion || 'unknown';
|
|
28
48
|
|
|
29
49
|
// Stage payload
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Unit tests for upgrade.mjs
|
|
3
|
+
// Run: node lib/upgrade.test.mjs
|
|
4
|
+
//
|
|
5
|
+
// Covers the tracked-payload warning: .workspace-update/ is gitignored from
|
|
6
|
+
// v0.19.0, but a workspace upgraded from an older template may still carry a
|
|
7
|
+
// tracked payload from a previous run — --upgrade must say so.
|
|
8
|
+
import { upgradeWorkspace } from './upgrade.mjs';
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { tmpdir } from 'os';
|
|
12
|
+
import { execSync, spawnSync } from 'child_process';
|
|
13
|
+
|
|
14
|
+
let failed = 0;
|
|
15
|
+
let passed = 0;
|
|
16
|
+
function check(label, ok) {
|
|
17
|
+
if (ok) { passed++; } else {
|
|
18
|
+
failed++;
|
|
19
|
+
console.error(` FAIL: ${label}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function captureConsole(fn) {
|
|
24
|
+
const out = { log: [], error: [] };
|
|
25
|
+
const origLog = console.log;
|
|
26
|
+
const origError = console.error;
|
|
27
|
+
console.log = (...a) => { out.log.push(a.join(' ')); };
|
|
28
|
+
console.error = (...a) => { out.error.push(a.join(' ')); };
|
|
29
|
+
try {
|
|
30
|
+
return { result: fn(), out };
|
|
31
|
+
} finally {
|
|
32
|
+
console.log = origLog;
|
|
33
|
+
console.error = origError;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildWorkspace() {
|
|
38
|
+
const root = mkdtempSync(join(tmpdir(), 'upgrade-test-'));
|
|
39
|
+
execSync('git init -q -b main', { cwd: root, stdio: 'pipe' });
|
|
40
|
+
execSync('git config user.email test@example.com', { cwd: root, stdio: 'pipe' });
|
|
41
|
+
execSync('git config user.name Test', { cwd: root, stdio: 'pipe' });
|
|
42
|
+
writeFileSync(join(root, 'workspace.json'), JSON.stringify({
|
|
43
|
+
workspace: { name: 'demo', initialized: true, templateVersion: '0.15.0' },
|
|
44
|
+
repos: {},
|
|
45
|
+
}, null, 2) + '\n');
|
|
46
|
+
execSync('git add workspace.json', { cwd: root, stdio: 'pipe' });
|
|
47
|
+
execSync('git commit -q -m init', { cwd: root, stdio: 'pipe' });
|
|
48
|
+
return root;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function lsFilesPayload(root) {
|
|
52
|
+
return spawnSync('git', ['ls-files', '.workspace-update'], {
|
|
53
|
+
cwd: root,
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
}).stdout.split('\n').filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('# upgrade');
|
|
59
|
+
|
|
60
|
+
// 1. Clean workspace: payload staged, no tracked-payload warning, and the
|
|
61
|
+
// freshly staged payload is not tracked.
|
|
62
|
+
{
|
|
63
|
+
const root = buildWorkspace();
|
|
64
|
+
try {
|
|
65
|
+
const { out } = captureConsole(() => upgradeWorkspace(root));
|
|
66
|
+
const stderr = out.error.join('\n');
|
|
67
|
+
check('no tracked-payload warning on a clean workspace',
|
|
68
|
+
!stderr.includes('git rm -r --cached'));
|
|
69
|
+
check('payload staged', existsSync(join(root, '.workspace-update', '.manifest.json')));
|
|
70
|
+
check('freshly staged payload is not tracked', lsFilesPayload(root).length === 0);
|
|
71
|
+
check('staging reported', out.log.join('\n').includes('Staged template payload'));
|
|
72
|
+
} finally {
|
|
73
|
+
rmSync(root, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Workspace tracking .workspace-update/ from a pre-v0.19 upgrade: the
|
|
78
|
+
// warning fires and names the untrack command.
|
|
79
|
+
{
|
|
80
|
+
const root = buildWorkspace();
|
|
81
|
+
try {
|
|
82
|
+
// Simulate the old behavior: a payload committed tracked.
|
|
83
|
+
const payload = join(root, '.workspace-update');
|
|
84
|
+
mkdirSync(payload, { recursive: true });
|
|
85
|
+
writeFileSync(join(payload, '.manifest.json'), '{"action":"upgrade"}\n');
|
|
86
|
+
execSync('git add -f .workspace-update', { cwd: root, stdio: 'pipe' });
|
|
87
|
+
execSync('git commit -q -m "track payload"', { cwd: root, stdio: 'pipe' });
|
|
88
|
+
check('fixture really tracks the payload', lsFilesPayload(root).length > 0);
|
|
89
|
+
|
|
90
|
+
const { out } = captureConsole(() => upgradeWorkspace(root));
|
|
91
|
+
const stderr = out.error.join('\n');
|
|
92
|
+
check('tracked-payload warning fires', stderr.includes('.workspace-update/') && stderr.includes('tracked by git'));
|
|
93
|
+
check('warning names the untrack command', stderr.includes('git rm -r --cached .workspace-update'));
|
|
94
|
+
} finally {
|
|
95
|
+
rmSync(root, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. Not a git repo at all: staging still works, warning stays silent.
|
|
100
|
+
{
|
|
101
|
+
const root = mkdtempSync(join(tmpdir(), 'upgrade-nogit-'));
|
|
102
|
+
try {
|
|
103
|
+
writeFileSync(join(root, 'workspace.json'), JSON.stringify({
|
|
104
|
+
workspace: { name: 'demo', initialized: true, templateVersion: '0.15.0' },
|
|
105
|
+
repos: {},
|
|
106
|
+
}, null, 2) + '\n');
|
|
107
|
+
const { out } = captureConsole(() => upgradeWorkspace(root));
|
|
108
|
+
check('no warning outside a git repo', !out.error.join('\n').includes('tracked by git'));
|
|
109
|
+
check('payload staged outside a git repo', existsSync(join(root, '.workspace-update', '.manifest.json')));
|
|
110
|
+
} finally {
|
|
111
|
+
rmSync(root, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (failed > 0) {
|
|
116
|
+
console.error(`${failed} check(s) failed, ${passed} passed`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
console.log(`${passed} checks passed`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ulysses-ai/create-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/ukt-solutions/create-ulysses-workspace.git"
|
|
21
|
+
"url": "git+https://github.com/ukt-solutions/create-ulysses-workspace.git"
|
|
22
22
|
},
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"node": ">=20.9.0"
|
|
27
27
|
},
|
|
28
28
|
"bin": {
|
|
29
|
-
"create-workspace": "
|
|
29
|
+
"create-workspace": "bin/create.mjs"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"bin/",
|
package/template/CLAUDE.md.tmpl
CHANGED
|
@@ -33,6 +33,9 @@ This is a claude-workspace. All conventions are defined in .claude/rules/.
|
|
|
33
33
|
- `/workspace-update` — apply template updates (runs maintenance before/after)
|
|
34
34
|
- `/setup-tracker` — wire this workspace to an issue tracker (GitHub Issues shipped; others pluggable)
|
|
35
35
|
- `/maintenance [audit|cleanup]` — workspace health checks and cleanup
|
|
36
|
+
- `/context-placement` — decide where durable content belongs and what each destination costs before writing it
|
|
37
|
+
- `/goal-driven-work` — run multi-phase autonomous work under `/goal` with phase artifacts and agent-team dispatch
|
|
38
|
+
- `/migrate-sessions` — drain session-model work sessions and switch the workspace to the task lifecycle
|
|
36
39
|
- `/build-docs-site` — build a comprehensive Docusaurus documentation site for a project
|
|
37
40
|
|
|
38
41
|
## Compact Instructions
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
import './require-node.mjs';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { compareVersions, getLatestVersion, readCache, writeCache } from './registry-check.mjs';
|
|
4
|
+
import { compareVersions, getLatestVersion, pickComparisonVersion, readCache, writeCache } from './registry-check.mjs';
|
|
5
5
|
|
|
6
6
|
const BANNER_FILENAME = 'local-only-template-freshness.md';
|
|
7
7
|
const CACHE_FILENAME = '.version-check.json';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Refresh the version cache if stale, then write or delete the banner file
|
|
11
|
-
* based on a comparison of workspace.templateVersion to the
|
|
11
|
+
* based on a comparison of workspace.templateVersion to the npm dist-tag
|
|
12
|
+
* matching its release channel (see pickComparisonVersion).
|
|
13
|
+
*
|
|
14
|
+
* The cache stores the raw dist-tags rather than the chosen version, so a
|
|
15
|
+
* channel switch between cache write and read (e.g. an upgrade from a beta
|
|
16
|
+
* to a stable release inside the TTL) still picks the right tag.
|
|
12
17
|
*
|
|
13
18
|
* Returns:
|
|
14
19
|
* { status: 'outdated', current, latest, checkedAt }
|
|
15
|
-
* { status: 'current', current, latest, checkedAt }
|
|
20
|
+
* { status: 'current', current, latest, prerelease, checkedAt }
|
|
16
21
|
* { status: 'unknown', current, latest: null, checkedAt: null }
|
|
17
22
|
* { skipped: 'uninitialized' }
|
|
18
23
|
*
|
|
24
|
+
* `prerelease` on a current workspace is a newer `beta` dist-tag, offered
|
|
25
|
+
* as information only — it never marks the install stale.
|
|
26
|
+
*
|
|
19
27
|
* Pure I/O is parameterized via fetchFn / nowFn for testability.
|
|
20
28
|
*/
|
|
21
29
|
export async function refreshIfStale({
|
|
@@ -46,9 +54,9 @@ export async function refreshIfStale({
|
|
|
46
54
|
const stale = cacheAgeMs > ttlMs;
|
|
47
55
|
|
|
48
56
|
if (stale) {
|
|
49
|
-
const fresh = await getLatestVersion({ fetchFn });
|
|
57
|
+
const fresh = await getLatestVersion({ current, fetchFn });
|
|
50
58
|
if (fresh.version) {
|
|
51
|
-
cache = {
|
|
59
|
+
cache = { tags: fresh.tags, checkedAt: now.toISOString() };
|
|
52
60
|
writeCache(cachePath, cache);
|
|
53
61
|
}
|
|
54
62
|
// On fetch error, keep whatever cache we already had (could be null).
|
|
@@ -60,7 +68,12 @@ export async function refreshIfStale({
|
|
|
60
68
|
return { status: 'unknown', current, latest: null, checkedAt: null };
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
const latest = cache.
|
|
71
|
+
const { version: latest, prerelease } = pickComparisonVersion(current, cache.tags);
|
|
72
|
+
if (!latest) {
|
|
73
|
+
// Cached tags carry nothing comparable for this channel. Refuse to
|
|
74
|
+
// guess rather than compare against the wrong tag.
|
|
75
|
+
return { status: 'unknown', current, latest: null, checkedAt: cache.checkedAt };
|
|
76
|
+
}
|
|
64
77
|
const cmp = compareVersions(current, latest);
|
|
65
78
|
if (cmp < 0) {
|
|
66
79
|
writeFileSync(
|
|
@@ -70,6 +83,6 @@ export async function refreshIfStale({
|
|
|
70
83
|
return { status: 'outdated', current, latest, checkedAt: cache.checkedAt };
|
|
71
84
|
} else {
|
|
72
85
|
if (existsSync(bannerPath)) unlinkSync(bannerPath);
|
|
73
|
-
return { status: 'current', current, latest, checkedAt: cache.checkedAt };
|
|
86
|
+
return { status: 'current', current, latest, prerelease, checkedAt: cache.checkedAt };
|
|
74
87
|
}
|
|
75
88
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import './require-node.mjs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SemVer 2.0 comparison limited to the formats this scaffolder publishes:
|
|
7
|
+
* `x.y.z` and `x.y.z-prerelease.N`. Returns -1, 0, or 1.
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* - Compare major, minor, patch numerically.
|
|
11
|
+
* - A pre-release version is older than the same x.y.z without a tag.
|
|
12
|
+
* - Pre-release identifiers compare per-identifier; numeric identifiers
|
|
13
|
+
* compare numerically (so `beta.10 > beta.2`), non-numeric lexically.
|
|
14
|
+
*/
|
|
15
|
+
export function compareVersions(a, b) {
|
|
16
|
+
if (a === b) return 0;
|
|
17
|
+
const [aBase, aPre] = a.split('-', 2);
|
|
18
|
+
const [bBase, bPre] = b.split('-', 2);
|
|
19
|
+
const aParts = aBase.split('.').map(Number);
|
|
20
|
+
const bParts = bBase.split('.').map(Number);
|
|
21
|
+
for (let i = 0; i < 3; i++) {
|
|
22
|
+
if ((aParts[i] || 0) < (bParts[i] || 0)) return -1;
|
|
23
|
+
if ((aParts[i] || 0) > (bParts[i] || 0)) return 1;
|
|
24
|
+
}
|
|
25
|
+
if (!aPre && !bPre) return 0;
|
|
26
|
+
if (!aPre && bPre) return 1;
|
|
27
|
+
if (aPre && !bPre) return -1;
|
|
28
|
+
const aIds = aPre.split('.');
|
|
29
|
+
const bIds = bPre.split('.');
|
|
30
|
+
const len = Math.max(aIds.length, bIds.length);
|
|
31
|
+
for (let i = 0; i < len; i++) {
|
|
32
|
+
const ai = aIds[i];
|
|
33
|
+
const bi = bIds[i];
|
|
34
|
+
if (ai === undefined) return -1;
|
|
35
|
+
if (bi === undefined) return 1;
|
|
36
|
+
const aNum = /^\d+$/.test(ai);
|
|
37
|
+
const bNum = /^\d+$/.test(bi);
|
|
38
|
+
if (aNum && bNum) {
|
|
39
|
+
const an = Number(ai), bn = Number(bi);
|
|
40
|
+
if (an < bn) return -1;
|
|
41
|
+
if (an > bn) return 1;
|
|
42
|
+
} else if (aNum && !bNum) {
|
|
43
|
+
return -1;
|
|
44
|
+
} else if (!aNum && bNum) {
|
|
45
|
+
return 1;
|
|
46
|
+
} else {
|
|
47
|
+
if (ai < bi) return -1;
|
|
48
|
+
if (ai > bi) return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DIST_TAGS_URL = 'https://registry.npmjs.org/-/package/@ulysses-ai/create-workspace/dist-tags';
|
|
55
|
+
const DEFAULT_TIMEOUT_MS = 3000;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Which release channel an installed version rides: `stable` for a plain
|
|
59
|
+
* `x.y.z`, otherwise the first pre-release identifier (`0.19.0-beta.3` →
|
|
60
|
+
* `beta`). Returns null when the string isn't a version this scaffolder
|
|
61
|
+
* publishes.
|
|
62
|
+
*/
|
|
63
|
+
export function channelOf(version) {
|
|
64
|
+
if (typeof version !== 'string') return null;
|
|
65
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(version);
|
|
66
|
+
if (!match) return null;
|
|
67
|
+
return match[4] ? match[4].split('.')[0] : 'stable';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Pick the registry version an installed version should compare against,
|
|
72
|
+
* given the package's dist-tags.
|
|
73
|
+
*
|
|
74
|
+
* A pre-release install tracks the highest semver among `latest` and its
|
|
75
|
+
* own channel's tag, so a lagging `latest` (which has sat behind `beta`
|
|
76
|
+
* for whole release cycles) never masks a newer build on the channel the
|
|
77
|
+
* workspace actually rides. A stable install compares against `latest`
|
|
78
|
+
* alone; when the `beta` tag outruns `latest`, that version is returned
|
|
79
|
+
* separately as an available pre-release, so callers can surface it
|
|
80
|
+
* without calling the install stale.
|
|
81
|
+
*
|
|
82
|
+
* Returns { version, channel, prerelease } — `version` is null when no
|
|
83
|
+
* usable tag is present.
|
|
84
|
+
*/
|
|
85
|
+
export function pickComparisonVersion(current, tags) {
|
|
86
|
+
const channel = channelOf(current) || 'stable';
|
|
87
|
+
const candidates = [];
|
|
88
|
+
if (typeof tags?.latest === 'string') candidates.push(tags.latest);
|
|
89
|
+
if (channel !== 'stable' && typeof tags?.[channel] === 'string') candidates.push(tags[channel]);
|
|
90
|
+
let version = null;
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
if (version === null || compareVersions(candidate, version) > 0) version = candidate;
|
|
93
|
+
}
|
|
94
|
+
let prerelease = null;
|
|
95
|
+
if (
|
|
96
|
+
channel === 'stable' &&
|
|
97
|
+
typeof tags?.latest === 'string' &&
|
|
98
|
+
typeof tags?.beta === 'string' &&
|
|
99
|
+
compareVersions(tags.beta, tags.latest) > 0
|
|
100
|
+
) {
|
|
101
|
+
prerelease = tags.beta;
|
|
102
|
+
}
|
|
103
|
+
return { version, channel, prerelease };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fetch the scaffolder's dist-tags from the npm registry and pick the
|
|
108
|
+
* version to compare the installed `current` version against.
|
|
109
|
+
* Returns { version, channel, tags, prerelease, error } — `error` is null
|
|
110
|
+
* exactly when `version` is non-null, and the other fields are null on
|
|
111
|
+
* failure. `tags` is the dist-tag map as published, `channel` is the
|
|
112
|
+
* installed version's channel, and `prerelease` is the `beta` build when
|
|
113
|
+
* one outruns `latest` from a stable install.
|
|
114
|
+
*
|
|
115
|
+
* Caller injects fetchFn for testing. Default uses global fetch (Node 18+).
|
|
116
|
+
*/
|
|
117
|
+
export async function getLatestVersion({ current = null, fetchFn = fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
120
|
+
const empty = { version: null, channel: null, tags: null, prerelease: null };
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetchFn(DIST_TAGS_URL, { signal: controller.signal });
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
return { ...empty, error: `registry returned ${res.status} ${res.statusText || ''}`.trim() };
|
|
125
|
+
}
|
|
126
|
+
const body = await res.json();
|
|
127
|
+
const tags = {};
|
|
128
|
+
if (body && typeof body === 'object' && !Array.isArray(body)) {
|
|
129
|
+
for (const [tag, value] of Object.entries(body)) {
|
|
130
|
+
if (typeof value === 'string') tags[tag] = value;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const picked = pickComparisonVersion(current, tags);
|
|
134
|
+
if (!picked.version) {
|
|
135
|
+
return { ...empty, error: 'registry response missing dist-tags' };
|
|
136
|
+
}
|
|
137
|
+
return { version: picked.version, channel: picked.channel, tags, prerelease: picked.prerelease, error: null };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return { ...empty, error: err?.message || String(err) };
|
|
140
|
+
} finally {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Read the version cache file. Returns the parsed object if it has a
|
|
147
|
+
* `tags` object holding at least one string dist-tag; otherwise null.
|
|
148
|
+
* Treats missing file, malformed JSON, and shape mismatches (including
|
|
149
|
+
* caches written before dist-tag support, which had a bare `latestVersion`)
|
|
150
|
+
* all as "no cache" — the next fetch rewrites the file in the new shape.
|
|
151
|
+
*/
|
|
152
|
+
export function readCache(path) {
|
|
153
|
+
if (!existsSync(path)) return null;
|
|
154
|
+
try {
|
|
155
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
156
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return null;
|
|
157
|
+
if (!data.tags || typeof data.tags !== 'object' || Array.isArray(data.tags)) return null;
|
|
158
|
+
if (!Object.values(data.tags).some((v) => typeof v === 'string')) return null;
|
|
159
|
+
return data;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Write the version cache file, creating parent directories as needed.
|
|
167
|
+
*/
|
|
168
|
+
export function writeCache(path, data) {
|
|
169
|
+
const dir = dirname(path);
|
|
170
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
171
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
|
|
172
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/.claude/skills/**"
|
|
4
|
+
- "**/.claude/scripts/**"
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
Activate this rule if the workspace creates PRs, watches CI runs, or interacts with releases from skills. Sibling to `work-item-tracking.md` (which covers issues); together they cover everything a workspace does against a code-hosting forge.
|
|
2
8
|
|
|
3
9
|
# Forge Operations
|
|
@@ -32,6 +32,10 @@ not need to be in context while editing documentation.
|
|
|
32
32
|
node .claude/scripts/context-footprint.mjs --root . --add <bytes> --as <destination>
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
A rule that applies only to certain files should carry `paths:` frontmatter — it costs nothing
|
|
36
|
+
until a matching file is touched — and the same script checks the live total against
|
|
37
|
+
`workspace.alwaysLoadedBudgetBytes`.
|
|
38
|
+
|
|
35
39
|
## The canonical test
|
|
36
40
|
|
|
37
41
|
Canonical describes what *is* and what *to do*, never what *to think*.
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "work-sessions/**"
|
|
4
|
+
- "**/session.md"
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
# Task List Mirroring
|
|
2
8
|
|
|
3
9
|
The Claude Code `TodoWrite` checklist is a live mirror of the workspace lifecycle. The durable backing store is a `## Tasks` section in `session.md`, round-tripped by `.claude/scripts/sync-tasks.mjs`. This rule defines the contract.
|