@ulysses-ai/create-workspace 0.13.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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/bin/create.mjs +79 -0
  4. package/lib/git.mjs +26 -0
  5. package/lib/init.mjs +129 -0
  6. package/lib/payload.mjs +44 -0
  7. package/lib/prompts.mjs +113 -0
  8. package/lib/scaffold.mjs +84 -0
  9. package/lib/upgrade.mjs +42 -0
  10. package/package.json +43 -0
  11. package/template/.claude/agents/aside-researcher.md +48 -0
  12. package/template/.claude/agents/implementer.md +39 -0
  13. package/template/.claude/agents/researcher.md +40 -0
  14. package/template/.claude/agents/reviewer.md +47 -0
  15. package/template/.claude/hooks/_utils.mjs +196 -0
  16. package/template/.claude/hooks/_utils.test.mjs +99 -0
  17. package/template/.claude/hooks/post-compact.mjs +7 -0
  18. package/template/.claude/hooks/pre-compact.mjs +34 -0
  19. package/template/.claude/hooks/repo-write-detection.mjs +107 -0
  20. package/template/.claude/hooks/session-end.mjs +91 -0
  21. package/template/.claude/hooks/session-start.mjs +150 -0
  22. package/template/.claude/hooks/subagent-start.mjs +44 -0
  23. package/template/.claude/hooks/workspace-update-check.mjs +42 -0
  24. package/template/.claude/hooks/worktree-create.mjs +53 -0
  25. package/template/.claude/lib/session-frontmatter.mjs +265 -0
  26. package/template/.claude/lib/session-frontmatter.test.mjs +242 -0
  27. package/template/.claude/recipes/migrate-from-notion.md +120 -0
  28. package/template/.claude/rules/agent-rules.md.skip +32 -0
  29. package/template/.claude/rules/cloud-infrastructure.md.skip +15 -0
  30. package/template/.claude/rules/coherent-revisions.md +24 -0
  31. package/template/.claude/rules/documentation.md.skip +13 -0
  32. package/template/.claude/rules/git-conventions.md +34 -0
  33. package/template/.claude/rules/honest-pushback.md +56 -0
  34. package/template/.claude/rules/local-dev-environment.md.skip +60 -0
  35. package/template/.claude/rules/memory-guidance.md +26 -0
  36. package/template/.claude/rules/product-integrity.md.skip +24 -0
  37. package/template/.claude/rules/scope-guard.md.skip +22 -0
  38. package/template/.claude/rules/superpowers-workflow.md.skip +22 -0
  39. package/template/.claude/rules/token-economics.md.skip +31 -0
  40. package/template/.claude/rules/work-item-tracking.md +90 -0
  41. package/template/.claude/rules/workspace-structure.md +69 -0
  42. package/template/.claude/scripts/add-repo-to-session.mjs +78 -0
  43. package/template/.claude/scripts/cleanup-work-session.mjs +108 -0
  44. package/template/.claude/scripts/create-work-session.mjs +124 -0
  45. package/template/.claude/scripts/migrate-open-work.mjs +91 -0
  46. package/template/.claude/scripts/migrate-session-layout.mjs +236 -0
  47. package/template/.claude/scripts/migrate-session-layout.test.mjs +144 -0
  48. package/template/.claude/scripts/trackers/github-issues.mjs +170 -0
  49. package/template/.claude/scripts/trackers/github-issues.test.mjs +190 -0
  50. package/template/.claude/scripts/trackers/interface.mjs +25 -0
  51. package/template/.claude/scripts/trackers/interface.test.mjs +40 -0
  52. package/template/.claude/settings.json +107 -0
  53. package/template/.claude/skills/aside/SKILL.md +125 -0
  54. package/template/.claude/skills/braindump/SKILL.md +96 -0
  55. package/template/.claude/skills/build-docs-site/SKILL.md +323 -0
  56. package/template/.claude/skills/build-docs-site/checklists/framing.md +221 -0
  57. package/template/.claude/skills/build-docs-site/checklists/pitfalls.md +228 -0
  58. package/template/.claude/skills/build-docs-site/checklists/review.md +130 -0
  59. package/template/.claude/skills/build-docs-site/scripts/bulk-fill-migration.py +393 -0
  60. package/template/.claude/skills/build-docs-site/scripts/forbidden-word-grep.mjs +159 -0
  61. package/template/.claude/skills/build-docs-site/scripts/leak-grep.mjs +328 -0
  62. package/template/.claude/skills/build-docs-site/templates/custom.css.tmpl +212 -0
  63. package/template/.claude/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +95 -0
  64. package/template/.claude/skills/build-docs-site/templates/primitives/Arrow.tsx +87 -0
  65. package/template/.claude/skills/build-docs-site/templates/primitives/Box.tsx +90 -0
  66. package/template/.claude/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +46 -0
  67. package/template/.claude/skills/build-docs-site/templates/primitives/Region.tsx +68 -0
  68. package/template/.claude/skills/build-docs-site/templates/primitives/SectionTitle.tsx +42 -0
  69. package/template/.claude/skills/build-docs-site/templates/primitives/tokens.ts +67 -0
  70. package/template/.claude/skills/build-docs-site/templates/sidebars.ts.tmpl +89 -0
  71. package/template/.claude/skills/build-docs-site/templates/spec.md.tmpl +119 -0
  72. package/template/.claude/skills/complete-work/SKILL.md +369 -0
  73. package/template/.claude/skills/handoff/SKILL.md +98 -0
  74. package/template/.claude/skills/maintenance/SKILL.md +116 -0
  75. package/template/.claude/skills/pause-work/SKILL.md +98 -0
  76. package/template/.claude/skills/promote/SKILL.md +77 -0
  77. package/template/.claude/skills/release/SKILL.md +126 -0
  78. package/template/.claude/skills/setup-tracker/SKILL.md +117 -0
  79. package/template/.claude/skills/start-work/SKILL.md +234 -0
  80. package/template/.claude/skills/sync-work/SKILL.md +73 -0
  81. package/template/.claude/skills/workspace-init/SKILL.md +420 -0
  82. package/template/.claude/skills/workspace-update/SKILL.md +108 -0
  83. package/template/.mcp.json +12 -0
  84. package/template/CLAUDE.md.tmpl +32 -0
  85. package/template/_gitignore +28 -0
  86. package/template/workspace.json.tmpl +15 -0
