@ulysses-ai/create-workspace 0.19.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/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/lib/freshness.mjs +20 -7
- package/template/_claude/lib/registry-check.mjs +80 -14
- package/template/_claude/scripts/chat-record.mjs +27 -19
- package/template/_claude/scripts/classify-update.mjs +117 -0
- package/template/_claude/scripts/cleanup-work-session.mjs +4 -2
- package/template/_claude/scripts/merge-mode.mjs +61 -0
- package/template/_claude/scripts/migrate-canonical-priority.mjs +7 -1
- package/template/_claude/scripts/migrate-claude-md-freshness-include.mjs +35 -10
- package/template/_claude/scripts/migrate-session-layout.mjs +19 -8
- package/template/_claude/scripts/migrate-sessions.mjs +444 -121
- package/template/_claude/scripts/task-pr.mjs +213 -105
- package/template/_claude/scripts/task-worktree.mjs +26 -16
- package/template/_claude/skills/complete-work/SKILL.md +16 -12
- package/template/_claude/skills/migrate-sessions/SKILL.md +17 -5
- package/template/_claude/skills/start-work/SKILL.md +4 -4
- package/template/_claude/skills/workspace-init/SKILL.md +20 -14
- package/template/_claude/skills/workspace-update/SKILL.md +73 -30
- package/template/_gitignore +3 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Merge-mode resolution for the task lifecycle (gh:173), shared by
|
|
2
|
+
// task-worktree.mjs (which branch a new task worktree starts from) and
|
|
3
|
+
// task-pr.mjs (push-and-PR versus merge-in-the-source-clone). It lives in
|
|
4
|
+
// its own module because those two import each other's helpers: housing
|
|
5
|
+
// the rule here keeps its definition singular without a circular import.
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { join, resolve } from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
// The workspace repo (the launcher) is addressed as "." — the one repo
|
|
12
|
+
// name that is not a directory under repos/.
|
|
13
|
+
const WORKSPACE_REPO = '.';
|
|
14
|
+
|
|
15
|
+
// "." is the workspace repo itself — the git repo at the root. Every other
|
|
16
|
+
// name is a directory under repos/.
|
|
17
|
+
function repoDirFor(rootDir, repo) {
|
|
18
|
+
return repo === WORKSPACE_REPO ? rootDir : join(rootDir, 'repos', repo);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readWorkspace(rootDir) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(join(rootDir, 'workspace.json'), 'utf-8'));
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error(`cannot read ${join(rootDir, 'workspace.json')} — is --root the launcher?`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Same remote shapes the forge adapters resolve a repo from. A URL that
|
|
30
|
+
// does not match is not forge-hosted, and the forge path supports
|
|
31
|
+
// forge-hosted repos only — a local/bare remote has no PR concept to aim at.
|
|
32
|
+
const FORGE_REMOTE_RE = /github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/;
|
|
33
|
+
|
|
34
|
+
function parseForgeRemote(url) {
|
|
35
|
+
const m = String(url).trim().match(FORGE_REMOTE_RE);
|
|
36
|
+
return m ? { owner: m[1], name: m[2] } : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve how a task repo merges: "local" — nothing pushed, merged in the
|
|
41
|
+
* repo's own source clone — when workspace.json asks for it
|
|
42
|
+
* (repos.{repo}.merge, or workspace.merge for the workspace repo; the
|
|
43
|
+
* right call for a clone whose origin is a third-party upstream nobody
|
|
44
|
+
* here may push to) or when the repo has no origin remote at all;
|
|
45
|
+
* "forge" — pushed and PR'd — when its origin parses as a forge-hosted
|
|
46
|
+
* owner/name. An origin that is neither (say a local bare mirror with no
|
|
47
|
+
* override) resolves to null: the caller stops with the override spelled
|
|
48
|
+
* out rather than pushing somewhere that cannot host a PR.
|
|
49
|
+
*/
|
|
50
|
+
function mergeModeFor(root, repo, deps = {}) {
|
|
51
|
+
const gitFn = deps.gitFn ?? spawnSync;
|
|
52
|
+
const rootDir = resolve(root);
|
|
53
|
+
const ws = readWorkspace(rootDir);
|
|
54
|
+
const override = repo === WORKSPACE_REPO ? ws?.workspace?.merge : ws?.repos?.[repo]?.merge;
|
|
55
|
+
if (override === 'local') return 'local';
|
|
56
|
+
const res = gitFn('git', ['-C', repoDirFor(rootDir, repo), 'remote', 'get-url', 'origin'], { encoding: 'utf8' });
|
|
57
|
+
if (res.error || res.status !== 0) return 'local'; // no origin — nowhere to push
|
|
58
|
+
return parseForgeRemote(String(res.stdout || '').trim()) ? 'forge' : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { WORKSPACE_REPO, repoDirFor, readWorkspace, parseForgeRemote, mergeModeFor };
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
// node migrate-canonical-priority.mjs [--root <path>]
|
|
8
8
|
//
|
|
9
9
|
// Walks <root>/workspace-context/shared/locked/*.md. For each file:
|
|
10
|
-
// - Skip non-.md files
|
|
10
|
+
// - Skip non-.md files, local-only-* files (machine-local drafts that
|
|
11
|
+
// happen to sit under shared/locked/ — never canonical), and files
|
|
12
|
+
// without parseable frontmatter (warn to stderr).
|
|
11
13
|
// - If `priority` is already set (any value), leave the file untouched.
|
|
12
14
|
// - Otherwise add `priority: critical` losslessly via updateSessionContent.
|
|
13
15
|
//
|
|
@@ -62,6 +64,10 @@ export function migrateCanonicalPriority({ root }) {
|
|
|
62
64
|
|
|
63
65
|
for (const name of entries) {
|
|
64
66
|
if (!name.endsWith('.md')) continue;
|
|
67
|
+
// local-only-* files are machine-local and gitignored — they are never
|
|
68
|
+
// canonical, so back-filling a canonical priority field on them would be
|
|
69
|
+
// both wrong and a surprise on the next build-workspace-context run.
|
|
70
|
+
if (name.startsWith('local-only-')) continue;
|
|
65
71
|
const full = join(lockedDir, name);
|
|
66
72
|
let st;
|
|
67
73
|
try { st = statSync(full); } catch { continue; }
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
// Idempotent migrator: ensures CLAUDE.md includes @local-only-template-freshness.md.
|
|
3
3
|
// Appends one line at end if missing. Preserves the rest of the file byte-for-byte.
|
|
4
4
|
//
|
|
5
|
-
// Run standalone: node
|
|
5
|
+
// Run standalone: node migrate-claude-md-freshness-include.mjs [--root <dir>]
|
|
6
6
|
// Or import { runMigration } and call programmatically.
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
//
|
|
8
|
+
// The root is --root when given, else the current working directory. It is
|
|
9
|
+
// never derived from this script's location: the upgrade payload runs this
|
|
10
|
+
// file from <workspace>/.workspace-update/.claude/scripts/, and a
|
|
11
|
+
// script-relative root would point inside the payload instead of at the
|
|
12
|
+
// workspace.
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, realpathSync } from 'fs';
|
|
14
|
+
import { join, resolve } from 'path';
|
|
9
15
|
import { fileURLToPath } from 'url';
|
|
10
16
|
|
|
11
17
|
const INCLUDE_LINE = '@local-only-template-freshness.md';
|
|
@@ -20,11 +26,30 @@ export function runMigration({ workspaceRoot }) {
|
|
|
20
26
|
return { action: 'appended' };
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
function isMainModule(metaUrl) {
|
|
30
|
+
if (!process.argv[1]) return false;
|
|
31
|
+
try {
|
|
32
|
+
return realpathSync(fileURLToPath(metaUrl)) === realpathSync(process.argv[1]);
|
|
33
|
+
} catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArgs(argv) {
|
|
37
|
+
const args = { root: process.cwd() };
|
|
38
|
+
for (let i = 2; i < argv.length; i++) {
|
|
39
|
+
const a = argv[i];
|
|
40
|
+
if (a === '--root') args.root = argv[++i];
|
|
41
|
+
else throw new Error(`Unknown arg: ${a}`);
|
|
42
|
+
}
|
|
43
|
+
return args;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isMainModule(import.meta.url)) {
|
|
47
|
+
try {
|
|
48
|
+
const args = parseArgs(process.argv);
|
|
49
|
+
const result = runMigration({ workspaceRoot: resolve(args.root) });
|
|
50
|
+
console.log(JSON.stringify(result));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`migrate-claude-md-freshness-include: ${err.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
30
55
|
}
|
|
@@ -22,15 +22,26 @@ import {
|
|
|
22
22
|
readdirSync,
|
|
23
23
|
copyFileSync,
|
|
24
24
|
statSync,
|
|
25
|
+
realpathSync,
|
|
25
26
|
} from 'fs';
|
|
26
27
|
import { join } from 'path';
|
|
28
|
+
import { fileURLToPath } from 'url';
|
|
27
29
|
import {
|
|
28
|
-
getWorkspaceRoot,
|
|
29
30
|
getWorkspacePaths,
|
|
30
31
|
getMainRoot,
|
|
31
32
|
readJSON,
|
|
32
33
|
} from '../hooks/_utils.mjs';
|
|
33
34
|
|
|
35
|
+
// Compare real paths: macOS temp dirs are symlinked (/var → /private/var),
|
|
36
|
+
// so a naive import.meta.url === `file://${process.argv[1]}` silently fails
|
|
37
|
+
// to detect main-module runs from tmpdir fixtures.
|
|
38
|
+
function isMainModule(metaUrl) {
|
|
39
|
+
if (!process.argv[1]) return false;
|
|
40
|
+
try {
|
|
41
|
+
return realpathSync(fileURLToPath(metaUrl)) === realpathSync(process.argv[1]);
|
|
42
|
+
} catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
export function migrateSession(root, sessionName) {
|
|
35
46
|
const { workSessionsDir } = getWorkspacePaths(root);
|
|
36
47
|
const sessionFolder = join(workSessionsDir, sessionName);
|
|
@@ -189,19 +200,19 @@ export function migrateMain(root) {
|
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
// CLI entry
|
|
192
|
-
if (import.meta.url
|
|
203
|
+
if (isMainModule(import.meta.url)) {
|
|
193
204
|
const args = process.argv.slice(2);
|
|
194
205
|
const getArg = (name) => {
|
|
195
206
|
const i = args.indexOf(`--${name}`);
|
|
196
207
|
return i >= 0 && args[i + 1] ? args[i + 1] : null;
|
|
197
208
|
};
|
|
198
209
|
|
|
199
|
-
// The
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
const root = getArg('root') || getMainRoot(
|
|
210
|
+
// The workspace root is --root when given, else the cwd — promoted to the
|
|
211
|
+
// launcher via the .active-session.json pointer when the cwd is a session
|
|
212
|
+
// worktree. It is never derived from this script's location: an upgrade
|
|
213
|
+
// payload runs this file from <workspace>/.workspace-update/.claude/scripts/,
|
|
214
|
+
// and a script-relative root would point inside the payload.
|
|
215
|
+
const root = getArg('root') || getMainRoot(process.cwd());
|
|
205
216
|
|
|
206
217
|
const runAll = args.includes('--all');
|
|
207
218
|
const runMain = args.includes('--main');
|