@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,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
|