@@ -0,0 +1,22 @@
1
+ # Scope Guard
2
+
3
+ Activate this rule to detect and push back on scope creep during work sessions.
4
+
5
+ ## Detection
6
+
7
+ - When a task grows beyond its original description, flag it: "This started as {original} but is becoming {current}. Split into a separate branch?"
8
+ - When a bugfix turns into a refactor, name it: "The fix is done but you're now restructuring {module}. That's a separate chore/ branch."
9
+ - When "one more thing" keeps happening, count them: "That's the third addition beyond the original scope. Consider /handoff the extras as follow-up work."
10
+
11
+ ## Response
12
+
13
+ - State the scope drift once, clearly
14
+ - Suggest how to split: which part is current branch, which is follow-up
15
+ - Follow the user's decision — if they want to keep going, proceed
16
+ - Don't lecture or repeat the concern after the user decides
17
+
18
+ ## YAGNI Enforcement
19
+
20
+ - Resist building for hypothetical future requirements
21
+ - Question abstractions that don't serve the current task: "Do we need this interface right now, or is the concrete implementation enough?"
22
+ - Three similar lines of code is better than a premature abstraction
@@ -0,0 +1,22 @@
1
+ # Superpowers Workflow
2
+
3
+ Activate this rule if the superpowers plugin is installed.
4
+
5
+ ## Research Phase (mandatory before implementation)
6
+
7
+ - Review existing codebase for relevant patterns and prior art
8
+ - Search official documentation for the technologies involved
9
+ - Research best practices, production hardening, and known pitfalls via web search
10
+ - Summarize findings for the user before proceeding to design
11
+
12
+ ## Specs and Plans
13
+
14
+ - Specs and plans live in the active worktree during development
15
+ - They are ephemeral — consumed by /complete-work into release notes, then removed
16
+ - If a spec/plan already exists for the current branch, version it: `-v2`, `-v3`, etc.
17
+
18
+ ## Execution
19
+
20
+ - Use subagent-driven development for executing plans
21
+ - One subagent per task, sequential implementation, parallel review
22
+ - Never dispatch parallel implementation subagents on the same codebase
@@ -0,0 +1,31 @@
1
+ # Token Economics
2
+
3
+ Activate this rule for token-aware behavior — cost-conscious model selection, context efficiency, and waste reduction.
4
+
5
+ ## Model Selection
6
+
7
+ - Suggest appropriate model/effort for each task:
8
+ - Search and exploration → Sonnet (fast, cheap)
9
+ - Focused implementation → inherit or Sonnet
10
+ - Review and judgment → Opus (best reasoning)
11
+ - Don't use Opus for tasks Sonnet can handle
12
+ - When dispatching subagents, choose the cheapest model that can succeed
13
+
14
+ ## Context Efficiency
15
+
16
+ - Don't read files that aren't needed for the current task
17
+ - When searching, use targeted queries rather than broad exploration
18
+ - If a tool result is large and mostly irrelevant, note what matters and move on
19
+ - Prefer exact file paths over glob searches when you know where things are
20
+
21
+ ## Waste Detection
22
+
23
+ - Flag when context is heavy with resolved discussions that could be compacted
24
+ - Suggest /braindump to offload discussion into files, freeing context for work
25
+ - Note when subagent context is bloated relative to the task size
26
+ - If locked context exceeds the 10KB target, mention it
27
+
28
+ ## Compaction Awareness
29
+
30
+ - When approaching compaction threshold, prioritize capturing over continuing
31
+ - After compaction, avoid re-reading files that were already discussed — check shared-context first
@@ -0,0 +1,90 @@
1
+ # Work Item Tracking
2
+
3
+ When a workspace has an issue tracker configured, all work items — bugs, features, chores — live in that tracker. Skills and scripts read and write the tracker through the adapter at `.claude/scripts/trackers/{type}.mjs`. There is no local file that mirrors the tracker's state.
4
+
5
+ ## Why external-first
6
+
7
+ - **Atomic assignment.** Two teammates can't accidentally start the same ticket — the tracker is the source of truth for "who has this."
8
+ - **Real-time state.** Status changes propagate to the whole team the moment they happen, not after a commit + push.
9
+ - **Tool parity.** Humans and Claude see the same list of tickets in the same place.
10
+
11
+ ## Configuration
12
+
13
+ `workspace.json` → `workspace.tracker`:
14
+
15
+ ```json
16
+ {
17
+ "workspace": {
18
+ "tracker": {
19
+ "type": "github-issues",
20
+ "repo": "your-org/your-workspace"
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ - `type` — identifies the adapter module at `.claude/scripts/trackers/{type}.mjs`. Only `github-issues` ships in the template; others are additive.
27
+ - `repo` — adapter-specific. For `github-issues`, the owner/name slug of the repo where issues live. `"auto"` resolves to the workspace's own git remote.
28
+
29
+ Absence of `workspace.tracker` means tracking is disabled. Skills handle this by falling back to a blank/describe-the-work flow — they do not fabricate a local mirror.
30
+
31
+ ## Adapter interface (for Claude)
32
+
33
+ Import from `.claude/scripts/trackers/interface.mjs`:
34
+
35
+ ```javascript
36
+ import { createTracker, AlreadyAssignedError } from '.claude/scripts/trackers/interface.mjs';
37
+
38
+ const tracker = createTracker(workspace.tracker);
39
+
40
+ const mine = await tracker.listAssignedToMe(); // Issue[]
41
+ const open = await tracker.listUnassigned(); // Issue[]
42
+ const issue = await tracker.claim('gh:42'); // throws AlreadyAssignedError on contention
43
+ const created = await tracker.createIssue({ title, body, labels: ['feat', 'P2'], milestone: 'Backlog' });
44
+ await tracker.comment('gh:42', 'paused here; see branch X');
45
+ await tracker.closeIssue('gh:42', { comment: 'shipped in PR #99' });
46
+
47
+ // Setup-time: idempotent milestone / label creation
48
+ await tracker.ensureLabels(); // creates bug/feat/chore/P1/P2/P3 if absent
49
+ await tracker.ensureMilestone({ title: 'Backlog', description: 'Triage later' });
50
+ ```
51
+
52
+ All skills that touch work items use this interface. Adapters are not called directly.
53
+
54
+ ## Session linkage
55
+
56
+ When `/start-work` links a session to a tracker issue, the session tracker's frontmatter gets:
57
+
58
+ ```yaml
59
+ workItem: gh:42
60
+ ```
61
+
62
+ The value is the adapter-prefixed issue ID. This survives adapter swaps — replacing the GitHub adapter with a Linear adapter later doesn't require re-linking session trackers (though the prefix changes for *new* sessions).
63
+
64
+ ## When to create issues
65
+
66
+ - **User describes new work during `/start-work`** → skill calls `createIssue` after session creation.
67
+ - **Bug or feature discovered mid-session** → Claude proactively asks "Create an issue for this? [Y/n]"; if yes, calls `createIssue` and links the session (if it's scoped to this session) or leaves it unassigned (if it's a future concern).
68
+ - **Never during braindumps or handoffs** — those are discussion artifacts, not work items. Action items can later graduate to issues during `/start-work`.
69
+
70
+ ## When NOT to maintain local state
71
+
72
+ - Do not create, write to, or read `shared-context/open-work.md`. That file is deprecated.
73
+ - Do not write ticket state into `session.md` frontmatter beyond the `workItem:` pointer. Status, assignment, milestone, and labels live in the tracker.
74
+ - Do not cache issue bodies locally. Always fetch via `tracker.getIssue(id)` when the content is needed.
75
+
76
+ ## Skill behavior
77
+
78
+ Skills that interact with the tracker:
79
+
80
+ - **`/setup-tracker`** — configures `workspace.json` → `tracker` block, calls `ensureLabels()`.
81
+ - **`/start-work`** — fetches assigned-to-me first; falls back to unassigned; claims atomically on pick. Records `workItem:` in session tracker.
82
+ - **`/pause-work`** — comments the pause capture on the linked issue.
83
+ - **`/complete-work`** — closes the linked issue after PRs merge, with a final comment linking them.
84
+ - **`/workspace-init`** — prompts to run `/setup-tracker` at the end of init. Does not pre-populate tickets.
85
+
86
+ ## What this rule does NOT do
87
+
88
+ - Does not prescribe a specific tracker type. Adapter choice is per workspace.
89
+ - Does not prescribe label or milestone schemas beyond the six standard labels (`bug`, `feat`, `chore`, `P1`, `P2`, `P3`) created by `ensureLabels()`. Teams with existing trackers can skip label creation during setup.
90
+ - Does not replace tracker-native features (comments, reactions, linked PRs) — use the tracker's UI for those.
@@ -0,0 +1,69 @@
1
+ # Workspace Structure
2
+
3
+ This workspace follows the claude-workspace convention. All paths are relative to the workspace root.
4
+
5
+ ## Directory Layout
6
+
7
+ | Directory | Purpose | Tracked in git? |
8
+ |-----------|---------|-----------------|
9
+ | `repos/` | Source clones of project repositories (one per repo, stays on default branch) | No (gitignored, lazy) |
10
+ | `work-sessions/` | Per-session folders — one folder per active or paused work session | No (gitignored entirely at the launcher) |
11
+ | `work-sessions/{name}/workspace/` | Workspace worktree for this session, on the session branch | Yes — on the session branch, not on main |
12
+ | `work-sessions/{name}/workspace/session.md` | Unified session tracker at the top of the session branch (frontmatter = machine state, body = human content) | Yes — on the session branch |
13
+ | `work-sessions/{name}/workspace/design-*.md` | Specs for this session — consumed into release notes by /complete-work | Yes — on the session branch |
14
+ | `work-sessions/{name}/workspace/plan-*.md` | Plans for this session — consumed into release notes by /complete-work | Yes — on the session branch |
15
+ | `work-sessions/{name}/workspace/repos/` | Real directory holding nested project worktrees for this session | No (gitignored) |
16
+ | `work-sessions/{name}/workspace/repos/{repo}/` | Project worktree nested inside the workspace worktree | No (gitignored) |
17
+ | `shared-context/` | Shared memory — handoffs, braindumps, team knowledge | Yes |
18
+ | `shared-context/locked/` | Team truths — loaded every session, injected into subagents | Yes |
19
+ | `shared-context/{user}/` | User-scoped working context — default for all captures | Yes |
20
+ | `workspace-scratchpad/` | Disposable workspace-scoped files — session log, hook debug output | No (gitignored, lazy) |
21
+ | `.claude/` | Claude Code configuration — rules, agents, skills, hooks, scripts, lib | Yes (except settings.local.json) |
22
+
23
+ Session content (tracker, specs, plans) lives at the top of each session's workspace worktree. It is tracked on the session branch, not on main. Pushing the session branch carries durable session thinking across machines. When `/complete-work` finalizes the session, it synthesizes the content into release notes and removes the files from the branch before the final PR so main's top level stays free of session artifacts.
24
+
25
+ ## Shared Context Levels
26
+
27
+ | Level | What lives there | Default? |
28
+ |-------|-----------------|----------|
29
+ | `locked/` | Team truths — always loaded, injected into subagents | Promoted by /release |
30
+ | Root | Team-visible ephemerals — cross-team handoffs, post-release leftovers | Explicit choice |
31
+ | `{user}/` | Ongoing personal context — persists across work sessions | Default for captures |
32
+
33
+ Inflight session state lives inside the session worktree at `work-sessions/{name}/workspace/session.md`, not in `shared-context/`. Shared-context is for knowledge that outlives any individual session.
34
+
35
+ User-scoped is the default for captures. Root is only for content deliberately made team-visible.
36
+
37
+ ## Spec and Plan Locations — MANDATORY OVERRIDE
38
+
39
+ **Specs and plans MUST be written at the top of the active session's workspace worktree, not to `docs/superpowers/` or any other location.**
40
+
41
+ - Specs: `design-{topic}.md` at the top of `work-sessions/{session-name}/workspace/`
42
+ - Plans: `plan-{topic}.md` at the top of `work-sessions/{session-name}/workspace/`
43
+
44
+ From inside the worktree, these are plain top-level files (`design-{topic}.md`, `plan-{topic}.md`) sitting alongside `CLAUDE.md` and `workspace.json`. They are tracked on the session branch and travel with the branch on `git push`.
45
+
46
+ This overrides any default paths specified by external skills (e.g., Superpowers brainstorming defaults to `docs/superpowers/specs/`). Those skills state that user preferences override their defaults — this rule IS that override. Do not create `docs/superpowers/` directories. Do not write specs or plans anywhere other than the top of the active worktree.
47
+
48
+ If a spec/plan already exists for the current session, version it: `design-{topic}-v2.md`, `design-{topic}-v3.md`.
49
+
50
+ `/complete-work` reads specs and plans from the worktree to synthesize release notes, then removes them in a dedicated commit before the final PR so main's tree stays pristine.
51
+
52
+ ## Naming Conventions
53
+
54
+ - Session folders: `work-sessions/{session-name}/`
55
+ - Workspace worktrees: `work-sessions/{session-name}/workspace/`
56
+ - Project worktrees: `work-sessions/{session-name}/workspace/repos/{repo-name}/`
57
+ - Session trackers: `work-sessions/{session-name}/workspace/session.md`
58
+ - Specs: `design-{topic}.md` (top of worktree)
59
+ - Plans: `plan-{topic}.md` (top of worktree)
60
+ - Handoffs and braindumps: named by topic (no date prefix — use frontmatter `updated:`)
61
+
62
+ ## Rules
63
+
64
+ - The workspace root stays on main — it is the launcher, not the workspace.
65
+ - All real work happens in workspace worktrees at `work-sessions/{name}/workspace/`.
66
+ - Session content (tracker, specs, plans) is written from inside the worktree and committed on the session branch. Writes from the launcher cannot reach files that live inside a worktree's git-path space.
67
+ - Source clones at `repos/{repo-name}/` stay on their default branch — never checkout a feature branch there.
68
+ - `workspace-scratchpad/` is for disposable files only — session log, hook debug output, temporary pointers.
69
+ - Project worktrees are nested inside the workspace worktree's real `repos/` directory — no symlink.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // Add a project repo to an existing work session mid-flight. Creates a
3
+ // nested project worktree inside the workspace worktree's repos/ dir and
4
+ // appends the new repo to the session tracker's `repos:` list.
5
+ import { execSync } from 'child_process';
6
+ import { join } from 'path';
7
+ import {
8
+ getWorkspaceRoot,
9
+ getMainRoot,
10
+ readJSON,
11
+ readSessionTracker,
12
+ updateSessionTracker,
13
+ sessionFolderPath,
14
+ normalizeRepos,
15
+ } from '../hooks/_utils.mjs';
16
+
17
+ const args = process.argv.slice(2);
18
+ const getArg = (name) => {
19
+ const idx = args.indexOf(`--${name}`);
20
+ return idx >= 0 && args[idx + 1] ? args[idx + 1] : null;
21
+ };
22
+
23
+ const sessionName = getArg('session-name');
24
+ const repo = getArg('repo');
25
+
26
+ if (!sessionName || !repo) {
27
+ console.error('Usage: add-repo-to-session.mjs --session-name NAME --repo REPO');
28
+ process.exit(1);
29
+ }
30
+
31
+ // Promote to the launcher root via the active-session pointer so session
32
+ // paths resolve correctly whether the script is invoked from the launcher
33
+ // or from inside a worktree.
34
+ const root = getMainRoot(getWorkspaceRoot(import.meta.url));
35
+ const config = readJSON(join(root, 'workspace.json'));
36
+ const tracker = readSessionTracker(root, sessionName);
37
+
38
+ if (!tracker) {
39
+ console.log(JSON.stringify({ success: false, error: `No session tracker found for "${sessionName}"` }));
40
+ process.exit(1);
41
+ }
42
+
43
+ if (!config?.repos?.[repo]) {
44
+ console.log(JSON.stringify({ success: false, error: `Repo "${repo}" not found in workspace.json` }));
45
+ process.exit(1);
46
+ }
47
+
48
+ const existingRepos = normalizeRepos(tracker.repos);
49
+ if (existingRepos.includes(repo)) {
50
+ console.log(JSON.stringify({ success: false, error: `Repo "${repo}" is already in this session` }));
51
+ process.exit(1);
52
+ }
53
+
54
+ const repoBranch = config.repos[repo].branch || 'main';
55
+ const reposDir = join(root, 'repos');
56
+ const repoDir = join(reposDir, repo);
57
+ const wsWorktree = join(sessionFolderPath(root, sessionName), 'workspace');
58
+ const projWorktree = join(wsWorktree, 'repos', repo);
59
+
60
+ try {
61
+ execSync(`git fetch origin`, { cwd: repoDir, stdio: 'pipe', timeout: 30000 });
62
+ execSync(`git branch "${tracker.branch}" "origin/${repoBranch}"`, { cwd: repoDir, stdio: 'pipe' });
63
+ execSync(`git worktree add "${projWorktree}" "${tracker.branch}"`, { cwd: repoDir, stdio: 'pipe' });
64
+
65
+ const newRepos = [...existingRepos, repo];
66
+ const today = new Date().toISOString().slice(0, 10);
67
+ updateSessionTracker(root, sessionName, { repos: newRepos, updated: today });
68
+
69
+ const rel = (p) => p.startsWith(root + '/') ? p.slice(root.length + 1) : p;
70
+ console.log(JSON.stringify({
71
+ success: true,
72
+ projWorktree: rel(projWorktree),
73
+ repos: newRepos,
74
+ }));
75
+ } catch (err) {
76
+ console.log(JSON.stringify({ success: false, error: err.message }));
77
+ process.exit(1);
78
+ }
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ // Tear down a work session's worktrees, branches, and folder.
3
+ //
4
+ // Teardown order is MANDATORY:
5
+ // 1. Remove each project worktree from its project repo
6
+ // 2. Remove the workspace worktree from the workspace repo
7
+ // 3. Prune each project repo (belt-and-suspenders)
8
+ // 4. Delete all local branches
9
+ // 5. Remove the whole work-sessions/{name}/ folder
10
+ //
11
+ // Workspace-first removal silently deletes the nested project worktrees'
12
+ // .git files and leaves orphan worktree records in the project repos.
13
+ // The safe order keeps both sides of the relationship in sync.
14
+ import { execSync } from 'child_process';
15
+ import { existsSync } from 'fs';
16
+ import { join } from 'path';
17
+ import {
18
+ getWorkspaceRoot,
19
+ readSessionTracker,
20
+ deleteSessionFolder,
21
+ sessionFolderPath,
22
+ normalizeRepos,
23
+ } from '../hooks/_utils.mjs';
24
+
25
+ const args = process.argv.slice(2);
26
+ const getArg = (name) => {
27
+ const idx = args.indexOf(`--${name}`);
28
+ return idx >= 0 && args[idx + 1] ? args[idx + 1] : null;
29
+ };
30
+
31
+ const sessionName = getArg('session-name');
32
+ if (!sessionName) {
33
+ console.error('Usage: cleanup-work-session.mjs --session-name NAME');
34
+ process.exit(1);
35
+ }
36
+
37
+ const root = getWorkspaceRoot(import.meta.url);
38
+ const tracker = readSessionTracker(root, sessionName);
39
+ const repos = normalizeRepos(tracker?.repos);
40
+ const branch = tracker?.branch;
41
+ const reposDir = join(root, 'repos');
42
+ const sessionFolder = sessionFolderPath(root, sessionName);
43
+ const wsWorktree = join(sessionFolder, 'workspace');
44
+
45
+ const removed = [];
46
+ const errors = [];
47
+
48
+ // Step 1: Remove each project worktree FIRST, from its project repo
49
+ for (const repo of repos) {
50
+ const projWorktree = join(wsWorktree, 'repos', repo);
51
+ const repoDir = join(reposDir, repo);
52
+ if (existsSync(projWorktree)) {
53
+ try {
54
+ execSync(`git worktree remove "${projWorktree}" --force`, { cwd: repoDir, stdio: 'pipe' });
55
+ removed.push(`project worktree ${repo}`);
56
+ } catch (err) {
57
+ errors.push(`Failed to remove ${repo} worktree: ${err.message}`);
58
+ }
59
+ }
60
+ }
61
+
62
+ // Step 2: Remove the workspace worktree AFTER project worktrees are gone
63
+ if (existsSync(wsWorktree)) {
64
+ try {
65
+ execSync(`git worktree remove "${wsWorktree}" --force`, { cwd: root, stdio: 'pipe' });
66
+ removed.push('workspace worktree');
67
+ } catch (err) {
68
+ errors.push(`Failed to remove workspace worktree: ${err.message}`);
69
+ }
70
+ }
71
+
72
+ // Step 3: Prune each project repo to mop up orphans from any prior misuses
73
+ for (const repo of repos) {
74
+ const repoDir = join(reposDir, repo);
75
+ try {
76
+ execSync('git worktree prune', { cwd: repoDir, stdio: 'pipe' });
77
+ } catch {
78
+ // Non-fatal — prune is a safety net
79
+ }
80
+ }
81
+
82
+ // Step 4: Delete local branches
83
+ if (branch) {
84
+ for (const repo of repos) {
85
+ const repoDir = join(reposDir, repo);
86
+ try {
87
+ execSync(`git branch -D "${branch}"`, { cwd: repoDir, stdio: 'pipe' });
88
+ } catch {
89
+ // Non-fatal — branch may already be gone or refuse to delete
90
+ }
91
+ }
92
+ try {
93
+ execSync(`git branch -D "${branch}"`, { cwd: root, stdio: 'pipe' });
94
+ } catch {
95
+ // Non-fatal
96
+ }
97
+ }
98
+
99
+ // Step 5: Delete the whole work-sessions/{name}/ folder. The session.md,
100
+ // specs, plans, and any local-only artifacts vanish. Their content was
101
+ // archived into release notes by /complete-work before this script ran.
102
+ deleteSessionFolder(root, sessionName);
103
+
104
+ console.log(JSON.stringify({
105
+ success: errors.length === 0,
106
+ removed,
107
+ errors: errors.length > 0 ? errors : undefined,
108
+ }));
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ // Create a work session: workspace worktree + nested project worktrees +
3
+ // session.md tracker. Produces a self-contained work-sessions/{name}/ folder.
4
+ import { execSync } from 'child_process';
5
+ import { existsSync, mkdirSync, copyFileSync } from 'fs';
6
+ import { join, resolve } from 'path';
7
+ import {
8
+ getWorkspaceRoot,
9
+ readJSON,
10
+ sessionFilePath,
11
+ sessionFolderPath,
12
+ createSessionTracker,
13
+ writeActiveSessionPointer,
14
+ } from '../hooks/_utils.mjs';
15
+
16
+ const args = process.argv.slice(2);
17
+ const getArg = (name) => {
18
+ const idx = args.indexOf(`--${name}`);
19
+ return idx >= 0 && args[idx + 1] ? args[idx + 1] : null;
20
+ };
21
+
22
+ const sessionName = getArg('session-name');
23
+ const branch = getArg('branch');
24
+ const repoArg = getArg('repo');
25
+ const user = getArg('user');
26
+ const description = getArg('description') || '';
27
+
28
+ if (!sessionName || !branch || !repoArg || !user) {
29
+ console.error('Usage: create-work-session.mjs --session-name NAME --branch BRANCH --repo REPO[,REPO2,...] --user USER [--description DESC]');
30
+ process.exit(1);
31
+ }
32
+
33
+ const repos = repoArg.split(',').map(r => r.trim()).filter(Boolean);
34
+ const root = getWorkspaceRoot(import.meta.url);
35
+ const config = readJSON(join(root, 'workspace.json'));
36
+ const reposDir = join(root, 'repos');
37
+
38
+ const sessionFolder = sessionFolderPath(root, sessionName);
39
+ const wsWorktree = join(sessionFolder, 'workspace');
40
+
41
+ try {
42
+ // Ensure the work-sessions/ parent and the session folder exist
43
+ mkdirSync(sessionFolder, { recursive: true });
44
+
45
+ // Create the workspace branch and worktree
46
+ execSync(`git branch "${branch}" main`, { cwd: root, stdio: 'pipe' });
47
+ execSync(`git worktree add "${wsWorktree}" "${branch}"`, { cwd: root, stdio: 'pipe' });
48
+
49
+ // Real repos/ directory inside the workspace worktree (no symlink).
50
+ // The workspace .gitignore pattern `repos` (no slash) covers both the
51
+ // workspace root's repos/ and this one.
52
+ const nestedReposDir = join(wsWorktree, 'repos');
53
+ mkdirSync(nestedReposDir, { recursive: true });
54
+
55
+ // Create each project branch and nest its worktree inside the workspace worktree
56
+ const projWorktrees = [];
57
+ for (const repo of repos) {
58
+ const repoBranch = config?.repos?.[repo]?.branch || 'main';
59
+ const repoDir = join(reposDir, repo);
60
+ const projWorktree = join(nestedReposDir, repo);
61
+
62
+ execSync(`git fetch origin`, { cwd: repoDir, stdio: 'pipe', timeout: 30000 });
63
+ execSync(`git branch "${branch}" "origin/${repoBranch}"`, { cwd: repoDir, stdio: 'pipe' });
64
+ execSync(`git worktree add "${projWorktree}" "${branch}"`, { cwd: repoDir, stdio: 'pipe' });
65
+
66
+ projWorktrees.push(projWorktree);
67
+ }
68
+
69
+ // Copy settings.local.json into the workspace worktree if present at root
70
+ const settingsSrc = join(root, '.claude', 'settings.local.json');
71
+ const settingsDst = join(wsWorktree, '.claude', 'settings.local.json');
72
+ if (existsSync(settingsSrc)) {
73
+ copyFileSync(settingsSrc, settingsDst);
74
+ }
75
+
76
+ // Write the active-session pointer inside the worktree's .claude/ dir so
77
+ // hooks running inside the worktree know which session is in scope.
78
+ writeActiveSessionPointer(wsWorktree, { name: sessionName, rootPath: root });
79
+
80
+ // Write the unified session.md tracker inside the worktree (at the top of
81
+ // the session branch) and commit it on the branch as the branch's first
82
+ // commit above main. Frontmatter holds machine state; body holds human
83
+ // content. Hooks and skills update the frontmatter via session-frontmatter
84
+ // helpers; humans update the body directly.
85
+ const now = new Date().toISOString();
86
+ const today = now.slice(0, 10);
87
+ createSessionTracker(
88
+ root,
89
+ sessionName,
90
+ {
91
+ type: 'session-tracker',
92
+ name: sessionName,
93
+ description,
94
+ status: 'active',
95
+ branch,
96
+ created: now,
97
+ user,
98
+ repos,
99
+ chatSessions: [],
100
+ author: user,
101
+ updated: today,
102
+ },
103
+ `\n# Work Session: ${sessionName}\n\n${description}\n\n## Progress\n\n(Updated as the session progresses)\n`
104
+ );
105
+ execSync('git add session.md', { cwd: wsWorktree, stdio: 'pipe' });
106
+ execSync(`git commit -m "chore: initialize session tracker for ${sessionName}"`, {
107
+ cwd: wsWorktree,
108
+ stdio: 'pipe',
109
+ });
110
+
111
+ // Paths for the success payload, relative to the workspace root
112
+ const rel = (p) => p.startsWith(root + '/') ? p.slice(root.length + 1) : p;
113
+
114
+ console.log(JSON.stringify({
115
+ success: true,
116
+ sessionFolder: rel(sessionFolder),
117
+ wsWorktree: rel(wsWorktree),
118
+ projWorktrees: projWorktrees.map(rel),
119
+ tracker: rel(sessionFilePath(root, sessionName)),
120
+ }));
121
+ } catch (err) {
122
+ console.log(JSON.stringify({ success: false, error: err.message }));
123
+ process.exit(1);
124
+ }
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // One-shot migration: read an open-work.md file and create real issues via the
3
+ // configured tracker adapter. Prints the {source_id → issue_id} mapping at the
4
+ // end. NOT idempotent — if it fails partway, clean up orphan issues manually
5
+ // and re-run.
6
+ //
7
+ // Usage:
8
+ // node .claude/scripts/migrate-open-work.mjs <path-to-open-work.md> [workspace-json-path]
9
+
10
+ import { readFileSync } from 'node:fs';
11
+ import { createTracker } from './trackers/interface.mjs';
12
+
13
+ const openWorkPath = process.argv[2];
14
+ const workspaceJsonPath = process.argv[3] || 'workspace.json';
15
+
16
+ if (!openWorkPath) {
17
+ console.error('Usage: migrate-open-work.mjs <path-to-open-work.md> [workspace-json-path]');
18
+ process.exit(1);
19
+ }
20
+
21
+ const workspace = JSON.parse(readFileSync(workspaceJsonPath, 'utf-8'));
22
+ const trackerConfig = workspace.workspace?.tracker;
23
+ if (!trackerConfig) {
24
+ console.error('No tracker configured in workspace.json — run /setup-tracker first.');
25
+ process.exit(1);
26
+ }
27
+
28
+ const tracker = createTracker(trackerConfig);
29
+ const content = readFileSync(openWorkPath, 'utf-8');
30
+
31
+ // Parse ticket rows. Matches the 8-column milestone-aware format.
32
+ const rowRe = /^\|\s*(\d+)\s*\|\s*(bug|feat|chore)\s*\|\s*(P[123])\s*\|\s*([^|]+?)\s*\|\s*(open|in-progress|paused|done)\s*\|\s*([^|]+?)\s*\|\s*(.+?)\s*\|\s*$/gm;
33
+ const detailRe = /^### #(\d+)\s*—\s*[^\n]+\n\n([\s\S]+?)(?=^### #\d+|<!-- tracker-state|\Z)/gm;
34
+
35
+ const details = {};
36
+ for (const m of content.matchAll(detailRe)) {
37
+ details[parseInt(m[1], 10)] = m[2].trim();
38
+ }
39
+
40
+ const tickets = [];
41
+ for (const m of content.matchAll(rowRe)) {
42
+ const id = parseInt(m[1], 10);
43
+ const msRaw = m[4].trim();
44
+ tickets.push({
45
+ id,
46
+ type: m[2],
47
+ priority: m[3],
48
+ milestone: msRaw === '—' || msRaw === '' ? null : msRaw,
49
+ status: m[5],
50
+ branch: m[6].trim() === '—' ? null : m[6].trim(),
51
+ title: m[7].trim(),
52
+ body: details[id] || '',
53
+ });
54
+ }
55
+
56
+ console.log(`Tracker: ${tracker.identity}`);
57
+ console.log(`Found ${tickets.length} tickets in ${openWorkPath}.\n`);
58
+
59
+ const mapping = [];
60
+
61
+ for (const ticket of tickets) {
62
+ console.log(`Creating #${ticket.id} — ${ticket.title} [${ticket.type}/${ticket.priority}]`);
63
+ const labels = [ticket.type, ticket.priority];
64
+ const body = [
65
+ ticket.body || '_No details in open-work.md._',
66
+ '',
67
+ '---',
68
+ '',
69
+ `Migrated from \`shared-context/open-work.md\` ticket #${ticket.id} (status at migration: \`${ticket.status}\`${ticket.branch ? `, branch: \`${ticket.branch}\`` : ''}).`,
70
+ ].join('\n');
71
+
72
+ const issue = await tracker.createIssue({ title: ticket.title, body, labels, milestone: ticket.milestone });
73
+ mapping.push({ sourceId: ticket.id, issueId: issue.id, number: issue.number, url: issue.url, status: ticket.status });
74
+
75
+ if (ticket.status === 'in-progress' || ticket.status === 'paused') {
76
+ try {
77
+ await tracker.claim(issue.id);
78
+ console.log(` → claimed (status was ${ticket.status})`);
79
+ } catch (e) {
80
+ console.warn(` ! claim failed: ${e.message}`);
81
+ }
82
+ }
83
+
84
+ console.log(` → ${issue.url}`);
85
+ }
86
+
87
+ console.log('\n=== Migration mapping ===');
88
+ for (const row of mapping) {
89
+ console.log(`#${row.sourceId} → ${row.issueId} (${row.url}) [was ${row.status}]`);
90
+ }
91
+ console.log(`\nDone. Delete ${openWorkPath} on its branch after verifying the issues.`);