@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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/bin/create.mjs +79 -0
- package/lib/git.mjs +26 -0
- package/lib/init.mjs +129 -0
- package/lib/payload.mjs +44 -0
- package/lib/prompts.mjs +113 -0
- package/lib/scaffold.mjs +84 -0
- package/lib/upgrade.mjs +42 -0
- package/package.json +43 -0
- package/template/.claude/agents/aside-researcher.md +48 -0
- package/template/.claude/agents/implementer.md +39 -0
- package/template/.claude/agents/researcher.md +40 -0
- package/template/.claude/agents/reviewer.md +47 -0
- package/template/.claude/hooks/_utils.mjs +196 -0
- package/template/.claude/hooks/_utils.test.mjs +99 -0
- package/template/.claude/hooks/post-compact.mjs +7 -0
- package/template/.claude/hooks/pre-compact.mjs +34 -0
- package/template/.claude/hooks/repo-write-detection.mjs +107 -0
- package/template/.claude/hooks/session-end.mjs +91 -0
- package/template/.claude/hooks/session-start.mjs +150 -0
- package/template/.claude/hooks/subagent-start.mjs +44 -0
- package/template/.claude/hooks/workspace-update-check.mjs +42 -0
- package/template/.claude/hooks/worktree-create.mjs +53 -0
- package/template/.claude/lib/session-frontmatter.mjs +265 -0
- package/template/.claude/lib/session-frontmatter.test.mjs +242 -0
- package/template/.claude/recipes/migrate-from-notion.md +120 -0
- package/template/.claude/rules/agent-rules.md.skip +32 -0
- package/template/.claude/rules/cloud-infrastructure.md.skip +15 -0
- package/template/.claude/rules/coherent-revisions.md +24 -0
- package/template/.claude/rules/documentation.md.skip +13 -0
- package/template/.claude/rules/git-conventions.md +34 -0
- package/template/.claude/rules/honest-pushback.md +56 -0
- package/template/.claude/rules/local-dev-environment.md.skip +60 -0
- package/template/.claude/rules/memory-guidance.md +26 -0
- package/template/.claude/rules/product-integrity.md.skip +24 -0
- package/template/.claude/rules/scope-guard.md.skip +22 -0
- package/template/.claude/rules/superpowers-workflow.md.skip +22 -0
- package/template/.claude/rules/token-economics.md.skip +31 -0
- package/template/.claude/rules/work-item-tracking.md +90 -0
- package/template/.claude/rules/workspace-structure.md +69 -0
- package/template/.claude/scripts/add-repo-to-session.mjs +78 -0
- package/template/.claude/scripts/cleanup-work-session.mjs +108 -0
- package/template/.claude/scripts/create-work-session.mjs +124 -0
- package/template/.claude/scripts/migrate-open-work.mjs +91 -0
- package/template/.claude/scripts/migrate-session-layout.mjs +236 -0
- package/template/.claude/scripts/migrate-session-layout.test.mjs +144 -0
- package/template/.claude/scripts/trackers/github-issues.mjs +170 -0
- package/template/.claude/scripts/trackers/github-issues.test.mjs +190 -0
- package/template/.claude/scripts/trackers/interface.mjs +25 -0
- package/template/.claude/scripts/trackers/interface.test.mjs +40 -0
- package/template/.claude/settings.json +107 -0
- package/template/.claude/skills/aside/SKILL.md +125 -0
- package/template/.claude/skills/braindump/SKILL.md +96 -0
- package/template/.claude/skills/build-docs-site/SKILL.md +323 -0
- package/template/.claude/skills/build-docs-site/checklists/framing.md +221 -0
- package/template/.claude/skills/build-docs-site/checklists/pitfalls.md +228 -0
- package/template/.claude/skills/build-docs-site/checklists/review.md +130 -0
- package/template/.claude/skills/build-docs-site/scripts/bulk-fill-migration.py +393 -0
- package/template/.claude/skills/build-docs-site/scripts/forbidden-word-grep.mjs +159 -0
- package/template/.claude/skills/build-docs-site/scripts/leak-grep.mjs +328 -0
- package/template/.claude/skills/build-docs-site/templates/custom.css.tmpl +212 -0
- package/template/.claude/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +95 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/Arrow.tsx +87 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/Box.tsx +90 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +46 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/Region.tsx +68 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/SectionTitle.tsx +42 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/tokens.ts +67 -0
- package/template/.claude/skills/build-docs-site/templates/sidebars.ts.tmpl +89 -0
- package/template/.claude/skills/build-docs-site/templates/spec.md.tmpl +119 -0
- package/template/.claude/skills/complete-work/SKILL.md +369 -0
- package/template/.claude/skills/handoff/SKILL.md +98 -0
- package/template/.claude/skills/maintenance/SKILL.md +116 -0
- package/template/.claude/skills/pause-work/SKILL.md +98 -0
- package/template/.claude/skills/promote/SKILL.md +77 -0
- package/template/.claude/skills/release/SKILL.md +126 -0
- package/template/.claude/skills/setup-tracker/SKILL.md +117 -0
- package/template/.claude/skills/start-work/SKILL.md +234 -0
- package/template/.claude/skills/sync-work/SKILL.md +73 -0
- package/template/.claude/skills/workspace-init/SKILL.md +420 -0
- package/template/.claude/skills/workspace-update/SKILL.md +108 -0
- package/template/.mcp.json +12 -0
- package/template/CLAUDE.md.tmpl +32 -0
- package/template/_gitignore +28 -0
- 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.`);
|