@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,190 @@
1
+ #!/usr/bin/env node
2
+ // Tests for the GitHub Issues adapter. Uses an injected spawnFn to mock gh calls.
3
+ // Run: node .claude/scripts/trackers/github-issues.test.mjs
4
+ import { createTracker, AlreadyAssignedError } from './interface.mjs';
5
+
6
+ let failed = 0, passed = 0;
7
+ const ok = () => { passed++; };
8
+ const fail = (msg) => { failed++; console.error(` FAIL: ${msg}`); };
9
+
10
+ // Build a spawnFn that returns canned responses keyed by argv.
11
+ function buildSpawn(responses) {
12
+ const calls = [];
13
+ const fn = (cmd, args, options) => {
14
+ calls.push({ cmd, args, input: options?.input });
15
+ const key = args.join(' ');
16
+ const resp = responses[key];
17
+ if (!resp) {
18
+ return { status: 1, stdout: '', stderr: `no mock for: ${cmd} ${key}` };
19
+ }
20
+ return { status: 0, stdout: resp, stderr: '' };
21
+ };
22
+ fn.calls = calls;
23
+ return fn;
24
+ }
25
+
26
+ // listAssignedToMe normalizes JSON into Issue[].
27
+ {
28
+ const spawnFn = buildSpawn({
29
+ 'api user --jq .login': 'alice\n',
30
+ 'issue list --repo foo/bar --assignee alice --state open --json number,title,body,state,assignees,labels,milestone,url,createdAt,updatedAt':
31
+ JSON.stringify([{ number: 1, title: 'Fix bug', body: 'details', state: 'OPEN', assignees: [{ login: 'alice' }], labels: [{ name: 'bug' }], milestone: { title: 'v0.1' }, url: 'https://github.com/foo/bar/issues/1', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-02T00:00:00Z' }]),
32
+ });
33
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
34
+ const issues = await t.listAssignedToMe();
35
+ if (issues.length === 1 && issues[0].id === 'gh:1' && issues[0].assignees[0] === 'alice'
36
+ && issues[0].labels[0] === 'bug' && issues[0].milestone === 'v0.1') ok();
37
+ else fail(`listAssignedToMe normalization wrong: ${JSON.stringify(issues)}`);
38
+ }
39
+
40
+ // listUnassigned uses no:assignee search.
41
+ {
42
+ const spawnFn = buildSpawn({
43
+ 'api user --jq .login': 'alice\n',
44
+ 'issue list --repo foo/bar --search no:assignee --state open --json number,title,body,state,assignees,labels,milestone,url,createdAt,updatedAt':
45
+ JSON.stringify([{ number: 2, title: 'Open work', body: '', state: 'OPEN', assignees: [], labels: [], milestone: null, url: 'https://github.com/foo/bar/issues/2', createdAt: '2026-01-03T00:00:00Z', updatedAt: '2026-01-03T00:00:00Z' }]),
46
+ });
47
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
48
+ const issues = await t.listUnassigned();
49
+ if (issues.length === 1 && issues[0].id === 'gh:2' && issues[0].assignees.length === 0) ok();
50
+ else fail(`listUnassigned wrong: ${JSON.stringify(issues)}`);
51
+ }
52
+
53
+ // claim throws AlreadyAssignedError when a different user is assigned.
54
+ {
55
+ const spawnFn = buildSpawn({
56
+ 'api user --jq .login': 'alice\n',
57
+ 'issue view 3 --repo foo/bar --json number,title,body,state,assignees,labels,milestone,url,createdAt,updatedAt':
58
+ JSON.stringify({ number: 3, title: 't', body: '', state: 'OPEN', assignees: [{ login: 'bob' }], labels: [], milestone: null, url: 'u', createdAt: 'd', updatedAt: 'd' }),
59
+ });
60
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
61
+ try { await t.claim('gh:3'); fail('claim should have thrown'); }
62
+ catch (e) { if (e instanceof AlreadyAssignedError && e.assignees[0] === 'bob') ok(); else fail(`wrong error: ${e}`); }
63
+ }
64
+
65
+ // claim is idempotent when already assigned to me.
66
+ {
67
+ const spawnFn = buildSpawn({
68
+ 'api user --jq .login': 'alice\n',
69
+ 'issue view 4 --repo foo/bar --json number,title,body,state,assignees,labels,milestone,url,createdAt,updatedAt':
70
+ JSON.stringify({ number: 4, title: 't', body: '', state: 'OPEN', assignees: [{ login: 'alice' }], labels: [], milestone: null, url: 'u', createdAt: 'd', updatedAt: 'd' }),
71
+ });
72
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
73
+ const issue = await t.claim('gh:4');
74
+ const edited = spawnFn.calls.some(c => c.args.includes('edit') && c.args.includes('--add-assignee'));
75
+ if (issue.id === 'gh:4' && !edited) ok();
76
+ else fail(`claim should be no-op when already assigned: edited=${edited}`);
77
+ }
78
+
79
+ // claim assigns when unassigned.
80
+ {
81
+ const viewResponse = JSON.stringify({ number: 5, title: 't', body: '', state: 'OPEN', assignees: [], labels: [], milestone: null, url: 'u', createdAt: 'd', updatedAt: 'd' });
82
+ const viewAssigned = JSON.stringify({ number: 5, title: 't', body: '', state: 'OPEN', assignees: [{ login: 'alice' }], labels: [], milestone: null, url: 'u', createdAt: 'd', updatedAt: 'd' });
83
+ let viewCallCount = 0;
84
+ const spawnFn = (cmd, args, options) => {
85
+ const key = args.join(' ');
86
+ if (key === 'api user --jq .login') return { status: 0, stdout: 'alice\n', stderr: '' };
87
+ if (key === 'issue view 5 --repo foo/bar --json number,title,body,state,assignees,labels,milestone,url,createdAt,updatedAt') {
88
+ viewCallCount++;
89
+ return { status: 0, stdout: viewCallCount === 1 ? viewResponse : viewAssigned, stderr: '' };
90
+ }
91
+ if (key === 'issue edit 5 --repo foo/bar --add-assignee alice') return { status: 0, stdout: '', stderr: '' };
92
+ return { status: 1, stdout: '', stderr: `no mock: ${key}` };
93
+ };
94
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
95
+ const issue = await t.claim('gh:5');
96
+ if (issue.assignees[0] === 'alice') ok();
97
+ else fail(`claim should assign: ${JSON.stringify(issue)}`);
98
+ }
99
+
100
+ // createIssue parses issue number from URL.
101
+ {
102
+ const spawnFn = (cmd, args, options) => {
103
+ const key = args.join(' ');
104
+ if (key.startsWith('issue create --repo foo/bar')) {
105
+ return { status: 0, stdout: 'https://github.com/foo/bar/issues/99\n', stderr: '' };
106
+ }
107
+ if (key === 'issue view 99 --repo foo/bar --json number,title,body,state,assignees,labels,milestone,url,createdAt,updatedAt') {
108
+ return { status: 0, stdout: JSON.stringify({ number: 99, title: 'New', body: 'b', state: 'OPEN', assignees: [], labels: [{ name: 'chore' }], milestone: null, url: 'https://github.com/foo/bar/issues/99', createdAt: 'd', updatedAt: 'd' }), stderr: '' };
109
+ }
110
+ return { status: 1, stdout: '', stderr: `no mock: ${key}` };
111
+ };
112
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
113
+ const issue = await t.createIssue({ title: 'New', body: 'b', labels: ['chore'] });
114
+ if (issue.id === 'gh:99') ok();
115
+ else fail(`createIssue wrong: ${JSON.stringify(issue)}`);
116
+ }
117
+
118
+ // ensureLabels calls gh label create --force for each of the six standard labels.
119
+ {
120
+ const created = [];
121
+ const spawnFn = (cmd, args) => {
122
+ if (args[0] === 'label' && args[1] === 'create') {
123
+ created.push(args[2]);
124
+ return { status: 0, stdout: '', stderr: '' };
125
+ }
126
+ return { status: 1, stdout: '', stderr: 'no mock' };
127
+ };
128
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
129
+ await t.ensureLabels();
130
+ const expected = ['bug', 'feat', 'chore', 'P1', 'P2', 'P3'];
131
+ if (JSON.stringify(created.sort()) === JSON.stringify(expected.sort())) ok();
132
+ else fail(`ensureLabels wrong: ${JSON.stringify(created)}`);
133
+ }
134
+
135
+ // gh failure surfaces stderr.
136
+ {
137
+ const spawnFn = () => ({ status: 1, stdout: '', stderr: 'gh is on fire' });
138
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
139
+ try { await t.listUnassigned(); fail('should have thrown'); }
140
+ catch (e) { if (/gh is on fire/.test(e.message)) ok(); else fail(`wrong error: ${e.message}`); }
141
+ }
142
+
143
+ // ensureMilestone returns existing milestone without POST when title matches.
144
+ {
145
+ const calls = [];
146
+ const spawnFn = (cmd, args) => {
147
+ calls.push(args.join(' '));
148
+ const key = args.join(' ');
149
+ if (key === 'api repos/foo/bar/milestones?state=all&per_page=100') {
150
+ return { status: 0, stdout: JSON.stringify([
151
+ { number: 1, title: 'Backlog', description: 'Triage later', state: 'open', due_on: null, html_url: 'https://github.com/foo/bar/milestone/1' },
152
+ ]), stderr: '' };
153
+ }
154
+ return { status: 1, stdout: '', stderr: `no mock: ${key}` };
155
+ };
156
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
157
+ const ms = await t.ensureMilestone({ title: 'Backlog' });
158
+ const posted = calls.some(c => c.includes('-X POST'));
159
+ if (ms.title === 'Backlog' && ms.number === 1 && !posted) ok();
160
+ else fail(`ensureMilestone should return existing without POST: posted=${posted}, ms=${JSON.stringify(ms)}`);
161
+ }
162
+
163
+ // ensureMilestone creates when title does not exist.
164
+ {
165
+ const spawnFn = (cmd, args) => {
166
+ const key = args.join(' ');
167
+ if (key === 'api repos/foo/bar/milestones?state=all&per_page=100') {
168
+ return { status: 0, stdout: JSON.stringify([]), stderr: '' };
169
+ }
170
+ if (key.startsWith('api repos/foo/bar/milestones -X POST')) {
171
+ return { status: 0, stdout: JSON.stringify({ number: 2, title: 'v0.1', description: 'alpha', state: 'open', due_on: null, html_url: 'https://github.com/foo/bar/milestone/2' }), stderr: '' };
172
+ }
173
+ return { status: 1, stdout: '', stderr: `no mock: ${key}` };
174
+ };
175
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
176
+ const ms = await t.ensureMilestone({ title: 'v0.1', description: 'alpha' });
177
+ if (ms.title === 'v0.1' && ms.number === 2 && ms.description === 'alpha') ok();
178
+ else fail(`ensureMilestone should create when absent: ${JSON.stringify(ms)}`);
179
+ }
180
+
181
+ // ensureMilestone rejects missing title.
182
+ {
183
+ const spawnFn = () => ({ status: 0, stdout: '[]', stderr: '' });
184
+ const t = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn });
185
+ try { await t.ensureMilestone({}); fail('should have thrown'); }
186
+ catch (e) { if (/title is required/.test(e.message)) ok(); else fail(`wrong error: ${e.message}`); }
187
+ }
188
+
189
+ console.log(`\n${passed} passed, ${failed} failed`);
190
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,25 @@
1
+ // Tracker adapter interface. Skills import only from this module.
2
+ // See design-tracker-abstraction.md for the full Issue shape and method contracts.
3
+
4
+ import { createGithubAdapter } from './github-issues.mjs';
5
+
6
+ export class AlreadyAssignedError extends Error {
7
+ constructor(issueId, assignees) {
8
+ super(`${issueId} is already assigned to ${assignees.join(', ')}`);
9
+ this.name = 'AlreadyAssignedError';
10
+ this.code = 'ALREADY_ASSIGNED';
11
+ this.assignees = assignees;
12
+ }
13
+ }
14
+
15
+ export function createTracker(config, options = {}) {
16
+ if (!config || typeof config !== 'object') {
17
+ throw new Error('No tracker configured — pass workspace.json\'s workspace.tracker block.');
18
+ }
19
+ switch (config.type) {
20
+ case 'github-issues':
21
+ return createGithubAdapter(config, options);
22
+ default:
23
+ throw new Error(`Unknown tracker type: ${config.type}`);
24
+ }
25
+ }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ // Tests for the tracker factory and AlreadyAssignedError.
3
+ // Run: node .claude/scripts/trackers/interface.test.mjs
4
+ import { createTracker, AlreadyAssignedError } from './interface.mjs';
5
+
6
+ let failed = 0, passed = 0;
7
+ const ok = (msg) => { passed++; };
8
+ const fail = (msg) => { failed++; console.error(` FAIL: ${msg}`); };
9
+
10
+ // AlreadyAssignedError carries assignees and a code.
11
+ {
12
+ const err = new AlreadyAssignedError('gh:42', ['alice', 'bob']);
13
+ if (err.code === 'ALREADY_ASSIGNED' && JSON.stringify(err.assignees) === '["alice","bob"]'
14
+ && err.message.includes('gh:42') && err.message.includes('alice')) ok();
15
+ else fail('AlreadyAssignedError should carry code and assignees');
16
+ }
17
+
18
+ // createTracker rejects missing config.
19
+ {
20
+ try { createTracker(); fail('should throw on missing config'); }
21
+ catch (e) { if (/No tracker configured/.test(e.message)) ok(); else fail(`wrong error: ${e.message}`); }
22
+ }
23
+
24
+ // createTracker rejects unknown type.
25
+ {
26
+ try { createTracker({ type: 'nope' }); fail('should throw on unknown type'); }
27
+ catch (e) { if (/Unknown tracker type/.test(e.message)) ok(); else fail(`wrong error: ${e.message}`); }
28
+ }
29
+
30
+ // createTracker builds a github-issues adapter without calling gh at construction (lazy).
31
+ {
32
+ const fakeSpawn = () => { throw new Error('spawn should not run at construction'); };
33
+ // With repo: 'foo/bar' literal, the adapter should NOT shell out to resolve the remote.
34
+ const adapter = createTracker({ type: 'github-issues', repo: 'foo/bar' }, { spawnFn: fakeSpawn });
35
+ if (adapter.identity === 'github-issues:foo/bar') ok();
36
+ else fail(`unexpected identity: ${adapter.identity}`);
37
+ }
38
+
39
+ console.log(`\n${passed} passed, ${failed} failed`);
40
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,107 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/workspace-update-check.mjs",
10
+ "timeout": 5000,
11
+ "statusMessage": "Checking for workspace updates..."
12
+ },
13
+ {
14
+ "type": "command",
15
+ "command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/session-start.mjs",
16
+ "timeout": 30000,
17
+ "statusMessage": "Syncing workspace..."
18
+ }
19
+ ]
20
+ }
21
+ ],
22
+ "SubagentStart": [
23
+ {
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/subagent-start.mjs",
28
+ "timeout": 5000,
29
+ "statusMessage": "Loading shared context..."
30
+ }
31
+ ]
32
+ }
33
+ ],
34
+ "PreCompact": [
35
+ {
36
+ "hooks": [
37
+ {
38
+ "type": "command",
39
+ "command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/pre-compact.mjs",
40
+ "timeout": 5000,
41
+ "statusMessage": "Checking for uncaptured context..."
42
+ }
43
+ ]
44
+ }
45
+ ],
46
+ "PostCompact": [
47
+ {
48
+ "hooks": [
49
+ {
50
+ "type": "command",
51
+ "command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/post-compact.mjs",
52
+ "timeout": 5000
53
+ }
54
+ ]
55
+ }
56
+ ],
57
+ "PreToolUse": [
58
+ {
59
+ "hooks": [
60
+ {
61
+ "type": "command",
62
+ "command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/workspace-update-check.mjs",
63
+ "timeout": 5000
64
+ },
65
+ {
66
+ "type": "command",
67
+ "command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/repo-write-detection.mjs",
68
+ "timeout": 5000
69
+ }
70
+ ]
71
+ }
72
+ ],
73
+ "SessionEnd": [
74
+ {
75
+ "hooks": [
76
+ {
77
+ "type": "command",
78
+ "command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/session-end.mjs",
79
+ "timeout": 15000,
80
+ "statusMessage": "Saving session state..."
81
+ }
82
+ ]
83
+ }
84
+ ],
85
+ "WorktreeCreate": [
86
+ {
87
+ "hooks": [
88
+ {
89
+ "type": "command",
90
+ "command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/worktree-create.mjs",
91
+ "timeout": 5000,
92
+ "statusMessage": "Checking for stale worktrees..."
93
+ }
94
+ ]
95
+ }
96
+ ]
97
+ },
98
+ "permissions": {
99
+ "allow": [
100
+ "Bash(git:*)",
101
+ "Bash(ls:*)"
102
+ ]
103
+ },
104
+ "enabledPlugins": {
105
+ "playwright@claude-plugins-official": false
106
+ }
107
+ }
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: aside
3
+ description: Capture a drive-by idea without interrupting your current work. Dispatches a background researcher by default, or use --quick for a simple note. Usage: /aside [--quick] <your thought>
4
+ ---
5
+
6
+ # Aside
7
+
8
+ Capture a drive-by idea without interrupting the current conversation. By default, dispatches a background subagent to research and expand the idea. Use `--quick` for a simple note with no research.
9
+
10
+ ## Parameters
11
+
12
+ - `/aside <thought>` — researched mode (default). Background subagent explores the idea.
13
+ - `/aside --quick <thought>` — note only. No subagent, no research. Just park the thought.
14
+
15
+ Everything after `/aside` (or `/aside --quick`) is the user's thought. No name parameter — the filename is generated from the content.
16
+
17
+ ## Quick Mode (`--quick`)
18
+
19
+ No subagent. Execute these steps directly:
20
+
21
+ 1. Parse the user's thought from the arguments (everything after `--quick`)
22
+ 2. Generate a kebab-case slug from the content (3-5 words that capture the core idea)
23
+ 3. Check if `shared-context/{user}/local-only-{slug}.md` exists. If so, append `-2`, `-3`, etc.
24
+ 4. Infer 2-3 threads worth exploring later for the Further Investigation section
25
+ 5. Write the file using the Quick Mode template below
26
+ 6. Report the file path to the user: "Noted: `shared-context/{user}/local-only-{slug}.md`"
27
+
28
+ ### Quick Mode Template
29
+
30
+ ```yaml
31
+ ---
32
+ state: ephemeral
33
+ lifecycle: active
34
+ type: braindump
35
+ variant: aside
36
+ author: {user}
37
+ updated: {YYYY-MM-DD}
38
+ ---
39
+
40
+ ## User's Original Thought
41
+ {Verbatim text from the user — copy exactly as provided}
42
+
43
+ ## Further Investigation
44
+ {2-3 bullet points: threads worth pulling on, questions to explore,
45
+ related areas to check. Quick inference, not deep research.}
46
+ ```
47
+
48
+ ## Research Mode (default)
49
+
50
+ Dispatch the `aside-researcher` agent in the background:
51
+
52
+ 1. Parse the user's thought from the arguments (everything after `/aside`)
53
+ 2. Generate a kebab-case slug from the content (3-5 words that capture the core idea)
54
+ 3. Check if `shared-context/{user}/local-only-{slug}.md` exists. If so, append `-2`, `-3`, etc.
55
+ 4. Determine the target file path: `shared-context/{user}/local-only-{slug}.md`
56
+ 5. Dispatch the `aside-researcher` agent using the Agent tool:
57
+ - `subagent_type`: use the `aside-researcher` agent definition
58
+ - `run_in_background: true`
59
+ - Prompt must include:
60
+ - The user's verbatim thought
61
+ - The target file path
62
+ - The workspace root path
63
+ - The Research Mode template (below)
64
+ 6. Confirm dispatch to the user: "Researching in the background. I'll let you know when it's done."
65
+ 7. When the agent completes, report: file path and a one-line summary of what was found
66
+
67
+ ### Research Mode Template
68
+
69
+ Include this template in the agent's prompt so it writes the correct format:
70
+
71
+ ```yaml
72
+ ---
73
+ state: ephemeral
74
+ lifecycle: active
75
+ type: braindump
76
+ variant: aside
77
+ author: {user}
78
+ updated: {YYYY-MM-DD}
79
+ ---
80
+
81
+ ## User's Original Thought
82
+ {Verbatim text from the user — copy exactly as provided, never paraphrase}
83
+
84
+ ## Agent Research
85
+ {Findings from the workspace, project repos, and web.
86
+ References specific file paths and prior art.
87
+ Structured as prose or sub-headings as the content demands.}
88
+
89
+ ## Synthesis
90
+ {How the user's thought connects to what was found.
91
+ Proposed next steps, design considerations.
92
+ Clearly framed as agent analysis, not user intent.}
93
+
94
+ ## Further Investigation
95
+ {Threads worth pulling on. What the agent couldn't answer.
96
+ Topics that would benefit from deeper exploration or user input.}
97
+ ```
98
+
99
+ ## File Naming
100
+
101
+ - **Location:** `shared-context/{user}/local-only-{slug}.md`
102
+ - **Slug:** Generated from the thought content. Kebab-case, 3-5 words. E.g., `refresh-token-caching`, `deploy-pipeline-idea`
103
+ - **Collision handling:** If the file exists, append `-2`, `-3`, etc.
104
+ - **Always `local-only-`** — gitignored, never auto-committed
105
+
106
+ ## Session Behavior
107
+
108
+ Asides are session-agnostic. Regardless of whether a work session is active:
109
+ - Files always go to `shared-context/{user}/`
110
+ - No interaction with the session tracker
111
+ - No interaction with `/complete-work` synthesis
112
+
113
+ ## Lifecycle
114
+
115
+ Asides stay as `local-only-*` files until deliberately promoted:
116
+ - `/promote` discovers them during its `local-only-*` scan
117
+ - `/maintenance` can flag stale asides
118
+ - `variant: aside` frontmatter distinguishes them from other local-only files
119
+
120
+ ## Notes
121
+
122
+ - The subagent receives locked context automatically via the SubagentStart hook
123
+ - The subagent does NOT receive conversation history — the user provides context inline as part of their thought
124
+ - Asides never modify existing files
125
+ - One aside = one file, always
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: braindump
3
+ description: Capture discussion-heavy topics into shared context. Use when reasoning, exploration, or design rationale should be preserved. Accepts optional name parameter.
4
+ ---
5
+
6
+ # Braindump
7
+
8
+ Capture discussion reasoning, exploration results, and design rationale into shared context. More freeform than /handoff — designed for "why we chose X" content. User-scoped by default.
9
+
10
+ ## Parameters
11
+ - `/braindump {name}` — create or update a named braindump
12
+ - `/braindump` (no param) — analyze session and suggest name(s)
13
+
14
+ > **Note:** `/braindump side` has moved to `/aside`. If the user invokes `/braindump side`, redirect them: "The side braindump is now `/aside`. Running it for you." Then invoke the `/aside` skill with their text.
15
+
16
+ ## Session-Aware Behavior
17
+
18
+ When called within an active work session (the active-session pointer at `.claude/.active-session.json` exists inside the current worktree):
19
+
20
+ - Default behavior: append reasoning and decisions to the session tracker body at `work-sessions/{session-name}/workspace/session.md`
21
+ - Add a new section to the tracker body (Context, Exploration, Decisions, Implications) — do NOT touch the frontmatter
22
+ - Auto-commit from inside the worktree so the update lands on the session branch:
23
+ ```bash
24
+ cd work-sessions/{session-name}/workspace
25
+ git add session.md
26
+ git commit -m "braindump: update {session-name} tracker"
27
+ ```
28
+
29
+ When called from the workspace root (no active session):
30
+ - Create a `local-only-{name}.md` file (root only allows local-only writes)
31
+ - Suggest starting a work session if the braindump is about actionable work
32
+
33
+ The flows below apply when NOT in an active work session, or when the user explicitly asks for a standalone braindump file.
34
+
35
+ ## Flow: Named
36
+
37
+ Follows the same naming, scoping (user/team/local-only), and commit flow as `/handoff` but with a different file format:
38
+
39
+ ```yaml
40
+ ---
41
+ state: ephemeral
42
+ lifecycle: active
43
+ type: braindump
44
+ topic: {name}
45
+ author: {user}
46
+ updated: {YYYY-MM-DD}
47
+ ---
48
+
49
+ ## Context
50
+ {What prompted this discussion}
51
+
52
+ ## Exploration
53
+ {What options were considered, what was researched}
54
+
55
+ ## Decisions
56
+ {What was decided and why — include tradeoffs that were weighed}
57
+
58
+ ## Implications
59
+ {What this decision means for future work}
60
+ ```
61
+
62
+ ## Flow: Side Braindump (deprecated)
63
+
64
+ `/braindump side` has been replaced by the `/aside` skill. If invoked:
65
+ 1. Inform the user: "The side braindump is now `/aside`. Running it for you."
66
+ 2. Invoke the `/aside` skill with the user's text
67
+
68
+ ## Flow: No Parameter
69
+
70
+ 1. Analyze the current session: what discussion topics are in play?
71
+ 2. If one clear topic: suggest a name, ask to confirm
72
+ 3. If multiple topics: "I see discussion about {topic-1} and {topic-2}. Split into separate braindumps?"
73
+ 4. Proceed with named flow for each
74
+
75
+ ## Updating Existing Braindumps
76
+
77
+ When updating, rewrite as a fresh snapshot (coherent-revisions rule). The updated braindump should read as if written in one pass.
78
+
79
+ ## Key Differences from /handoff
80
+ - `/handoff` is structured around work state (branch, status, next steps)
81
+ - `/braindump` is structured around reasoning (context, exploration, decisions, implications)
82
+ - Use `/handoff` when you're tracking a workstream
83
+ - Use `/braindump` when you're capturing a discussion or decision
84
+
85
+ ## Auto-commit
86
+ Same as `/handoff` — commit the file alone:
87
+ ```bash
88
+ git add shared-context/{path-to-file}
89
+ git commit -m "braindump: {name}"
90
+ ```
91
+
92
+ ## Notes
93
+ - User-scoped is the default
94
+ - One topic, one file — don't mix unrelated ideas in one braindump
95
+ - Drive-by ideas now use `/aside` instead of `/braindump side`
96
+ - Auto-committing context files without user request is a workflow artifact — this intentionally bypasses the "do not commit unless asked" convention