@ulysses-ai/create-workspace 0.14.0-beta.3 → 0.15.0-beta.1
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 +12 -25
- package/lib/scaffold.mjs +3 -2
- package/package.json +1 -1
- package/template/.claude/agents/reviewer.md +1 -1
- package/template/.claude/hooks/pre-compact.mjs +1 -1
- package/template/.claude/hooks/repo-write-detection.mjs +2 -2
- package/template/.claude/hooks/session-start.mjs +10 -7
- package/template/.claude/hooks/subagent-start.mjs +3 -3
- package/template/.claude/recipes/migrate-from-notion.md +6 -6
- package/template/.claude/rules/coherent-revisions.md +2 -2
- package/template/.claude/rules/local-dev-environment.md.skip +2 -2
- package/template/.claude/rules/memory-guidance.md +23 -14
- package/template/.claude/rules/token-economics.md.skip +2 -2
- package/template/.claude/rules/work-item-tracking.md +1 -1
- package/template/.claude/rules/workspace-structure.md +36 -15
- package/template/.claude/scripts/build-workspace-context.mjs +712 -0
- package/template/.claude/scripts/capture-context.mjs +217 -0
- package/template/.claude/scripts/generate-claude-local.mjs +104 -0
- package/template/.claude/scripts/migrate-canonical-priority.mjs +108 -0
- package/template/.claude/scripts/migrate-open-work.mjs +1 -1
- package/template/.claude/scripts/migrate-to-workspace-context.mjs +520 -0
- package/template/.claude/scripts/sweep-references.mjs +177 -0
- package/template/.claude/skills/aside/SKILL.md +49 -44
- package/template/.claude/skills/braindump/SKILL.md +25 -19
- package/template/.claude/skills/build-docs-site/SKILL.md +1 -1
- package/template/.claude/skills/build-docs-site/checklists/framing.md +1 -1
- package/template/.claude/skills/complete-work/SKILL.md +91 -3
- package/template/.claude/skills/handoff/SKILL.md +31 -30
- package/template/.claude/skills/maintenance/SKILL.md +90 -22
- package/template/.claude/skills/pause-work/SKILL.md +1 -1
- package/template/.claude/skills/promote/SKILL.md +18 -8
- package/template/.claude/skills/release/SKILL.md +20 -13
- package/template/.claude/skills/start-work/SKILL.md +1 -1
- package/template/.claude/skills/workspace-init/SKILL.md +12 -12
- package/template/.claude/skills/workspace-update/SKILL.md +7 -1
- package/template/CLAUDE.md.tmpl +4 -3
- package/template/_gitignore +1 -0
- package/template/workspace.json.tmpl +3 -2
- package/template/.claude/hooks/_bash-output-advisory.test.mjs +0 -88
- package/template/.claude/hooks/_utils.test.mjs +0 -99
- package/template/.claude/lib/freshness.test.mjs +0 -175
- package/template/.claude/lib/registry-check.test.mjs +0 -130
- package/template/.claude/lib/session-frontmatter.test.mjs +0 -242
- package/template/.claude/scripts/build-shared-context-index.mjs +0 -212
- package/template/.claude/scripts/build-shared-context-index.test.mjs +0 -318
- package/template/.claude/scripts/migrate-claude-md-freshness-include.test.mjs +0 -54
- package/template/.claude/scripts/migrate-session-layout.test.mjs +0 -144
- package/template/.claude/scripts/sync-tasks.test.mjs +0 -350
- package/template/.claude/scripts/trackers/github-issues.test.mjs +0 -190
- package/template/.claude/scripts/trackers/interface.test.mjs +0 -40
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Centralized capture helper for /braindump, /handoff, /aside, /promote.
|
|
3
|
+
// Mechanizes the path math, naming convention, and frontmatter so skills
|
|
4
|
+
// don't have to duplicate it (and can't drift from each other).
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// echo "<body>" | node capture-context.mjs \
|
|
8
|
+
// --type braindump|handoff|research \
|
|
9
|
+
// --topic kebab-case-slug \
|
|
10
|
+
// --scope shared|team-member \
|
|
11
|
+
// [--user alice] # required when --scope team-member
|
|
12
|
+
// [--description "..."] # one-line summary, used by index
|
|
13
|
+
// [--variant aside] # arbitrary frontmatter marker
|
|
14
|
+
// [--local-only] # prefix filename with local-only- (gitignored)
|
|
15
|
+
// [--update] # overwrite if file exists; default appends -2, -3, ...
|
|
16
|
+
// [--root <path>] # workspace root (defaults to cwd)
|
|
17
|
+
// [--print-only] # don't write; just print the planned path
|
|
18
|
+
//
|
|
19
|
+
// Path layout:
|
|
20
|
+
// shared scope: workspace-context/shared/{type}_{topic}.md
|
|
21
|
+
// team-member scope: workspace-context/team-member/{user}/{type}_{topic}.md
|
|
22
|
+
// With --local-only, the file basename is prefixed `local-only-` (e.g.
|
|
23
|
+
// `local-only-research_my-topic.md`), so the gitignore pattern keeps it
|
|
24
|
+
// out of git.
|
|
25
|
+
//
|
|
26
|
+
// Stdin: the markdown body to write under the frontmatter. Required unless
|
|
27
|
+
// --print-only is passed.
|
|
28
|
+
//
|
|
29
|
+
// Stdout: a single line with the absolute path of the written (or planned) file.
|
|
30
|
+
|
|
31
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, realpathSync } from 'node:fs';
|
|
32
|
+
import { dirname, join, resolve } from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
|
|
35
|
+
function isMainModule(metaUrl) {
|
|
36
|
+
if (!process.argv[1]) return false;
|
|
37
|
+
try {
|
|
38
|
+
return realpathSync(fileURLToPath(metaUrl)) === realpathSync(process.argv[1]);
|
|
39
|
+
} catch { return false; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const VALID_TYPES = new Set(['braindump', 'handoff', 'research']);
|
|
43
|
+
const VALID_SCOPES = new Set(['shared', 'team-member']);
|
|
44
|
+
|
|
45
|
+
const WC_DIR = 'workspace-context';
|
|
46
|
+
const SHARED_DIR = 'shared';
|
|
47
|
+
const TEAM_MEMBER_DIR = 'team-member';
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const args = {
|
|
51
|
+
type: null,
|
|
52
|
+
topic: null,
|
|
53
|
+
scope: null,
|
|
54
|
+
user: null,
|
|
55
|
+
description: null,
|
|
56
|
+
variant: null,
|
|
57
|
+
localOnly: false,
|
|
58
|
+
update: false,
|
|
59
|
+
root: process.cwd(),
|
|
60
|
+
printOnly: false,
|
|
61
|
+
};
|
|
62
|
+
for (let i = 2; i < argv.length; i++) {
|
|
63
|
+
const a = argv[i];
|
|
64
|
+
if (a === '--type') args.type = argv[++i];
|
|
65
|
+
else if (a === '--topic') args.topic = argv[++i];
|
|
66
|
+
else if (a === '--scope') args.scope = argv[++i];
|
|
67
|
+
else if (a === '--user') args.user = argv[++i];
|
|
68
|
+
else if (a === '--description') args.description = argv[++i];
|
|
69
|
+
else if (a === '--variant') args.variant = argv[++i];
|
|
70
|
+
else if (a === '--local-only') args.localOnly = true;
|
|
71
|
+
else if (a === '--update') args.update = true;
|
|
72
|
+
else if (a === '--root') args.root = argv[++i];
|
|
73
|
+
else if (a === '--print-only') args.printOnly = true;
|
|
74
|
+
else throw new Error(`Unknown arg: ${a}`);
|
|
75
|
+
}
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validate(args) {
|
|
80
|
+
if (!args.type || !VALID_TYPES.has(args.type)) {
|
|
81
|
+
throw new Error(`--type must be one of: ${[...VALID_TYPES].join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
if (!args.topic || !/^[a-z0-9][a-z0-9-]*$/.test(args.topic)) {
|
|
84
|
+
throw new Error('--topic must be kebab-case (lowercase letters, digits, hyphens)');
|
|
85
|
+
}
|
|
86
|
+
if (!args.scope || !VALID_SCOPES.has(args.scope)) {
|
|
87
|
+
throw new Error(`--scope must be one of: ${[...VALID_SCOPES].join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
if (args.scope === 'team-member' && !args.user) {
|
|
90
|
+
throw new Error('--user is required when --scope is team-member');
|
|
91
|
+
}
|
|
92
|
+
if (args.scope === 'team-member' && !/^[A-Za-z0-9_-]+$/.test(args.user)) {
|
|
93
|
+
throw new Error('--user must be alphanumeric (with optional - or _)');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function computeDir(args) {
|
|
98
|
+
if (args.scope === 'shared') {
|
|
99
|
+
return join(args.root, WC_DIR, SHARED_DIR);
|
|
100
|
+
}
|
|
101
|
+
return join(args.root, WC_DIR, TEAM_MEMBER_DIR, args.user);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function computeBaseFilename(args) {
|
|
105
|
+
const prefix = args.localOnly ? 'local-only-' : '';
|
|
106
|
+
return `${prefix}${args.type}_${args.topic}.md`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveCollision(dir, baseFilename, update) {
|
|
110
|
+
const initial = join(dir, baseFilename);
|
|
111
|
+
if (update) return initial;
|
|
112
|
+
if (!existsSync(initial)) return initial;
|
|
113
|
+
|
|
114
|
+
const dot = baseFilename.lastIndexOf('.');
|
|
115
|
+
const stem = baseFilename.slice(0, dot);
|
|
116
|
+
const ext = baseFilename.slice(dot);
|
|
117
|
+
for (let i = 2; i < 1000; i++) {
|
|
118
|
+
const candidate = join(dir, `${stem}-${i}${ext}`);
|
|
119
|
+
if (!existsSync(candidate)) return candidate;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Could not find unique filename for ${baseFilename} (1000+ collisions)`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function todayISO() {
|
|
125
|
+
return new Date().toISOString().slice(0, 10);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildFrontmatter(args) {
|
|
129
|
+
const fm = {
|
|
130
|
+
state: 'ephemeral',
|
|
131
|
+
lifecycle: 'active',
|
|
132
|
+
type: args.type,
|
|
133
|
+
topic: args.topic,
|
|
134
|
+
};
|
|
135
|
+
if (args.scope === 'team-member') fm.author = args.user;
|
|
136
|
+
if (args.variant) fm.variant = args.variant;
|
|
137
|
+
if (args.description) fm.description = args.description;
|
|
138
|
+
fm.updated = todayISO();
|
|
139
|
+
return fm;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderFrontmatter(fm) {
|
|
143
|
+
const lines = ['---'];
|
|
144
|
+
for (const [k, v] of Object.entries(fm)) {
|
|
145
|
+
lines.push(`${k}: ${v}`);
|
|
146
|
+
}
|
|
147
|
+
lines.push('---');
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readStdinSync() {
|
|
152
|
+
try {
|
|
153
|
+
return readFileSync(0, 'utf-8');
|
|
154
|
+
} catch {
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ensureDir(dir) {
|
|
160
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function plan(args) {
|
|
164
|
+
validate(args);
|
|
165
|
+
const dir = computeDir(args);
|
|
166
|
+
const baseFilename = computeBaseFilename(args);
|
|
167
|
+
const filePath = resolveCollision(dir, baseFilename, args.update);
|
|
168
|
+
return { dir, filePath, frontmatter: buildFrontmatter(args) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function write(args, body) {
|
|
172
|
+
const planned = plan(args);
|
|
173
|
+
ensureDir(planned.dir);
|
|
174
|
+
const content = renderFrontmatter(planned.frontmatter) + '\n\n' + body.replace(/^\n+/, '');
|
|
175
|
+
const finalContent = content.endsWith('\n') ? content : content + '\n';
|
|
176
|
+
writeFileSync(planned.filePath, finalContent);
|
|
177
|
+
return planned.filePath;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function main() {
|
|
181
|
+
const args = parseArgs(process.argv);
|
|
182
|
+
args.root = resolve(args.root);
|
|
183
|
+
|
|
184
|
+
if (args.printOnly) {
|
|
185
|
+
const planned = plan(args);
|
|
186
|
+
process.stdout.write(planned.filePath + '\n');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const body = readStdinSync();
|
|
191
|
+
if (!body || !body.trim()) {
|
|
192
|
+
throw new Error('No body content on stdin (use --print-only to skip writing)');
|
|
193
|
+
}
|
|
194
|
+
const filePath = write(args, body);
|
|
195
|
+
process.stdout.write(filePath + '\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (isMainModule(import.meta.url)) {
|
|
199
|
+
try {
|
|
200
|
+
main();
|
|
201
|
+
} catch (err) {
|
|
202
|
+
process.stderr.write(`capture-context: ${err.message}\n`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export {
|
|
208
|
+
parseArgs,
|
|
209
|
+
validate,
|
|
210
|
+
computeDir,
|
|
211
|
+
computeBaseFilename,
|
|
212
|
+
resolveCollision,
|
|
213
|
+
buildFrontmatter,
|
|
214
|
+
renderFrontmatter,
|
|
215
|
+
plan,
|
|
216
|
+
write,
|
|
217
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Write CLAUDE.local.md at the workspace root with the per-user import.
|
|
3
|
+
//
|
|
4
|
+
// CLAUDE.local.md is gitignored — it carries user-scoped context that
|
|
5
|
+
// shouldn't propagate across team members. The single source of identity
|
|
6
|
+
// is `.claude/settings.local.json` → `workspace.user`.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node generate-claude-local.mjs [--root <path>] [--force]
|
|
10
|
+
//
|
|
11
|
+
// Behavior:
|
|
12
|
+
// - Reads workspace.user from .claude/settings.local.json
|
|
13
|
+
// - Refuses to overwrite an existing CLAUDE.local.md unless --force
|
|
14
|
+
// - Idempotent: same input → same content
|
|
15
|
+
//
|
|
16
|
+
// Exit codes:
|
|
17
|
+
// 0 — wrote (or would have written) the file
|
|
18
|
+
// 1 — error (missing settings, settings missing user, refusal to overwrite)
|
|
19
|
+
|
|
20
|
+
import { readFileSync, writeFileSync, existsSync, realpathSync } from 'node:fs';
|
|
21
|
+
import { join, resolve } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
function isMainModule(metaUrl) {
|
|
25
|
+
if (!process.argv[1]) return false;
|
|
26
|
+
try {
|
|
27
|
+
return realpathSync(fileURLToPath(metaUrl)) === realpathSync(process.argv[1]);
|
|
28
|
+
} catch { return false; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const args = { root: process.cwd(), force: false };
|
|
33
|
+
for (let i = 2; i < argv.length; i++) {
|
|
34
|
+
const a = argv[i];
|
|
35
|
+
if (a === '--root') args.root = argv[++i];
|
|
36
|
+
else if (a === '--force') args.force = true;
|
|
37
|
+
else throw new Error(`Unknown arg: ${a}`);
|
|
38
|
+
}
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readWorkspaceUser(root) {
|
|
43
|
+
const settingsPath = join(root, '.claude', 'settings.local.json');
|
|
44
|
+
if (!existsSync(settingsPath)) {
|
|
45
|
+
throw new Error(`Missing ${settingsPath} — run /workspace-init first`);
|
|
46
|
+
}
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
throw new Error(`Could not parse ${settingsPath}: ${e.message}`);
|
|
52
|
+
}
|
|
53
|
+
const user = parsed?.workspace?.user;
|
|
54
|
+
if (!user || typeof user !== 'string') {
|
|
55
|
+
throw new Error(`workspace.user not set in ${settingsPath}`);
|
|
56
|
+
}
|
|
57
|
+
if (!/^[A-Za-z0-9_-]+$/.test(user)) {
|
|
58
|
+
throw new Error(`workspace.user "${user}" must be alphanumeric (with optional - or _)`);
|
|
59
|
+
}
|
|
60
|
+
return user;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderClaudeLocal(user) {
|
|
64
|
+
return `## My Context
|
|
65
|
+
@workspace-context/team-member/${user}/index.md
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function generateClaudeLocal(root, { force = false } = {}) {
|
|
70
|
+
const user = readWorkspaceUser(root);
|
|
71
|
+
const target = join(root, 'CLAUDE.local.md');
|
|
72
|
+
const content = renderClaudeLocal(user);
|
|
73
|
+
|
|
74
|
+
if (existsSync(target) && !force) {
|
|
75
|
+
const existing = readFileSync(target, 'utf-8');
|
|
76
|
+
if (existing === content) {
|
|
77
|
+
return { path: target, status: 'unchanged' };
|
|
78
|
+
}
|
|
79
|
+
throw new Error(
|
|
80
|
+
`${target} already exists with different content. Re-run with --force to overwrite.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeFileSync(target, content);
|
|
85
|
+
return { path: target, status: existsSync(target) ? 'written' : 'written' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function main() {
|
|
89
|
+
const args = parseArgs(process.argv);
|
|
90
|
+
const root = resolve(args.root);
|
|
91
|
+
const result = generateClaudeLocal(root, { force: args.force });
|
|
92
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isMainModule(import.meta.url)) {
|
|
96
|
+
try {
|
|
97
|
+
main();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
process.stderr.write(`generate-claude-local: ${err.message}\n`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { readWorkspaceUser, renderClaudeLocal, generateClaudeLocal };
|
|
@@ -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
|
+
}
|
|
@@ -67,7 +67,7 @@ for (const ticket of tickets) {
|
|
|
67
67
|
'',
|
|
68
68
|
'---',
|
|
69
69
|
'',
|
|
70
|
-
`Migrated from \`
|
|
70
|
+
`Migrated from \`workspace-context/open-work.md\` ticket #${ticket.id} (status at migration: \`${ticket.status}\`${ticket.branch ? `, branch: \`${ticket.branch}\`` : ''}).`,
|
|
71
71
|
].join('\n');
|
|
72
72
|
|
|
73
73
|
const issue = await tracker.createIssue({ title: ticket.title, body, labels, milestone: ticket.milestone });
|