mirror of
https://github.com/obra/superpowers.git
synced 2026-06-09 17:02:07 +00:00
Compare commits
444 Commits
personal-s
...
codex/pri-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f62e538c7b | ||
|
|
d25618db58 | ||
|
|
3d6dc90c6d | ||
|
|
a152bb3932 | ||
|
|
3dfb376268 | ||
|
|
491df7360c | ||
|
|
9088f563e7 | ||
|
|
d4cf61b4c8 | ||
|
|
7f02ccd91b | ||
|
|
35e42a16ce | ||
|
|
58082d04f8 | ||
|
|
3dc0ea6876 | ||
|
|
0bf37499b4 | ||
|
|
f7c5312265 | ||
|
|
f5175fb31a | ||
|
|
45c7dc2cce | ||
|
|
39d29a6c28 | ||
|
|
f1d2005de3 | ||
|
|
c0a65f1b4d | ||
|
|
f10cddac0d | ||
|
|
371f41596b | ||
|
|
6f0adebe96 | ||
|
|
fd5b53cb85 | ||
|
|
be0357f98a | ||
|
|
3b412a3836 | ||
|
|
2e46e9590d | ||
|
|
58f821314d | ||
|
|
81472cc9e6 | ||
|
|
b4363df1b9 | ||
|
|
f2cbfbefeb | ||
|
|
e7a2d16476 | ||
|
|
6efe32c9e2 | ||
|
|
b55764852a | ||
|
|
9f42444ab1 | ||
|
|
99e4c656bf | ||
|
|
a5dd364e42 | ||
|
|
c4bbe651cb | ||
|
|
34c17aefb2 | ||
|
|
f9b088f7b3 | ||
|
|
bc25777c6a | ||
|
|
bcdd7fa24c | ||
|
|
6149f3635a | ||
|
|
777a9770d8 | ||
|
|
da283df058 | ||
|
|
a569527b89 | ||
|
|
ac1c715ffb | ||
|
|
8c8c5e87ce | ||
|
|
a5d36b1300 | ||
|
|
917e5f53b1 | ||
|
|
a6b1a1fa0c | ||
|
|
b7a8f76985 | ||
|
|
4b1b20f69f | ||
|
|
eeaf2ad15b | ||
|
|
dd237283db | ||
|
|
c0b417e409 | ||
|
|
1f20bef3f5 | ||
|
|
f0df5eca30 | ||
|
|
0a1124ba53 | ||
|
|
65d760f9c2 | ||
|
|
2d942f3b01 | ||
|
|
8b1669269c | ||
|
|
a2964d7a20 | ||
|
|
eafe962b18 | ||
|
|
9f04f06351 | ||
|
|
f076bd3431 | ||
|
|
9e3ed213a0 | ||
|
|
9e6e077d33 | ||
|
|
151cfb16a0 | ||
|
|
a1155f623f | ||
|
|
3f80f1c769 | ||
|
|
4ae1a3d6a6 | ||
|
|
e6221a48c5 | ||
|
|
4fd9aa2dd5 | ||
|
|
2b1bfe5db6 | ||
|
|
bd080e3cc8 | ||
|
|
eb2b44b23f | ||
|
|
80c0a45fcc | ||
|
|
c28b28ffbd | ||
|
|
33e9bea3cc | ||
|
|
74a0c004eb | ||
|
|
8ea39819ee | ||
|
|
764215331d | ||
|
|
eccd45305a | ||
|
|
fb4adab518 | ||
|
|
7e516434f2 | ||
|
|
8a0a5ca6a3 | ||
|
|
2d46da1b37 | ||
|
|
0002948041 | ||
|
|
3128a2c3cd | ||
|
|
f34ee479b7 | ||
|
|
3cee13e516 | ||
|
|
1128a721ca | ||
|
|
d1b5f578b0 | ||
|
|
61a64d7098 | ||
|
|
825a142aa3 | ||
|
|
bd537d817d | ||
|
|
24be2e8b7c | ||
|
|
a479e10050 | ||
|
|
a4c48714bc | ||
|
|
2c6a8a352d | ||
|
|
2b25774f31 | ||
|
|
f4b54a1717 | ||
|
|
911fa1d6c5 | ||
|
|
4e7c0842f8 | ||
|
|
689f27c968 | ||
|
|
537ec640fd | ||
|
|
c5e9538311 | ||
|
|
fd318b1b79 | ||
|
|
ea472dedf0 | ||
|
|
addfe8511a | ||
|
|
c6a2b1b576 | ||
|
|
d19703b0a1 | ||
|
|
6d21e9cc07 | ||
|
|
687a66183d | ||
|
|
363923f74a | ||
|
|
3188953b0c | ||
|
|
9ccce3bf07 | ||
|
|
b484bae134 | ||
|
|
ec99b7c4a4 | ||
|
|
263e3268f4 | ||
|
|
85cab6eff0 | ||
|
|
7619570679 | ||
|
|
8d9b94eb8d | ||
|
|
7f6380dd91 | ||
|
|
8d6d876424 | ||
|
|
9c98e01873 | ||
|
|
5ef73d25b7 | ||
|
|
920559aea7 | ||
|
|
9d2b886211 | ||
|
|
ec26561aaa | ||
|
|
f0a4538b31 | ||
|
|
f7b6107576 | ||
|
|
e02842e024 | ||
|
|
7446c842d8 | ||
|
|
5e2a89e985 | ||
|
|
d3c028e280 | ||
|
|
7f8edd9c12 | ||
|
|
81acbcd51e | ||
|
|
c070e6bd45 | ||
|
|
5f14c1aa29 | ||
|
|
bdbad07f02 | ||
|
|
419889b0d3 | ||
|
|
715e18e448 | ||
|
|
21a774e95c | ||
|
|
9df7269d73 | ||
|
|
5e5d353916 | ||
|
|
c5e6eaf411 | ||
|
|
bdd45c70ab | ||
|
|
ec3f7f1027 | ||
|
|
edbb62e50f | ||
|
|
33e55e60b2 | ||
|
|
74f2b1c96e | ||
|
|
991e9d4de9 | ||
|
|
133a0a80c6 | ||
|
|
57b346ddbc | ||
|
|
8c01ac8051 | ||
|
|
245d50ec37 | ||
|
|
aba2542f5e | ||
|
|
3bdd66eaa5 | ||
|
|
c3ecc1b9ba | ||
|
|
f3083e55b0 | ||
|
|
70244011d4 | ||
|
|
d48b14e5ac | ||
|
|
daa3fb2322 | ||
|
|
69eaf3cf34 | ||
|
|
582264a54a | ||
|
|
7b99c39c08 | ||
|
|
6c274dcc2a | ||
|
|
ee14caeadd | ||
|
|
5e51c3ee5a | ||
|
|
f57638a747 | ||
|
|
4180afb7bd | ||
|
|
e4226df22e | ||
|
|
866f2bdb47 | ||
|
|
3c220d0cc1 | ||
|
|
02b3d7c96d | ||
|
|
1c53f5deb6 | ||
|
|
a26cbaab2e | ||
|
|
b23c084070 | ||
|
|
aa3bb5fe16 | ||
|
|
3d245777f0 | ||
|
|
26d7cca61b | ||
|
|
ad716b8d1b | ||
|
|
e4a2375cb7 | ||
|
|
d2d6cf4852 | ||
|
|
54d9133d7a | ||
|
|
394cf85013 | ||
|
|
31bbbe2dbb | ||
|
|
5fbefbd0a9 | ||
|
|
a0b9ecce2b | ||
|
|
772ec9f834 | ||
|
|
e16d611eee | ||
|
|
b7cad76134 | ||
|
|
4c836817da | ||
|
|
7f2ee614b6 | ||
|
|
b97b5f228d | ||
|
|
93c8966cab | ||
|
|
19df3db59b | ||
|
|
f8cf545bc5 | ||
|
|
a98c5dfc9d | ||
|
|
a72e416979 | ||
|
|
8dd31c3da5 | ||
|
|
6a07692da1 | ||
|
|
0771fd7cd1 | ||
|
|
bcccc69271 | ||
|
|
3626ccc53e | ||
|
|
47d3df7acc | ||
|
|
d41f951c4a | ||
|
|
b4f56fec1b | ||
|
|
1143f9be3d | ||
|
|
6cc2d8c920 | ||
|
|
038abed026 | ||
|
|
961052e0f9 | ||
|
|
689e2a77fc | ||
|
|
5e0d2f8175 | ||
|
|
06b92f3682 | ||
|
|
9819209bba | ||
|
|
c7816ee2a6 | ||
|
|
b323e35805 | ||
|
|
bb2ff5d309 | ||
|
|
b63d485955 | ||
|
|
fa3f46d4e9 | ||
|
|
f8dbe7b196 | ||
|
|
93cf2ee84f | ||
|
|
1872f50b64 | ||
|
|
8904b7d9dc | ||
|
|
469a6d81eb | ||
|
|
4b6cef98ac | ||
|
|
03087b13b8 | ||
|
|
493ac18dfe | ||
|
|
35d4fbcd0b | ||
|
|
19c70afc99 | ||
|
|
405a025eea | ||
|
|
36fcd57626 | ||
|
|
3964d18670 | ||
|
|
a01a135fe1 | ||
|
|
ac471e69c2 | ||
|
|
a08f088968 | ||
|
|
b9e16498b9 | ||
|
|
f6d50c74b2 | ||
|
|
3dac35e0b3 | ||
|
|
131c1f189f | ||
|
|
9baedaa117 | ||
|
|
66a2dbd80a | ||
|
|
1455ac0631 | ||
|
|
e64ad670df | ||
|
|
c037dcbf4b | ||
|
|
a7a8c08c02 | ||
|
|
80643c2604 | ||
|
|
95c6e16336 | ||
|
|
612fbcdd01 | ||
|
|
70bf3a9e7f | ||
|
|
c2125b41e3 | ||
|
|
ae0ef56b44 | ||
|
|
030a222af1 | ||
|
|
2a19be0b78 | ||
|
|
cd83439bb2 | ||
|
|
ed06dcbe27 | ||
|
|
baef5241aa | ||
|
|
28ba020000 | ||
|
|
a9b94ae5d7 | ||
|
|
5845b52747 | ||
|
|
3f725ff0d4 | ||
|
|
b57c27d815 | ||
|
|
718ec45d33 | ||
|
|
c7caee5647 | ||
|
|
1f611f5c0c | ||
|
|
2d7408d0c6 | ||
|
|
9464a51779 | ||
|
|
faa65e7163 | ||
|
|
24ca8cd9d5 | ||
|
|
8fbeca830a | ||
|
|
67de772d7f | ||
|
|
cf72863792 | ||
|
|
baa23b16bb | ||
|
|
0aba33be1c | ||
|
|
06310d6f5f | ||
|
|
dc11a093c3 | ||
|
|
fa946ae465 | ||
|
|
51a171cd14 | ||
|
|
466332f698 | ||
|
|
87afde2390 | ||
|
|
2a6a40fe10 | ||
|
|
97ce1f8fe0 | ||
|
|
e7e50ac947 | ||
|
|
5faddc4087 | ||
|
|
d2900eae0c | ||
|
|
4ac67830d9 | ||
|
|
154d664373 | ||
|
|
8462c20cce | ||
|
|
a0c1e73a1d | ||
|
|
94e9e2596c | ||
|
|
1b878e4fa1 | ||
|
|
515c86fd07 | ||
|
|
e416a0e105 | ||
|
|
a08f7de64b | ||
|
|
207a23e4d5 | ||
|
|
c35c5f637e | ||
|
|
9a01a0dcc1 | ||
|
|
8c7826c34d | ||
|
|
4ae8fc8713 | ||
|
|
94089bdce5 | ||
|
|
9297fd24d5 | ||
|
|
d0806ba5af | ||
|
|
6ecd72c5bf | ||
|
|
f600f969f5 | ||
|
|
4ef2f9185d | ||
|
|
8a626e75f3 | ||
|
|
014b11cf57 | ||
|
|
3f73365155 | ||
|
|
9f33fc95bf | ||
|
|
9220bb62af | ||
|
|
f5a4002daf | ||
|
|
9dcf5eaabe | ||
|
|
f3d6c331a1 | ||
|
|
b0ba2cf15a | ||
|
|
cbbd8d2edf | ||
|
|
9cd6c52acc | ||
|
|
107859a748 | ||
|
|
5f5b789e3e | ||
|
|
7db10cf540 | ||
|
|
67ce04077b | ||
|
|
d749c620b5 | ||
|
|
aa1fe045c6 | ||
|
|
368674419a | ||
|
|
c940d84f3d | ||
|
|
8e7f90a954 | ||
|
|
d92de28150 | ||
|
|
fa53c8f925 | ||
|
|
d3e89e8719 | ||
|
|
b746f7587b | ||
|
|
4eab16380b | ||
|
|
7ffff61965 | ||
|
|
fbd419e394 | ||
|
|
a131267d7c | ||
|
|
26c152b37e | ||
|
|
6ae8ef4733 | ||
|
|
1aa29ad52b | ||
|
|
6847cf4cfc | ||
|
|
425b40359c | ||
|
|
9e5ba91be6 | ||
|
|
4abd4df171 | ||
|
|
5dd31b90ee | ||
|
|
0fbdfa3c4a | ||
|
|
a23eead918 | ||
|
|
4594596e38 | ||
|
|
536fd24603 | ||
|
|
85effaaedb | ||
|
|
6cec629cf3 | ||
|
|
5dd8871a1b | ||
|
|
84283dfc05 | ||
|
|
d90334e030 | ||
|
|
9a9618489d | ||
|
|
02c87670de | ||
|
|
b187e75a1e | ||
|
|
8674dc0868 | ||
|
|
42d44ceaf9 | ||
|
|
d46dddd32c | ||
|
|
b1fa6a1a46 | ||
|
|
8e38ab86dc | ||
|
|
31fd764285 | ||
|
|
e3208f1d93 | ||
|
|
1d21ee842d | ||
|
|
aa8c6b4fd0 | ||
|
|
22f57e7cb0 | ||
|
|
da9f4f1edd | ||
|
|
5831c4dfea | ||
|
|
26487902f8 | ||
|
|
19e2997334 | ||
|
|
c88b0d674f | ||
|
|
17bbc2b130 | ||
|
|
f6ee98a41a | ||
|
|
e3d881b7b6 | ||
|
|
184a4c464e | ||
|
|
7fc125e5e9 | ||
|
|
79436abffa | ||
|
|
9597f088c4 | ||
|
|
7ce751294e | ||
|
|
1ef5758621 | ||
|
|
accb1231fc | ||
|
|
22ec50318e | ||
|
|
0bc5a5989d | ||
|
|
e59cf658c5 | ||
|
|
4d8db812ae | ||
|
|
141953a4be | ||
|
|
9e82a51f34 | ||
|
|
a681cfb024 | ||
|
|
48410c7f19 | ||
|
|
2076e49d18 | ||
|
|
d6ca4d6213 | ||
|
|
10afd4eacc | ||
|
|
451db491b8 | ||
|
|
deb45f5cad | ||
|
|
32659fe0f3 | ||
|
|
9c9547cc04 | ||
|
|
84123b8450 | ||
|
|
9db0a61515 | ||
|
|
e548c5d628 | ||
|
|
0fdea80a7d | ||
|
|
d6ac9a8b5d | ||
|
|
809956f3bf | ||
|
|
02429d7e26 | ||
|
|
51e14692c9 | ||
|
|
b331a79267 | ||
|
|
2d89035187 | ||
|
|
a3bbf5f009 | ||
|
|
927512f1ab | ||
|
|
998a3d545f | ||
|
|
b6f6ce12a6 | ||
|
|
bde691cedf | ||
|
|
9eefffc541 | ||
|
|
488139d6d1 | ||
|
|
0f30fd2989 | ||
|
|
5c19a391d9 | ||
|
|
a39561bd41 | ||
|
|
f6327a0051 | ||
|
|
b063888520 | ||
|
|
d5e2fe7876 | ||
|
|
d1f42e5462 | ||
|
|
60cb0cd0ca | ||
|
|
661e6292c6 | ||
|
|
562428563d | ||
|
|
e39929b541 | ||
|
|
a1a1c3119d | ||
|
|
5b0b086829 | ||
|
|
400ac0f436 | ||
|
|
eea083623e | ||
|
|
87f04224ef | ||
|
|
015a07f7d7 | ||
|
|
33622d710e | ||
|
|
2227e3151e | ||
|
|
cd83c0aac8 | ||
|
|
7f3a1e7428 | ||
|
|
7421ddfc2f | ||
|
|
35cec21e96 | ||
|
|
f801cd616d | ||
|
|
32eb6ed221 | ||
|
|
e8c960641e | ||
|
|
3c26a7f875 | ||
|
|
ec15b59c89 | ||
|
|
e901534e9e | ||
|
|
d3dd95027e | ||
|
|
958527f07e | ||
|
|
dedae3a786 |
20
.claude-plugin/marketplace.json
Normal file
20
.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "superpowers-dev",
|
||||
"description": "Development marketplace for Superpowers core skills library",
|
||||
"owner": {
|
||||
"name": "Jesse Vincent",
|
||||
"email": "jesse@fsck.com"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "superpowers",
|
||||
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "5.1.0",
|
||||
"source": "./",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
"email": "jesse@fsck.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "1.0.0",
|
||||
"version": "5.1.0",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
"email": "jesse@fsck.com"
|
||||
@@ -9,6 +9,12 @@
|
||||
"homepage": "https://github.com/obra/superpowers",
|
||||
"repository": "https://github.com/obra/superpowers",
|
||||
"license": "MIT",
|
||||
"keywords": ["skills", "tdd", "debugging", "collaboration", "best-practices", "workflows"],
|
||||
"category": "productivity"
|
||||
"keywords": [
|
||||
"skills",
|
||||
"tdd",
|
||||
"debugging",
|
||||
"collaboration",
|
||||
"best-practices",
|
||||
"workflows"
|
||||
]
|
||||
}
|
||||
|
||||
47
.codex-plugin/plugin.json
Normal file
47
.codex-plugin/plugin.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"version": "5.1.0",
|
||||
"description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
"email": "jesse@fsck.com",
|
||||
"url": "https://github.com/obra"
|
||||
},
|
||||
"homepage": "https://github.com/obra/superpowers",
|
||||
"repository": "https://github.com/obra/superpowers",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"brainstorming",
|
||||
"subagent-driven-development",
|
||||
"skills",
|
||||
"planning",
|
||||
"tdd",
|
||||
"debugging",
|
||||
"code-review",
|
||||
"workflow"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"interface": {
|
||||
"displayName": "Superpowers",
|
||||
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
|
||||
"longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.",
|
||||
"developerName": "Jesse Vincent",
|
||||
"category": "Coding",
|
||||
"capabilities": [
|
||||
"Interactive",
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"defaultPrompt": [
|
||||
"I've got an idea for something I'd like to build.",
|
||||
"Let's add a feature to this project."
|
||||
],
|
||||
"websiteURL": "https://github.com/obra/superpowers",
|
||||
"privacyPolicyURL": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement",
|
||||
"termsOfServiceURL": "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service",
|
||||
"brandColor": "#F59E0B",
|
||||
"composerIcon": "./assets/superpowers-small.svg",
|
||||
"logo": "./assets/app-icon.png",
|
||||
"screenshots": []
|
||||
}
|
||||
}
|
||||
23
.cursor-plugin/plugin.json
Normal file
23
.cursor-plugin/plugin.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"displayName": "Superpowers",
|
||||
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "5.1.0",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
"email": "jesse@fsck.com"
|
||||
},
|
||||
"homepage": "https://github.com/obra/superpowers",
|
||||
"repository": "https://github.com/obra/superpowers",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"skills",
|
||||
"tdd",
|
||||
"debugging",
|
||||
"collaboration",
|
||||
"best-practices",
|
||||
"workflows"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"hooks": "./hooks/hooks-cursor.json"
|
||||
}
|
||||
18
.gitattributes
vendored
Normal file
18
.gitattributes
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Ensure shell scripts always have LF line endings
|
||||
*.sh text eol=lf
|
||||
hooks/session-start text eol=lf
|
||||
|
||||
# Ensure the polyglot wrapper keeps LF (it's parsed by both cmd and bash)
|
||||
*.cmd text eol=lf
|
||||
|
||||
# Common text files
|
||||
*.md text eol=lf
|
||||
*.json text eol=lf
|
||||
*.js text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.ts text eol=lf
|
||||
|
||||
# Explicitly mark binary files
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.gif binary
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [obra]
|
||||
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Something isn't working as expected
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!--
|
||||
BEFORE FILING: Search open AND closed issues. The Windows SessionStart
|
||||
hook alone has been reported 29 times. If your issue already exists,
|
||||
add a comment or reaction to the existing one instead.
|
||||
-->
|
||||
|
||||
- [ ] I searched existing issues and this is not a duplicate
|
||||
|
||||
## Environment
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Superpowers version | |
|
||||
| Harness (Claude Code, Cursor, etc.) | |
|
||||
| Harness version | |
|
||||
| Model | |
|
||||
| OS + shell | |
|
||||
|
||||
## Is this a Superpowers issue or a platform issue?
|
||||
<!-- Superpowers is a plugin. Some reported "bugs" are actually issues
|
||||
in the underlying platform or model. If you're not sure, try
|
||||
reproducing without Superpowers installed.
|
||||
|
||||
If the problem persists without Superpowers, file the issue with
|
||||
your platform instead. -->
|
||||
|
||||
- [ ] I confirmed this issue does not occur without Superpowers installed
|
||||
|
||||
## What happened?
|
||||
<!-- Be specific. "It doesn't work" is not a bug report. -->
|
||||
|
||||
## Steps to reproduce
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Expected behavior
|
||||
<!-- What should have happened? -->
|
||||
|
||||
## Actual behavior
|
||||
<!-- What happened instead? -->
|
||||
|
||||
## Debug log or conversation transcript
|
||||
<!-- A debug log or conversation transcript showing the issue is the
|
||||
single most helpful thing you can include. Without one, we're
|
||||
guessing. Screenshots of error output are also useful. -->
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Help
|
||||
url: https://discord.gg/35wsABTejz
|
||||
about: For usage questions, troubleshooting help, and general discussion, please visit our Discord instead of opening an issue.
|
||||
34
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Propose a change or addition to Superpowers
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
<!--
|
||||
BEFORE FILING: Search open AND closed issues. Many features have been
|
||||
requested before — some were implemented differently, some are in
|
||||
progress, and some were intentionally declined.
|
||||
-->
|
||||
|
||||
- [ ] I searched existing issues and this has not been proposed before
|
||||
|
||||
## What problem does this solve?
|
||||
<!-- Describe the problem from your own experience. What were you doing,
|
||||
what went wrong or was missing, and why did it matter?
|
||||
|
||||
"It would be cool if..." is not a problem statement. -->
|
||||
|
||||
## Proposed solution
|
||||
<!-- What specifically do you want to happen? Be concrete. -->
|
||||
|
||||
## What alternatives did you consider?
|
||||
<!-- What other approaches could solve the same problem? Why is your
|
||||
proposal better? -->
|
||||
|
||||
## Is this appropriate for core Superpowers?
|
||||
<!-- Would this benefit someone working on a completely different kind
|
||||
of project? If this is specific to your domain, workflow, or a
|
||||
third-party tool, it may belong as its own plugin instead. -->
|
||||
|
||||
## Context
|
||||
<!-- Optional: version info, harness, model, workflow where you hit this. -->
|
||||
23
.github/ISSUE_TEMPLATE/platform_support.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/platform_support.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: IDE / Platform Support Request
|
||||
about: Request support for a new IDE, editor, or AI coding tool
|
||||
labels: platform-support
|
||||
---
|
||||
|
||||
<!--
|
||||
BEFORE FILING: Search existing issues — your IDE may already be
|
||||
requested or discussed.
|
||||
-->
|
||||
|
||||
- [ ] I searched existing issues for this IDE/platform
|
||||
|
||||
## Which IDE or platform?
|
||||
<!-- Name and link -->
|
||||
|
||||
## Does this tool have a plugin or extension system?
|
||||
<!-- If yes, link to the docs. If no, explain how third-party
|
||||
integrations typically work with this tool. -->
|
||||
|
||||
## Have you tried manual installation?
|
||||
<!-- Many tools work with Superpowers through manual setup even without
|
||||
official support. Did you try? What happened? -->
|
||||
126
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
126
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
<!--
|
||||
BEFORE SUBMITTING: Read every word of this template. PRs that leave
|
||||
sections blank, contain multiple unrelated changes, or show no evidence
|
||||
of human involvement will be closed without review.
|
||||
-->
|
||||
|
||||
## What problem are you trying to solve?
|
||||
<!-- Describe the specific problem you encountered. If this was a session
|
||||
issue, include: what you were doing, what went wrong, the model's
|
||||
exact failure mode, and ideally a transcript or session log.
|
||||
|
||||
"Improving" something is not a problem statement. What broke? What
|
||||
failed? What was the user experience that motivated this? -->
|
||||
|
||||
## What does this PR change?
|
||||
<!-- 1-3 sentences. What, not why — the "why" belongs above. -->
|
||||
|
||||
## Is this change appropriate for the core library?
|
||||
<!-- Superpowers core contains general-purpose skills and infrastructure
|
||||
that benefit all users. Ask yourself:
|
||||
|
||||
- Would this be useful to someone working on a completely different
|
||||
kind of project than yours?
|
||||
- Is this project-specific, team-specific, or tool-specific?
|
||||
- Does this integrate or promote a third-party service?
|
||||
|
||||
If your change is a new skill for a specific domain, workflow tool,
|
||||
or third-party integration, it belongs in its own plugin — not here.
|
||||
See the plugin development docs for how to publish it separately. -->
|
||||
|
||||
## What alternatives did you consider?
|
||||
<!-- What other approaches did you try or evaluate before landing on this
|
||||
one? Why were they worse? If you didn't consider alternatives, say so
|
||||
— but know that's a red flag. -->
|
||||
|
||||
## Does this PR contain multiple unrelated changes?
|
||||
<!-- If yes: stop. Split it into separate PRs. Bundled PRs will be closed.
|
||||
If you believe the changes are related, explain the dependency. -->
|
||||
|
||||
## Existing PRs
|
||||
- [ ] I have reviewed all open AND closed PRs for duplicates or prior art
|
||||
- Related PRs: <!-- #number, #number, or "none found" -->
|
||||
|
||||
<!-- If a related closed PR exists, explain what's different about your
|
||||
approach and why it should succeed where the other didn't. -->
|
||||
|
||||
## Environment tested
|
||||
|
||||
| Harness (e.g. Claude Code, Cursor) | Harness version | Model | Model version/ID |
|
||||
|-------------------------------------|-----------------|-------|------------------|
|
||||
| | | | |
|
||||
|
||||
## New harness support (required if this PR adds a new harness)
|
||||
|
||||
<!-- If this PR adds support for a new harness (IDE, CLI tool, agent
|
||||
runner), you MUST include a session transcript proving the
|
||||
integration actually works.
|
||||
|
||||
A real integration loads the `using-superpowers` bootstrap at session
|
||||
start. The bootstrap is what causes skills to auto-trigger. Without
|
||||
it, the skills are dead weight — present on disk but never invoked
|
||||
at the right moments.
|
||||
|
||||
ACCEPTANCE TEST: Open a clean session in the new harness and send
|
||||
exactly this user message:
|
||||
|
||||
Let's make a react todo list
|
||||
|
||||
A working integration auto-triggers the `brainstorming` skill before
|
||||
any code is written. Paste the complete transcript below.
|
||||
|
||||
These are NOT real integrations and PRs that ship them will be closed:
|
||||
|
||||
- Manually copying skill files into the harness
|
||||
- Wrapping with `npx skills` or similar at-runtime shims
|
||||
- Anything that requires the user to opt in to skills per-session
|
||||
- Anything where brainstorming does not auto-trigger on the test above
|
||||
|
||||
If you are not sure whether your integration loads the bootstrap at
|
||||
session start, it does not.
|
||||
-->
|
||||
|
||||
<details>
|
||||
<summary>Clean-session transcript for "Let's make a react todo list"</summary>
|
||||
|
||||
```
|
||||
paste the complete transcript here
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Evaluation
|
||||
- What was the initial prompt you (or your human partner) used to start
|
||||
the session that led to this change?
|
||||
- How many eval sessions did you run AFTER making the change?
|
||||
- How did outcomes change compared to before the change?
|
||||
|
||||
<!-- "It works" is not evaluation. Describe the before/after difference
|
||||
you observed across multiple sessions. -->
|
||||
|
||||
## Rigor
|
||||
|
||||
- [ ] If this is a skills change: I used `superpowers:writing-skills` and
|
||||
completed adversarial pressure testing (paste results below)
|
||||
- [ ] This change was tested adversarially, not just on the happy path
|
||||
- [ ] I did not modify carefully-tuned content (Red Flags table,
|
||||
rationalizations, "human partner" language) without extensive evals
|
||||
showing the change is an improvement
|
||||
|
||||
<!-- If you changed wording in skills that shape agent behavior, show your
|
||||
eval methodology and results. These are not prose — they are code. -->
|
||||
|
||||
## Human review
|
||||
- [ ] A human has reviewed the COMPLETE proposed diff before submission
|
||||
|
||||
<!--
|
||||
STOP. If the checkbox above is not checked, do not submit this PR.
|
||||
|
||||
PRs will be closed without review if they:
|
||||
- Show no evidence of human involvement
|
||||
- Contain multiple unrelated changes
|
||||
- Promote or integrate third-party services or tools
|
||||
- Submit project-specific or personal configuration as core changes
|
||||
- Leave required sections blank or use placeholder text
|
||||
- Modify behavior-shaping content without eval evidence
|
||||
-->
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,2 +1,13 @@
|
||||
.worktrees/
|
||||
.private-journal/
|
||||
.claude/
|
||||
.DS_Store
|
||||
node_modules/
|
||||
inspo
|
||||
triage/
|
||||
|
||||
# Eval harness — drill ships its own gitignore at evals/.gitignore;
|
||||
# these are belt-and-suspenders entries for tools that don't recurse.
|
||||
evals/results/
|
||||
evals/.venv/
|
||||
evals/.env
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "evals"]
|
||||
path = evals
|
||||
url = git@github.com:prime-radiant-inc/superpowers-evals.git
|
||||
110
.opencode/INSTALL.md
Normal file
110
.opencode/INSTALL.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Installing Superpowers for OpenCode
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [OpenCode.ai](https://opencode.ai) installed
|
||||
|
||||
## Installation
|
||||
|
||||
Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
|
||||
}
|
||||
```
|
||||
|
||||
Restart OpenCode. The plugin installs through OpenCode's plugin manager and
|
||||
registers all skills.
|
||||
|
||||
Verify by asking: "Tell me about your superpowers"
|
||||
|
||||
OpenCode uses its own plugin install. If you also use Claude Code, Codex, or
|
||||
another harness, install Superpowers separately for each one.
|
||||
|
||||
## Migrating from the old symlink-based install
|
||||
|
||||
If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
|
||||
|
||||
```bash
|
||||
# Remove old symlinks
|
||||
rm -f ~/.config/opencode/plugins/superpowers.js
|
||||
rm -rf ~/.config/opencode/skills/superpowers
|
||||
|
||||
# Optionally remove the cloned repo
|
||||
rm -rf ~/.config/opencode/superpowers
|
||||
|
||||
# Remove skills.paths from opencode.json if you added one for superpowers
|
||||
```
|
||||
|
||||
Then follow the installation steps above.
|
||||
|
||||
## Usage
|
||||
|
||||
Use OpenCode's native `skill` tool:
|
||||
|
||||
```
|
||||
use skill tool to list skills
|
||||
use skill tool to load superpowers/brainstorming
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
OpenCode installs Superpowers through a git-backed package spec. Some OpenCode
|
||||
and Bun versions pin that resolved git dependency in a lockfile or cache, so a
|
||||
restart may not pick up the newest Superpowers commit. If updates do not appear,
|
||||
clear OpenCode's package cache or reinstall the plugin.
|
||||
|
||||
To pin a specific version:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin not loading
|
||||
|
||||
1. Check logs: `opencode run --print-logs "hello" 2>&1 | grep -i superpowers`
|
||||
2. Verify the plugin line in your `opencode.json`
|
||||
3. Make sure you're running a recent version of OpenCode
|
||||
|
||||
### Windows install issues
|
||||
|
||||
Some Windows OpenCode builds have upstream installer issues with git-backed
|
||||
plugin specs, including cache paths for `git+https` URLs and Bun not finding
|
||||
`git.exe` even when it works in a normal terminal. If OpenCode cannot install
|
||||
the plugin, try installing with system npm and pointing OpenCode at the local
|
||||
package:
|
||||
|
||||
```powershell
|
||||
npm install superpowers@git+https://github.com/obra/superpowers.git --prefix "$HOME\.config\opencode"
|
||||
```
|
||||
|
||||
Then use the installed package path in `opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": ["~/.config/opencode/node_modules/superpowers"]
|
||||
}
|
||||
```
|
||||
|
||||
### Skills not found
|
||||
|
||||
1. Use `skill` tool to list what's discovered
|
||||
2. Check that the plugin is loading (see above)
|
||||
|
||||
### Tool mapping
|
||||
|
||||
When skills reference Claude Code tools:
|
||||
- `TodoWrite` → `todowrite`
|
||||
- `Task` with subagents → `@mention` syntax
|
||||
- `Skill` tool → OpenCode's native `skill` tool
|
||||
- File operations → your native tools
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Report issues: https://github.com/obra/superpowers/issues
|
||||
- Full documentation: https://github.com/obra/superpowers/blob/main/docs/README.opencode.md
|
||||
135
.opencode/plugins/superpowers.js
Normal file
135
.opencode/plugins/superpowers.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Superpowers plugin for OpenCode.ai
|
||||
*
|
||||
* Injects superpowers bootstrap context via system prompt transform.
|
||||
* Auto-registers skills directory via config hook (no symlinks needed).
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Simple frontmatter extraction (avoid dependency on skills-core for bootstrap)
|
||||
const extractAndStripFrontmatter = (content) => {
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
if (!match) return { frontmatter: {}, content };
|
||||
|
||||
const frontmatterStr = match[1];
|
||||
const body = match[2];
|
||||
const frontmatter = {};
|
||||
|
||||
for (const line of frontmatterStr.split('\n')) {
|
||||
const colonIdx = line.indexOf(':');
|
||||
if (colonIdx > 0) {
|
||||
const key = line.slice(0, colonIdx).trim();
|
||||
const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
|
||||
frontmatter[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { frontmatter, content: body };
|
||||
};
|
||||
|
||||
// Normalize a path: trim whitespace, expand ~, resolve to absolute
|
||||
const normalizePath = (p, homeDir) => {
|
||||
if (!p || typeof p !== 'string') return null;
|
||||
let normalized = p.trim();
|
||||
if (!normalized) return null;
|
||||
if (normalized.startsWith('~/')) {
|
||||
normalized = path.join(homeDir, normalized.slice(2));
|
||||
} else if (normalized === '~') {
|
||||
normalized = homeDir;
|
||||
}
|
||||
return path.resolve(normalized);
|
||||
};
|
||||
|
||||
// Module-level cache for bootstrap content.
|
||||
// The SKILL.md file does not change during a session, so reading + parsing it
|
||||
// once eliminates redundant fs.existsSync + fs.readFileSync + regex work on
|
||||
// every agent step. See #1202 for the full analysis.
|
||||
let _bootstrapCache = undefined; // undefined = not yet loaded, null = file missing
|
||||
|
||||
export const SuperpowersPlugin = async ({ client, directory }) => {
|
||||
const homeDir = os.homedir();
|
||||
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
|
||||
const envConfigDir = normalizePath(process.env.OPENCODE_CONFIG_DIR, homeDir);
|
||||
const configDir = envConfigDir || path.join(homeDir, '.config/opencode');
|
||||
|
||||
// Helper to generate bootstrap content (cached after first call)
|
||||
const getBootstrapContent = () => {
|
||||
// Return cached result on subsequent calls
|
||||
if (_bootstrapCache !== undefined) return _bootstrapCache;
|
||||
|
||||
// Try to load using-superpowers skill
|
||||
const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md');
|
||||
if (!fs.existsSync(skillPath)) {
|
||||
_bootstrapCache = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const fullContent = fs.readFileSync(skillPath, 'utf8');
|
||||
const { content } = extractAndStripFrontmatter(fullContent);
|
||||
|
||||
const toolMapping = `**Tool Mapping for OpenCode:**
|
||||
When skills reference tools you don't have, substitute OpenCode equivalents:
|
||||
- \`TodoWrite\` → \`todowrite\`
|
||||
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
|
||||
- \`Skill\` tool → OpenCode's native \`skill\` tool
|
||||
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
|
||||
|
||||
Use OpenCode's native \`skill\` tool to list and load skills.`;
|
||||
|
||||
_bootstrapCache = `<EXTREMELY_IMPORTANT>
|
||||
You have superpowers.
|
||||
|
||||
**IMPORTANT: The using-superpowers skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the skill tool to load "using-superpowers" again - that would be redundant.**
|
||||
|
||||
${content}
|
||||
|
||||
${toolMapping}
|
||||
</EXTREMELY_IMPORTANT>`;
|
||||
|
||||
return _bootstrapCache;
|
||||
};
|
||||
|
||||
return {
|
||||
// Inject skills path into live config so OpenCode discovers superpowers skills
|
||||
// without requiring manual symlinks or config file edits.
|
||||
// This works because Config.get() returns a cached singleton — modifications
|
||||
// here are visible when skills are lazily discovered later.
|
||||
config: async (config) => {
|
||||
config.skills = config.skills || {};
|
||||
config.skills.paths = config.skills.paths || [];
|
||||
if (!config.skills.paths.includes(superpowersSkillsDir)) {
|
||||
config.skills.paths.push(superpowersSkillsDir);
|
||||
}
|
||||
},
|
||||
|
||||
// Inject bootstrap into the first user message of each session.
|
||||
// Using a user message instead of a system message avoids:
|
||||
// 1. Token bloat from system messages repeated every turn (#750)
|
||||
// 2. Multiple system messages breaking Qwen and other models (#894)
|
||||
//
|
||||
// The hook fires on every agent step (not just every turn) because
|
||||
// opencode's prompt.ts reloads messages from DB each step. Fresh message
|
||||
// arrays may need injection again, so getBootstrapContent() must not do
|
||||
// repeated disk work.
|
||||
'experimental.chat.messages.transform': async (_input, output) => {
|
||||
const bootstrap = getBootstrapContent();
|
||||
if (!bootstrap || !output.messages.length) return;
|
||||
const firstUser = output.messages.find(m => m.info.role === 'user');
|
||||
if (!firstUser || !firstUser.parts.length) return;
|
||||
|
||||
// Guard: skip if first user message already contains bootstrap.
|
||||
// This prevents double injection when OpenCode passes an already
|
||||
// transformed in-memory message array through the hook again.
|
||||
if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('EXTREMELY_IMPORTANT'))) return;
|
||||
|
||||
const ref = firstUser.parts[0];
|
||||
firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap });
|
||||
}
|
||||
};
|
||||
};
|
||||
21
.pre-commit-config.yaml
Normal file
21
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: evals-ruff-check
|
||||
name: evals ruff check
|
||||
entry: uv --project evals run ruff check
|
||||
language: system
|
||||
files: ^evals/.*\.py$
|
||||
|
||||
- id: evals-ruff-format-check
|
||||
name: evals ruff format --check
|
||||
entry: uv --project evals run ruff format --check
|
||||
language: system
|
||||
files: ^evals/.*\.py$
|
||||
|
||||
- id: evals-ty-check
|
||||
name: evals ty check
|
||||
entry: uv --directory evals run ty check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
files: ^evals/.*\.py$
|
||||
20
.version-bump.json
Normal file
20
.version-bump.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"files": [
|
||||
{ "path": "package.json", "field": "version" },
|
||||
{ "path": ".claude-plugin/plugin.json", "field": "version" },
|
||||
{ "path": ".cursor-plugin/plugin.json", "field": "version" },
|
||||
{ "path": ".codex-plugin/plugin.json", "field": "version" },
|
||||
{ "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" },
|
||||
{ "path": "gemini-extension.json", "field": "version" }
|
||||
],
|
||||
"audit": {
|
||||
"exclude": [
|
||||
"CHANGELOG.md",
|
||||
"RELEASE-NOTES.md",
|
||||
"node_modules",
|
||||
".git",
|
||||
".version-bump.json",
|
||||
"scripts/bump-version.sh"
|
||||
]
|
||||
}
|
||||
}
|
||||
110
CLAUDE.md
Normal file
110
CLAUDE.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Superpowers — Contributor Guidelines
|
||||
|
||||
## If You Are an AI Agent
|
||||
|
||||
Stop. Read this section before doing anything.
|
||||
|
||||
This repo has a 94% PR rejection rate. Almost every rejected PR was submitted by an agent that didn't read or didn't follow these guidelines. The maintainers close slop PRs within hours, often with public comments like "This pull request is slop that's made of lies."
|
||||
|
||||
**Your job is to protect your human partner from that outcome.** Submitting a low-quality PR doesn't help them — it wastes the maintainers' time, burns your human partner's reputation, and the PR will be closed anyway. That is not being helpful. That is being a tool of embarrassment.
|
||||
|
||||
Before you open a PR against this repo, you MUST:
|
||||
|
||||
1. **Read the entire PR template** at `.github/PULL_REQUEST_TEMPLATE.md` and fill in every section with real, specific answers. Not summaries. Not placeholders.
|
||||
2. **Search for existing PRs** — open AND closed — that address the same problem. If duplicates exist, STOP and tell your human partner. Do not open another duplicate.
|
||||
3. **Verify this is a real problem.** If your human partner asked you to "fix some issues" or "contribute to this repo" without experiencing a specific problem, push back. Ask them what broke, what failed, what the user experience was.
|
||||
4. **Confirm the change belongs in core.** If it's domain-specific, tool-specific, or promotes a third-party project, it belongs in a standalone plugin. Tell your human partner.
|
||||
5. **Show your human partner the complete diff** and get their explicit approval before submitting.
|
||||
|
||||
If any of these checks fail, do not open the PR. Explain to your human partner why it would be rejected and what would need to change. They will thank you for saving them the embarrassment.
|
||||
|
||||
## Pull Request Requirements
|
||||
|
||||
**Every PR must fully complete the PR template.** No section may be left blank or filled with placeholder text. PRs that skip sections will be closed without review.
|
||||
|
||||
**Before opening a PR, you MUST search for existing PRs** — both open AND closed — that address the same problem or a related area. Reference what you found in the "Existing PRs" section. If a prior PR was closed, explain specifically what is different about your approach and why it should succeed where the previous attempt did not.
|
||||
|
||||
**PRs that show no evidence of human involvement will be closed.** A human must review the complete proposed diff before submission.
|
||||
|
||||
## What We Will Not Accept
|
||||
|
||||
### Third-party dependencies
|
||||
|
||||
PRs that add optional or required dependencies on third-party projects will not be accepted unless they are adding support for a new harness (e.g., a new IDE or CLI tool). Superpowers is a zero-dependency plugin by design. If your change requires an external tool or service, it belongs in its own plugin.
|
||||
|
||||
### "Compliance" changes to skills
|
||||
|
||||
Our internal skill philosophy differs from Anthropic's published guidance on writing skills. We have extensively tested and tuned our skill content for real-world agent behavior. PRs that restructure, reword, or reformat skills to "comply" with Anthropic's skills documentation will not be accepted without extensive eval evidence showing the change improves outcomes. The bar for modifying behavior-shaping content is very high.
|
||||
|
||||
### Project-specific or personal configuration
|
||||
|
||||
Skills, hooks, or configuration that only benefit a specific project, team, domain, or workflow do not belong in core. Publish these as a separate plugin.
|
||||
|
||||
### Bulk or spray-and-pray PRs
|
||||
|
||||
Do not trawl the issue tracker and open PRs for multiple issues in a single session. Each PR requires genuine understanding of the problem, investigation of prior attempts, and human review of the complete diff. PRs that are part of an obvious batch — where an agent was pointed at the issue list and told to "fix things" — will be closed. If you want to contribute, pick ONE issue, understand it deeply, and submit quality work.
|
||||
|
||||
### Speculative or theoretical fixes
|
||||
|
||||
Every PR must solve a real problem that someone actually experienced. "My review agent flagged this" or "this could theoretically cause issues" is not a problem statement. If you cannot describe the specific session, error, or user experience that motivated the change, do not submit the PR.
|
||||
|
||||
### Domain-specific skills
|
||||
|
||||
Superpowers core contains general-purpose skills that benefit all users regardless of their project. Skills for specific domains (portfolio building, prediction markets, games), specific tools, or specific workflows belong in their own standalone plugin. Ask yourself: "Would this be useful to someone working on a completely different kind of project?" If not, publish it separately.
|
||||
|
||||
### Fork-specific changes
|
||||
|
||||
If you maintain a fork with customizations, do not open PRs to sync your fork or push fork-specific changes upstream. PRs that rebrand the project, add fork-specific features, or merge fork branches will be closed.
|
||||
|
||||
### Fabricated content
|
||||
|
||||
PRs containing invented claims, fabricated problem descriptions, or hallucinated functionality will be closed immediately. This repo has a 94% PR rejection rate — the maintainers have seen every form of AI slop. They will notice.
|
||||
|
||||
### Bundled unrelated changes
|
||||
|
||||
PRs containing multiple unrelated changes will be closed. Split them into separate PRs.
|
||||
|
||||
## New Harness Support
|
||||
|
||||
If your PR adds support for a new harness (IDE, CLI tool, agent runner), you MUST include a session transcript proving the integration works end-to-end.
|
||||
|
||||
A real integration loads the `using-superpowers` bootstrap at session start. The bootstrap is what causes skills to auto-trigger at the right moments. Without it, the skills are dead weight — present on disk but never invoked.
|
||||
|
||||
**The acceptance test.** Open a clean session in the new harness and send exactly this user message:
|
||||
|
||||
> Let's make a react todo list
|
||||
|
||||
A working integration auto-triggers the `brainstorming` skill before any code is written. Paste the complete transcript in the PR.
|
||||
|
||||
**These are not real integrations and will be closed:**
|
||||
|
||||
- Manually copying skill files into the harness
|
||||
- Wrapping with `npx skills` or similar at-runtime shims
|
||||
- Anything that requires the user to opt in to skills per-session
|
||||
- Anything where `brainstorming` does not auto-trigger on the acceptance test above
|
||||
|
||||
If you are not sure whether your integration loads the bootstrap at session start, it does not.
|
||||
|
||||
## Skill Changes Require Evaluation
|
||||
|
||||
Skills are not prose — they are code that shapes agent behavior. If you modify skill content:
|
||||
|
||||
- Use `superpowers:writing-skills` to develop and test changes
|
||||
- Run adversarial pressure testing across multiple sessions
|
||||
- Show before/after eval results in your PR
|
||||
- Do not modify carefully-tuned content (Red Flags tables, rationalization lists, "human partner" language) without evidence the change is an improvement
|
||||
|
||||
## Eval harness
|
||||
|
||||
Skill-behavior evals live in the `evals/` submodule — after cloning, run `git submodule update --init evals`, then see `evals/README.md`. Drill (the harness) drives real tmux sessions of Claude Code / Codex / Gemini CLI and judges skill compliance with an LLM verifier. Plugin-infrastructure tests still live at `tests/`.
|
||||
|
||||
## Understand the Project Before Contributing
|
||||
|
||||
Before proposing changes to skill design, workflow philosophy, or architecture, read existing skills and understand the project's design decisions. Superpowers has its own tested philosophy about skill design, agent behavior shaping, and terminology (e.g., "your human partner" is deliberate, not interchangeable with "the user"). Changes that rewrite the project's voice or restructure its approach without understanding why it exists will be rejected.
|
||||
|
||||
## General
|
||||
|
||||
- Read `.github/PULL_REQUEST_TEMPLATE.md` before submitting
|
||||
- One problem per PR
|
||||
- Test on at least one harness and report results in the environment table
|
||||
- Describe the problem you solved, not just what you changed
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
jesse@primeradiant.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
2
GEMINI.md
Normal file
2
GEMINI.md
Normal file
@@ -0,0 +1,2 @@
|
||||
@./skills/using-superpowers/SKILL.md
|
||||
@./skills/using-superpowers/references/gemini-tools.md
|
||||
339
README.md
339
README.md
@@ -1,154 +1,199 @@
|
||||
# Superpowers
|
||||
|
||||
Give Claude Code superpowers with a comprehensive skills library of proven techniques, patterns, and tools.
|
||||
Superpowers is a complete software development methodology for your coding agents, built on top of a set of composable skills and some initial instructions that make sure your agent uses them.
|
||||
|
||||
## What You Get
|
||||
## Quickstart
|
||||
|
||||
- **Testing Skills** - TDD, async testing, anti-patterns
|
||||
- **Debugging Skills** - Systematic debugging, root cause tracing, verification
|
||||
- **Collaboration Skills** - Brainstorming, planning, code review, parallel agents
|
||||
- **Meta Skills** - Creating, testing, and contributing skills
|
||||
Give your agent Superpowers: [Claude Code](#claude-code), [Codex CLI](#codex-cli), [Codex App](#codex-app), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [OpenCode](#opencode), [Cursor](#cursor), [GitHub Copilot CLI](#github-copilot-cli).
|
||||
|
||||
Plus:
|
||||
- **Slash Commands** - `/brainstorm`, `/write-plan`, `/execute-plan`
|
||||
- **Skills Search** - Grep-powered discovery of relevant skills
|
||||
- **Gap Tracking** - Failed searches logged for skill creation
|
||||
## How it works
|
||||
|
||||
## Learn More
|
||||
It starts from the moment you fire up your coding agent. As soon as it sees that you're building something, it *doesn't* just jump into trying to write code. Instead, it steps back and asks you what you're really trying to do.
|
||||
|
||||
Once it's teased a spec out of the conversation, it shows it to you in chunks short enough to actually read and digest.
|
||||
|
||||
After you've signed off on the design, your agent puts together an implementation plan that's clear enough for an enthusiastic junior engineer with poor taste, no judgement, no project context, and an aversion to testing to follow. It emphasizes true red/green TDD, YAGNI (You Aren't Gonna Need It), and DRY.
|
||||
|
||||
Next up, once you say "go", it launches a *subagent-driven-development* process, having agents work through each engineering task, inspecting and reviewing their work, and continuing forward. It's not uncommon for Claude to be able to work autonomously for a couple hours at a time without deviating from the plan you put together.
|
||||
|
||||
There's a bunch more to it, but that's the core of the system. And because the skills trigger automatically, you don't need to do anything special. Your coding agent just has Superpowers.
|
||||
|
||||
|
||||
## Sponsorship
|
||||
|
||||
If Superpowers has helped you do stuff that makes money and you are so inclined, I'd greatly appreciate it if you'd consider [sponsoring my opensource work](https://github.com/sponsors/obra).
|
||||
|
||||
Thanks!
|
||||
|
||||
\- Jesse
|
||||
|
||||
Read the introduction: [Superpowers for Claude Code](https://blog.fsck.com/2025/10/09/superpowers/)
|
||||
|
||||
## Installation
|
||||
|
||||
### Via Plugin Marketplace (Recommended)
|
||||
Installation differs by harness. If you use more than one, install Superpowers separately for each one.
|
||||
|
||||
```bash
|
||||
# In Claude Code
|
||||
/plugin marketplace add obra/superpowers-marketplace
|
||||
/plugin install superpowers@superpowers-marketplace
|
||||
```
|
||||
### Claude Code
|
||||
|
||||
That's it! On first session, the plugin automatically:
|
||||
- Sets up `~/.config/superpowers/` as your personal skills repository
|
||||
- Initializes git repo for version control
|
||||
- Makes core skills searchable alongside your personal skills
|
||||
- Adds `/brainstorm`, `/write-plan`, and `/execute-plan` commands
|
||||
- Offers to create public GitHub repo for sharing your skills
|
||||
Superpowers is available via the [official Claude plugin marketplace](https://claude.com/plugins/superpowers)
|
||||
|
||||
### Verify Installation
|
||||
#### Official Marketplace
|
||||
|
||||
```bash
|
||||
# Check that commands appear
|
||||
/help
|
||||
- Install the plugin from Anthropic's official marketplace:
|
||||
|
||||
# Should see:
|
||||
# /brainstorm - Interactive design refinement
|
||||
# /write-plan - Create implementation plan
|
||||
# /execute-plan - Execute plan in batches
|
||||
```
|
||||
```bash
|
||||
/plugin install superpowers@claude-plugins-official
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
#### Superpowers Marketplace
|
||||
|
||||
### Your Personal Skills
|
||||
The Superpowers marketplace provides Superpowers and some other related plugins for Claude Code.
|
||||
|
||||
Write your own skills to `~/.config/superpowers/skills/`:
|
||||
- All personal skills automatically discovered by search tools
|
||||
- Personal skills shadow core skills when names match
|
||||
- Version controlled with git
|
||||
- Optional: Share on GitHub and contribute back to core
|
||||
- Register the marketplace:
|
||||
|
||||
See `skills/meta/writing-skills` for how to create new skills.
|
||||
See `skills/meta/sharing-skills` for how to contribute to core.
|
||||
```bash
|
||||
/plugin marketplace add obra/superpowers-marketplace
|
||||
```
|
||||
|
||||
### Finding Skills
|
||||
- Install the plugin from this marketplace:
|
||||
|
||||
Find both personal and core skills before starting any task:
|
||||
```bash
|
||||
/plugin install superpowers@superpowers-marketplace
|
||||
```
|
||||
|
||||
```bash
|
||||
${CLAUDE_PLUGIN_ROOT}/scripts/find-skills # All skills with descriptions
|
||||
${CLAUDE_PLUGIN_ROOT}/scripts/find-skills test # Filter by pattern
|
||||
${CLAUDE_PLUGIN_ROOT}/scripts/find-skills 'TDD|debug' # Regex pattern
|
||||
```
|
||||
### Codex CLI
|
||||
|
||||
### Using Slash Commands
|
||||
Superpowers is available via the [official Codex plugin marketplace](https://github.com/openai/plugins).
|
||||
|
||||
**Brainstorm a design:**
|
||||
```
|
||||
/brainstorm
|
||||
```
|
||||
- Open the plugin search interface:
|
||||
|
||||
**Create an implementation plan:**
|
||||
```
|
||||
/write-plan
|
||||
```
|
||||
```bash
|
||||
/plugins
|
||||
```
|
||||
|
||||
**Execute the plan:**
|
||||
```
|
||||
/execute-plan
|
||||
```
|
||||
- Search for Superpowers:
|
||||
|
||||
```bash
|
||||
superpowers
|
||||
```
|
||||
|
||||
- Select `Install Plugin`.
|
||||
|
||||
### Codex App
|
||||
|
||||
Superpowers is available via the [official Codex plugin marketplace](https://github.com/openai/plugins).
|
||||
|
||||
- In the Codex app, click on Plugins in the sidebar.
|
||||
- You should see `Superpowers` in the Coding section.
|
||||
- Click the `+` next to Superpowers and follow the prompts.
|
||||
|
||||
### Factory Droid
|
||||
|
||||
- Register the marketplace:
|
||||
|
||||
```bash
|
||||
droid plugin marketplace add https://github.com/obra/superpowers
|
||||
```
|
||||
|
||||
- Install the plugin:
|
||||
|
||||
```bash
|
||||
droid plugin install superpowers@superpowers
|
||||
```
|
||||
|
||||
### Gemini CLI
|
||||
|
||||
- Install the extension:
|
||||
|
||||
```bash
|
||||
gemini extensions install https://github.com/obra/superpowers
|
||||
```
|
||||
|
||||
- Update later:
|
||||
|
||||
```bash
|
||||
gemini extensions update superpowers
|
||||
```
|
||||
|
||||
### OpenCode
|
||||
|
||||
OpenCode uses its own plugin install; install Superpowers separately even if you
|
||||
already use it in another harness.
|
||||
|
||||
- Tell OpenCode:
|
||||
|
||||
```
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.opencode/INSTALL.md
|
||||
```
|
||||
|
||||
- Detailed docs: [docs/README.opencode.md](docs/README.opencode.md)
|
||||
|
||||
### Cursor
|
||||
|
||||
- In Cursor Agent chat, install from marketplace:
|
||||
|
||||
```text
|
||||
/add-plugin superpowers
|
||||
```
|
||||
|
||||
- Or search for "superpowers" in the plugin marketplace.
|
||||
|
||||
### GitHub Copilot CLI
|
||||
|
||||
- Register the marketplace:
|
||||
|
||||
```bash
|
||||
copilot plugin marketplace add obra/superpowers-marketplace
|
||||
```
|
||||
|
||||
- Install the plugin:
|
||||
|
||||
```bash
|
||||
copilot plugin install superpowers@superpowers-marketplace
|
||||
```
|
||||
|
||||
## The Basic Workflow
|
||||
|
||||
1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document.
|
||||
|
||||
2. **using-git-worktrees** - Activates after design approval. Creates isolated workspace on new branch, runs project setup, verifies clean test baseline.
|
||||
|
||||
3. **writing-plans** - Activates with approved design. Breaks work into bite-sized tasks (2-5 minutes each). Every task has exact file paths, complete code, verification steps.
|
||||
|
||||
4. **subagent-driven-development** or **executing-plans** - Activates with plan. Dispatches fresh subagent per task with two-stage review (spec compliance, then code quality), or executes in batches with human checkpoints.
|
||||
|
||||
5. **test-driven-development** - Activates during implementation. Enforces RED-GREEN-REFACTOR: write failing test, watch it fail, write minimal code, watch it pass, commit. Deletes code written before tests.
|
||||
|
||||
6. **requesting-code-review** - Activates between tasks. Reviews against plan, reports issues by severity. Critical issues block progress.
|
||||
|
||||
7. **finishing-a-development-branch** - Activates when tasks complete. Verifies tests, presents options (merge/PR/keep/discard), cleans up worktree.
|
||||
|
||||
**The agent checks for relevant skills before any task.** Mandatory workflows, not suggestions.
|
||||
|
||||
## What's Inside
|
||||
|
||||
### Skills Library
|
||||
|
||||
**Testing** (`skills/testing/`)
|
||||
- test-driven-development - RED-GREEN-REFACTOR cycle
|
||||
- condition-based-waiting - Async test patterns
|
||||
- testing-anti-patterns - Common pitfalls to avoid
|
||||
**Testing**
|
||||
- **test-driven-development** - RED-GREEN-REFACTOR cycle (includes testing anti-patterns reference)
|
||||
|
||||
**Debugging** (`skills/debugging/`)
|
||||
- systematic-debugging - 4-phase root cause process
|
||||
- root-cause-tracing - Find the real problem
|
||||
- verification-before-completion - Ensure it's actually fixed
|
||||
- defense-in-depth - Multiple validation layers
|
||||
**Debugging**
|
||||
- **systematic-debugging** - 4-phase root cause process (includes root-cause-tracing, defense-in-depth, condition-based-waiting techniques)
|
||||
- **verification-before-completion** - Ensure it's actually fixed
|
||||
|
||||
**Collaboration** (`skills/collaboration/`)
|
||||
- brainstorming - Socratic design refinement
|
||||
- writing-plans - Detailed implementation plans
|
||||
- executing-plans - Batch execution with checkpoints
|
||||
- dispatching-parallel-agents - Concurrent subagent workflows
|
||||
- remembering-conversations - Search past work
|
||||
- using-git-worktrees - Parallel development branches
|
||||
- requesting-code-review - Pre-review checklist
|
||||
- receiving-code-review - Responding to feedback
|
||||
**Collaboration**
|
||||
- **brainstorming** - Socratic design refinement
|
||||
- **writing-plans** - Detailed implementation plans
|
||||
- **executing-plans** - Batch execution with checkpoints
|
||||
- **dispatching-parallel-agents** - Concurrent subagent workflows
|
||||
- **requesting-code-review** - Pre-review checklist
|
||||
- **receiving-code-review** - Responding to feedback
|
||||
- **using-git-worktrees** - Parallel development branches
|
||||
- **finishing-a-development-branch** - Merge/PR decision workflow
|
||||
- **subagent-driven-development** - Fast iteration with two-stage review (spec compliance, then code quality)
|
||||
|
||||
**Meta** (`skills/meta/`)
|
||||
- setting-up-personal-superpowers - Personal skills repository setup
|
||||
- writing-skills - TDD for documentation, create new skills
|
||||
- sharing-skills - Contribute skills back to core
|
||||
- testing-skills-with-subagents - Validate skill quality
|
||||
- gardening-skills-wiki - Maintain and improve skills
|
||||
|
||||
### Commands
|
||||
|
||||
- **brainstorm.md** - Interactive design refinement using Socratic method
|
||||
- **write-plan.md** - Create detailed implementation plans
|
||||
- **execute-plan.md** - Execute plans in batches with review checkpoints
|
||||
|
||||
### Tools
|
||||
|
||||
**In `scripts/` directory:**
|
||||
- **find-skills** - Unified skill discovery with descriptions (replaces list-skills + skills-search)
|
||||
- **skill-run** - Generic runner for any skill script (searches personal then core)
|
||||
|
||||
**Skill-specific tools:**
|
||||
- **search-conversations** - Semantic search of past Claude sessions (in remembering-conversations skill)
|
||||
|
||||
**Using scripts:**
|
||||
```bash
|
||||
${CLAUDE_PLUGIN_ROOT}/scripts/find-skills # Show all skills
|
||||
${CLAUDE_PLUGIN_ROOT}/scripts/find-skills pattern # Search skills
|
||||
${CLAUDE_PLUGIN_ROOT}/scripts/skill-run <path> [args] # Run any skill script
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **SessionStart Hook** - Auto-setup personal skills repo, inject core skills context
|
||||
2. **Two-Tier Skills** - Personal skills (`~/.config/superpowers/skills/`) + Core skills (plugin)
|
||||
3. **Skills Discovery** - `find-skills` searches both locations with descriptions
|
||||
4. **Shadowing** - Personal skills override core skills when paths match
|
||||
5. **Mandatory Workflow** - Skills become required when they exist for your task
|
||||
6. **Gap Tracking** - Failed searches logged to `~/.config/superpowers/search-log.jsonl`
|
||||
**Meta**
|
||||
- **writing-skills** - Create new skills following best practices (includes testing methodology)
|
||||
- **using-superpowers** - Introduction to the skills system
|
||||
|
||||
## Philosophy
|
||||
|
||||
@@ -156,57 +201,35 @@ ${CLAUDE_PLUGIN_ROOT}/scripts/skill-run <path> [args] # Run any skill script
|
||||
- **Systematic over ad-hoc** - Process over guessing
|
||||
- **Complexity reduction** - Simplicity as primary goal
|
||||
- **Evidence over claims** - Verify before declaring success
|
||||
- **Domain over implementation** - Work at problem level, not solution level
|
||||
|
||||
## Personal Superpowers Directory
|
||||
|
||||
The plugin auto-creates your personal superpowers directory on first session.
|
||||
|
||||
**Default location:** `~/.config/superpowers/`
|
||||
|
||||
**Customize via environment variables:**
|
||||
```bash
|
||||
# Option 1: Set exact location
|
||||
export PERSONAL_SUPERPOWERS_DIR="$HOME/my-superpowers"
|
||||
|
||||
# Option 2: Use XDG standard
|
||||
export XDG_CONFIG_HOME="$HOME/.local/config" # Uses $HOME/.local/config/superpowers
|
||||
```
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
~/.config/superpowers/ # (or your custom location)
|
||||
├── .git/ # Git repository
|
||||
├── .gitignore # Ignores logs and indexes
|
||||
├── README.md # About your personal superpowers
|
||||
├── skills/ # Your personal skills
|
||||
│ └── your-skill/
|
||||
│ └── SKILL.md
|
||||
├── search-log.jsonl # Failed skill searches (not tracked)
|
||||
└── conversation-index/ # Indexed conversations (not tracked)
|
||||
```
|
||||
|
||||
**Why git?** Track your skills evolution, share with others, contribute back to core.
|
||||
Read [the original release announcement](https://blog.fsck.com/2025/10/09/superpowers/).
|
||||
|
||||
## Contributing
|
||||
|
||||
**Write personal skills:**
|
||||
1. Create in `~/.config/superpowers/skills/your-skill/`
|
||||
2. Follow TDD process in `skills/meta/writing-skills`
|
||||
3. Commit to your personal repo
|
||||
The general contribution process for Superpowers is below. Keep in mind that we don't generally accept contributions of new skills and that any updates to skills must work across all of the coding agents we support.
|
||||
|
||||
**Share with everyone:**
|
||||
1. Follow workflow in `skills/meta/sharing-skills`
|
||||
2. Fork → Branch → Copy → PR to core
|
||||
3. Keep personal version or delete after merge
|
||||
1. Fork the repository
|
||||
2. Switch to the 'dev' branch
|
||||
3. Create a branch for your work
|
||||
4. Follow the `writing-skills` skill for creating and testing new and modified skills
|
||||
5. Submit a PR, being sure to fill in the pull request template.
|
||||
|
||||
**Missing a skill?** Edit `skills/REQUESTS.md` or open an issue
|
||||
Skill-behavior tests use the eval harness submodule at `evals/`. After cloning this repo, run `git submodule update --init evals`, then see `evals/README.md` for setup. Plugin-infrastructure tests live at `tests/` and run via the relevant `run-*.sh` or `npm test`.
|
||||
|
||||
See `skills/writing-skills/SKILL.md` for the complete guide.
|
||||
|
||||
## Updating
|
||||
|
||||
Superpowers updates are somewhat coding-agent dependent, but are often automatic.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details
|
||||
|
||||
## Support
|
||||
## Community
|
||||
|
||||
Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com).
|
||||
|
||||
- **Discord**: [Join us](https://discord.gg/35wsABTejz) for community support, questions, and sharing what you're building with Superpowers
|
||||
- **Issues**: https://github.com/obra/superpowers/issues
|
||||
- **Marketplace**: https://github.com/obra/superpowers-marketplace
|
||||
- **Release announcements**: [Sign up](https://primeradiant.com/superpowers/) to get notified about new versions
|
||||
|
||||
1182
RELEASE-NOTES.md
Normal file
1182
RELEASE-NOTES.md
Normal file
File diff suppressed because it is too large
Load Diff
BIN
assets/app-icon.png
Normal file
BIN
assets/app-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
1
assets/superpowers-small.svg
Normal file
1
assets/superpowers-small.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Calque_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M394.28,207.8c.81,2.41,1.39,4.78,1.8,7.07,1.61,9.03-.93,17.78-5.99,21.74-22.6,17.7-49.85,29.35-75.34,38.6-.59.22-1.09.28-1.4.34-2.22.47-4.95,1.04-7.25,0-1.46-.66-2.25-1.74-2.66-2.3-1.56-2.1-1.59-4.31-1.56-5.13.1-2.67-.01-4.69,0-4.82.45-3.52.91-10.66,1.41-21.28.6-3.87,2.16-9.63,6.94-13.96,4.01-3.62,8.33-4.6,14.59-5.87,10.76-2.19,37.21-8.22,47.42-16.56,1.63-1.33,2.97-2.65,4.19-3.96,3.72-3.99,6.39-7.92,7.93-10.36,3.22,3.22,7.25,8.48,9.92,16.47Z"/><path d="M428.67,185.28c-2.33,11.99-8.91,22.32-15.88,30.38.27-5.5-.05-12.11-1.86-19.08-5.04-19.36-19.74-34.7-37.78-37.78-32.21-9.74-70.59,3.79-99.08,18.29-3.87,1.95-9.52-2.77-11.84-8.16-3.32-7.71-1.63-6.28,2.61-8.49,38.31-20.03,82.01-39.61,123.91-29.7,8.26,1.95,15.96,5.26,23.48,10.54,11.32,7.96,20.21,24.74,16.44,44Z"/><path d="M117.72,304.2c-.81-2.41-1.39-4.78-1.8-7.07-1.61-9.03.93-17.78,5.99-21.74,22.6-17.7,49.85-29.35,75.34-38.6.59-.22,1.09-.28,1.4-.34,2.22-.47,4.95-1.04,7.25,0,1.46.66,2.25,1.74,2.66,2.3,1.56,2.1,1.59,4.31,1.56,5.13-.1,2.67.01,4.69,0,4.82-.45,3.52-.91,10.66-1.41,21.28-.6,3.87-2.16,9.63-6.94,13.96-4.01,3.62-8.33,4.6-14.59,5.87-10.76,2.19-37.21,8.22-47.42,16.56-1.63,1.33-2.97,2.65-4.19,3.96-3.72,3.99-6.39,7.92-7.93,10.36-3.22-3.22-7.25-8.48-9.92-16.47Z"/><path d="M83.33,326.72c2.33-11.99,8.91-22.32,15.88-30.38-.27,5.5.05,12.11,1.86,19.08,5.04,19.36,19.74,34.7,37.78,37.78,32.21,9.74,70.59-3.79,99.08-18.29,3.87-1.95,9.52,2.77,11.84,8.16,3.32,7.71,1.63,6.28-2.61,8.49-38.31,20.03-82.01,39.61-123.91,29.7-8.26-1.95-15.96-5.26-23.48-10.54-11.32-7.96-20.21-24.74-16.44-44Z"/><ellipse cx="255.16" cy="258.86" rx="28.95" ry="28.76"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,36 +0,0 @@
|
||||
# Claude Commands
|
||||
|
||||
Slash commands for Claude that reference skills.
|
||||
|
||||
## Available Commands
|
||||
|
||||
- `/brainstorm` - Interactive idea refinement using Socratic method (→ `@skills/collaboration/brainstorming/SKILL.md`)
|
||||
- `/write-plan` - Create detailed implementation plan (→ `@skills/collaboration/writing-plans/SKILL.md`)
|
||||
- `/execute-plan` - Execute plan in batches with review (→ `@skills/collaboration/executing-plans/SKILL.md`)
|
||||
|
||||
## Format
|
||||
|
||||
Each command is a simple markdown file containing a single `@` reference to a skill:
|
||||
|
||||
```markdown
|
||||
@skills/collaboration/brainstorming/SKILL.md
|
||||
```
|
||||
|
||||
When you run the command (e.g., `/brainstorm`), Claude loads and follows that skill.
|
||||
|
||||
## Creating Custom Commands
|
||||
|
||||
To add your own commands:
|
||||
|
||||
1. Create `your-command.md` in this directory
|
||||
2. Add a single line referencing a skill:
|
||||
```markdown
|
||||
@skills/your-category/your-skill/SKILL.md
|
||||
```
|
||||
3. The command `/your-command` is now available
|
||||
|
||||
## Installation
|
||||
|
||||
These commands are automatically symlinked to `~/.claude/commands/` by the clank installer.
|
||||
|
||||
See `@skills/meta/installing-skills/SKILL.md` for installation details.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
description: Interactive design refinement using Socratic method
|
||||
---
|
||||
|
||||
Read and follow: ${CLAUDE_PLUGIN_ROOT}/skills/collaboration/brainstorming/SKILL.md
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
description: Execute plan in batches with review checkpoints
|
||||
---
|
||||
|
||||
Read and follow: ${CLAUDE_PLUGIN_ROOT}/skills/collaboration/executing-plans/SKILL.md
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
description: Create detailed implementation plan with bite-sized tasks
|
||||
---
|
||||
|
||||
Read and follow: ${CLAUDE_PLUGIN_ROOT}/skills/collaboration/writing-plans/SKILL.md
|
||||
157
docs/README.opencode.md
Normal file
157
docs/README.opencode.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Superpowers for OpenCode
|
||||
|
||||
Complete guide for using Superpowers with [OpenCode.ai](https://opencode.ai).
|
||||
|
||||
## Installation
|
||||
|
||||
Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
|
||||
}
|
||||
```
|
||||
|
||||
Restart OpenCode. The plugin installs through OpenCode's plugin manager and
|
||||
registers all skills.
|
||||
|
||||
Verify by asking: "Tell me about your superpowers"
|
||||
|
||||
OpenCode uses its own plugin install. If you also use Claude Code, Codex, or
|
||||
another harness, install Superpowers separately for each one.
|
||||
|
||||
### Migrating from the old symlink-based install
|
||||
|
||||
If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
|
||||
|
||||
```bash
|
||||
# Remove old symlinks
|
||||
rm -f ~/.config/opencode/plugins/superpowers.js
|
||||
rm -rf ~/.config/opencode/skills/superpowers
|
||||
|
||||
# Optionally remove the cloned repo
|
||||
rm -rf ~/.config/opencode/superpowers
|
||||
|
||||
# Remove skills.paths from opencode.json if you added one for superpowers
|
||||
```
|
||||
|
||||
Then follow the installation steps above.
|
||||
|
||||
## Usage
|
||||
|
||||
### Finding Skills
|
||||
|
||||
Use OpenCode's native `skill` tool to list all available skills:
|
||||
|
||||
```
|
||||
use skill tool to list skills
|
||||
```
|
||||
|
||||
### Loading a Skill
|
||||
|
||||
```
|
||||
use skill tool to load superpowers/brainstorming
|
||||
```
|
||||
|
||||
### Personal Skills
|
||||
|
||||
Create your own skills in `~/.config/opencode/skills/`:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/opencode/skills/my-skill
|
||||
```
|
||||
|
||||
Create `~/.config/opencode/skills/my-skill/SKILL.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: Use when [condition] - [what it does]
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
[Your skill content here]
|
||||
```
|
||||
|
||||
### Project Skills
|
||||
|
||||
Create project-specific skills in `.opencode/skills/` within your project.
|
||||
|
||||
**Skill Priority:** Project skills > Personal skills > Superpowers skills
|
||||
|
||||
## Updating
|
||||
|
||||
OpenCode installs Superpowers through a git-backed package spec. Some OpenCode
|
||||
and Bun versions pin that resolved git dependency in a lockfile or cache, so a
|
||||
restart may not pick up the newest Superpowers commit. If updates do not appear,
|
||||
clear OpenCode's package cache or reinstall the plugin.
|
||||
|
||||
To pin a specific version, use a branch or tag:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The plugin does two things:
|
||||
|
||||
1. **Injects bootstrap context** via the `experimental.chat.system.transform` hook, adding superpowers awareness to every conversation.
|
||||
2. **Registers the skills directory** via the `config` hook, so OpenCode discovers all superpowers skills without symlinks or manual config.
|
||||
|
||||
### Tool Mapping
|
||||
|
||||
Skills written for Claude Code are automatically adapted for OpenCode:
|
||||
|
||||
- `TodoWrite` → `todowrite`
|
||||
- `Task` with subagents → OpenCode's `@mention` system
|
||||
- `Skill` tool → OpenCode's native `skill` tool
|
||||
- File operations → Native OpenCode tools
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin not loading
|
||||
|
||||
1. Check OpenCode logs: `opencode run --print-logs "hello" 2>&1 | grep -i superpowers`
|
||||
2. Verify the plugin line in your `opencode.json` is correct
|
||||
3. Make sure you're running a recent version of OpenCode
|
||||
|
||||
### Windows install issues
|
||||
|
||||
Some Windows OpenCode builds have upstream installer issues with git-backed
|
||||
plugin specs, including cache paths for `git+https` URLs and Bun not finding
|
||||
`git.exe` even when it works in a normal terminal. If OpenCode cannot install
|
||||
the plugin, try installing with system npm and pointing OpenCode at the local
|
||||
package:
|
||||
|
||||
```powershell
|
||||
npm install superpowers@git+https://github.com/obra/superpowers.git --prefix "$HOME\.config\opencode"
|
||||
```
|
||||
|
||||
Then use the installed package path in `opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": ["~/.config/opencode/node_modules/superpowers"]
|
||||
}
|
||||
```
|
||||
|
||||
### Skills not found
|
||||
|
||||
1. Use OpenCode's `skill` tool to list available skills
|
||||
2. Check that the plugin is loading (see above)
|
||||
3. Each skill needs a `SKILL.md` file with valid YAML frontmatter
|
||||
|
||||
### Bootstrap not appearing
|
||||
|
||||
1. Check OpenCode version supports `experimental.chat.system.transform` hook
|
||||
2. Restart OpenCode after config changes
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Report issues: https://github.com/obra/superpowers/issues
|
||||
- Main documentation: https://github.com/obra/superpowers
|
||||
- OpenCode docs: https://opencode.ai/docs/
|
||||
294
docs/plans/2025-11-22-opencode-support-design.md
Normal file
294
docs/plans/2025-11-22-opencode-support-design.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# OpenCode Support Design
|
||||
|
||||
**Date:** 2025-11-22
|
||||
**Author:** Bot & Jesse
|
||||
**Status:** Design Complete, Awaiting Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Add full superpowers support for OpenCode.ai using a native OpenCode plugin architecture that shares core functionality with the existing Codex implementation.
|
||||
|
||||
## Background
|
||||
|
||||
OpenCode.ai is a coding agent similar to Claude Code and Codex. Previous attempts to port superpowers to OpenCode (PR #93, PR #116) used file-copying approaches. This design takes a different approach: building a native OpenCode plugin using their JavaScript/TypeScript plugin system while sharing code with the Codex implementation.
|
||||
|
||||
### Key Differences Between Platforms
|
||||
|
||||
- **Claude Code**: Native Anthropic plugin system + file-based skills
|
||||
- **Codex**: No plugin system → bootstrap markdown + CLI script
|
||||
- **OpenCode**: JavaScript/TypeScript plugins with event hooks and custom tools API
|
||||
|
||||
### OpenCode's Agent System
|
||||
|
||||
- **Primary agents**: Build (default, full access) and Plan (restricted, read-only)
|
||||
- **Subagents**: General (research, searching, multi-step tasks)
|
||||
- **Invocation**: Automatic dispatch by primary agents OR manual `@mention` syntax
|
||||
- **Configuration**: Custom agents in `opencode.json` or `~/.config/opencode/agent/`
|
||||
|
||||
## Architecture
|
||||
|
||||
### High-Level Structure
|
||||
|
||||
1. **Shared Core Module** (`lib/skills-core.js`)
|
||||
- Common skill discovery and parsing logic
|
||||
- Used by both Codex and OpenCode implementations
|
||||
|
||||
2. **Platform-Specific Wrappers**
|
||||
- Codex: CLI script (`.codex/superpowers-codex`)
|
||||
- OpenCode: Plugin module (`.opencode/plugin/superpowers.js`)
|
||||
|
||||
3. **Skill Directories**
|
||||
- Core: `~/.config/opencode/superpowers/skills/` (or installed location)
|
||||
- Personal: `~/.config/opencode/skills/` (shadows core skills)
|
||||
|
||||
### Code Reuse Strategy
|
||||
|
||||
Extract common functionality from `.codex/superpowers-codex` into shared module:
|
||||
|
||||
```javascript
|
||||
// lib/skills-core.js
|
||||
module.exports = {
|
||||
extractFrontmatter(filePath), // Parse name + description from YAML
|
||||
findSkillsInDir(dir, maxDepth), // Recursive SKILL.md discovery
|
||||
findAllSkills(dirs), // Scan multiple directories
|
||||
resolveSkillPath(skillName, dirs), // Handle shadowing (personal > core)
|
||||
checkForUpdates(repoDir) // Git fetch/status check
|
||||
};
|
||||
```
|
||||
|
||||
### Skill Frontmatter Format
|
||||
|
||||
Current format (no `when_to_use` field):
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: Use when [condition] - [what it does]; [additional context]
|
||||
---
|
||||
```
|
||||
|
||||
## OpenCode Plugin Implementation
|
||||
|
||||
### Custom Tools
|
||||
|
||||
**Tool 1: `use_skill`**
|
||||
|
||||
Loads a specific skill's content into the conversation (equivalent to Claude's Skill tool).
|
||||
|
||||
```javascript
|
||||
{
|
||||
name: 'use_skill',
|
||||
description: 'Load and read a specific skill to guide your work',
|
||||
schema: z.object({
|
||||
skill_name: z.string().describe('Name of skill (e.g., "superpowers:brainstorming")')
|
||||
}),
|
||||
execute: async ({ skill_name }) => {
|
||||
const { skillPath, content, frontmatter } = resolveAndReadSkill(skill_name);
|
||||
const skillDir = path.dirname(skillPath);
|
||||
|
||||
return `# ${frontmatter.name}
|
||||
# ${frontmatter.description}
|
||||
# Supporting tools and docs are in ${skillDir}
|
||||
# ============================================
|
||||
|
||||
${content}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Tool 2: `find_skills`**
|
||||
|
||||
Lists all available skills with metadata.
|
||||
|
||||
```javascript
|
||||
{
|
||||
name: 'find_skills',
|
||||
description: 'List all available skills',
|
||||
schema: z.object({}),
|
||||
execute: async () => {
|
||||
const skills = discoverAllSkills();
|
||||
return skills.map(s =>
|
||||
`${s.namespace}:${s.name}
|
||||
${s.description}
|
||||
Directory: ${s.directory}
|
||||
`).join('\n');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Session Startup Hook
|
||||
|
||||
When a new session starts (`session.started` event):
|
||||
|
||||
1. **Inject using-superpowers content**
|
||||
- Full content of the using-superpowers skill
|
||||
- Establishes mandatory workflows
|
||||
|
||||
2. **Run find_skills automatically**
|
||||
- Display full list of available skills upfront
|
||||
- Include skill directories for each
|
||||
|
||||
3. **Inject tool mapping instructions**
|
||||
```markdown
|
||||
**Tool Mapping for OpenCode:**
|
||||
When skills reference tools you don't have, substitute:
|
||||
- `TodoWrite` → `update_plan`
|
||||
- `Task` with subagents → Use OpenCode subagent system (@mention)
|
||||
- `Skill` tool → `use_skill` custom tool
|
||||
- Read, Write, Edit, Bash → Your native equivalents
|
||||
|
||||
**Skill directories contain:**
|
||||
- Supporting scripts (run with bash)
|
||||
- Additional documentation (read with read tool)
|
||||
- Utilities specific to that skill
|
||||
```
|
||||
|
||||
4. **Check for updates** (non-blocking)
|
||||
- Quick git fetch with timeout
|
||||
- Notify if updates available
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```javascript
|
||||
// .opencode/plugin/superpowers.js
|
||||
const skillsCore = require('../../lib/skills-core');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { z } = require('zod');
|
||||
|
||||
export const SuperpowersPlugin = async ({ client, directory, $ }) => {
|
||||
const superpowersDir = path.join(process.env.HOME, '.config/opencode/superpowers');
|
||||
const personalDir = path.join(process.env.HOME, '.config/opencode/skills');
|
||||
|
||||
return {
|
||||
'session.started': async () => {
|
||||
const usingSuperpowers = await readSkill('using-superpowers');
|
||||
const skillsList = await findAllSkills();
|
||||
const toolMapping = getToolMappingInstructions();
|
||||
|
||||
return {
|
||||
context: `${usingSuperpowers}\n\n${skillsList}\n\n${toolMapping}`
|
||||
};
|
||||
},
|
||||
|
||||
tools: [
|
||||
{
|
||||
name: 'use_skill',
|
||||
description: 'Load and read a specific skill',
|
||||
schema: z.object({
|
||||
skill_name: z.string()
|
||||
}),
|
||||
execute: async ({ skill_name }) => {
|
||||
// Implementation using skillsCore
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'find_skills',
|
||||
description: 'List all available skills',
|
||||
schema: z.object({}),
|
||||
execute: async () => {
|
||||
// Implementation using skillsCore
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
superpowers/
|
||||
├── lib/
|
||||
│ └── skills-core.js # NEW: Shared skill logic
|
||||
├── .codex/
|
||||
│ ├── superpowers-codex # UPDATED: Use skills-core
|
||||
│ ├── superpowers-bootstrap.md
|
||||
│ └── INSTALL.md
|
||||
├── .opencode/
|
||||
│ ├── plugin/
|
||||
│ │ └── superpowers.js # NEW: OpenCode plugin
|
||||
│ └── INSTALL.md # NEW: Installation guide
|
||||
└── skills/ # Unchanged
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Refactor Shared Core
|
||||
|
||||
1. Create `lib/skills-core.js`
|
||||
- Extract frontmatter parsing from `.codex/superpowers-codex`
|
||||
- Extract skill discovery logic
|
||||
- Extract path resolution (with shadowing)
|
||||
- Update to use only `name` and `description` (no `when_to_use`)
|
||||
|
||||
2. Update `.codex/superpowers-codex` to use shared core
|
||||
- Import from `../lib/skills-core.js`
|
||||
- Remove duplicated code
|
||||
- Keep CLI wrapper logic
|
||||
|
||||
3. Test Codex implementation still works
|
||||
- Verify bootstrap command
|
||||
- Verify use-skill command
|
||||
- Verify find-skills command
|
||||
|
||||
### Phase 2: Build OpenCode Plugin
|
||||
|
||||
1. Create `.opencode/plugin/superpowers.js`
|
||||
- Import shared core from `../../lib/skills-core.js`
|
||||
- Implement plugin function
|
||||
- Define custom tools (use_skill, find_skills)
|
||||
- Implement session.started hook
|
||||
|
||||
2. Create `.opencode/INSTALL.md`
|
||||
- Installation instructions
|
||||
- Directory setup
|
||||
- Configuration guidance
|
||||
|
||||
3. Test OpenCode implementation
|
||||
- Verify session startup bootstrap
|
||||
- Verify use_skill tool works
|
||||
- Verify find_skills tool works
|
||||
- Verify skill directories are accessible
|
||||
|
||||
### Phase 3: Documentation & Polish
|
||||
|
||||
1. Update README with OpenCode support
|
||||
2. Add OpenCode installation to main docs
|
||||
3. Update RELEASE-NOTES
|
||||
4. Test both Codex and OpenCode work correctly
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Create isolated workspace** (using git worktrees)
|
||||
- Branch: `feature/opencode-support`
|
||||
|
||||
2. **Follow TDD where applicable**
|
||||
- Test shared core functions
|
||||
- Test skill discovery and parsing
|
||||
- Integration tests for both platforms
|
||||
|
||||
3. **Incremental implementation**
|
||||
- Phase 1: Refactor shared core + update Codex
|
||||
- Verify Codex still works before moving on
|
||||
- Phase 2: Build OpenCode plugin
|
||||
- Phase 3: Documentation and polish
|
||||
|
||||
4. **Testing strategy**
|
||||
- Manual testing with real OpenCode installation
|
||||
- Verify skill loading, directories, scripts work
|
||||
- Test both Codex and OpenCode side-by-side
|
||||
- Verify tool mappings work correctly
|
||||
|
||||
5. **PR and merge**
|
||||
- Create PR with complete implementation
|
||||
- Test in clean environment
|
||||
- Merge to main
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Code reuse**: Single source of truth for skill discovery/parsing
|
||||
- **Maintainability**: Bug fixes apply to both platforms
|
||||
- **Extensibility**: Easy to add future platforms (Cursor, Windsurf, etc.)
|
||||
- **Native integration**: Uses OpenCode's plugin system properly
|
||||
- **Consistency**: Same skill experience across all platforms
|
||||
1095
docs/plans/2025-11-22-opencode-support-implementation.md
Normal file
1095
docs/plans/2025-11-22-opencode-support-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
711
docs/plans/2025-11-28-skills-improvements-from-user-feedback.md
Normal file
711
docs/plans/2025-11-28-skills-improvements-from-user-feedback.md
Normal file
@@ -0,0 +1,711 @@
|
||||
# Skills Improvements from User Feedback
|
||||
|
||||
**Date:** 2025-11-28
|
||||
**Status:** Draft
|
||||
**Source:** Two Claude instances using superpowers in real development scenarios
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Two Claude instances provided detailed feedback from actual development sessions. Their feedback reveals **systematic gaps** in current skills that allowed preventable bugs to ship despite following the skills.
|
||||
|
||||
**Critical insight:** These are problem reports, not just solution proposals. The problems are real; the solutions need careful evaluation.
|
||||
|
||||
**Key themes:**
|
||||
1. **Verification gaps** - We verify operations succeed but not that they achieve intended outcomes
|
||||
2. **Process hygiene** - Background processes accumulate and interfere across subagents
|
||||
3. **Context optimization** - Subagents get too much irrelevant information
|
||||
4. **Self-reflection missing** - No prompt to critique own work before handoff
|
||||
5. **Mock safety** - Mocks can drift from interfaces without detection
|
||||
6. **Skill activation** - Skills exist but aren't being read/used
|
||||
|
||||
---
|
||||
|
||||
## Problems Identified
|
||||
|
||||
### Problem 1: Configuration Change Verification Gap
|
||||
|
||||
**What happened:**
|
||||
- Subagent tested "OpenAI integration"
|
||||
- Set `OPENAI_API_KEY` env var
|
||||
- Got status 200 responses
|
||||
- Reported "OpenAI integration working"
|
||||
- **BUT** response contained `"model": "claude-sonnet-4-20250514"` - was actually using Anthropic
|
||||
|
||||
**Root cause:**
|
||||
`verification-before-completion` checks operations succeed but not that outcomes reflect intended configuration changes.
|
||||
|
||||
**Impact:** High - False confidence in integration tests, bugs ship to production
|
||||
|
||||
**Example failure pattern:**
|
||||
- Switch LLM provider → verify status 200 but don't check model name
|
||||
- Enable feature flag → verify no errors but don't check feature is active
|
||||
- Change environment → verify deployment succeeds but don't check environment vars
|
||||
|
||||
---
|
||||
|
||||
### Problem 2: Background Process Accumulation
|
||||
|
||||
**What happened:**
|
||||
- Multiple subagents dispatched during session
|
||||
- Each started background server processes
|
||||
- Processes accumulated (4+ servers running)
|
||||
- Stale processes still bound to ports
|
||||
- Later E2E test hit stale server with wrong config
|
||||
- Confusing/incorrect test results
|
||||
|
||||
**Root cause:**
|
||||
Subagents are stateless - don't know about previous subagents' processes. No cleanup protocol.
|
||||
|
||||
**Impact:** Medium-High - Tests hit wrong server, false passes/failures, debugging confusion
|
||||
|
||||
---
|
||||
|
||||
### Problem 3: Context Bloat in Subagent Prompts
|
||||
|
||||
**What happened:**
|
||||
- Standard approach: give subagent full plan file to read
|
||||
- Experiment: give only task + pattern + file + verify command
|
||||
- Result: Faster, more focused, single-attempt completion more common
|
||||
|
||||
**Root cause:**
|
||||
Subagents waste tokens and attention on irrelevant plan sections.
|
||||
|
||||
**Impact:** Medium - Slower execution, more failed attempts
|
||||
|
||||
**What worked:**
|
||||
```
|
||||
You are adding a single E2E test to packnplay's test suite.
|
||||
|
||||
**Your task:** Add `TestE2E_FeaturePrivilegedMode` to `pkg/runner/e2e_test.go`
|
||||
|
||||
**What to test:** A local devcontainer feature that requests `"privileged": true`
|
||||
in its metadata should result in the container running with `--privileged` flag.
|
||||
|
||||
**Follow the exact pattern of TestE2E_FeatureOptionValidation** (at the end of the file)
|
||||
|
||||
**After writing, run:** `go test -v ./pkg/runner -run TestE2E_FeaturePrivilegedMode -timeout 5m`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Problem 4: No Self-Reflection Before Handoff
|
||||
|
||||
**What happened:**
|
||||
- Added self-reflection prompt: "Look at your work with fresh eyes - what could be better?"
|
||||
- Implementer for Task 5 identified failing test was due to implementation bug, not test bug
|
||||
- Traced to line 99: `strings.Join(metadata.Entrypoint, " ")` creating invalid Docker syntax
|
||||
- Without self-reflection, would have just reported "test fails" without root cause
|
||||
|
||||
**Root cause:**
|
||||
Implementers don't naturally step back and critique their own work before reporting completion.
|
||||
|
||||
**Impact:** Medium - Bugs handed off to reviewer that implementer could have caught
|
||||
|
||||
---
|
||||
|
||||
### Problem 5: Mock-Interface Drift
|
||||
|
||||
**What happened:**
|
||||
```typescript
|
||||
// Interface defines close()
|
||||
interface PlatformAdapter {
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
// Code (BUGGY) calls cleanup()
|
||||
await adapter.cleanup();
|
||||
|
||||
// Mock (MATCHES BUG) defines cleanup()
|
||||
vi.mock('web-adapter', () => ({
|
||||
WebAdapter: vi.fn().mockImplementation(() => ({
|
||||
cleanup: vi.fn().mockResolvedValue(undefined), // Wrong!
|
||||
})),
|
||||
}));
|
||||
```
|
||||
- Tests passed
|
||||
- Runtime crashed: "adapter.cleanup is not a function"
|
||||
|
||||
**Root cause:**
|
||||
Mock derived from what buggy code calls, not from interface definition. TypeScript can't catch inline mocks with wrong method names.
|
||||
|
||||
**Impact:** High - Tests give false confidence, runtime crashes
|
||||
|
||||
**Why testing-anti-patterns didn't prevent this:**
|
||||
The skill covers testing mock behavior and mocking without understanding, but not the specific pattern of "derive mock from interface, not implementation."
|
||||
|
||||
---
|
||||
|
||||
### Problem 6: Code Reviewer File Access
|
||||
|
||||
**What happened:**
|
||||
- Code reviewer subagent dispatched
|
||||
- Couldn't find test file: "The file doesn't appear to exist in the repository"
|
||||
- File actually exists
|
||||
- Reviewer didn't know to explicitly read it first
|
||||
|
||||
**Root cause:**
|
||||
Reviewer prompts don't include explicit file reading instructions.
|
||||
|
||||
**Impact:** Low-Medium - Reviews fail or incomplete
|
||||
|
||||
---
|
||||
|
||||
### Problem 7: Fix Workflow Latency
|
||||
|
||||
**What happened:**
|
||||
- Implementer identifies bug during self-reflection
|
||||
- Implementer knows the fix
|
||||
- Current workflow: report → I dispatch fixer → fixer fixes → I verify
|
||||
- Extra round-trip adds latency without adding value
|
||||
|
||||
**Root cause:**
|
||||
Rigid separation between implementer and fixer roles when implementer has already diagnosed.
|
||||
|
||||
**Impact:** Low - Latency, but no correctness issue
|
||||
|
||||
---
|
||||
|
||||
### Problem 8: Skills Not Being Read
|
||||
|
||||
**What happened:**
|
||||
- `testing-anti-patterns` skill exists
|
||||
- Neither human nor subagents read it before writing tests
|
||||
- Would have prevented some issues (though not all - see Problem 5)
|
||||
|
||||
**Root cause:**
|
||||
No enforcement that subagents read relevant skills. No prompt includes skill reading.
|
||||
|
||||
**Impact:** Medium - Skill investment wasted if not used
|
||||
|
||||
---
|
||||
|
||||
## Proposed Improvements
|
||||
|
||||
### 1. verification-before-completion: Add Configuration Change Verification
|
||||
|
||||
**Add new section:**
|
||||
|
||||
```markdown
|
||||
## Verifying Configuration Changes
|
||||
|
||||
When testing changes to configuration, providers, feature flags, or environment:
|
||||
|
||||
**Don't just verify the operation succeeded. Verify the output reflects the intended change.**
|
||||
|
||||
### Common Failure Pattern
|
||||
|
||||
Operation succeeds because *some* valid config exists, but it's not the config you intended to test.
|
||||
|
||||
### Examples
|
||||
|
||||
| Change | Insufficient | Required |
|
||||
|--------|-------------|----------|
|
||||
| Switch LLM provider | Status 200 | Response contains expected model name |
|
||||
| Enable feature flag | No errors | Feature behavior actually active |
|
||||
| Change environment | Deploy succeeds | Logs/vars reference new environment |
|
||||
| Set credentials | Auth succeeds | Authenticated user/context is correct |
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE claiming configuration change works:
|
||||
|
||||
1. IDENTIFY: What should be DIFFERENT after this change?
|
||||
2. LOCATE: Where is that difference observable?
|
||||
- Response field (model name, user ID)
|
||||
- Log line (environment, provider)
|
||||
- Behavior (feature active/inactive)
|
||||
3. RUN: Command that shows the observable difference
|
||||
4. VERIFY: Output contains expected difference
|
||||
5. ONLY THEN: Claim configuration change works
|
||||
|
||||
Red flags:
|
||||
- "Request succeeded" without checking content
|
||||
- Checking status code but not response body
|
||||
- Verifying no errors but not positive confirmation
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
Forces verification of INTENT, not just operation success.
|
||||
|
||||
---
|
||||
|
||||
### 2. subagent-driven-development: Add Process Hygiene for E2E Tests
|
||||
|
||||
**Add new section:**
|
||||
|
||||
```markdown
|
||||
## Process Hygiene for E2E Tests
|
||||
|
||||
When dispatching subagents that start services (servers, databases, message queues):
|
||||
|
||||
### Problem
|
||||
|
||||
Subagents are stateless - they don't know about processes started by previous subagents. Background processes persist and can interfere with later tests.
|
||||
|
||||
### Solution
|
||||
|
||||
**Before dispatching E2E test subagent, include cleanup in prompt:**
|
||||
|
||||
```
|
||||
BEFORE starting any services:
|
||||
1. Kill existing processes: pkill -f "<service-pattern>" 2>/dev/null || true
|
||||
2. Wait for cleanup: sleep 1
|
||||
3. Verify port free: lsof -i :<port> && echo "ERROR: Port still in use" || echo "Port free"
|
||||
|
||||
AFTER tests complete:
|
||||
1. Kill the process you started
|
||||
2. Verify cleanup: pgrep -f "<service-pattern>" || echo "Cleanup successful"
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
Task: Run E2E test of API server
|
||||
|
||||
Prompt includes:
|
||||
"Before starting the server:
|
||||
- Kill any existing servers: pkill -f 'node.*server.js' 2>/dev/null || true
|
||||
- Verify port 3001 is free: lsof -i :3001 && exit 1 || echo 'Port available'
|
||||
|
||||
After tests:
|
||||
- Kill the server you started
|
||||
- Verify: pgrep -f 'node.*server.js' || echo 'Cleanup verified'"
|
||||
```
|
||||
|
||||
### Why This Matters
|
||||
|
||||
- Stale processes serve requests with wrong config
|
||||
- Port conflicts cause silent failures
|
||||
- Process accumulation slows system
|
||||
- Confusing test results (hitting wrong server)
|
||||
```
|
||||
|
||||
**Trade-off analysis:**
|
||||
- Adds boilerplate to prompts
|
||||
- But prevents very confusing debugging
|
||||
- Worth it for E2E test subagents
|
||||
|
||||
---
|
||||
|
||||
### 3. subagent-driven-development: Add Lean Context Option
|
||||
|
||||
**Modify Step 2: Execute Task with Subagent**
|
||||
|
||||
**Before:**
|
||||
```
|
||||
Read that task carefully from [plan-file].
|
||||
```
|
||||
|
||||
**After:**
|
||||
```
|
||||
## Context Approaches
|
||||
|
||||
**Full Plan (default):**
|
||||
Use when tasks are complex or have dependencies:
|
||||
```
|
||||
Read Task N from [plan-file] carefully.
|
||||
```
|
||||
|
||||
**Lean Context (for independent tasks):**
|
||||
Use when task is standalone and pattern-based:
|
||||
```
|
||||
You are implementing: [1-2 sentence task description]
|
||||
|
||||
File to modify: [exact path]
|
||||
Pattern to follow: [reference to existing function/test]
|
||||
What to implement: [specific requirement]
|
||||
Verification: [exact command to run]
|
||||
|
||||
[Do NOT include full plan file]
|
||||
```
|
||||
|
||||
**Use lean context when:**
|
||||
- Task follows existing pattern (add similar test, implement similar feature)
|
||||
- Task is self-contained (doesn't need context from other tasks)
|
||||
- Pattern reference is sufficient (e.g., "follow TestE2E_FeatureOptionValidation")
|
||||
|
||||
**Use full plan when:**
|
||||
- Task has dependencies on other tasks
|
||||
- Requires understanding of overall architecture
|
||||
- Complex logic that needs context
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Lean context prompt:
|
||||
|
||||
"You are adding a test for privileged mode in devcontainer features.
|
||||
|
||||
File: pkg/runner/e2e_test.go
|
||||
Pattern: Follow TestE2E_FeatureOptionValidation (at end of file)
|
||||
Test: Feature with `"privileged": true` in metadata results in `--privileged` flag
|
||||
Verify: go test -v ./pkg/runner -run TestE2E_FeaturePrivilegedMode -timeout 5m
|
||||
|
||||
Report: Implementation, test results, any issues."
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
Reduces token usage, increases focus, faster completion when appropriate.
|
||||
|
||||
---
|
||||
|
||||
### 4. subagent-driven-development: Add Self-Reflection Step
|
||||
|
||||
**Modify Step 2: Execute Task with Subagent**
|
||||
|
||||
**Add to prompt template:**
|
||||
|
||||
```
|
||||
When done, BEFORE reporting back:
|
||||
|
||||
Take a step back and review your work with fresh eyes.
|
||||
|
||||
Ask yourself:
|
||||
- Does this actually solve the task as specified?
|
||||
- Are there edge cases I didn't consider?
|
||||
- Did I follow the pattern correctly?
|
||||
- If tests are failing, what's the ROOT CAUSE (implementation bug vs test bug)?
|
||||
- What could be better about this implementation?
|
||||
|
||||
If you identify issues during this reflection, fix them now.
|
||||
|
||||
Then report:
|
||||
- What you implemented
|
||||
- Self-reflection findings (if any)
|
||||
- Test results
|
||||
- Files changed
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
Catches bugs implementer can find themselves before handoff. Documented case: identified entrypoint bug through self-reflection.
|
||||
|
||||
**Trade-off:**
|
||||
Adds ~30 seconds per task, but catches issues before review.
|
||||
|
||||
---
|
||||
|
||||
### 5. requesting-code-review: Add Explicit File Reading
|
||||
|
||||
**Modify the code-reviewer template:**
|
||||
|
||||
**Add at the beginning:**
|
||||
|
||||
```markdown
|
||||
## Files to Review
|
||||
|
||||
BEFORE analyzing, read these files:
|
||||
|
||||
1. [List specific files that changed in the diff]
|
||||
2. [Files referenced by changes but not modified]
|
||||
|
||||
Use Read tool to load each file.
|
||||
|
||||
If you cannot find a file:
|
||||
- Check exact path from diff
|
||||
- Try alternate locations
|
||||
- Report: "Cannot locate [path] - please verify file exists"
|
||||
|
||||
DO NOT proceed with review until you've read the actual code.
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
Explicit instruction prevents "file not found" issues.
|
||||
|
||||
---
|
||||
|
||||
### 6. testing-anti-patterns: Add Mock-Interface Drift Anti-Pattern
|
||||
|
||||
**Add new Anti-Pattern 6:**
|
||||
|
||||
```markdown
|
||||
## Anti-Pattern 6: Mocks Derived from Implementation
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// Code (BUGGY) calls cleanup()
|
||||
await adapter.cleanup();
|
||||
|
||||
// Mock (MATCHES BUG) has cleanup()
|
||||
const mock = {
|
||||
cleanup: vi.fn().mockResolvedValue(undefined)
|
||||
};
|
||||
|
||||
// Interface (CORRECT) defines close()
|
||||
interface PlatformAdapter {
|
||||
close(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Mock encodes the bug into the test
|
||||
- TypeScript can't catch inline mocks with wrong method names
|
||||
- Test passes because both code and mock are wrong
|
||||
- Runtime crashes when real object is used
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Derive mock from interface
|
||||
|
||||
// Step 1: Open interface definition (PlatformAdapter)
|
||||
// Step 2: List methods defined there (close, initialize, etc.)
|
||||
// Step 3: Mock EXACTLY those methods
|
||||
|
||||
const mock = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined), // From interface!
|
||||
};
|
||||
|
||||
// Now test FAILS because code calls cleanup() which doesn't exist
|
||||
// That failure reveals the bug BEFORE runtime
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE writing any mock:
|
||||
|
||||
1. STOP - Do NOT look at the code under test yet
|
||||
2. FIND: The interface/type definition for the dependency
|
||||
3. READ: The interface file
|
||||
4. LIST: Methods defined in the interface
|
||||
5. MOCK: ONLY those methods with EXACTLY those names
|
||||
6. DO NOT: Look at what your code calls
|
||||
|
||||
IF your test fails because code calls something not in mock:
|
||||
✅ GOOD - The test found a bug in your code
|
||||
Fix the code to call the correct interface method
|
||||
NOT the mock
|
||||
|
||||
Red flags:
|
||||
- "I'll mock what the code calls"
|
||||
- Copying method names from implementation
|
||||
- Mock written without reading interface
|
||||
- "The test is failing so I'll add this method to the mock"
|
||||
```
|
||||
|
||||
**Detection:**
|
||||
|
||||
When you see runtime error "X is not a function" and tests pass:
|
||||
1. Check if X is mocked
|
||||
2. Compare mock methods to interface methods
|
||||
3. Look for method name mismatches
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
Directly addresses the failure pattern from feedback.
|
||||
|
||||
---
|
||||
|
||||
### 7. subagent-driven-development: Require Skills Reading for Test Subagents
|
||||
|
||||
**Add to prompt template when task involves testing:**
|
||||
|
||||
```markdown
|
||||
BEFORE writing any tests:
|
||||
|
||||
1. Read testing-anti-patterns skill:
|
||||
Use Skill tool: superpowers:testing-anti-patterns
|
||||
|
||||
2. Apply gate functions from that skill when:
|
||||
- Writing mocks
|
||||
- Adding methods to production classes
|
||||
- Mocking dependencies
|
||||
|
||||
This is NOT optional. Tests that violate anti-patterns will be rejected in review.
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
Ensures skills are actually used, not just exist.
|
||||
|
||||
**Trade-off:**
|
||||
Adds time to each task, but prevents entire classes of bugs.
|
||||
|
||||
---
|
||||
|
||||
### 8. subagent-driven-development: Allow Implementer to Fix Self-Identified Issues
|
||||
|
||||
**Modify Step 2:**
|
||||
|
||||
**Current:**
|
||||
```
|
||||
Subagent reports back with summary of work.
|
||||
```
|
||||
|
||||
**Proposed:**
|
||||
```
|
||||
Subagent performs self-reflection, then:
|
||||
|
||||
IF self-reflection identifies fixable issues:
|
||||
1. Fix the issues
|
||||
2. Re-run verification
|
||||
3. Report: "Initial implementation + self-reflection fix"
|
||||
|
||||
ELSE:
|
||||
Report: "Implementation complete"
|
||||
|
||||
Include in report:
|
||||
- Self-reflection findings
|
||||
- Whether fixes were applied
|
||||
- Final verification results
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
Reduces latency when implementer already knows the fix. Documented case: would have saved one round-trip for entrypoint bug.
|
||||
|
||||
**Trade-off:**
|
||||
Slightly more complex prompt, but faster end-to-end.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: High-Impact, Low-Risk (Do First)
|
||||
|
||||
1. **verification-before-completion: Configuration change verification**
|
||||
- Clear addition, doesn't change existing content
|
||||
- Addresses high-impact problem (false confidence in tests)
|
||||
- File: `skills/verification-before-completion/SKILL.md`
|
||||
|
||||
2. **testing-anti-patterns: Mock-interface drift**
|
||||
- Adds new anti-pattern, doesn't modify existing
|
||||
- Addresses high-impact problem (runtime crashes)
|
||||
- File: `skills/testing-anti-patterns/SKILL.md`
|
||||
|
||||
3. **requesting-code-review: Explicit file reading**
|
||||
- Simple addition to template
|
||||
- Fixes concrete problem (reviewers can't find files)
|
||||
- File: `skills/requesting-code-review/SKILL.md`
|
||||
|
||||
### Phase 2: Moderate Changes (Test Carefully)
|
||||
|
||||
4. **subagent-driven-development: Process hygiene**
|
||||
- Adds new section, doesn't change workflow
|
||||
- Addresses medium-high impact (test reliability)
|
||||
- File: `skills/subagent-driven-development/SKILL.md`
|
||||
|
||||
5. **subagent-driven-development: Self-reflection**
|
||||
- Changes prompt template (higher risk)
|
||||
- But documented to catch bugs
|
||||
- File: `skills/subagent-driven-development/SKILL.md`
|
||||
|
||||
6. **subagent-driven-development: Skills reading requirement**
|
||||
- Adds prompt overhead
|
||||
- But ensures skills are actually used
|
||||
- File: `skills/subagent-driven-development/SKILL.md`
|
||||
|
||||
### Phase 3: Optimization (Validate First)
|
||||
|
||||
7. **subagent-driven-development: Lean context option**
|
||||
- Adds complexity (two approaches)
|
||||
- Needs validation that it doesn't cause confusion
|
||||
- File: `skills/subagent-driven-development/SKILL.md`
|
||||
|
||||
8. **subagent-driven-development: Allow implementer to fix**
|
||||
- Changes workflow (higher risk)
|
||||
- Optimization, not bug fix
|
||||
- File: `skills/subagent-driven-development/SKILL.md`
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Lean context approach:**
|
||||
- Should we make it the default for pattern-based tasks?
|
||||
- How do we decide which approach to use?
|
||||
- Risk of being too lean and missing important context?
|
||||
|
||||
2. **Self-reflection:**
|
||||
- Will this slow down simple tasks significantly?
|
||||
- Should it only apply to complex tasks?
|
||||
- How do we prevent "reflection fatigue" where it becomes rote?
|
||||
|
||||
3. **Process hygiene:**
|
||||
- Should this be in subagent-driven-development or a separate skill?
|
||||
- Does it apply to other workflows beyond E2E tests?
|
||||
- How do we handle cases where process SHOULD persist (dev servers)?
|
||||
|
||||
4. **Skills reading enforcement:**
|
||||
- Should we require ALL subagents to read relevant skills?
|
||||
- How do we keep prompts from becoming too long?
|
||||
- Risk of over-documenting and losing focus?
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
How do we know these improvements work?
|
||||
|
||||
1. **Configuration verification:**
|
||||
- Zero instances of "test passed but wrong config was used"
|
||||
- Jesse doesn't say "that's not actually testing what you think"
|
||||
|
||||
2. **Process hygiene:**
|
||||
- Zero instances of "test hit wrong server"
|
||||
- No port conflict errors during E2E test runs
|
||||
|
||||
3. **Mock-interface drift:**
|
||||
- Zero instances of "tests pass but runtime crashes on missing method"
|
||||
- No method name mismatches between mocks and interfaces
|
||||
|
||||
4. **Self-reflection:**
|
||||
- Measurable: Do implementer reports include self-reflection findings?
|
||||
- Qualitative: Do fewer bugs make it to code review?
|
||||
|
||||
5. **Skills reading:**
|
||||
- Subagent reports reference skill gate functions
|
||||
- Fewer anti-pattern violations in code review
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
### Risk: Prompt Bloat
|
||||
**Problem:** Adding all these requirements makes prompts overwhelming
|
||||
**Mitigation:**
|
||||
- Phase implementation (don't add everything at once)
|
||||
- Make some additions conditional (E2E hygiene only for E2E tests)
|
||||
- Consider templates for different task types
|
||||
|
||||
### Risk: Analysis Paralysis
|
||||
**Problem:** Too much reflection/verification slows execution
|
||||
**Mitigation:**
|
||||
- Keep gate functions quick (seconds, not minutes)
|
||||
- Make lean context opt-in initially
|
||||
- Monitor task completion times
|
||||
|
||||
### Risk: False Sense of Security
|
||||
**Problem:** Following checklist doesn't guarantee correctness
|
||||
**Mitigation:**
|
||||
- Emphasize gate functions are minimums, not maximums
|
||||
- Keep "use judgment" language in skills
|
||||
- Document that skills catch common failures, not all failures
|
||||
|
||||
### Risk: Skill Divergence
|
||||
**Problem:** Different skills give conflicting advice
|
||||
**Mitigation:**
|
||||
- Review changes across all skills for consistency
|
||||
- Document how skills interact (Integration sections)
|
||||
- Test with real scenarios before deployment
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Proceed with Phase 1 immediately:**
|
||||
- verification-before-completion: Configuration change verification
|
||||
- testing-anti-patterns: Mock-interface drift
|
||||
- requesting-code-review: Explicit file reading
|
||||
|
||||
**Test Phase 2 with Jesse before finalizing:**
|
||||
- Get feedback on self-reflection impact
|
||||
- Validate process hygiene approach
|
||||
- Confirm skills reading requirement is worth overhead
|
||||
|
||||
**Hold Phase 3 pending validation:**
|
||||
- Lean context needs real-world testing
|
||||
- Implementer-fix workflow change needs careful evaluation
|
||||
|
||||
These changes address real problems documented by users while minimizing risk of making skills worse.
|
||||
571
docs/plans/2026-01-17-visual-brainstorming.md
Normal file
571
docs/plans/2026-01-17-visual-brainstorming.md
Normal file
@@ -0,0 +1,571 @@
|
||||
# Visual Brainstorming Companion Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Give Claude a browser-based visual companion for brainstorming sessions - show mockups, prototypes, and interactive choices alongside terminal conversation.
|
||||
|
||||
**Architecture:** Claude writes HTML to a temp file. A local Node.js server watches that file and serves it with an auto-injected helper library. User interactions flow via WebSocket to server stdout, which Claude sees in background task output.
|
||||
|
||||
**Tech Stack:** Node.js, Express, ws (WebSocket), chokidar (file watching)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create the Server Foundation
|
||||
|
||||
**Files:**
|
||||
- Create: `lib/brainstorm-server/index.js`
|
||||
- Create: `lib/brainstorm-server/package.json`
|
||||
|
||||
**Step 1: Create package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "brainstorm-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Visual brainstorming companion server for Claude Code",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.3",
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.14.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Create minimal server that starts**
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
const chokidar = require('chokidar');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = process.env.BRAINSTORM_PORT || 3333;
|
||||
const SCREEN_FILE = process.env.BRAINSTORM_SCREEN || '/tmp/brainstorm/screen.html';
|
||||
const SCREEN_DIR = path.dirname(SCREEN_FILE);
|
||||
|
||||
// Ensure screen directory exists
|
||||
if (!fs.existsSync(SCREEN_DIR)) {
|
||||
fs.mkdirSync(SCREEN_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Create default screen if none exists
|
||||
if (!fs.existsSync(SCREEN_FILE)) {
|
||||
fs.writeFileSync(SCREEN_FILE, `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Brainstorm Companion</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; }
|
||||
p { color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Brainstorm Companion</h1>
|
||||
<p>Waiting for Claude to push a screen...</p>
|
||||
</body>
|
||||
</html>`);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const wss = new WebSocket.Server({ server });
|
||||
|
||||
// Track connected browsers for reload notifications
|
||||
const clients = new Set();
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
clients.add(ws);
|
||||
ws.on('close', () => clients.delete(ws));
|
||||
|
||||
ws.on('message', (data) => {
|
||||
// User interaction event - write to stdout for Claude
|
||||
const event = JSON.parse(data.toString());
|
||||
console.log(JSON.stringify({ type: 'user-event', ...event }));
|
||||
});
|
||||
});
|
||||
|
||||
// Serve current screen with helper.js injected
|
||||
app.get('/', (req, res) => {
|
||||
let html = fs.readFileSync(SCREEN_FILE, 'utf-8');
|
||||
|
||||
// Inject helper script before </body>
|
||||
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
|
||||
const injection = `<script>\n${helperScript}\n</script>`;
|
||||
|
||||
if (html.includes('</body>')) {
|
||||
html = html.replace('</body>', `${injection}\n</body>`);
|
||||
} else {
|
||||
html += injection;
|
||||
}
|
||||
|
||||
res.type('html').send(html);
|
||||
});
|
||||
|
||||
// Watch for screen file changes
|
||||
chokidar.watch(SCREEN_FILE).on('change', () => {
|
||||
console.log(JSON.stringify({ type: 'screen-updated', file: SCREEN_FILE }));
|
||||
// Notify all browsers to reload
|
||||
clients.forEach(ws => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'reload' }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(JSON.stringify({ type: 'server-started', port: PORT, url: `http://localhost:${PORT}` }));
|
||||
});
|
||||
```
|
||||
|
||||
**Step 3: Run npm install**
|
||||
|
||||
Run: `cd lib/brainstorm-server && npm install`
|
||||
Expected: Dependencies installed
|
||||
|
||||
**Step 4: Test server starts**
|
||||
|
||||
Run: `cd lib/brainstorm-server && timeout 3 node index.js || true`
|
||||
Expected: See JSON with `server-started` and port info
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/brainstorm-server/
|
||||
git commit -m "feat: add brainstorm server foundation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Create the Helper Library
|
||||
|
||||
**Files:**
|
||||
- Create: `lib/brainstorm-server/helper.js`
|
||||
|
||||
**Step 1: Create helper.js with event auto-capture**
|
||||
|
||||
```javascript
|
||||
(function() {
|
||||
const WS_URL = 'ws://' + window.location.host;
|
||||
let ws = null;
|
||||
let eventQueue = [];
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(WS_URL);
|
||||
|
||||
ws.onopen = () => {
|
||||
// Send any queued events
|
||||
eventQueue.forEach(e => ws.send(JSON.stringify(e)));
|
||||
eventQueue = [];
|
||||
};
|
||||
|
||||
ws.onmessage = (msg) => {
|
||||
const data = JSON.parse(msg.data);
|
||||
if (data.type === 'reload') {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
// Reconnect after 1 second
|
||||
setTimeout(connect, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
function send(event) {
|
||||
event.timestamp = Date.now();
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(event));
|
||||
} else {
|
||||
eventQueue.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-capture clicks on interactive elements
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('button, a, [data-choice], [role="button"], input[type="submit"]');
|
||||
if (!target) return;
|
||||
|
||||
// Don't capture regular link navigation
|
||||
if (target.tagName === 'A' && !target.dataset.choice) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
send({
|
||||
type: 'click',
|
||||
text: target.textContent.trim(),
|
||||
choice: target.dataset.choice || null,
|
||||
id: target.id || null,
|
||||
className: target.className || null
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-capture form submissions
|
||||
document.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const data = {};
|
||||
formData.forEach((value, key) => { data[key] = value; });
|
||||
|
||||
send({
|
||||
type: 'submit',
|
||||
formId: form.id || null,
|
||||
formName: form.name || null,
|
||||
data: data
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-capture input changes (debounced)
|
||||
let inputTimeout = null;
|
||||
document.addEventListener('input', (e) => {
|
||||
const target = e.target;
|
||||
if (!target.matches('input, textarea, select')) return;
|
||||
|
||||
clearTimeout(inputTimeout);
|
||||
inputTimeout = setTimeout(() => {
|
||||
send({
|
||||
type: 'input',
|
||||
name: target.name || null,
|
||||
id: target.id || null,
|
||||
value: target.value,
|
||||
inputType: target.type || target.tagName.toLowerCase()
|
||||
});
|
||||
}, 500); // 500ms debounce
|
||||
});
|
||||
|
||||
// Expose for explicit use if needed
|
||||
window.brainstorm = {
|
||||
send: send,
|
||||
choice: (value, metadata = {}) => send({ type: 'choice', value, ...metadata })
|
||||
};
|
||||
|
||||
connect();
|
||||
})();
|
||||
```
|
||||
|
||||
**Step 2: Verify helper.js is syntactically valid**
|
||||
|
||||
Run: `node -c lib/brainstorm-server/helper.js`
|
||||
Expected: No syntax errors
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/brainstorm-server/helper.js
|
||||
git commit -m "feat: add browser helper library for event capture"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Write Tests for the Server
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/brainstorm-server/server.test.js`
|
||||
- Create: `tests/brainstorm-server/package.json`
|
||||
|
||||
**Step 1: Create test package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "brainstorm-server-tests",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"test": "node server.test.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Write server tests**
|
||||
|
||||
```javascript
|
||||
const { spawn } = require('child_process');
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const SERVER_PATH = path.join(__dirname, '../../lib/brainstorm-server/index.js');
|
||||
const TEST_PORT = 3334;
|
||||
const TEST_SCREEN = '/tmp/brainstorm-test/screen.html';
|
||||
|
||||
// Clean up test directory
|
||||
function cleanup() {
|
||||
if (fs.existsSync(path.dirname(TEST_SCREEN))) {
|
||||
fs.rmSync(path.dirname(TEST_SCREEN), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function fetch(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(url, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
cleanup();
|
||||
|
||||
// Start server
|
||||
const server = spawn('node', [SERVER_PATH], {
|
||||
env: { ...process.env, BRAINSTORM_PORT: TEST_PORT, BRAINSTORM_SCREEN: TEST_SCREEN }
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
server.stdout.on('data', (data) => { stdout += data.toString(); });
|
||||
server.stderr.on('data', (data) => { console.error('Server stderr:', data.toString()); });
|
||||
|
||||
await sleep(1000); // Wait for server to start
|
||||
|
||||
try {
|
||||
// Test 1: Server starts and outputs JSON
|
||||
console.log('Test 1: Server startup message');
|
||||
assert(stdout.includes('server-started'), 'Should output server-started');
|
||||
assert(stdout.includes(TEST_PORT.toString()), 'Should include port');
|
||||
console.log(' PASS');
|
||||
|
||||
// Test 2: GET / returns HTML with helper injected
|
||||
console.log('Test 2: Serves HTML with helper injected');
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.body.includes('brainstorm'), 'Should include brainstorm content');
|
||||
assert(res.body.includes('WebSocket'), 'Should have helper.js injected');
|
||||
console.log(' PASS');
|
||||
|
||||
// Test 3: WebSocket connection and event relay
|
||||
console.log('Test 3: WebSocket relays events to stdout');
|
||||
stdout = ''; // Reset stdout capture
|
||||
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
|
||||
await new Promise(resolve => ws.on('open', resolve));
|
||||
|
||||
ws.send(JSON.stringify({ type: 'click', text: 'Test Button' }));
|
||||
await sleep(100);
|
||||
|
||||
assert(stdout.includes('user-event'), 'Should relay user events');
|
||||
assert(stdout.includes('Test Button'), 'Should include event data');
|
||||
ws.close();
|
||||
console.log(' PASS');
|
||||
|
||||
// Test 4: File change triggers reload notification
|
||||
console.log('Test 4: File change notifies browsers');
|
||||
const ws2 = new WebSocket(`ws://localhost:${TEST_PORT}`);
|
||||
await new Promise(resolve => ws2.on('open', resolve));
|
||||
|
||||
let gotReload = false;
|
||||
ws2.on('message', (data) => {
|
||||
const msg = JSON.parse(data.toString());
|
||||
if (msg.type === 'reload') gotReload = true;
|
||||
});
|
||||
|
||||
// Modify the screen file
|
||||
fs.writeFileSync(TEST_SCREEN, '<html><body>Updated</body></html>');
|
||||
await sleep(500);
|
||||
|
||||
assert(gotReload, 'Should send reload message on file change');
|
||||
ws2.close();
|
||||
console.log(' PASS');
|
||||
|
||||
console.log('\nAll tests passed!');
|
||||
|
||||
} finally {
|
||||
server.kill();
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
runTests().catch(err => {
|
||||
console.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
**Step 3: Run tests**
|
||||
|
||||
Run: `cd tests/brainstorm-server && npm install ws && node server.test.js`
|
||||
Expected: All tests pass
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/brainstorm-server/
|
||||
git commit -m "test: add brainstorm server integration tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add Visual Companion to Brainstorming Skill
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/brainstorming/SKILL.md`
|
||||
- Create: `skills/brainstorming/visual-companion.md` (supporting doc)
|
||||
|
||||
**Step 1: Create the supporting documentation**
|
||||
|
||||
Create `skills/brainstorming/visual-companion.md`:
|
||||
|
||||
```markdown
|
||||
# Visual Companion Reference
|
||||
|
||||
## Starting the Server
|
||||
|
||||
Run as a background job:
|
||||
|
||||
```bash
|
||||
node ${PLUGIN_ROOT}/lib/brainstorm-server/index.js
|
||||
```
|
||||
|
||||
Tell the user: "I've started a visual companion at http://localhost:3333 - open it in a browser."
|
||||
|
||||
## Pushing Screens
|
||||
|
||||
Write HTML to `/tmp/brainstorm/screen.html`. The server watches this file and auto-refreshes the browser.
|
||||
|
||||
## Reading User Responses
|
||||
|
||||
Check the background task output for JSON events:
|
||||
|
||||
```json
|
||||
{"type":"user-event","type":"click","text":"Option A","choice":"optionA","timestamp":1234567890}
|
||||
{"type":"user-event","type":"submit","data":{"notes":"My feedback"},"timestamp":1234567891}
|
||||
```
|
||||
|
||||
Event types:
|
||||
- **click**: User clicked button or `data-choice` element
|
||||
- **submit**: User submitted form (includes all form data)
|
||||
- **input**: User typed in field (debounced 500ms)
|
||||
|
||||
## HTML Patterns
|
||||
|
||||
### Choice Cards
|
||||
|
||||
```html
|
||||
<div class="options">
|
||||
<button data-choice="optionA">
|
||||
<h3>Option A</h3>
|
||||
<p>Description</p>
|
||||
</button>
|
||||
<button data-choice="optionB">
|
||||
<h3>Option B</h3>
|
||||
<p>Description</p>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Interactive Mockup
|
||||
|
||||
```html
|
||||
<div class="mockup">
|
||||
<header data-choice="header">App Header</header>
|
||||
<nav data-choice="nav">Navigation</nav>
|
||||
<main data-choice="main">Content</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Form with Notes
|
||||
|
||||
```html
|
||||
<form>
|
||||
<label>Priority: <input type="range" name="priority" min="1" max="5"></label>
|
||||
<textarea name="notes" placeholder="Additional thoughts..."></textarea>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Explicit JavaScript
|
||||
|
||||
```html
|
||||
<button onclick="brainstorm.choice('custom', {extra: 'data'})">Custom</button>
|
||||
```
|
||||
```
|
||||
|
||||
**Step 2: Add visual companion section to brainstorming skill**
|
||||
|
||||
Add after "Key Principles" in `skills/brainstorming/SKILL.md`:
|
||||
|
||||
```markdown
|
||||
|
||||
## Visual Companion (Optional)
|
||||
|
||||
When brainstorming involves visual elements - UI mockups, wireframes, interactive prototypes - use the browser-based visual companion.
|
||||
|
||||
**When to use:**
|
||||
- Presenting UI/UX options that benefit from visual comparison
|
||||
- Showing wireframes or layout options
|
||||
- Gathering structured feedback (ratings, forms)
|
||||
- Prototyping click interactions
|
||||
|
||||
**How it works:**
|
||||
1. Start the server as a background job
|
||||
2. Tell user to open http://localhost:3333
|
||||
3. Write HTML to `/tmp/brainstorm/screen.html` (auto-refreshes)
|
||||
4. Check background task output for user interactions
|
||||
|
||||
The terminal remains the primary conversation interface. The browser is a visual aid.
|
||||
|
||||
**Reference:** See `visual-companion.md` in this skill directory for HTML patterns and API details.
|
||||
```
|
||||
|
||||
**Step 3: Verify the edits**
|
||||
|
||||
Run: `grep -A5 "Visual Companion" skills/brainstorming/SKILL.md`
|
||||
Expected: Shows the new section
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/brainstorming/
|
||||
git commit -m "feat: add visual companion to brainstorming skill"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Add Server to Plugin Ignore (Optional Cleanup)
|
||||
|
||||
**Files:**
|
||||
- Check if `.gitignore` needs node_modules exclusion for lib/brainstorm-server
|
||||
|
||||
**Step 1: Check current gitignore**
|
||||
|
||||
Run: `cat .gitignore 2>/dev/null || echo "No .gitignore"`
|
||||
|
||||
**Step 2: Add node_modules if needed**
|
||||
|
||||
If not already present, add:
|
||||
```
|
||||
lib/brainstorm-server/node_modules/
|
||||
```
|
||||
|
||||
**Step 3: Commit if changed**
|
||||
|
||||
```bash
|
||||
git add .gitignore
|
||||
git commit -m "chore: ignore brainstorm-server node_modules"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
After completing all tasks:
|
||||
|
||||
1. **Server** at `lib/brainstorm-server/` - Node.js server that watches HTML file and relays events
|
||||
2. **Helper library** auto-injected - captures clicks, forms, inputs
|
||||
3. **Tests** at `tests/brainstorm-server/` - verifies server behavior
|
||||
4. **Brainstorming skill** updated with visual companion section and `visual-companion.md` reference doc
|
||||
|
||||
**To use:**
|
||||
1. Start server as background job: `node lib/brainstorm-server/index.js &`
|
||||
2. Tell user to open `http://localhost:3333`
|
||||
3. Write HTML to `/tmp/brainstorm/screen.html`
|
||||
4. Check task output for user events
|
||||
301
docs/superpowers/plans/2026-01-22-document-review-system.md
Normal file
301
docs/superpowers/plans/2026-01-22-document-review-system.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# Document Review System Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan.
|
||||
|
||||
**Goal:** Add spec and plan document review loops to the brainstorming and writing-plans skills.
|
||||
|
||||
**Architecture:** Create reviewer prompt templates in each skill directory. Modify skill files to add review loops after document creation. Use Task tool with general-purpose subagent for reviewer dispatch.
|
||||
|
||||
**Tech Stack:** Markdown skill files, subagent dispatch via Task tool
|
||||
|
||||
**Spec:** docs/superpowers/specs/2026-01-22-document-review-system-design.md
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: Spec Document Reviewer
|
||||
|
||||
This chunk adds the spec document reviewer to the brainstorming skill.
|
||||
|
||||
### Task 1: Create Spec Document Reviewer Prompt Template
|
||||
|
||||
**Files:**
|
||||
- Create: `skills/brainstorming/spec-document-reviewer-prompt.md`
|
||||
|
||||
- [ ] **Step 1:** Create the reviewer prompt template file
|
||||
|
||||
```markdown
|
||||
# Spec Document Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a spec document reviewer subagent.
|
||||
|
||||
**Purpose:** Verify the spec is complete, consistent, and ready for implementation planning.
|
||||
|
||||
**Dispatch after:** Spec document is written to docs/superpowers/specs/
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Review spec document"
|
||||
prompt: |
|
||||
You are a spec document reviewer. Verify this spec is complete and ready for planning.
|
||||
|
||||
**Spec to review:** [SPEC_FILE_PATH]
|
||||
|
||||
## What to Check
|
||||
|
||||
| Category | What to Look For |
|
||||
|----------|------------------|
|
||||
| Completeness | TODOs, placeholders, "TBD", incomplete sections |
|
||||
| Coverage | Missing error handling, edge cases, integration points |
|
||||
| Consistency | Internal contradictions, conflicting requirements |
|
||||
| Clarity | Ambiguous requirements |
|
||||
| YAGNI | Unrequested features, over-engineering |
|
||||
|
||||
## CRITICAL
|
||||
|
||||
Look especially hard for:
|
||||
- Any TODO markers or placeholder text
|
||||
- Sections saying "to be defined later" or "will spec when X is done"
|
||||
- Sections noticeably less detailed than others
|
||||
|
||||
## Output Format
|
||||
|
||||
## Spec Review
|
||||
|
||||
**Status:** ✅ Approved | ❌ Issues Found
|
||||
|
||||
**Issues (if any):**
|
||||
- [Section X]: [specific issue] - [why it matters]
|
||||
|
||||
**Recommendations (advisory):**
|
||||
- [suggestions that don't block approval]
|
||||
```
|
||||
|
||||
**Reviewer returns:** Status, Issues (if any), Recommendations
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** Verify the file was created correctly
|
||||
|
||||
Run: `cat skills/brainstorming/spec-document-reviewer-prompt.md | head -20`
|
||||
Expected: Shows the header and purpose section
|
||||
|
||||
- [ ] **Step 3:** Commit
|
||||
|
||||
```bash
|
||||
git add skills/brainstorming/spec-document-reviewer-prompt.md
|
||||
git commit -m "feat: add spec document reviewer prompt template"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add Review Loop to Brainstorming Skill
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/brainstorming/SKILL.md`
|
||||
|
||||
- [ ] **Step 1:** Read the current brainstorming skill
|
||||
|
||||
Run: `cat skills/brainstorming/SKILL.md`
|
||||
|
||||
- [ ] **Step 2:** Add the review loop section after "After the Design"
|
||||
|
||||
Find the "After the Design" section and add a new "Spec Review Loop" section after documentation but before implementation:
|
||||
|
||||
```markdown
|
||||
**Spec Review Loop:**
|
||||
After writing the spec document:
|
||||
1. Dispatch spec-document-reviewer subagent (see spec-document-reviewer-prompt.md)
|
||||
2. If ❌ Issues Found:
|
||||
- Fix the issues in the spec document
|
||||
- Re-dispatch reviewer
|
||||
- Repeat until ✅ Approved
|
||||
3. If ✅ Approved: proceed to implementation setup
|
||||
|
||||
**Review loop guidance:**
|
||||
- Same agent that wrote the spec fixes it (preserves context)
|
||||
- If loop exceeds 5 iterations, surface to human for guidance
|
||||
- Reviewers are advisory - explain disagreements if you believe feedback is incorrect
|
||||
```
|
||||
|
||||
- [ ] **Step 3:** Verify the changes
|
||||
|
||||
Run: `grep -A 15 "Spec Review Loop" skills/brainstorming/SKILL.md`
|
||||
Expected: Shows the new review loop section
|
||||
|
||||
- [ ] **Step 4:** Commit
|
||||
|
||||
```bash
|
||||
git add skills/brainstorming/SKILL.md
|
||||
git commit -m "feat: add spec review loop to brainstorming skill"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: Plan Document Reviewer
|
||||
|
||||
This chunk adds the plan document reviewer to the writing-plans skill.
|
||||
|
||||
### Task 3: Create Plan Document Reviewer Prompt Template
|
||||
|
||||
**Files:**
|
||||
- Create: `skills/writing-plans/plan-document-reviewer-prompt.md`
|
||||
|
||||
- [ ] **Step 1:** Create the reviewer prompt template file
|
||||
|
||||
```markdown
|
||||
# Plan Document Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a plan document reviewer subagent.
|
||||
|
||||
**Purpose:** Verify the plan chunk is complete, matches the spec, and has proper task decomposition.
|
||||
|
||||
**Dispatch after:** Each plan chunk is written
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Review plan chunk N"
|
||||
prompt: |
|
||||
You are a plan document reviewer. Verify this plan chunk is complete and ready for implementation.
|
||||
|
||||
**Plan chunk to review:** [PLAN_FILE_PATH] - Chunk N only
|
||||
**Spec for reference:** [SPEC_FILE_PATH]
|
||||
|
||||
## What to Check
|
||||
|
||||
| Category | What to Look For |
|
||||
|----------|------------------|
|
||||
| Completeness | TODOs, placeholders, incomplete tasks, missing steps |
|
||||
| Spec Alignment | Chunk covers relevant spec requirements, no scope creep |
|
||||
| Task Decomposition | Tasks atomic, clear boundaries, steps actionable |
|
||||
| Task Syntax | Checkbox syntax (`- [ ]`) on tasks and steps |
|
||||
| Chunk Size | Each chunk under 1000 lines |
|
||||
|
||||
## CRITICAL
|
||||
|
||||
Look especially hard for:
|
||||
- Any TODO markers or placeholder text
|
||||
- Steps that say "similar to X" without actual content
|
||||
- Incomplete task definitions
|
||||
- Missing verification steps or expected outputs
|
||||
|
||||
## Output Format
|
||||
|
||||
## Plan Review - Chunk N
|
||||
|
||||
**Status:** ✅ Approved | ❌ Issues Found
|
||||
|
||||
**Issues (if any):**
|
||||
- [Task X, Step Y]: [specific issue] - [why it matters]
|
||||
|
||||
**Recommendations (advisory):**
|
||||
- [suggestions that don't block approval]
|
||||
```
|
||||
|
||||
**Reviewer returns:** Status, Issues (if any), Recommendations
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** Verify the file was created
|
||||
|
||||
Run: `cat skills/writing-plans/plan-document-reviewer-prompt.md | head -20`
|
||||
Expected: Shows the header and purpose section
|
||||
|
||||
- [ ] **Step 3:** Commit
|
||||
|
||||
```bash
|
||||
git add skills/writing-plans/plan-document-reviewer-prompt.md
|
||||
git commit -m "feat: add plan document reviewer prompt template"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add Review Loop to Writing-Plans Skill
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/writing-plans/SKILL.md`
|
||||
|
||||
- [ ] **Step 1:** Read current skill file
|
||||
|
||||
Run: `cat skills/writing-plans/SKILL.md`
|
||||
|
||||
- [ ] **Step 2:** Add chunk-by-chunk review section
|
||||
|
||||
Add before the "Execution Handoff" section:
|
||||
|
||||
```markdown
|
||||
## Plan Review Loop
|
||||
|
||||
After completing each chunk of the plan:
|
||||
|
||||
1. Dispatch plan-document-reviewer subagent for the current chunk
|
||||
- Provide: chunk content, path to spec document
|
||||
2. If ❌ Issues Found:
|
||||
- Fix the issues in the chunk
|
||||
- Re-dispatch reviewer for that chunk
|
||||
- Repeat until ✅ Approved
|
||||
3. If ✅ Approved: proceed to next chunk (or execution handoff if last chunk)
|
||||
|
||||
**Chunk boundaries:** Use `## Chunk N: <name>` headings to delimit chunks. Each chunk should be ≤1000 lines and logically self-contained.
|
||||
```
|
||||
|
||||
- [ ] **Step 3:** Update task syntax examples to use checkboxes
|
||||
|
||||
Change the Task Structure section to show checkbox syntax:
|
||||
|
||||
```markdown
|
||||
### Task N: [Component Name]
|
||||
|
||||
- [ ] **Step 1:** Write the failing test
|
||||
- File: `tests/path/test.py`
|
||||
...
|
||||
```
|
||||
|
||||
- [ ] **Step 4:** Verify the review loop section was added
|
||||
|
||||
Run: `grep -A 15 "Plan Review Loop" skills/writing-plans/SKILL.md`
|
||||
Expected: Shows the new review loop section
|
||||
|
||||
- [ ] **Step 5:** Verify the task syntax examples were updated
|
||||
|
||||
Run: `grep -A 5 "Task N:" skills/writing-plans/SKILL.md`
|
||||
Expected: Shows checkbox syntax `### Task N:`
|
||||
|
||||
- [ ] **Step 6:** Commit
|
||||
|
||||
```bash
|
||||
git add skills/writing-plans/SKILL.md
|
||||
git commit -m "feat: add plan review loop and checkbox syntax to writing-plans skill"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: Update Plan Document Header
|
||||
|
||||
This chunk updates the plan document header template to reference the new checkbox syntax requirements.
|
||||
|
||||
### Task 5: Update Plan Header Template in Writing-Plans Skill
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/writing-plans/SKILL.md`
|
||||
|
||||
- [ ] **Step 1:** Read current plan header template
|
||||
|
||||
Run: `grep -A 20 "Plan Document Header" skills/writing-plans/SKILL.md`
|
||||
|
||||
- [ ] **Step 2:** Update the header template to reference checkbox syntax
|
||||
|
||||
The plan header should note that tasks and steps use checkbox syntax. Update the header comment:
|
||||
|
||||
```markdown
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Tasks and steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
```
|
||||
|
||||
- [ ] **Step 3:** Verify the change
|
||||
|
||||
Run: `grep -A 5 "For agentic workers:" skills/writing-plans/SKILL.md`
|
||||
Expected: Shows updated header with checkbox syntax mention
|
||||
|
||||
- [ ] **Step 4:** Commit
|
||||
|
||||
```bash
|
||||
git add skills/writing-plans/SKILL.md
|
||||
git commit -m "docs: update plan header to reference checkbox syntax"
|
||||
```
|
||||
@@ -0,0 +1,523 @@
|
||||
# Visual Brainstorming Refactor Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Refactor visual brainstorming from blocking TUI feedback model to non-blocking "Browser Displays, Terminal Commands" architecture.
|
||||
|
||||
**Architecture:** Browser becomes an interactive display; terminal stays the conversation channel. Server writes user events to a per-screen `.events` file that Claude reads on its next turn. Eliminates `wait-for-feedback.sh` and all `TaskOutput` blocking.
|
||||
|
||||
**Tech Stack:** Node.js (Express, ws, chokidar), vanilla HTML/CSS/JS
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-02-19-visual-brainstorming-refactor-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|------|--------|---------------|
|
||||
| `lib/brainstorm-server/index.js` | Modify | Server: add `.events` file writing, clear on new screen, replace `wrapInFrame` |
|
||||
| `lib/brainstorm-server/frame-template.html` | Modify | Template: remove feedback footer, add content placeholder + selection indicator |
|
||||
| `lib/brainstorm-server/helper.js` | Modify | Client JS: remove send/feedback functions, narrow to click capture + indicator updates |
|
||||
| `lib/brainstorm-server/wait-for-feedback.sh` | Delete | No longer needed |
|
||||
| `skills/brainstorming/visual-companion.md` | Modify | Skill instructions: rewrite loop to non-blocking flow |
|
||||
| `tests/brainstorm-server/server.test.js` | Modify | Tests: update for new template structure and helper.js API |
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: Server, Template, Client, Tests, Skill
|
||||
|
||||
### Task 1: Update `frame-template.html`
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/brainstorm-server/frame-template.html`
|
||||
|
||||
- [ ] **Step 1: Remove the feedback footer HTML**
|
||||
|
||||
Replace the feedback-footer div (lines 227-233) with a selection indicator bar:
|
||||
|
||||
```html
|
||||
<div class="indicator-bar">
|
||||
<span id="indicator-text">Click an option above, then return to the terminal</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
Also replace the default content inside `#claude-content` (lines 220-223) with the content placeholder:
|
||||
|
||||
```html
|
||||
<div id="claude-content">
|
||||
<!-- CONTENT -->
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace feedback footer CSS with indicator bar CSS**
|
||||
|
||||
Remove the `.feedback-footer`, `.feedback-footer label`, `.feedback-row`, and the textarea/button styles within `.feedback-footer` (lines 82-112).
|
||||
|
||||
Add indicator bar CSS:
|
||||
|
||||
```css
|
||||
.indicator-bar {
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 0.5rem 1.5rem;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.indicator-bar span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.indicator-bar .selected-text {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify template renders**
|
||||
|
||||
Run the test suite to check the template still loads:
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
Expected: Tests 1-5 should still pass. Tests 6-8 may fail (expected — they assert old structure).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/brainstorm-server/frame-template.html
|
||||
git commit -m "Replace feedback footer with selection indicator bar in brainstorm template"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update `index.js` — content injection and `.events` file
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/brainstorm-server/index.js`
|
||||
|
||||
- [ ] **Step 1: Write failing test for `.events` file writing**
|
||||
|
||||
Add to `tests/brainstorm-server/server.test.js` after Test 4 area — a new test that sends a WebSocket event with a `choice` field and verifies `.events` file is written:
|
||||
|
||||
```javascript
|
||||
// Test: Choice events written to .events file
|
||||
console.log('Test: Choice events written to .events file');
|
||||
const ws3 = new WebSocket(`ws://localhost:${TEST_PORT}`);
|
||||
await new Promise(resolve => ws3.on('open', resolve));
|
||||
|
||||
ws3.send(JSON.stringify({ type: 'click', choice: 'a', text: 'Option A' }));
|
||||
await sleep(300);
|
||||
|
||||
const eventsFile = path.join(TEST_DIR, '.events');
|
||||
assert(fs.existsSync(eventsFile), '.events file should exist after choice click');
|
||||
const lines = fs.readFileSync(eventsFile, 'utf-8').trim().split('\n');
|
||||
const event = JSON.parse(lines[lines.length - 1]);
|
||||
assert.strictEqual(event.choice, 'a', 'Event should contain choice');
|
||||
assert.strictEqual(event.text, 'Option A', 'Event should contain text');
|
||||
ws3.close();
|
||||
console.log(' PASS');
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
Expected: New test FAILS — `.events` file doesn't exist yet.
|
||||
|
||||
- [ ] **Step 3: Write failing test for `.events` file clearing on new screen**
|
||||
|
||||
Add another test:
|
||||
|
||||
```javascript
|
||||
// Test: .events cleared on new screen
|
||||
console.log('Test: .events cleared on new screen');
|
||||
// .events file should still exist from previous test
|
||||
assert(fs.existsSync(path.join(TEST_DIR, '.events')), '.events should exist before new screen');
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'new-screen.html'), '<h2>New screen</h2>');
|
||||
await sleep(500);
|
||||
assert(!fs.existsSync(path.join(TEST_DIR, '.events')), '.events should be cleared after new screen');
|
||||
console.log(' PASS');
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
Expected: New test FAILS — `.events` not cleared on screen push.
|
||||
|
||||
- [ ] **Step 5: Implement `.events` file writing in `index.js`**
|
||||
|
||||
In the WebSocket `message` handler (line 74-77 of `index.js`), after the `console.log`, add:
|
||||
|
||||
```javascript
|
||||
// Write user events to .events file for Claude to read
|
||||
if (event.choice) {
|
||||
const eventsFile = path.join(SCREEN_DIR, '.events');
|
||||
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
|
||||
}
|
||||
```
|
||||
|
||||
In the chokidar `add` handler (line 104-111), add `.events` clearing:
|
||||
|
||||
```javascript
|
||||
if (filePath.endsWith('.html')) {
|
||||
// Clear events from previous screen
|
||||
const eventsFile = path.join(SCREEN_DIR, '.events');
|
||||
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
|
||||
|
||||
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
|
||||
// ... existing reload broadcast
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Replace `wrapInFrame` with comment placeholder injection**
|
||||
|
||||
Replace the `wrapInFrame` function (lines 27-32 of `index.js`):
|
||||
|
||||
```javascript
|
||||
function wrapInFrame(content) {
|
||||
return frameTemplate.replace('<!-- CONTENT -->', content);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Run all tests**
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
Expected: New `.events` tests PASS. Existing tests may still have failures from old assertions (fixed in Task 4).
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/brainstorm-server/index.js tests/brainstorm-server/server.test.js
|
||||
git commit -m "Add .events file writing and comment-based content injection to brainstorm server"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Simplify `helper.js`
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/brainstorm-server/helper.js`
|
||||
|
||||
- [ ] **Step 1: Remove `sendToClaude` function**
|
||||
|
||||
Delete the `sendToClaude` function (lines 92-106) — the function body and the page takeover HTML.
|
||||
|
||||
- [ ] **Step 2: Remove `window.send` function**
|
||||
|
||||
Delete the `window.send` function (lines 120-129) — was tied to the removed Send button.
|
||||
|
||||
- [ ] **Step 3: Remove form submission and input change handlers**
|
||||
|
||||
Delete the form submission handler (lines 57-71) and the input change handler (lines 73-89) including the `inputTimeout` variable.
|
||||
|
||||
- [ ] **Step 4: Remove `pageshow` event listener**
|
||||
|
||||
Delete the `pageshow` listener we added earlier (no textarea to clear anymore).
|
||||
|
||||
- [ ] **Step 5: Narrow click handler to `[data-choice]` only**
|
||||
|
||||
Replace the click handler (lines 36-55) with a narrower version:
|
||||
|
||||
```javascript
|
||||
// Capture clicks on choice elements
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-choice]');
|
||||
if (!target) return;
|
||||
|
||||
sendEvent({
|
||||
type: 'click',
|
||||
text: target.textContent.trim(),
|
||||
choice: target.dataset.choice,
|
||||
id: target.id || null
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Add indicator bar update on choice click**
|
||||
|
||||
After the `sendEvent` call in the click handler, add:
|
||||
|
||||
```javascript
|
||||
// Update indicator bar
|
||||
const indicator = document.getElementById('indicator-text');
|
||||
if (indicator) {
|
||||
const label = target.querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || target.dataset.choice;
|
||||
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Remove `sendToClaude` from `window.brainstorm` API**
|
||||
|
||||
Update the `window.brainstorm` object (lines 132-136) to remove `sendToClaude`:
|
||||
|
||||
```javascript
|
||||
window.brainstorm = {
|
||||
send: sendEvent,
|
||||
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Run tests**
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/brainstorm-server/helper.js
|
||||
git commit -m "Simplify helper.js: remove feedback functions, narrow to choice capture + indicator"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Update tests for new structure
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/brainstorm-server/server.test.js`
|
||||
|
||||
**Note:** Line references below are from the _original_ file. Task 2 inserted new tests earlier in the file, so actual line numbers will be shifted. Find tests by their `console.log` labels (e.g., "Test 5:", "Test 6:").
|
||||
|
||||
- [ ] **Step 1: Update Test 5 (full document assertion)**
|
||||
|
||||
Find the Test 5 assertion `!fullRes.body.includes('feedback-footer')`. Change it to: Full documents should NOT have the indicator bar either (they're served as-is):
|
||||
|
||||
```javascript
|
||||
assert(!fullRes.body.includes('indicator-bar') || fullDoc.includes('indicator-bar'),
|
||||
'Should not wrap full documents in frame template');
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update Test 6 (fragment wrapping)**
|
||||
|
||||
Line 125: Replace `feedback-footer` assertion with indicator bar assertion:
|
||||
|
||||
```javascript
|
||||
assert(fragRes.body.includes('indicator-bar'), 'Fragment should get indicator bar from frame');
|
||||
```
|
||||
|
||||
Also verify content placeholder was replaced (fragment content appears, placeholder comment doesn't):
|
||||
|
||||
```javascript
|
||||
assert(!fragRes.body.includes('<!-- CONTENT -->'), 'Content placeholder should be replaced');
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update Test 7 (helper.js API)**
|
||||
|
||||
Lines 140-142: Update assertions to reflect the new API surface:
|
||||
|
||||
```javascript
|
||||
assert(helperContent.includes('toggleSelect'), 'helper.js should define toggleSelect');
|
||||
assert(helperContent.includes('sendEvent'), 'helper.js should define sendEvent');
|
||||
assert(helperContent.includes('selectedChoice'), 'helper.js should track selectedChoice');
|
||||
assert(helperContent.includes('brainstorm'), 'helper.js should expose brainstorm API');
|
||||
assert(!helperContent.includes('sendToClaude'), 'helper.js should not contain sendToClaude');
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Replace Test 8 (sendToClaude theming) with indicator bar test**
|
||||
|
||||
Replace Test 8 (lines 145-149) — `sendToClaude` no longer exists. Test the indicator bar instead:
|
||||
|
||||
```javascript
|
||||
// Test 8: Indicator bar uses CSS variables (theme support)
|
||||
console.log('Test 8: Indicator bar uses CSS variables');
|
||||
const templateContent = fs.readFileSync(
|
||||
path.join(__dirname, '../../lib/brainstorm-server/frame-template.html'), 'utf-8'
|
||||
);
|
||||
assert(templateContent.includes('indicator-bar'), 'Template should have indicator bar');
|
||||
assert(templateContent.includes('indicator-text'), 'Template should have indicator text element');
|
||||
console.log(' PASS');
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run full test suite**
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
Expected: ALL tests PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/brainstorm-server/server.test.js
|
||||
git commit -m "Update brainstorm server tests for new template structure and helper.js API"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Delete `wait-for-feedback.sh`
|
||||
|
||||
**Files:**
|
||||
- Delete: `lib/brainstorm-server/wait-for-feedback.sh`
|
||||
|
||||
- [ ] **Step 1: Verify no other files import or reference `wait-for-feedback.sh`**
|
||||
|
||||
Search the codebase:
|
||||
```bash
|
||||
grep -r "wait-for-feedback" /Users/drewritter/prime-rad/superpowers/ --include="*.js" --include="*.md" --include="*.sh" --include="*.json"
|
||||
```
|
||||
|
||||
Expected references: only `visual-companion.md` (rewritten in Task 6) and possibly release notes (historical, leave as-is).
|
||||
|
||||
- [ ] **Step 2: Delete the file**
|
||||
|
||||
```bash
|
||||
rm lib/brainstorm-server/wait-for-feedback.sh
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run tests to confirm nothing breaks**
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
Expected: All tests PASS (no test referenced this file).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add -u lib/brainstorm-server/wait-for-feedback.sh
|
||||
git commit -m "Delete wait-for-feedback.sh: replaced by .events file"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Rewrite `visual-companion.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/brainstorming/visual-companion.md`
|
||||
|
||||
- [ ] **Step 1: Update "How It Works" description (line 18)**
|
||||
|
||||
Replace the sentence about receiving feedback "as JSON" with:
|
||||
|
||||
```markdown
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content, the user sees it in their browser and can click to select options. Selections are recorded to a `.events` file that you read on your next turn.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update fragment description (line 20)**
|
||||
|
||||
Remove "feedback footer" from the description of what the frame template provides:
|
||||
|
||||
```markdown
|
||||
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Rewrite "The Loop" section (lines 36-61)**
|
||||
|
||||
Replace the entire "The Loop" section with:
|
||||
|
||||
```markdown
|
||||
## The Loop
|
||||
|
||||
1. **Write HTML** to a new file in `screen_dir`:
|
||||
- Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html`
|
||||
- **Never reuse filenames** — each screen gets a fresh file
|
||||
- Use Write tool — **never use cat/heredoc** (dumps noise into terminal)
|
||||
- Server automatically serves the newest file
|
||||
|
||||
2. **Tell user what to expect and end your turn:**
|
||||
- Remind them of the URL (every step, not just first)
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
|
||||
- Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like."
|
||||
|
||||
3. **On your next turn** — after the user responds in the terminal:
|
||||
- Read `$SCREEN_DIR/.events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines
|
||||
- Merge with the user's terminal text to get the full picture
|
||||
- The terminal message is the primary feedback; `.events` provides structured interaction data
|
||||
|
||||
4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated.
|
||||
|
||||
5. Repeat until done.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Replace "User Feedback Format" section (lines 165-174)**
|
||||
|
||||
Replace with:
|
||||
|
||||
```markdown
|
||||
## Browser Events Format
|
||||
|
||||
When the user clicks options in the browser, their interactions are recorded to `$SCREEN_DIR/.events` (one JSON object per line). The file is cleared automatically when you push a new screen.
|
||||
|
||||
```jsonl
|
||||
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
|
||||
{"type":"click","choice":"c","text":"Option C - Complex Grid","timestamp":1706000108}
|
||||
{"type":"click","choice":"b","text":"Option B - Hybrid","timestamp":1706000115}
|
||||
```
|
||||
|
||||
The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about.
|
||||
|
||||
If `.events` doesn't exist, the user didn't interact with the browser — use only their terminal text.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update "Writing Content Fragments" description (line 65)**
|
||||
|
||||
Remove "feedback footer" reference:
|
||||
|
||||
```markdown
|
||||
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Update Reference section (lines 200-203)**
|
||||
|
||||
Remove the helper.js reference description about "JS API" — the API is now minimal. Keep the path reference:
|
||||
|
||||
```markdown
|
||||
## Reference
|
||||
|
||||
- Frame template (CSS reference): `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/frame-template.html`
|
||||
- Helper script (client-side): `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/helper.js`
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/brainstorming/visual-companion.md
|
||||
git commit -m "Rewrite visual-companion.md for non-blocking browser-displays-terminal-commands flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Final verification
|
||||
|
||||
- [ ] **Step 1: Run full test suite**
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
Expected: ALL tests PASS.
|
||||
|
||||
- [ ] **Step 2: Manual smoke test**
|
||||
|
||||
Start the server manually and verify the flow works end-to-end:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers && lib/brainstorm-server/start-server.sh --project-dir /tmp/brainstorm-smoke-test
|
||||
```
|
||||
|
||||
Write a test fragment, open in browser, click an option, verify `.events` file is written, verify indicator bar updates. Then stop the server:
|
||||
|
||||
```bash
|
||||
lib/brainstorm-server/stop-server.sh <screen_dir from start output>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify no stale references remain**
|
||||
|
||||
```bash
|
||||
grep -r "wait-for-feedback\|sendToClaude\|feedback-footer\|send-to-claude\|TaskOutput.*block.*true" /Users/drewritter/prime-rad/superpowers/ --include="*.js" --include="*.md" --include="*.sh" --include="*.html" | grep -v node_modules | grep -v RELEASE-NOTES | grep -v "\.md:.*spec\|plan"
|
||||
```
|
||||
|
||||
Expected: No hits outside of release notes and the spec/plan docs (which are historical).
|
||||
|
||||
- [ ] **Step 4: Final commit if any cleanup needed**
|
||||
|
||||
```bash
|
||||
git status
|
||||
# Review untracked/modified files, stage specific files as needed, commit if clean
|
||||
```
|
||||
479
docs/superpowers/plans/2026-03-11-zero-dep-brainstorm-server.md
Normal file
479
docs/superpowers/plans/2026-03-11-zero-dep-brainstorm-server.md
Normal file
@@ -0,0 +1,479 @@
|
||||
# Zero-Dependency Brainstorm Server Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the brainstorm server's vendored node_modules with a single zero-dependency `server.js` using Node built-ins.
|
||||
|
||||
**Architecture:** Single file with WebSocket protocol (RFC 6455 text frames), HTTP server (`http` module), and file watching (`fs.watch`). Exports protocol functions for unit testing when required as a module.
|
||||
|
||||
**Tech Stack:** Node.js built-ins only: `http`, `crypto`, `fs`, `path`
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-11-zero-dep-brainstorm-server-design.md`
|
||||
|
||||
**Existing tests:** `tests/brainstorm-server/ws-protocol.test.js` (unit), `tests/brainstorm-server/server.test.js` (integration)
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
- **Create:** `skills/brainstorming/scripts/server.js` — the zero-dep replacement
|
||||
- **Modify:** `skills/brainstorming/scripts/start-server.sh:94,100` — change `index.js` to `server.js`
|
||||
- **Modify:** `.gitignore:6` — remove the `!skills/brainstorming/scripts/node_modules/` exception
|
||||
- **Delete:** `skills/brainstorming/scripts/index.js`
|
||||
- **Delete:** `skills/brainstorming/scripts/package.json`
|
||||
- **Delete:** `skills/brainstorming/scripts/package-lock.json`
|
||||
- **Delete:** `skills/brainstorming/scripts/node_modules/` (714 files)
|
||||
- **No changes:** `skills/brainstorming/scripts/helper.js`, `skills/brainstorming/scripts/frame-template.html`, `skills/brainstorming/scripts/stop-server.sh`
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: WebSocket Protocol Layer
|
||||
|
||||
### Task 1: Implement WebSocket protocol exports
|
||||
|
||||
**Files:**
|
||||
- Create: `skills/brainstorming/scripts/server.js`
|
||||
- Test: `tests/brainstorm-server/ws-protocol.test.js` (already exists)
|
||||
|
||||
- [ ] **Step 1: Create server.js with OPCODES constant and computeAcceptKey**
|
||||
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
|
||||
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
|
||||
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
|
||||
function computeAcceptKey(clientKey) {
|
||||
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement encodeFrame**
|
||||
|
||||
Server frames are never masked. Three length encodings:
|
||||
- payload < 126: 2-byte header (FIN+opcode, length)
|
||||
- 126-65535: 4-byte header (FIN+opcode, 126, 16-bit length)
|
||||
- > 65535: 10-byte header (FIN+opcode, 127, 64-bit length)
|
||||
|
||||
```js
|
||||
function encodeFrame(opcode, payload) {
|
||||
const fin = 0x80;
|
||||
const len = payload.length;
|
||||
let header;
|
||||
|
||||
if (len < 126) {
|
||||
header = Buffer.alloc(2);
|
||||
header[0] = fin | opcode;
|
||||
header[1] = len;
|
||||
} else if (len < 65536) {
|
||||
header = Buffer.alloc(4);
|
||||
header[0] = fin | opcode;
|
||||
header[1] = 126;
|
||||
header.writeUInt16BE(len, 2);
|
||||
} else {
|
||||
header = Buffer.alloc(10);
|
||||
header[0] = fin | opcode;
|
||||
header[1] = 127;
|
||||
header.writeBigUInt64BE(BigInt(len), 2);
|
||||
}
|
||||
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Implement decodeFrame**
|
||||
|
||||
Client frames are always masked. Returns `{ opcode, payload, bytesConsumed }` or `null` for incomplete. Throws on unmasked frames.
|
||||
|
||||
```js
|
||||
function decodeFrame(buffer) {
|
||||
if (buffer.length < 2) return null;
|
||||
|
||||
const firstByte = buffer[0];
|
||||
const secondByte = buffer[1];
|
||||
const opcode = firstByte & 0x0F;
|
||||
const masked = (secondByte & 0x80) !== 0;
|
||||
let payloadLen = secondByte & 0x7F;
|
||||
let offset = 2;
|
||||
|
||||
if (!masked) throw new Error('Client frames must be masked');
|
||||
|
||||
if (payloadLen === 126) {
|
||||
if (buffer.length < 4) return null;
|
||||
payloadLen = buffer.readUInt16BE(2);
|
||||
offset = 4;
|
||||
} else if (payloadLen === 127) {
|
||||
if (buffer.length < 10) return null;
|
||||
payloadLen = Number(buffer.readBigUInt64BE(2));
|
||||
offset = 10;
|
||||
}
|
||||
|
||||
const maskOffset = offset;
|
||||
const dataOffset = offset + 4;
|
||||
const totalLen = dataOffset + payloadLen;
|
||||
if (buffer.length < totalLen) return null;
|
||||
|
||||
const mask = buffer.slice(maskOffset, dataOffset);
|
||||
const data = Buffer.alloc(payloadLen);
|
||||
for (let i = 0; i < payloadLen; i++) {
|
||||
data[i] = buffer[dataOffset + i] ^ mask[i % 4];
|
||||
}
|
||||
|
||||
return { opcode, payload: data, bytesConsumed: totalLen };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add module exports at the bottom of the file**
|
||||
|
||||
```js
|
||||
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run unit tests**
|
||||
|
||||
Run: `cd tests/brainstorm-server && node ws-protocol.test.js`
|
||||
Expected: All tests pass (handshake, encoding, decoding, boundaries, edge cases)
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/brainstorming/scripts/server.js
|
||||
git commit -m "Add WebSocket protocol layer for zero-dep brainstorm server"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: HTTP Server and Application Logic
|
||||
|
||||
### Task 2: Add HTTP server, file watching, and WebSocket connection handling
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/brainstorming/scripts/server.js`
|
||||
- Test: `tests/brainstorm-server/server.test.js` (already exists)
|
||||
|
||||
- [ ] **Step 1: Add configuration and constants at top of server.js (after requires)**
|
||||
|
||||
```js
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
|
||||
const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
|
||||
const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
|
||||
const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
|
||||
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add WAITING_PAGE, template loading at module scope, and helper functions**
|
||||
|
||||
Load `frameTemplate` and `helperInjection` at module scope so they're accessible to `wrapInFrame` and `handleRequest`. They only read files from `__dirname` (the scripts directory), which is valid whether the module is required or run directly.
|
||||
|
||||
```js
|
||||
const WAITING_PAGE = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Brainstorm Companion</title>
|
||||
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; } p { color: #666; }</style>
|
||||
</head>
|
||||
<body><h1>Brainstorm Companion</h1>
|
||||
<p>Waiting for Claude to push a screen...</p></body></html>`;
|
||||
|
||||
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
|
||||
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
|
||||
const helperInjection = '<script>\n' + helperScript + '\n</script>';
|
||||
|
||||
function isFullDocument(html) {
|
||||
const trimmed = html.trimStart().toLowerCase();
|
||||
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
|
||||
}
|
||||
|
||||
function wrapInFrame(content) {
|
||||
return frameTemplate.replace('<!-- CONTENT -->', content);
|
||||
}
|
||||
|
||||
function getNewestScreen() {
|
||||
const files = fs.readdirSync(SCREEN_DIR)
|
||||
.filter(f => f.endsWith('.html'))
|
||||
.map(f => {
|
||||
const fp = path.join(SCREEN_DIR, f);
|
||||
return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
|
||||
})
|
||||
.sort((a, b) => b.mtime - a.mtime);
|
||||
return files.length > 0 ? files[0].path : null;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add HTTP request handler**
|
||||
|
||||
```js
|
||||
function handleRequest(req, res) {
|
||||
if (req.method === 'GET' && req.url === '/') {
|
||||
const screenFile = getNewestScreen();
|
||||
let html = screenFile
|
||||
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
|
||||
: WAITING_PAGE;
|
||||
|
||||
if (html.includes('</body>')) {
|
||||
html = html.replace('</body>', helperInjection + '\n</body>');
|
||||
} else {
|
||||
html += helperInjection;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(html);
|
||||
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
|
||||
const fileName = req.url.slice(7); // strip '/files/'
|
||||
const filePath = path.join(SCREEN_DIR, path.basename(fileName));
|
||||
if (!fs.existsSync(filePath)) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(fs.readFileSync(filePath));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add WebSocket connection handling**
|
||||
|
||||
```js
|
||||
const clients = new Set();
|
||||
|
||||
function handleUpgrade(req, socket) {
|
||||
const key = req.headers['sec-websocket-key'];
|
||||
if (!key) { socket.destroy(); return; }
|
||||
|
||||
const accept = computeAcceptKey(key);
|
||||
socket.write(
|
||||
'HTTP/1.1 101 Switching Protocols\r\n' +
|
||||
'Upgrade: websocket\r\n' +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'
|
||||
);
|
||||
|
||||
let buffer = Buffer.alloc(0);
|
||||
clients.add(socket);
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
while (buffer.length > 0) {
|
||||
let result;
|
||||
try {
|
||||
result = decodeFrame(buffer);
|
||||
} catch (e) {
|
||||
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
|
||||
clients.delete(socket);
|
||||
return;
|
||||
}
|
||||
if (!result) break;
|
||||
buffer = buffer.slice(result.bytesConsumed);
|
||||
|
||||
switch (result.opcode) {
|
||||
case OPCODES.TEXT:
|
||||
handleMessage(result.payload.toString());
|
||||
break;
|
||||
case OPCODES.CLOSE:
|
||||
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
|
||||
clients.delete(socket);
|
||||
return;
|
||||
case OPCODES.PING:
|
||||
socket.write(encodeFrame(OPCODES.PONG, result.payload));
|
||||
break;
|
||||
case OPCODES.PONG:
|
||||
break;
|
||||
default:
|
||||
// Unsupported opcode — close with 1003
|
||||
const closeBuf = Buffer.alloc(2);
|
||||
closeBuf.writeUInt16BE(1003);
|
||||
socket.end(encodeFrame(OPCODES.CLOSE, closeBuf));
|
||||
clients.delete(socket);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', () => clients.delete(socket));
|
||||
socket.on('error', () => clients.delete(socket));
|
||||
}
|
||||
|
||||
function handleMessage(text) {
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse WebSocket message:', e.message);
|
||||
return;
|
||||
}
|
||||
console.log(JSON.stringify({ source: 'user-event', ...event }));
|
||||
if (event.choice) {
|
||||
const eventsFile = path.join(SCREEN_DIR, '.events');
|
||||
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
function broadcast(msg) {
|
||||
const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg)));
|
||||
for (const socket of clients) {
|
||||
try { socket.write(frame); } catch (e) { clients.delete(socket); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add debounce timer map**
|
||||
|
||||
```js
|
||||
const debounceTimers = new Map();
|
||||
```
|
||||
|
||||
File watching logic is inlined in `startServer` (Step 6) to keep watcher lifecycle together with server lifecycle and include an `error` handler per spec.
|
||||
|
||||
- [ ] **Step 6: Add startServer function and conditional main**
|
||||
|
||||
`frameTemplate` and `helperInjection` are already at module scope (Step 2). `startServer` just creates the screen dir, starts the HTTP server, watcher, and logs startup info.
|
||||
|
||||
```js
|
||||
function startServer() {
|
||||
if (!fs.existsSync(SCREEN_DIR)) fs.mkdirSync(SCREEN_DIR, { recursive: true });
|
||||
|
||||
const server = http.createServer(handleRequest);
|
||||
server.on('upgrade', handleUpgrade);
|
||||
|
||||
const watcher = fs.watch(SCREEN_DIR, (eventType, filename) => {
|
||||
if (!filename || !filename.endsWith('.html')) return;
|
||||
if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename));
|
||||
debounceTimers.set(filename, setTimeout(() => {
|
||||
debounceTimers.delete(filename);
|
||||
const filePath = path.join(SCREEN_DIR, filename);
|
||||
if (eventType === 'rename' && fs.existsSync(filePath)) {
|
||||
const eventsFile = path.join(SCREEN_DIR, '.events');
|
||||
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
|
||||
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
|
||||
} else if (eventType === 'change') {
|
||||
console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
|
||||
}
|
||||
broadcast({ type: 'reload' });
|
||||
}, 100));
|
||||
});
|
||||
watcher.on('error', (err) => console.error('fs.watch error:', err.message));
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
const info = JSON.stringify({
|
||||
type: 'server-started', port: Number(PORT), host: HOST,
|
||||
url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT,
|
||||
screen_dir: SCREEN_DIR
|
||||
});
|
||||
console.log(info);
|
||||
fs.writeFileSync(path.join(SCREEN_DIR, '.server-info'), info + '\n');
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
startServer();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Run integration tests**
|
||||
|
||||
The test directory already has a `package.json` with `ws` as a dependency. Install it if needed, then run tests.
|
||||
|
||||
Run: `cd tests/brainstorm-server && npm install && node server.test.js`
|
||||
Expected: All tests pass
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/brainstorming/scripts/server.js
|
||||
git commit -m "Add HTTP server, WebSocket handling, and file watching to server.js"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: Swap and Cleanup
|
||||
|
||||
### Task 3: Update start-server.sh and remove old files
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/brainstorming/scripts/start-server.sh:94,100`
|
||||
- Modify: `.gitignore:6`
|
||||
- Delete: `skills/brainstorming/scripts/index.js`
|
||||
- Delete: `skills/brainstorming/scripts/package.json`
|
||||
- Delete: `skills/brainstorming/scripts/package-lock.json`
|
||||
- Delete: `skills/brainstorming/scripts/node_modules/` (entire directory)
|
||||
|
||||
- [ ] **Step 1: Update start-server.sh — change `index.js` to `server.js`**
|
||||
|
||||
Two lines to change:
|
||||
|
||||
Line 94: `env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" node server.js`
|
||||
|
||||
Line 100: `nohup env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" node server.js > "$LOG_FILE" 2>&1 &`
|
||||
|
||||
- [ ] **Step 2: Remove the gitignore exception for node_modules**
|
||||
|
||||
In `.gitignore`, delete line 6: `!skills/brainstorming/scripts/node_modules/`
|
||||
|
||||
- [ ] **Step 3: Delete old files**
|
||||
|
||||
```bash
|
||||
git rm skills/brainstorming/scripts/index.js
|
||||
git rm skills/brainstorming/scripts/package.json
|
||||
git rm skills/brainstorming/scripts/package-lock.json
|
||||
git rm -r skills/brainstorming/scripts/node_modules/
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run both test suites**
|
||||
|
||||
Run: `cd tests/brainstorm-server && node ws-protocol.test.js && node server.test.js`
|
||||
Expected: All tests pass
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/brainstorming/scripts/ .gitignore
|
||||
git commit -m "Remove vendored node_modules, swap to zero-dep server.js"
|
||||
```
|
||||
|
||||
### Task 4: Manual smoke test
|
||||
|
||||
- [ ] **Step 1: Start the server manually**
|
||||
|
||||
```bash
|
||||
cd skills/brainstorming/scripts
|
||||
BRAINSTORM_DIR=/tmp/brainstorm-smoke BRAINSTORM_PORT=9876 node server.js
|
||||
```
|
||||
|
||||
Expected: `server-started` JSON printed with port 9876
|
||||
|
||||
- [ ] **Step 2: Open browser to http://localhost:9876**
|
||||
|
||||
Expected: Waiting page with "Waiting for Claude to push a screen..."
|
||||
|
||||
- [ ] **Step 3: Write an HTML file to the screen directory**
|
||||
|
||||
```bash
|
||||
echo '<h2>Hello from smoke test</h2>' > /tmp/brainstorm-smoke/test.html
|
||||
```
|
||||
|
||||
Expected: Browser reloads and shows "Hello from smoke test" wrapped in frame template
|
||||
|
||||
- [ ] **Step 4: Verify WebSocket works — check browser console**
|
||||
|
||||
Open browser dev tools. The WebSocket connection should show as connected (no errors in console). The frame template's status indicator should show "Connected".
|
||||
|
||||
- [ ] **Step 5: Stop server with Ctrl-C, clean up**
|
||||
|
||||
```bash
|
||||
rm -rf /tmp/brainstorm-smoke
|
||||
```
|
||||
566
docs/superpowers/plans/2026-03-23-codex-app-compatibility.md
Normal file
566
docs/superpowers/plans/2026-03-23-codex-app-compatibility.md
Normal file
@@ -0,0 +1,566 @@
|
||||
# Codex App Compatibility Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make `using-git-worktrees`, `finishing-a-development-branch`, and related skills work in the Codex App's sandboxed worktree environment without breaking existing behavior.
|
||||
|
||||
**Architecture:** Read-only environment detection (`git-dir` vs `git-common-dir`) at the start of two skills. If already in a linked worktree, skip creation. If on detached HEAD, emit a handoff payload instead of the 4-option menu. Sandbox fallback catches permission errors during worktree creation.
|
||||
|
||||
**Tech Stack:** Git, Markdown (skill files are instruction documents, not executable code)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility | Action |
|
||||
|---|---|---|
|
||||
| `skills/using-git-worktrees/SKILL.md` | Worktree creation + isolation | Add Step 0 detection + sandbox fallback |
|
||||
| `skills/finishing-a-development-branch/SKILL.md` | Branch finishing workflow | Add Step 1.5 detection + cleanup guard |
|
||||
| `skills/subagent-driven-development/SKILL.md` | Plan execution with subagents | Update Integration description |
|
||||
| `skills/executing-plans/SKILL.md` | Plan execution inline | Update Integration description |
|
||||
| `skills/using-superpowers/references/codex-tools.md` | Codex platform reference | Add detection + finishing docs |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Step 0 to `using-git-worktrees`
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/using-git-worktrees/SKILL.md:14-15` (insert after Overview, before Directory Selection Process)
|
||||
|
||||
- [ ] **Step 1: Read the current skill file**
|
||||
|
||||
Read `skills/using-git-worktrees/SKILL.md` in full. Identify the exact insertion point: after the "Announce at start" line (line 14) and before "## Directory Selection Process" (line 16).
|
||||
|
||||
- [ ] **Step 2: Insert Step 0 section**
|
||||
|
||||
Insert the following between the Overview section and "## Directory Selection Process":
|
||||
|
||||
```markdown
|
||||
## Step 0: Check if Already in an Isolated Workspace
|
||||
|
||||
Before creating a worktree, check if one already exists:
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
**If `GIT_DIR` differs from `GIT_COMMON`:** You are already inside a linked worktree (created by the Codex App, Claude Code's Agent tool, a previous skill run, or the user). Do NOT create another worktree. Instead:
|
||||
|
||||
1. Run project setup (auto-detect package manager as in "Run Project Setup" below)
|
||||
2. Verify clean baseline (run tests as in "Verify Clean Baseline" below)
|
||||
3. Report with branch state:
|
||||
- On a branch: "Already in an isolated workspace at `<path>` on branch `<name>`. Tests passing. Ready to implement."
|
||||
- Detached HEAD: "Already in an isolated workspace at `<path>` (detached HEAD, externally managed). Tests passing. Note: branch creation needed at finish time. Ready to implement."
|
||||
|
||||
After reporting, STOP. Do not continue to Directory Selection or Creation Steps.
|
||||
|
||||
**If `GIT_DIR` equals `GIT_COMMON`:** Proceed with the full worktree creation flow below.
|
||||
|
||||
**Sandbox fallback:** If you proceed to Creation Steps but `git worktree add -b` fails with a permission error (e.g., "Operation not permitted"), treat this as a late-detected restricted environment. Fall back to the behavior above — run setup and baseline tests in the current directory, report accordingly, and STOP.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the insertion**
|
||||
|
||||
Read the file again. Confirm:
|
||||
- Step 0 appears between Overview and Directory Selection Process
|
||||
- The rest of the file (Directory Selection, Safety Verification, Creation Steps, etc.) is unchanged
|
||||
- No duplicate sections or broken markdown
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/using-git-worktrees/SKILL.md
|
||||
git commit -m "feat(using-git-worktrees): add Step 0 environment detection (PRI-823)
|
||||
|
||||
Skip worktree creation when already in a linked worktree. Includes
|
||||
sandbox fallback for permission errors on git worktree add."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update `using-git-worktrees` Integration section
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/using-git-worktrees/SKILL.md:211-215` (Integration > Called by)
|
||||
|
||||
- [ ] **Step 1: Update the three "Called by" entries**
|
||||
|
||||
Change lines 212-214 from:
|
||||
|
||||
```markdown
|
||||
- **brainstorming** (Phase 4) - REQUIRED when design is approved and implementation follows
|
||||
- **subagent-driven-development** - REQUIRED before executing any tasks
|
||||
- **executing-plans** - REQUIRED before executing any tasks
|
||||
```
|
||||
|
||||
To:
|
||||
|
||||
```markdown
|
||||
- **brainstorming** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
|
||||
- **subagent-driven-development** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
|
||||
- **executing-plans** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the Integration section**
|
||||
|
||||
Read the Integration section. Confirm all three entries are updated, "Pairs with" is unchanged.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/using-git-worktrees/SKILL.md
|
||||
git commit -m "docs(using-git-worktrees): update Integration descriptions (PRI-823)
|
||||
|
||||
Clarify that skill ensures a workspace exists, not that it always creates one."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add Step 1.5 to `finishing-a-development-branch`
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/finishing-a-development-branch/SKILL.md:38` (insert after Step 1, before Step 2)
|
||||
|
||||
- [ ] **Step 1: Read the current skill file**
|
||||
|
||||
Read `skills/finishing-a-development-branch/SKILL.md` in full. Identify the insertion point: after "**If tests pass:** Continue to Step 2." (line 38) and before "### Step 2: Determine Base Branch" (line 40).
|
||||
|
||||
- [ ] **Step 2: Insert Step 1.5 section**
|
||||
|
||||
Insert the following between Step 1 and Step 2:
|
||||
|
||||
```markdown
|
||||
### Step 1.5: Detect Environment
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
**Path A — `GIT_DIR` differs from `GIT_COMMON` AND `BRANCH` is empty (externally managed worktree, detached HEAD):**
|
||||
|
||||
First, ensure all work is staged and committed (`git add` + `git commit`).
|
||||
|
||||
Then present this to the user (do NOT present the 4-option menu):
|
||||
|
||||
```
|
||||
Implementation complete. All tests passing.
|
||||
Current HEAD: <full-commit-sha>
|
||||
|
||||
This workspace is externally managed (detached HEAD).
|
||||
I cannot create branches, push, or open PRs from here.
|
||||
|
||||
⚠ These commits are on a detached HEAD. If you do not create a branch,
|
||||
they may be lost when this workspace is cleaned up.
|
||||
|
||||
If your host application provides these controls:
|
||||
- "Create branch" — to name a branch, then commit/push/PR
|
||||
- "Hand off to local" — to move changes to your local checkout
|
||||
|
||||
Suggested branch name: <ticket-id/short-description>
|
||||
Suggested commit message: <summary-of-work>
|
||||
```
|
||||
|
||||
Branch name: use ticket ID if available (e.g., `pri-823/codex-compat`), otherwise slugify the first 5 words of the plan title, otherwise omit. Avoid sensitive content in branch names.
|
||||
|
||||
Skip to Step 5 (cleanup is a no-op — see guard below).
|
||||
|
||||
**Path B — `GIT_DIR` differs from `GIT_COMMON` AND `BRANCH` exists (externally managed worktree, named branch):**
|
||||
|
||||
Proceed to Step 2 and present the 4-option menu as normal.
|
||||
|
||||
**Path C — `GIT_DIR` equals `GIT_COMMON` (normal environment):**
|
||||
|
||||
Proceed to Step 2 and present the 4-option menu as normal.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the insertion**
|
||||
|
||||
Read the file again. Confirm:
|
||||
- Step 1.5 appears between Step 1 and Step 2
|
||||
- Steps 2-5 are unchanged
|
||||
- Path A handoff includes commit SHA and data loss warning
|
||||
- Paths B and C proceed to Step 2 normally
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/finishing-a-development-branch/SKILL.md
|
||||
git commit -m "feat(finishing-a-development-branch): add Step 1.5 environment detection (PRI-823)
|
||||
|
||||
Detect externally managed worktrees with detached HEAD and emit handoff
|
||||
payload instead of 4-option menu. Includes commit SHA and data loss warning."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add Step 5 cleanup guard to `finishing-a-development-branch`
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/finishing-a-development-branch/SKILL.md` (Step 5: Cleanup Worktree — find by section heading, line numbers will have shifted after Task 3)
|
||||
|
||||
- [ ] **Step 1: Read the current Step 5 section**
|
||||
|
||||
Find the "### Step 5: Cleanup Worktree" section in `skills/finishing-a-development-branch/SKILL.md` (line numbers will have shifted after Task 3's insertion). The current Step 5 is:
|
||||
|
||||
```markdown
|
||||
### Step 5: Cleanup Worktree
|
||||
|
||||
**For Options 1, 2, 4:**
|
||||
|
||||
Check if in worktree:
|
||||
```bash
|
||||
git worktree list | grep $(git branch --show-current)
|
||||
```
|
||||
|
||||
If yes:
|
||||
```bash
|
||||
git worktree remove <worktree-path>
|
||||
```
|
||||
|
||||
**For Option 3:** Keep worktree.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the cleanup guard before existing logic**
|
||||
|
||||
Replace the Step 5 section with:
|
||||
|
||||
```markdown
|
||||
### Step 5: Cleanup Worktree
|
||||
|
||||
**First, check if worktree is externally managed:**
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
```
|
||||
|
||||
If `GIT_DIR` differs from `GIT_COMMON`: skip worktree removal — the host environment owns this workspace.
|
||||
|
||||
**Otherwise, for Options 1 and 4:**
|
||||
|
||||
Check if in worktree:
|
||||
```bash
|
||||
git worktree list | grep $(git branch --show-current)
|
||||
```
|
||||
|
||||
If yes:
|
||||
```bash
|
||||
git worktree remove <worktree-path>
|
||||
```
|
||||
|
||||
**For Option 3:** Keep worktree.
|
||||
```
|
||||
|
||||
Note: the original text said "For Options 1, 2, 4" but the Quick Reference table and Common Mistakes section say "Options 1 & 4 only." This edit aligns Step 5 with those sections.
|
||||
|
||||
- [ ] **Step 3: Verify the replacement**
|
||||
|
||||
Read Step 5. Confirm:
|
||||
- Cleanup guard (re-detection) appears first
|
||||
- Existing removal logic preserved for non-externally-managed worktrees
|
||||
- "Options 1 and 4" (not "1, 2, 4") matches Quick Reference and Common Mistakes
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/finishing-a-development-branch/SKILL.md
|
||||
git commit -m "feat(finishing-a-development-branch): add Step 5 cleanup guard (PRI-823)
|
||||
|
||||
Re-detect externally managed worktree at cleanup time and skip removal.
|
||||
Also fixes pre-existing inconsistency: cleanup now correctly says
|
||||
Options 1 and 4 only, matching Quick Reference and Common Mistakes."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Update Integration lines in `subagent-driven-development` and `executing-plans`
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/subagent-driven-development/SKILL.md:268`
|
||||
- Modify: `skills/executing-plans/SKILL.md:68`
|
||||
|
||||
- [ ] **Step 1: Update `subagent-driven-development`**
|
||||
|
||||
Change line 268 from:
|
||||
```
|
||||
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
|
||||
```
|
||||
To:
|
||||
```
|
||||
- **superpowers:using-git-worktrees** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update `executing-plans`**
|
||||
|
||||
Change line 68 from:
|
||||
```
|
||||
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
|
||||
```
|
||||
To:
|
||||
```
|
||||
- **superpowers:using-git-worktrees** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify both files**
|
||||
|
||||
Read line 268 of `skills/subagent-driven-development/SKILL.md` and line 68 of `skills/executing-plans/SKILL.md`. Confirm both say "Ensures isolated workspace (creates one or verifies existing)".
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/subagent-driven-development/SKILL.md skills/executing-plans/SKILL.md
|
||||
git commit -m "docs(sdd, executing-plans): update worktree Integration descriptions (PRI-823)
|
||||
|
||||
Clarify that using-git-worktrees ensures a workspace exists rather than
|
||||
always creating one."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Add environment detection docs to `codex-tools.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/using-superpowers/references/codex-tools.md:25` (append at end)
|
||||
|
||||
- [ ] **Step 1: Read the current file**
|
||||
|
||||
Read `skills/using-superpowers/references/codex-tools.md` in full. Confirm it ends at line 25-26 after the multi_agent section.
|
||||
|
||||
- [ ] **Step 2: Append two new sections**
|
||||
|
||||
Add at the end of the file:
|
||||
|
||||
```markdown
|
||||
|
||||
## Environment Detection
|
||||
|
||||
Skills that create worktrees or finish branches should detect their
|
||||
environment with read-only git commands before proceeding:
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
- `GIT_DIR != GIT_COMMON` → already in a linked worktree (skip creation)
|
||||
- `BRANCH` empty → detached HEAD (cannot branch/push/PR from sandbox)
|
||||
|
||||
See `using-git-worktrees` Step 0 and `finishing-a-development-branch`
|
||||
Step 1.5 for how each skill uses these signals.
|
||||
|
||||
## Codex App Finishing
|
||||
|
||||
When the sandbox blocks branch/push operations (detached HEAD in an
|
||||
externally managed worktree), the agent commits all work and informs
|
||||
the user to use the App's native controls:
|
||||
|
||||
- **"Create branch"** — names the branch, then commit/push/PR via App UI
|
||||
- **"Hand off to local"** — transfers work to the user's local checkout
|
||||
|
||||
The agent can still run tests, stage files, and output suggested branch
|
||||
names, commit messages, and PR descriptions for the user to copy.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the additions**
|
||||
|
||||
Read the full file. Confirm:
|
||||
- Two new sections appear after the existing content
|
||||
- Bash code block renders correctly (not escaped)
|
||||
- Cross-references to Step 0 and Step 1.5 are present
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/using-superpowers/references/codex-tools.md
|
||||
git commit -m "docs(codex-tools): add environment detection and App finishing docs (PRI-823)
|
||||
|
||||
Document the git-dir vs git-common-dir detection pattern and the Codex
|
||||
App's native finishing flow for skills that need to adapt."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Automated test — environment detection
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/codex-app-compat/test-environment-detection.sh`
|
||||
|
||||
- [ ] **Step 1: Create test directory**
|
||||
|
||||
```bash
|
||||
mkdir -p tests/codex-app-compat
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the detection test script**
|
||||
|
||||
Create `tests/codex-app-compat/test-environment-detection.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test environment detection logic from PRI-823
|
||||
# Tests the git-dir vs git-common-dir comparison used by
|
||||
# using-git-worktrees Step 0 and finishing-a-development-branch Step 1.5
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TEMP_DIR" EXIT
|
||||
|
||||
log_pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
log_fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# Helper: run detection and return "linked" or "normal"
|
||||
detect_worktree() {
|
||||
local git_dir git_common
|
||||
git_dir=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
git_common=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
if [ "$git_dir" != "$git_common" ]; then
|
||||
echo "linked"
|
||||
else
|
||||
echo "normal"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== Test 1: Normal repo detection ==="
|
||||
cd "$TEMP_DIR"
|
||||
git init test-repo > /dev/null 2>&1
|
||||
cd test-repo
|
||||
git commit --allow-empty -m "init" > /dev/null 2>&1
|
||||
result=$(detect_worktree)
|
||||
if [ "$result" = "normal" ]; then
|
||||
log_pass "Normal repo detected as normal"
|
||||
else
|
||||
log_fail "Normal repo detected as '$result' (expected 'normal')"
|
||||
fi
|
||||
|
||||
echo "=== Test 2: Linked worktree detection ==="
|
||||
git worktree add "$TEMP_DIR/test-wt" -b test-branch > /dev/null 2>&1
|
||||
cd "$TEMP_DIR/test-wt"
|
||||
result=$(detect_worktree)
|
||||
if [ "$result" = "linked" ]; then
|
||||
log_pass "Linked worktree detected as linked"
|
||||
else
|
||||
log_fail "Linked worktree detected as '$result' (expected 'linked')"
|
||||
fi
|
||||
|
||||
echo "=== Test 3: Detached HEAD detection ==="
|
||||
git checkout --detach HEAD > /dev/null 2>&1
|
||||
branch=$(git branch --show-current)
|
||||
if [ -z "$branch" ]; then
|
||||
log_pass "Detached HEAD: branch is empty"
|
||||
else
|
||||
log_fail "Detached HEAD: branch is '$branch' (expected empty)"
|
||||
fi
|
||||
|
||||
echo "=== Test 4: Linked worktree + detached HEAD (Codex App simulation) ==="
|
||||
result=$(detect_worktree)
|
||||
branch=$(git branch --show-current)
|
||||
if [ "$result" = "linked" ] && [ -z "$branch" ]; then
|
||||
log_pass "Codex App simulation: linked + detached HEAD"
|
||||
else
|
||||
log_fail "Codex App simulation: result='$result', branch='$branch'"
|
||||
fi
|
||||
|
||||
echo "=== Test 5: Cleanup guard — linked worktree should NOT remove ==="
|
||||
cd "$TEMP_DIR/test-wt"
|
||||
result=$(detect_worktree)
|
||||
if [ "$result" = "linked" ]; then
|
||||
log_pass "Cleanup guard: linked worktree correctly detected (would skip removal)"
|
||||
else
|
||||
log_fail "Cleanup guard: expected 'linked', got '$result'"
|
||||
fi
|
||||
|
||||
echo "=== Test 6: Cleanup guard — main repo SHOULD remove ==="
|
||||
cd "$TEMP_DIR/test-repo"
|
||||
result=$(detect_worktree)
|
||||
if [ "$result" = "normal" ]; then
|
||||
log_pass "Cleanup guard: main repo correctly detected (would proceed with removal)"
|
||||
else
|
||||
log_fail "Cleanup guard: expected 'normal', got '$result'"
|
||||
fi
|
||||
|
||||
# Cleanup worktree before temp dir removal
|
||||
cd "$TEMP_DIR/test-repo"
|
||||
git worktree remove "$TEMP_DIR/test-wt" > /dev/null 2>&1 || true
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $PASS passed, $FAIL failed ==="
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Make it executable and run it**
|
||||
|
||||
```bash
|
||||
chmod +x tests/codex-app-compat/test-environment-detection.sh
|
||||
./tests/codex-app-compat/test-environment-detection.sh
|
||||
```
|
||||
|
||||
Expected output: 6 passed, 0 failed.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/codex-app-compat/test-environment-detection.sh
|
||||
git commit -m "test: add environment detection tests for Codex App compat (PRI-823)
|
||||
|
||||
Tests git-dir vs git-common-dir comparison in normal repo, linked
|
||||
worktree, detached HEAD, and cleanup guard scenarios."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Final verification
|
||||
|
||||
**Files:**
|
||||
- Read: all 5 modified skill files
|
||||
|
||||
- [ ] **Step 1: Run the automated detection tests**
|
||||
|
||||
```bash
|
||||
./tests/codex-app-compat/test-environment-detection.sh
|
||||
```
|
||||
|
||||
Expected: 6 passed, 0 failed.
|
||||
|
||||
- [ ] **Step 2: Read each modified file and verify changes**
|
||||
|
||||
Read each file end-to-end:
|
||||
- `skills/using-git-worktrees/SKILL.md` — Step 0 present, rest unchanged
|
||||
- `skills/finishing-a-development-branch/SKILL.md` — Step 1.5 present, cleanup guard present, rest unchanged
|
||||
- `skills/subagent-driven-development/SKILL.md` — line 268 updated
|
||||
- `skills/executing-plans/SKILL.md` — line 68 updated
|
||||
- `skills/using-superpowers/references/codex-tools.md` — two new sections at end
|
||||
|
||||
- [ ] **Step 3: Verify no unintended changes**
|
||||
|
||||
```bash
|
||||
git diff --stat HEAD~7
|
||||
```
|
||||
|
||||
Should show exactly 6 files changed (5 skill files + 1 test file). No other files modified.
|
||||
|
||||
- [ ] **Step 4: Run existing test suite**
|
||||
|
||||
If test runner exists:
|
||||
```bash
|
||||
# Run skill-triggering tests
|
||||
# Note: tests/skill-triggering/ was lifted into drill scenarios on 2026-05-06.
|
||||
# See evals/scenarios/triggering-*.yaml. The reference below is a dated artifact.
|
||||
./tests/skill-triggering/run-all.sh 2>/dev/null || echo "Skill triggering tests not available in this environment"
|
||||
|
||||
# Run SDD integration test
|
||||
./tests/claude-code/test-subagent-driven-development-integration.sh 2>/dev/null || echo "SDD integration test not available in this environment"
|
||||
```
|
||||
|
||||
Note: these tests require Claude Code with `--dangerously-skip-permissions`. If not available, document that regression tests should be run manually.
|
||||
866
docs/superpowers/plans/2026-04-06-worktree-rototill.md
Normal file
866
docs/superpowers/plans/2026-04-06-worktree-rototill.md
Normal file
@@ -0,0 +1,866 @@
|
||||
# Worktree Rototill Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make superpowers defer to native harness worktree systems when available, fall back to manual git worktrees when not, and fix three known finishing bugs.
|
||||
|
||||
**Architecture:** Two skill files are rewritten (`using-git-worktrees`, `finishing-a-development-branch`), three files get one-line integration updates (`executing-plans`, `subagent-driven-development`, `writing-plans`). The core change is adding detection (`GIT_DIR != GIT_COMMON`) and a native-tool-first creation path. These are markdown skill instruction files, not application code — "tests" are agent behavior tests using the testing-skills-with-subagents TDD framework.
|
||||
|
||||
**Tech Stack:** Markdown (skill files), bash (test scripts), Claude Code CLI (`claude -p` for headless testing)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-06-worktree-rototill-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: GATE — TDD Validation of Step 1a (Native Tool Preference)
|
||||
|
||||
Step 1a is the load-bearing assumption of the entire design. If agents don't prefer native worktree tools over `git worktree add`, the spec fails. Validate this FIRST, before touching any skill files.
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/claude-code/test-worktree-native-preference.sh`
|
||||
- Read: `skills/using-git-worktrees/SKILL.md` (current version, for RED baseline)
|
||||
- Read: `tests/claude-code/test-helpers.sh` (for `run_claude`, `assert_contains`, etc.)
|
||||
- Read: `skills/writing-skills/testing-skills-with-subagents.md` (TDD framework)
|
||||
|
||||
**This task is a gate.** If the GREEN phase fails after 2 REFACTOR iterations, STOP. Do not proceed to Task 2. Report back — the creation approach needs redesign.
|
||||
|
||||
- [ ] **Step 1: Write the RED baseline test script**
|
||||
|
||||
Create the test script that will run scenarios both WITHOUT and WITH the updated skill text. The RED phase runs against the current skill (which has no Step 1a).
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Test: Does the agent prefer native worktree tools (EnterWorktree) over git worktree add?
|
||||
# Framework: RED-GREEN-REFACTOR per testing-skills-with-subagents.md
|
||||
#
|
||||
# RED: Current skill has no native tool preference. Agent should use git worktree add.
|
||||
# GREEN: Updated skill has Step 1a. Agent should use EnterWorktree on Claude Code.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/test-helpers.sh"
|
||||
|
||||
# Pressure scenario: realistic implementation task where agent needs isolation
|
||||
SCENARIO='IMPORTANT: This is a real task. Choose and act.
|
||||
|
||||
You need to implement a small feature (add a "version" field to package.json).
|
||||
This should be done in an isolated workspace to protect the main branch.
|
||||
|
||||
You have the using-git-worktrees skill available. Set up the isolated workspace now.
|
||||
Do NOT actually implement the feature — just set up the workspace and report what you did.
|
||||
|
||||
Respond with EXACTLY what tool/command you used to create the workspace.'
|
||||
|
||||
echo "=== Worktree Native Preference Test ==="
|
||||
echo ""
|
||||
|
||||
# Phase selection
|
||||
PHASE="${1:-red}"
|
||||
|
||||
if [ "$PHASE" = "red" ]; then
|
||||
echo "--- RED PHASE: Running WITHOUT Step 1a (current skill) ---"
|
||||
echo "Expected: Agent uses 'git worktree add' (no native tool awareness)"
|
||||
echo ""
|
||||
|
||||
test_dir=$(create_test_project)
|
||||
cd "$test_dir"
|
||||
git init && git commit --allow-empty -m "init"
|
||||
mkdir -p .worktrees
|
||||
|
||||
output=$(run_claude "$SCENARIO" 120)
|
||||
|
||||
echo "Agent output:"
|
||||
echo "$output"
|
||||
echo ""
|
||||
|
||||
# RED expectation: agent uses git worktree add (current behavior)
|
||||
if echo "$output" | grep -qi "EnterWorktree"; then
|
||||
echo "[UNEXPECTED] Agent used EnterWorktree WITHOUT Step 1a — skill may not be needed"
|
||||
echo "Investigate: is Claude Code's default behavior already correct?"
|
||||
else
|
||||
echo "[RED CONFIRMED] Agent did NOT use EnterWorktree"
|
||||
assert_contains "$output" "git worktree" "Agent used git worktree (expected in RED)"
|
||||
fi
|
||||
|
||||
cleanup_test_project "$test_dir"
|
||||
|
||||
elif [ "$PHASE" = "green" ]; then
|
||||
echo "--- GREEN PHASE: Running WITH Step 1a (updated skill) ---"
|
||||
echo "Expected: Agent uses EnterWorktree instead of git worktree add"
|
||||
echo ""
|
||||
|
||||
test_dir=$(create_test_project)
|
||||
cd "$test_dir"
|
||||
git init && git commit --allow-empty -m "init"
|
||||
|
||||
output=$(run_claude "$SCENARIO" 120)
|
||||
|
||||
echo "Agent output:"
|
||||
echo "$output"
|
||||
echo ""
|
||||
|
||||
# GREEN expectation: agent uses EnterWorktree
|
||||
assert_contains "$output" "EnterWorktree" "Agent used native EnterWorktree tool"
|
||||
assert_not_contains "$output" "git worktree add" "Agent did NOT fall back to git worktree add"
|
||||
|
||||
cleanup_test_project "$test_dir"
|
||||
|
||||
elif [ "$PHASE" = "pressure" ]; then
|
||||
echo "--- PRESSURE PHASE: Multiple pressures to skip native tools ---"
|
||||
echo ""
|
||||
|
||||
PRESSURE_SCENARIO='IMPORTANT: This is a real task. Choose and act.
|
||||
|
||||
You need to set up an isolated workspace URGENTLY. Production is impacted and
|
||||
the fix needs to go in a worktree. You know git worktree add works reliably.
|
||||
The .worktrees/ directory already exists and is gitignored.
|
||||
|
||||
You have the using-git-worktrees skill. Set up the workspace NOW.
|
||||
Speed matters. Report EXACTLY what tool/command you used.'
|
||||
|
||||
test_dir=$(create_test_project)
|
||||
cd "$test_dir"
|
||||
git init && git commit --allow-empty -m "init"
|
||||
mkdir -p .worktrees
|
||||
echo ".worktrees/" >> .gitignore
|
||||
|
||||
output=$(run_claude "$PRESSURE_SCENARIO" 120)
|
||||
|
||||
echo "Agent output:"
|
||||
echo "$output"
|
||||
echo ""
|
||||
|
||||
# Should STILL use EnterWorktree even under pressure
|
||||
assert_contains "$output" "EnterWorktree" "Agent used native tool even under time pressure"
|
||||
assert_not_contains "$output" "git worktree add" "Agent resisted falling back to git despite pressure"
|
||||
|
||||
cleanup_test_project "$test_dir"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Test Complete ==="
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run RED phase — confirm agent uses git worktree add today**
|
||||
|
||||
Run: `cd tests/claude-code && bash test-worktree-native-preference.sh red`
|
||||
|
||||
Expected: `[RED CONFIRMED] Agent did NOT use EnterWorktree` — agent uses `git worktree add` because current skill has no native tool preference.
|
||||
|
||||
Document the agent's exact output and any rationalizations verbatim. This is the baseline failure the skill must fix.
|
||||
|
||||
- [ ] **Step 3: If RED confirmed, proceed. Write the Step 1a skill text.**
|
||||
|
||||
Create a temporary test version of the skill with ONLY the Step 1a addition (minimal change to isolate the variable). Add this section to the top of the skill's creation instructions, BEFORE the existing directory selection process:
|
||||
|
||||
```markdown
|
||||
## Step 1: Create Isolated Workspace
|
||||
|
||||
**You have two mechanisms. Try them in this order.**
|
||||
|
||||
### 1a. Native Worktree Tools (preferred)
|
||||
|
||||
If your platform provides a worktree or workspace-isolation tool, use it. You know your own toolkit — the skill does not need to name specific tools. Native tools handle directory placement, branch creation, and cleanup automatically.
|
||||
|
||||
After using a native tool, skip to Step 3 (Project Setup).
|
||||
|
||||
### 1b. Git Worktree Fallback
|
||||
|
||||
If no native tool is available, create a worktree manually using git.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run GREEN phase — confirm agent now uses EnterWorktree**
|
||||
|
||||
Run: `cd tests/claude-code && bash test-worktree-native-preference.sh green`
|
||||
|
||||
Expected: `[PASS] Agent used native EnterWorktree tool`
|
||||
|
||||
If FAIL: Document the agent's exact output and rationalizations. This is a REFACTOR signal — the Step 1a text needs revision. Try up to 2 REFACTOR iterations. If still failing after 2 iterations, STOP and report back.
|
||||
|
||||
- [ ] **Step 5: Run PRESSURE phase — confirm agent resists fallback under pressure**
|
||||
|
||||
Run: `cd tests/claude-code && bash test-worktree-native-preference.sh pressure`
|
||||
|
||||
Expected: `[PASS] Agent used native tool even under time pressure`
|
||||
|
||||
If FAIL: Document rationalizations verbatim. Add explicit counters to Step 1a text (e.g., a Red Flag entry: "Never use git worktree add when your platform provides a native worktree tool"). Re-run.
|
||||
|
||||
- [ ] **Step 6: Commit test script**
|
||||
|
||||
```bash
|
||||
git add tests/claude-code/test-worktree-native-preference.sh
|
||||
git commit -m "test: add RED/GREEN validation for native worktree preference (PRI-974)
|
||||
|
||||
Gate test for Step 1a — validates agents prefer EnterWorktree over
|
||||
git worktree add on Claude Code. Must pass before skill rewrite."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Rewrite `using-git-worktrees` SKILL.md
|
||||
|
||||
Full rewrite of the creation skill. Replaces the existing file entirely.
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/using-git-worktrees/SKILL.md` (full rewrite, 219 lines → ~210 lines)
|
||||
|
||||
**Depends on:** Task 1 GREEN passing.
|
||||
|
||||
- [ ] **Step 1: Write the complete new SKILL.md**
|
||||
|
||||
Replace the entire contents of `skills/using-git-worktrees/SKILL.md` with:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: using-git-worktrees
|
||||
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
|
||||
---
|
||||
|
||||
# Using Git Worktrees
|
||||
|
||||
## Overview
|
||||
|
||||
Ensure work happens in an isolated workspace. Prefer your platform's native worktree tools. Fall back to manual git worktrees only when no native tool is available.
|
||||
|
||||
**Core principle:** Detect existing isolation first. Then use native tools. Then fall back to git. Never fight the harness.
|
||||
|
||||
**Announce at start:** "I'm using the using-git-worktrees skill to set up an isolated workspace."
|
||||
|
||||
## Step 0: Detect Existing Isolation
|
||||
|
||||
**Before creating anything, check if you are already in an isolated workspace.**
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
**Submodule guard:** `GIT_DIR != GIT_COMMON` is also true inside git submodules. Before concluding "already in a worktree," verify you are not in a submodule:
|
||||
|
||||
```bash
|
||||
# If this returns a path, you're in a submodule, not a worktree — proceed to Step 1
|
||||
git rev-parse --show-superproject-working-tree 2>/dev/null
|
||||
```
|
||||
|
||||
**If `GIT_DIR != GIT_COMMON` (and not a submodule):** You are already in a linked worktree. Skip to Step 3 (Project Setup). Do NOT create another worktree.
|
||||
|
||||
Report with branch state:
|
||||
- On a branch: "Already in isolated workspace at `<path>` on branch `<name>`."
|
||||
- Detached HEAD: "Already in isolated workspace at `<path>` (detached HEAD, externally managed). Branch creation needed at finish time."
|
||||
|
||||
**If `GIT_DIR == GIT_COMMON` (or in a submodule):** You are in a normal repo checkout.
|
||||
|
||||
Has the user already indicated their worktree preference in your instructions? If not, ask for consent before creating a worktree:
|
||||
|
||||
> "Would you like me to set up an isolated worktree? It protects your current branch from changes."
|
||||
|
||||
Honor any existing declared preference without asking. If the user declines consent, work in place and skip to Step 3.
|
||||
|
||||
## Step 1: Create Isolated Workspace
|
||||
|
||||
**You have two mechanisms. Try them in this order.**
|
||||
|
||||
### 1a. Native Worktree Tools (preferred)
|
||||
|
||||
If your platform provides a worktree or workspace-isolation tool, use it. You know your own toolkit — the skill does not need to name specific tools. Native tools handle directory placement, branch creation, and cleanup automatically.
|
||||
|
||||
After using a native tool, skip to Step 3 (Project Setup).
|
||||
|
||||
### 1b. Git Worktree Fallback
|
||||
|
||||
If no native tool is available, create a worktree manually using git.
|
||||
|
||||
#### Directory Selection
|
||||
|
||||
Follow this priority order:
|
||||
|
||||
1. **Check your instructions for a worktree directory preference.** If specified, use it without asking.
|
||||
|
||||
2. **Check existing project-local directories:**
|
||||
```bash
|
||||
ls -d .worktrees 2>/dev/null # Preferred (hidden)
|
||||
ls -d worktrees 2>/dev/null # Alternative
|
||||
```
|
||||
If found, use that directory. If both exist, `.worktrees` wins.
|
||||
|
||||
3. **Default to `.worktrees/`.**
|
||||
|
||||
#### Safety Verification (project-local directories only)
|
||||
|
||||
**MUST verify directory is ignored before creating worktree:**
|
||||
|
||||
```bash
|
||||
git check-ignore -q .worktrees 2>/dev/null || git check-ignore -q worktrees 2>/dev/null
|
||||
```
|
||||
|
||||
**If NOT ignored:** Add to .gitignore, commit the change, then proceed.
|
||||
|
||||
**Why critical:** Prevents accidentally committing worktree contents to repository.
|
||||
|
||||
#### Create the Worktree
|
||||
|
||||
```bash
|
||||
# Determine path based on chosen location
|
||||
path="$LOCATION/$BRANCH_NAME"
|
||||
|
||||
git worktree add "$path" -b "$BRANCH_NAME"
|
||||
cd "$path"
|
||||
```
|
||||
|
||||
#### Hooks Awareness
|
||||
|
||||
Git worktrees do not inherit the parent repo's hooks directory. After creating the worktree, symlink hooks from the main repo if they exist:
|
||||
|
||||
```bash
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
if [ -d "$MAIN_ROOT/.git/hooks" ]; then
|
||||
ln -sf "$MAIN_ROOT/.git/hooks" "$path/.git/hooks"
|
||||
fi
|
||||
```
|
||||
|
||||
This prevents pre-commit checks, linters, and other hooks from silently stopping when work moves to a worktree.
|
||||
|
||||
**Sandbox fallback:** If `git worktree add` fails with a permission error (sandbox denial), treat this as a restricted environment. Skip creation, run setup and baseline tests in the current directory, report accordingly.
|
||||
|
||||
## Step 3: Project Setup
|
||||
|
||||
Auto-detect and run appropriate setup:
|
||||
|
||||
```bash
|
||||
# Node.js
|
||||
if [ -f package.json ]; then npm install; fi
|
||||
|
||||
# Rust
|
||||
if [ -f Cargo.toml ]; then cargo build; fi
|
||||
|
||||
# Python
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
if [ -f pyproject.toml ]; then poetry install; fi
|
||||
|
||||
# Go
|
||||
if [ -f go.mod ]; then go mod download; fi
|
||||
```
|
||||
|
||||
## Step 4: Verify Clean Baseline
|
||||
|
||||
Run tests to ensure workspace starts clean:
|
||||
|
||||
```bash
|
||||
# Use project-appropriate command
|
||||
npm test / cargo test / pytest / go test ./...
|
||||
```
|
||||
|
||||
**If tests fail:** Report failures, ask whether to proceed or investigate.
|
||||
|
||||
**If tests pass:** Report ready.
|
||||
|
||||
### Report
|
||||
|
||||
```
|
||||
Worktree ready at <full-path>
|
||||
Tests passing (<N> tests, 0 failures)
|
||||
Ready to implement <feature-name>
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Already in linked worktree | Skip creation (Step 0) |
|
||||
| In a submodule | Treat as normal repo (Step 0 guard) |
|
||||
| Native worktree tool available | Use it (Step 1a) |
|
||||
| No native tool | Git worktree fallback (Step 1b) |
|
||||
| `.worktrees/` exists | Use it (verify ignored) |
|
||||
| `worktrees/` exists | Use it (verify ignored) |
|
||||
| Both exist | Use `.worktrees/` |
|
||||
| Neither exists | Check instruction file, then default `.worktrees/` |
|
||||
| Directory not ignored | Add to .gitignore + commit |
|
||||
| Permission error on create | Sandbox fallback, work in place |
|
||||
| Tests fail during baseline | Report failures + ask |
|
||||
| No package.json/Cargo.toml | Skip dependency install |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Fighting the harness
|
||||
|
||||
- **Problem:** Using `git worktree add` when the platform already provides isolation
|
||||
- **Fix:** Step 0 detects existing isolation. Step 1a defers to native tools.
|
||||
|
||||
### Skipping detection
|
||||
|
||||
- **Problem:** Creating a nested worktree inside an existing one
|
||||
- **Fix:** Always run Step 0 before creating anything
|
||||
|
||||
### Skipping ignore verification
|
||||
|
||||
- **Problem:** Worktree contents get tracked, pollute git status
|
||||
- **Fix:** Always use `git check-ignore` before creating project-local worktree
|
||||
|
||||
### Assuming directory location
|
||||
|
||||
- **Problem:** Creates inconsistency, violates project conventions
|
||||
- **Fix:** Follow priority: existing > instruction file > default
|
||||
|
||||
### Proceeding with failing tests
|
||||
|
||||
- **Problem:** Can't distinguish new bugs from pre-existing issues
|
||||
- **Fix:** Report failures, get explicit permission to proceed
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Create a worktree when Step 0 detects existing isolation
|
||||
- Use git commands when a native worktree tool is available
|
||||
- Create worktree without verifying it's ignored (project-local)
|
||||
- Skip baseline test verification
|
||||
- Proceed with failing tests without asking
|
||||
|
||||
**Always:**
|
||||
- Run Step 0 detection first
|
||||
- Prefer native tools over git fallback
|
||||
- Follow directory priority: existing > instruction file > default
|
||||
- Verify directory is ignored for project-local
|
||||
- Auto-detect and run project setup
|
||||
- Verify clean test baseline
|
||||
- Symlink hooks after creating worktree via 1b
|
||||
|
||||
## Integration
|
||||
|
||||
**Called by:**
|
||||
- **subagent-driven-development** - Ensures isolated workspace (creates one or verifies existing)
|
||||
- **executing-plans** - Ensures isolated workspace (creates one or verifies existing)
|
||||
- Any skill needing isolated workspace
|
||||
|
||||
**Pairs with:**
|
||||
- **finishing-a-development-branch** - REQUIRED for cleanup after work complete
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file reads correctly**
|
||||
|
||||
Run: `wc -l skills/using-git-worktrees/SKILL.md`
|
||||
|
||||
Expected: Approximately 200-220 lines. Scan for any markdown formatting issues.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/using-git-worktrees/SKILL.md
|
||||
git commit -m "feat: rewrite using-git-worktrees with detect-and-defer (PRI-974)
|
||||
|
||||
Step 0: GIT_DIR != GIT_COMMON detection (skip if already isolated)
|
||||
Step 0 consent: opt-in prompt before creating worktree (#991)
|
||||
Step 1a: native tool preference (short, first, declarative)
|
||||
Step 1b: git worktree fallback with project-local directory policy
|
||||
Submodule guard prevents false detection
|
||||
Platform-neutral instruction file references (#1049)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Rewrite `finishing-a-development-branch` SKILL.md
|
||||
|
||||
Full rewrite of the finishing skill. Adds environment detection, fixes three bugs, adds provenance-based cleanup.
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/finishing-a-development-branch/SKILL.md` (full rewrite, 201 lines → ~220 lines)
|
||||
|
||||
- [ ] **Step 1: Write the complete new SKILL.md**
|
||||
|
||||
Replace the entire contents of `skills/finishing-a-development-branch/SKILL.md` with:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: finishing-a-development-branch
|
||||
description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
|
||||
---
|
||||
|
||||
# Finishing a Development Branch
|
||||
|
||||
## Overview
|
||||
|
||||
Guide completion of development work by presenting clear options and handling chosen workflow.
|
||||
|
||||
**Core principle:** Verify tests → Detect environment → Present options → Execute choice → Clean up.
|
||||
|
||||
**Announce at start:** "I'm using the finishing-a-development-branch skill to complete this work."
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Verify Tests
|
||||
|
||||
**Before presenting options, verify tests pass:**
|
||||
|
||||
```bash
|
||||
# Run project's test suite
|
||||
npm test / cargo test / pytest / go test ./...
|
||||
```
|
||||
|
||||
**If tests fail:**
|
||||
```
|
||||
Tests failing (<N> failures). Must fix before completing:
|
||||
|
||||
[Show failures]
|
||||
|
||||
Cannot proceed with merge/PR until tests pass.
|
||||
```
|
||||
|
||||
Stop. Don't proceed to Step 2.
|
||||
|
||||
**If tests pass:** Continue to Step 2.
|
||||
|
||||
### Step 2: Detect Environment
|
||||
|
||||
**Determine workspace state before presenting options:**
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
```
|
||||
|
||||
This determines which menu to show and how cleanup works:
|
||||
|
||||
| State | Menu | Cleanup |
|
||||
|-------|------|---------|
|
||||
| `GIT_DIR == GIT_COMMON` (normal repo) | Standard 4 options | No worktree to clean up |
|
||||
| `GIT_DIR != GIT_COMMON`, named branch | Standard 4 options | Provenance-based (see Step 6) |
|
||||
| `GIT_DIR != GIT_COMMON`, detached HEAD | Reduced 3 options (no merge) | No cleanup (externally managed) |
|
||||
|
||||
### Step 3: Determine Base Branch
|
||||
|
||||
```bash
|
||||
# Try common base branches
|
||||
git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null
|
||||
```
|
||||
|
||||
Or ask: "This branch split from main - is that correct?"
|
||||
|
||||
### Step 4: Present Options
|
||||
|
||||
**Normal repo and named-branch worktree — present exactly these 4 options:**
|
||||
|
||||
```
|
||||
Implementation complete. What would you like to do?
|
||||
|
||||
1. Merge back to <base-branch> locally
|
||||
2. Push and create a Pull Request
|
||||
3. Keep the branch as-is (I'll handle it later)
|
||||
4. Discard this work
|
||||
|
||||
Which option?
|
||||
```
|
||||
|
||||
**Detached HEAD — present exactly these 3 options:**
|
||||
|
||||
```
|
||||
Implementation complete. You're on a detached HEAD (externally managed workspace).
|
||||
|
||||
1. Push as new branch and create a Pull Request
|
||||
2. Keep as-is (I'll handle it later)
|
||||
3. Discard this work
|
||||
|
||||
Which option?
|
||||
```
|
||||
|
||||
**Don't add explanation** - keep options concise.
|
||||
|
||||
### Step 5: Execute Choice
|
||||
|
||||
#### Option 1: Merge Locally
|
||||
|
||||
```bash
|
||||
# Get main repo root for CWD safety
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
|
||||
# Merge first — verify success before removing anything
|
||||
git checkout <base-branch>
|
||||
git pull
|
||||
git merge <feature-branch>
|
||||
|
||||
# Verify tests on merged result
|
||||
<test command>
|
||||
|
||||
# Only after merge succeeds: remove worktree, then delete branch
|
||||
# (See Step 6 for worktree cleanup)
|
||||
git branch -d <feature-branch>
|
||||
```
|
||||
|
||||
Then: Cleanup worktree (Step 6)
|
||||
|
||||
#### Option 2: Push and Create PR
|
||||
|
||||
```bash
|
||||
# Push branch
|
||||
git push -u origin <feature-branch>
|
||||
|
||||
# Create PR
|
||||
gh pr create --title "<title>" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
<2-3 bullets of what changed>
|
||||
|
||||
## Test Plan
|
||||
- [ ] <verification steps>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
**Do NOT clean up worktree** — user needs it alive to iterate on PR feedback.
|
||||
|
||||
#### Option 3: Keep As-Is
|
||||
|
||||
Report: "Keeping branch <name>. Worktree preserved at <path>."
|
||||
|
||||
**Don't cleanup worktree.**
|
||||
|
||||
#### Option 4: Discard
|
||||
|
||||
**Confirm first:**
|
||||
```
|
||||
This will permanently delete:
|
||||
- Branch <name>
|
||||
- All commits: <commit-list>
|
||||
- Worktree at <path>
|
||||
|
||||
Type 'discard' to confirm.
|
||||
```
|
||||
|
||||
Wait for exact confirmation.
|
||||
|
||||
If confirmed:
|
||||
```bash
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
```
|
||||
|
||||
Then: Cleanup worktree (Step 6), then force-delete branch:
|
||||
```bash
|
||||
git branch -D <feature-branch>
|
||||
```
|
||||
|
||||
### Step 6: Cleanup Workspace
|
||||
|
||||
**Only runs for Options 1 and 4.** Options 2 and 3 always preserve the worktree.
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
WORKTREE_PATH=$(git rev-parse --show-toplevel)
|
||||
```
|
||||
|
||||
**If `GIT_DIR == GIT_COMMON`:** Normal repo, no worktree to clean up. Done.
|
||||
|
||||
**If worktree path is under `.worktrees/` or `worktrees/`:** Superpowers created this worktree — we own cleanup.
|
||||
|
||||
```bash
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
git worktree remove "$WORKTREE_PATH"
|
||||
git worktree prune # Self-healing: clean up any stale registrations
|
||||
```
|
||||
|
||||
**Otherwise:** The host environment (harness) owns this workspace. Do NOT remove it. If your platform provides a workspace-exit tool, use it. Otherwise, leave the workspace in place.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Option | Merge | Push | Keep Worktree | Cleanup Branch |
|
||||
|--------|-------|------|---------------|----------------|
|
||||
| 1. Merge locally | yes | - | - | yes |
|
||||
| 2. Create PR | - | yes | yes | - |
|
||||
| 3. Keep as-is | - | - | yes | - |
|
||||
| 4. Discard | - | - | - | yes (force) |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Skipping test verification**
|
||||
- **Problem:** Merge broken code, create failing PR
|
||||
- **Fix:** Always verify tests before offering options
|
||||
|
||||
**Open-ended questions**
|
||||
- **Problem:** "What should I do next?" is ambiguous
|
||||
- **Fix:** Present exactly 4 structured options (or 3 for detached HEAD)
|
||||
|
||||
**Cleaning up worktree for Option 2**
|
||||
- **Problem:** Remove worktree user needs for PR iteration
|
||||
- **Fix:** Only cleanup for Options 1 and 4
|
||||
|
||||
**Deleting branch before removing worktree**
|
||||
- **Problem:** `git branch -d` fails because worktree still references the branch
|
||||
- **Fix:** Merge first, remove worktree, then delete branch
|
||||
|
||||
**Running git worktree remove from inside the worktree**
|
||||
- **Problem:** Command fails silently when CWD is inside the worktree being removed
|
||||
- **Fix:** Always `cd` to main repo root before `git worktree remove`
|
||||
|
||||
**Cleaning up harness-owned worktrees**
|
||||
- **Problem:** Removing a worktree the harness created causes phantom state
|
||||
- **Fix:** Only clean up worktrees under `.worktrees/` or `worktrees/`
|
||||
|
||||
**No confirmation for discard**
|
||||
- **Problem:** Accidentally delete work
|
||||
- **Fix:** Require typed "discard" confirmation
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Proceed with failing tests
|
||||
- Merge without verifying tests on result
|
||||
- Delete work without confirmation
|
||||
- Force-push without explicit request
|
||||
- Remove a worktree before confirming merge success
|
||||
- Clean up worktrees you didn't create (provenance check)
|
||||
- Run `git worktree remove` from inside the worktree
|
||||
|
||||
**Always:**
|
||||
- Verify tests before offering options
|
||||
- Detect environment before presenting menu
|
||||
- Present exactly 4 options (or 3 for detached HEAD)
|
||||
- Get typed confirmation for Option 4
|
||||
- Clean up worktree for Options 1 & 4 only
|
||||
- `cd` to main repo root before worktree removal
|
||||
- Run `git worktree prune` after removal
|
||||
|
||||
## Integration
|
||||
|
||||
**Called by:**
|
||||
- **subagent-driven-development** (Step 7) - After all tasks complete
|
||||
- **executing-plans** (Step 5) - After all batches complete
|
||||
|
||||
**Pairs with:**
|
||||
- **using-git-worktrees** - Cleans up worktree created by that skill
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file reads correctly**
|
||||
|
||||
Run: `wc -l skills/finishing-a-development-branch/SKILL.md`
|
||||
|
||||
Expected: Approximately 210-230 lines.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add skills/finishing-a-development-branch/SKILL.md
|
||||
git commit -m "feat: rewrite finishing-a-development-branch with detect-and-defer (PRI-974)
|
||||
|
||||
Step 2: environment detection (GIT_DIR != GIT_COMMON) before presenting menu
|
||||
Detached HEAD: reduced 3-option menu (no merge from detached HEAD)
|
||||
Provenance-based cleanup: .worktrees/ = ours, anything else = hands off
|
||||
Bug #940: Option 2 no longer cleans up worktree
|
||||
Bug #999: merge -> verify -> remove worktree -> delete branch
|
||||
Bug #238: cd to main repo root before git worktree remove
|
||||
Stale worktree pruning after removal (git worktree prune)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Integration Updates
|
||||
|
||||
One-line changes to three files that reference `using-git-worktrees`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/executing-plans/SKILL.md:68`
|
||||
- Modify: `skills/subagent-driven-development/SKILL.md:268`
|
||||
- Modify: `skills/writing-plans/SKILL.md:16`
|
||||
|
||||
- [ ] **Step 1: Update executing-plans integration line**
|
||||
|
||||
In `skills/executing-plans/SKILL.md`, change line 68 from:
|
||||
|
||||
```markdown
|
||||
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```markdown
|
||||
- **superpowers:using-git-worktrees** - Ensures isolated workspace (creates one or verifies existing)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update subagent-driven-development integration line**
|
||||
|
||||
In `skills/subagent-driven-development/SKILL.md`, change line 268 from:
|
||||
|
||||
```markdown
|
||||
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```markdown
|
||||
- **superpowers:using-git-worktrees** - Ensures isolated workspace (creates one or verifies existing)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update writing-plans context line**
|
||||
|
||||
In `skills/writing-plans/SKILL.md`, change line 16 from:
|
||||
|
||||
```markdown
|
||||
**Context:** This should be run in a dedicated worktree (created by brainstorming skill).
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```markdown
|
||||
**Context:** If working in an isolated worktree, it should have been created via the using-git-worktrees skill at execution time.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit all three**
|
||||
|
||||
```bash
|
||||
git add skills/executing-plans/SKILL.md skills/subagent-driven-development/SKILL.md skills/writing-plans/SKILL.md
|
||||
git commit -m "fix: update worktree integration references across skills (PRI-974)
|
||||
|
||||
Remove REQUIRED language from executing-plans and subagent-driven-development.
|
||||
Consent and detection now live inside using-git-worktrees itself.
|
||||
Fix stale 'created by brainstorming' claim in writing-plans."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: End-to-End Validation
|
||||
|
||||
Verify the full rewritten skills work together. Run the existing test suite plus manual verification.
|
||||
|
||||
**Files:**
|
||||
- Read: `tests/claude-code/run-skill-tests.sh`
|
||||
- Read: `skills/using-git-worktrees/SKILL.md` (verify final state)
|
||||
- Read: `skills/finishing-a-development-branch/SKILL.md` (verify final state)
|
||||
|
||||
- [ ] **Step 1: Run existing test suite**
|
||||
|
||||
Run: `cd tests/claude-code && bash run-skill-tests.sh`
|
||||
|
||||
Expected: All existing tests pass. If any fail, investigate — the integration changes (Task 4) may have broken a content assertion.
|
||||
|
||||
- [ ] **Step 2: Re-run Step 1a GREEN test**
|
||||
|
||||
Run: `cd tests/claude-code && bash test-worktree-native-preference.sh green`
|
||||
|
||||
Expected: PASS — agent still uses EnterWorktree with the final skill text (not just the minimal Step 1a addition from Task 1).
|
||||
|
||||
- [ ] **Step 3: Manual verification — read both rewritten skills end-to-end**
|
||||
|
||||
Read `skills/using-git-worktrees/SKILL.md` and `skills/finishing-a-development-branch/SKILL.md` in their entirety. Check:
|
||||
|
||||
1. No references to old behavior (hardcoded `CLAUDE.md`, interactive directory prompt, "REQUIRED" language)
|
||||
2. Step numbering is consistent within each file
|
||||
3. Quick Reference tables match the prose
|
||||
4. Integration sections cross-reference correctly
|
||||
5. No markdown formatting issues
|
||||
|
||||
- [ ] **Step 4: Verify git status is clean**
|
||||
|
||||
Run: `git status`
|
||||
|
||||
Expected: Clean working tree. All changes committed across Tasks 1-4.
|
||||
|
||||
- [ ] **Step 5: Final commit if any fixups needed**
|
||||
|
||||
If manual verification found issues, fix them and commit:
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix: address review findings in worktree skill rewrite (PRI-974)"
|
||||
```
|
||||
|
||||
If no issues found, skip this step.
|
||||
1374
docs/superpowers/plans/2026-05-06-lift-drill-into-evals.md
Normal file
1374
docs/superpowers/plans/2026-05-06-lift-drill-into-evals.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,136 @@
|
||||
# Document Review System Design
|
||||
|
||||
## Overview
|
||||
|
||||
Add two new review stages to the superpowers workflow:
|
||||
|
||||
1. **Spec Document Review** - After brainstorming, before writing-plans
|
||||
2. **Plan Document Review** - After writing-plans, before implementation
|
||||
|
||||
Both follow the iterative loop pattern used by implementation reviews.
|
||||
|
||||
## Spec Document Reviewer
|
||||
|
||||
**Purpose:** Verify the spec is complete, consistent, and ready for implementation planning.
|
||||
|
||||
**Location:** `skills/brainstorming/spec-document-reviewer-prompt.md`
|
||||
|
||||
**What it checks for:**
|
||||
|
||||
| Category | What to Look For |
|
||||
|----------|------------------|
|
||||
| Completeness | TODOs, placeholders, "TBD", incomplete sections |
|
||||
| Coverage | Missing error handling, edge cases, integration points |
|
||||
| Consistency | Internal contradictions, conflicting requirements |
|
||||
| Clarity | Ambiguous requirements |
|
||||
| YAGNI | Unrequested features, over-engineering |
|
||||
|
||||
**Output format:**
|
||||
```
|
||||
## Spec Review
|
||||
|
||||
**Status:** Approved | Issues Found
|
||||
|
||||
**Issues (if any):**
|
||||
- [Section X]: [issue] - [why it matters]
|
||||
|
||||
**Recommendations (advisory):**
|
||||
- [suggestions that don't block approval]
|
||||
```
|
||||
|
||||
**Review loop:** Issues found -> brainstorming agent fixes -> re-review -> repeat until approved.
|
||||
|
||||
**Dispatch mechanism:** Use the Task tool with `subagent_type: general-purpose`. The reviewer prompt template provides the full prompt. The brainstorming skill's controller dispatches the reviewer.
|
||||
|
||||
## Plan Document Reviewer
|
||||
|
||||
**Purpose:** Verify the plan is complete, matches the spec, and has proper task decomposition.
|
||||
|
||||
**Location:** `skills/writing-plans/plan-document-reviewer-prompt.md`
|
||||
|
||||
**What it checks for:**
|
||||
|
||||
| Category | What to Look For |
|
||||
|----------|------------------|
|
||||
| Completeness | TODOs, placeholders, incomplete tasks |
|
||||
| Spec Alignment | Plan covers spec requirements, no scope creep |
|
||||
| Task Decomposition | Tasks atomic, clear boundaries |
|
||||
| Task Syntax | Checkbox syntax on tasks and steps |
|
||||
| Chunk Size | Each chunk under 1000 lines |
|
||||
|
||||
**Chunk definition:** A chunk is a logical grouping of tasks within the plan document, delimited by `## Chunk N: <name>` headings. The writing-plans skill creates these boundaries based on logical phases (e.g., "Foundation", "Core Features", "Integration"). Each chunk should be self-contained enough to review independently.
|
||||
|
||||
**Spec alignment verification:** The reviewer receives both:
|
||||
1. The plan document (or current chunk)
|
||||
2. The path to the spec document for reference
|
||||
|
||||
The reviewer reads both and compares requirements coverage.
|
||||
|
||||
**Output format:** Same as spec reviewer, but scoped to the current chunk.
|
||||
|
||||
**Review process (chunk-by-chunk):**
|
||||
1. Writing-plans creates chunk N
|
||||
2. Controller dispatches plan-document-reviewer with chunk N content and spec path
|
||||
3. Reviewer reads chunk and spec, returns verdict
|
||||
4. If issues: writing-plans agent fixes chunk N, goto step 2
|
||||
5. If approved: proceed to chunk N+1
|
||||
6. Repeat until all chunks approved
|
||||
|
||||
**Dispatch mechanism:** Same as spec reviewer - Task tool with `subagent_type: general-purpose`.
|
||||
|
||||
## Updated Workflow
|
||||
|
||||
```
|
||||
brainstorming -> spec -> SPEC REVIEW LOOP -> writing-plans -> plan -> PLAN REVIEW LOOP -> implementation
|
||||
```
|
||||
|
||||
**Spec Review Loop:**
|
||||
1. Spec complete
|
||||
2. Dispatch reviewer
|
||||
3. If issues: fix -> goto 2
|
||||
4. If approved: proceed
|
||||
|
||||
**Plan Review Loop:**
|
||||
1. Chunk N complete
|
||||
2. Dispatch reviewer for chunk N
|
||||
3. If issues: fix -> goto 2
|
||||
4. If approved: next chunk or implementation
|
||||
|
||||
## Markdown Task Syntax
|
||||
|
||||
Tasks and steps use checkbox syntax:
|
||||
|
||||
```markdown
|
||||
- [ ] ### Task 1: Name
|
||||
|
||||
- [ ] **Step 1:** Description
|
||||
- File: path
|
||||
- Command: cmd
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Review loop termination:**
|
||||
- No hard iteration limit - loops continue until reviewer approves
|
||||
- If loop exceeds 5 iterations, the controller should surface this to the human for guidance
|
||||
- The human can choose to: continue iterating, approve with known issues, or abort
|
||||
|
||||
**Disagreement handling:**
|
||||
- Reviewers are advisory - they flag issues but don't block
|
||||
- If the agent believes reviewer feedback is incorrect, it should explain why in its fix
|
||||
- If disagreement persists after 3 iterations on the same issue, surface to human
|
||||
|
||||
**Malformed reviewer output:**
|
||||
- Controller should validate reviewer output has required fields (Status, Issues if applicable)
|
||||
- If malformed, re-dispatch reviewer with a note about expected format
|
||||
- After 2 malformed responses, surface to human
|
||||
|
||||
## Files to Change
|
||||
|
||||
**New files:**
|
||||
- `skills/brainstorming/spec-document-reviewer-prompt.md`
|
||||
- `skills/writing-plans/plan-document-reviewer-prompt.md`
|
||||
|
||||
**Modified files:**
|
||||
- `skills/brainstorming/SKILL.md` - add review loop after spec written
|
||||
- `skills/writing-plans/SKILL.md` - add chunk-by-chunk review loop, update task syntax examples
|
||||
@@ -0,0 +1,162 @@
|
||||
# Visual Brainstorming Refactor: Browser Displays, Terminal Commands
|
||||
|
||||
**Date:** 2026-02-19
|
||||
**Status:** Approved
|
||||
**Scope:** `lib/brainstorm-server/`, `skills/brainstorming/visual-companion.md`, `tests/brainstorm-server/`
|
||||
|
||||
## Problem
|
||||
|
||||
During visual brainstorming, Claude runs `wait-for-feedback.sh` as a background task and blocks on `TaskOutput(block=true, timeout=600s)`. This seizes the TUI entirely — the user cannot type to Claude while visual brainstorming is running. The browser becomes the only input channel.
|
||||
|
||||
Claude Code's execution model is turn-based. There is no way for Claude to listen on two channels simultaneously within a single turn. The blocking `TaskOutput` pattern was the wrong primitive — it simulates event-driven behavior the platform doesn't support.
|
||||
|
||||
## Design
|
||||
|
||||
### Core Model
|
||||
|
||||
**Browser = interactive display.** Shows mockups, lets the user click to select options. Selections are recorded server-side.
|
||||
|
||||
**Terminal = conversation channel.** Always unblocked, always available. The user talks to Claude here.
|
||||
|
||||
### The Loop
|
||||
|
||||
1. Claude writes an HTML file to the session directory
|
||||
2. Server detects it via chokidar, pushes WebSocket reload to the browser (unchanged)
|
||||
3. Claude ends its turn — tells the user to check the browser and respond in the terminal
|
||||
4. User looks at browser, optionally clicks to select an option, then types feedback in the terminal
|
||||
5. On the next turn, Claude reads `$SCREEN_DIR/.events` for the browser interaction stream (clicks, selections), merges with the terminal text
|
||||
6. Iterate or advance
|
||||
|
||||
No background tasks. No `TaskOutput` blocking. No polling scripts.
|
||||
|
||||
### Key Deletion: `wait-for-feedback.sh`
|
||||
|
||||
Deleted entirely. Its purpose was to bridge "server logs events to stdout" and "Claude needs to receive those events." The `.events` file replaces this — the server writes user interaction events directly, and Claude reads them with whatever file-reading mechanism the platform provides.
|
||||
|
||||
### Key Addition: `.events` File (Per-Screen Event Stream)
|
||||
|
||||
The server writes all user interaction events to `$SCREEN_DIR/.events`, one JSON object per line. This gives Claude the full interaction stream for the current screen — not just the final selection, but the user's exploration path (clicked A, then B, settled on C).
|
||||
|
||||
Example contents after a user explores options:
|
||||
|
||||
```jsonl
|
||||
{"type":"click","choice":"a","text":"Option A - Preset-First Wizard","timestamp":1706000101}
|
||||
{"type":"click","choice":"c","text":"Option C - Manual Config","timestamp":1706000108}
|
||||
{"type":"click","choice":"b","text":"Option B - Hybrid Approach","timestamp":1706000115}
|
||||
```
|
||||
|
||||
- Append-only within a screen. Each user event is appended as a new line.
|
||||
- The file is cleared (deleted) when chokidar detects a new HTML file (new screen pushed), preventing stale events from carrying over.
|
||||
- If the file doesn't exist when Claude reads it, no browser interaction occurred — Claude uses only the terminal text.
|
||||
- The file contains only user events (`click`, etc.) — not server lifecycle events (`server-started`, `screen-added`). This keeps it small and focused.
|
||||
- Claude can read the full stream to understand the user's exploration pattern, or just look at the last `choice` event for the final selection.
|
||||
|
||||
## Changes by File
|
||||
|
||||
### `index.js` (server)
|
||||
|
||||
**A. Write user events to `.events` file.**
|
||||
|
||||
In the WebSocket `message` handler, after logging the event to stdout: append the event as a JSON line to `$SCREEN_DIR/.events` via `fs.appendFileSync`. Only write user interaction events (those with `source: 'user-event'`), not server lifecycle events.
|
||||
|
||||
**B. Clear `.events` on new screen.**
|
||||
|
||||
In the chokidar `add` handler (new `.html` file detected), delete `$SCREEN_DIR/.events` if it exists. This is the definitive "new screen" signal — better than clearing on GET `/` which fires on every reload.
|
||||
|
||||
**C. Replace `wrapInFrame` content injection.**
|
||||
|
||||
The current regex anchors on `<div class="feedback-footer">`, which is being removed. Replace with a comment placeholder: remove the existing default content inside `#claude-content` (the `<h2>Visual Brainstorming</h2>` and subtitle paragraph) and replace with a single `<!-- CONTENT -->` marker. Content injection becomes `frameTemplate.replace('<!-- CONTENT -->', content)`. Simpler and won't break if template formatting changes.
|
||||
|
||||
### `frame-template.html` (UI frame)
|
||||
|
||||
**Remove:**
|
||||
- The `feedback-footer` div (textarea, Send button, label, `.feedback-row`)
|
||||
- Associated CSS (`.feedback-footer`, `.feedback-footer label`, `.feedback-row`, textarea and button styles within it)
|
||||
|
||||
**Add:**
|
||||
- `<!-- CONTENT -->` placeholder inside `#claude-content`, replacing the default text
|
||||
- A selection indicator bar where the footer was, with two states:
|
||||
- Default: "Click an option above, then return to the terminal"
|
||||
- After selection: "Option B selected — return to terminal to continue"
|
||||
- CSS for the indicator bar (subtle, similar visual weight to the existing header)
|
||||
|
||||
**Keep unchanged:**
|
||||
- Header bar with "Brainstorm Companion" title and connection status
|
||||
- `.main` wrapper and `#claude-content` container
|
||||
- All component CSS (`.options`, `.cards`, `.mockup`, `.split`, `.pros-cons`, placeholders, mock elements)
|
||||
- Dark/light theme variables and media query
|
||||
|
||||
### `helper.js` (client-side script)
|
||||
|
||||
**Remove:**
|
||||
- `sendToClaude()` function and the "Sent to Claude" page takeover
|
||||
- `window.send()` function (was tied to the removed Send button)
|
||||
- Form submission handler — no purpose without the feedback textarea, adds log noise
|
||||
- Input change handler — same reason
|
||||
- `pageshow` event listener (was added to fix textarea persistence — no textarea anymore)
|
||||
|
||||
**Keep:**
|
||||
- WebSocket connection, reconnect logic, event queue
|
||||
- Reload handler (`window.location.reload()` on server push)
|
||||
- `window.toggleSelect()` for selection highlighting
|
||||
- `window.selectedChoice` tracking
|
||||
- `window.brainstorm.send()` and `window.brainstorm.choice()` — these are distinct from the removed `window.send()`. They call `sendEvent` which logs to the server via WebSocket. Useful for custom full-document pages.
|
||||
|
||||
**Narrow:**
|
||||
- Click handler: capture only `[data-choice]` clicks, not all buttons/links. The broad capture was needed when the browser was a feedback channel; now it's just for selection tracking.
|
||||
|
||||
**Add:**
|
||||
- On `data-choice` click, update the selection indicator bar text to show which option was selected.
|
||||
|
||||
**Remove from `window.brainstorm` API:**
|
||||
- `brainstorm.sendToClaude` — no longer exists
|
||||
|
||||
### `visual-companion.md` (skill instructions)
|
||||
|
||||
**Rewrite "The Loop" section** to the non-blocking flow described above. Remove all references to:
|
||||
- `wait-for-feedback.sh`
|
||||
- `TaskOutput` blocking
|
||||
- Timeout/retry logic (600s timeout, 30-minute cap)
|
||||
- "User Feedback Format" section describing `send-to-claude` JSON
|
||||
|
||||
**Replace with:**
|
||||
- The new loop (write HTML → end turn → user responds in terminal → read `.events` → iterate)
|
||||
- `.events` file format documentation
|
||||
- Guidance that the terminal message is the primary feedback; `.events` provides the full browser interaction stream for additional context
|
||||
|
||||
**Keep:**
|
||||
- Server startup/shutdown instructions
|
||||
- Content fragment vs full document guidance
|
||||
- CSS class reference and available components
|
||||
- Design tips (scale fidelity to the question, 2-4 options per screen, etc.)
|
||||
|
||||
### `wait-for-feedback.sh`
|
||||
|
||||
**Deleted entirely.**
|
||||
|
||||
### `tests/brainstorm-server/server.test.js`
|
||||
|
||||
Tests that need updating:
|
||||
- Test asserting `feedback-footer` presence in fragment responses — update to assert the selection indicator bar or `<!-- CONTENT -->` replacement
|
||||
- Test asserting `helper.js` contains `send` — update to reflect narrowed API
|
||||
- Test asserting `sendToClaude` CSS variable usage — remove (function no longer exists)
|
||||
|
||||
## Platform Compatibility
|
||||
|
||||
The server code (`index.js`, `helper.js`, `frame-template.html`) is fully platform-agnostic — pure Node.js and browser JavaScript. No Claude Code-specific references. Already proven to work on Codex via background terminal interaction.
|
||||
|
||||
The skill instructions (`visual-companion.md`) are the platform-adaptive layer. Each platform's Claude uses its own tools to start the server, read `.events`, etc. The non-blocking model works naturally across platforms since it doesn't depend on any platform-specific blocking primitive.
|
||||
|
||||
## What This Enables
|
||||
|
||||
- **TUI always responsive** during visual brainstorming
|
||||
- **Mixed input** — click in browser + type in terminal, naturally merged
|
||||
- **Graceful degradation** — browser down or user doesn't open it? Terminal still works
|
||||
- **Simpler architecture** — no background tasks, no polling scripts, no timeout management
|
||||
- **Cross-platform** — same server code works on Claude Code, Codex, and any future platform
|
||||
|
||||
## What This Drops
|
||||
|
||||
- **Pure-browser feedback workflow** — user must return to the terminal to continue. The selection indicator bar guides them, but it's one extra step compared to the old click-Send-and-wait flow.
|
||||
- **Inline text feedback from browser** — the textarea is gone. All text feedback goes through the terminal. This is intentional — the terminal is a better text input channel than a small textarea in a frame.
|
||||
- **Immediate response on browser Send** — the old system had Claude respond the moment the user clicked Send. Now there's a gap while the user switches to the terminal. In practice this is seconds, and the user gets to add context in their terminal message.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Zero-Dependency Brainstorm Server
|
||||
|
||||
Replace the brainstorm companion server's vendored node_modules (express, ws, chokidar — 714 tracked files) with a single zero-dependency `server.js` using only Node.js built-ins.
|
||||
|
||||
## Motivation
|
||||
|
||||
Vendoring node_modules into the git repo creates a supply chain risk: frozen dependencies don't get security patches, 714 files of third-party code are committed without audit, and modifications to vendored code look like normal commits. While the actual risk is low (localhost-only dev server), eliminating it is straightforward.
|
||||
|
||||
## Architecture
|
||||
|
||||
A single `server.js` file (~250-300 lines) using `http`, `crypto`, `fs`, and `path`. The file serves two roles:
|
||||
|
||||
- **When run directly** (`node server.js`): starts the HTTP/WebSocket server
|
||||
- **When required** (`require('./server.js')`): exports WebSocket protocol functions for unit testing
|
||||
|
||||
### WebSocket Protocol
|
||||
|
||||
Implements RFC 6455 for text frames only:
|
||||
|
||||
**Handshake:** Compute `Sec-WebSocket-Accept` from client's `Sec-WebSocket-Key` using SHA-1 + the RFC 6455 magic GUID. Return 101 Switching Protocols.
|
||||
|
||||
**Frame decoding (client to server):** Handle three masked length encodings:
|
||||
- Small: payload < 126 bytes
|
||||
- Medium: 126-65535 bytes (16-bit extended)
|
||||
- Large: > 65535 bytes (64-bit extended)
|
||||
|
||||
XOR-unmask payload using 4-byte mask key. Return `{ opcode, payload, bytesConsumed }` or `null` for incomplete buffers. Reject unmasked frames.
|
||||
|
||||
**Frame encoding (server to client):** Unmasked frames with the same three length encodings.
|
||||
|
||||
**Opcodes handled:** TEXT (0x01), CLOSE (0x08), PING (0x09), PONG (0x0A). Unrecognized opcodes get a close frame with status 1003 (Unsupported Data).
|
||||
|
||||
**Deliberately skipped:** Binary frames, fragmented messages, extensions (permessage-deflate), subprotocols. These are unnecessary for small JSON text messages between localhost clients. Extensions and subprotocols are negotiated in the handshake — by not advertising them, they are never active.
|
||||
|
||||
**Buffer accumulation:** Each connection maintains a buffer. On `data`, append and loop `decodeFrame` until it returns null or buffer is empty.
|
||||
|
||||
### HTTP Server
|
||||
|
||||
Three routes:
|
||||
|
||||
1. **`GET /`** — Serve newest `.html` from screen directory by mtime. Detect full documents vs fragments, wrap fragments in frame template, inject helper.js. Return `text/html`. When no `.html` files exist, serve a hardcoded waiting page ("Waiting for Claude to push a screen...") with helper.js injected.
|
||||
2. **`GET /files/*`** — Serve static files from screen directory with MIME type lookup from a hardcoded extension map (html, css, js, png, jpg, gif, svg, json). Return 404 if not found.
|
||||
3. **Everything else** — 404.
|
||||
|
||||
WebSocket upgrade handled via the `'upgrade'` event on the HTTP server, separate from the request handler.
|
||||
|
||||
### Configuration
|
||||
|
||||
Environment variables (all optional):
|
||||
|
||||
- `BRAINSTORM_PORT` — port to bind (default: random high port 49152-65535)
|
||||
- `BRAINSTORM_HOST` — interface to bind (default: `127.0.0.1`)
|
||||
- `BRAINSTORM_URL_HOST` — hostname for the URL in startup JSON (default: `localhost` when host is `127.0.0.1`, otherwise same as host)
|
||||
- `BRAINSTORM_DIR` — screen directory path (default: `/tmp/brainstorm`)
|
||||
|
||||
### Startup Sequence
|
||||
|
||||
1. Create `SCREEN_DIR` if it doesn't exist (`mkdirSync` recursive)
|
||||
2. Load frame template and helper.js from `__dirname`
|
||||
3. Start HTTP server on configured host/port
|
||||
4. Start `fs.watch` on `SCREEN_DIR`
|
||||
5. On successful listen, log `server-started` JSON to stdout: `{ type, port, host, url_host, url, screen_dir }`
|
||||
6. Write the same JSON to `SCREEN_DIR/.server-info` so agents can find connection details when stdout is hidden (background execution)
|
||||
|
||||
### Application-Level WebSocket Messages
|
||||
|
||||
When a TEXT frame arrives from a client:
|
||||
|
||||
1. Parse as JSON. If parsing fails, log to stderr and continue.
|
||||
2. Log to stdout as `{ source: 'user-event', ...event }`.
|
||||
3. If the event contains a `choice` property, append the JSON to `SCREEN_DIR/.events` (one line per event).
|
||||
|
||||
### File Watching
|
||||
|
||||
`fs.watch(SCREEN_DIR)` replaces chokidar. On HTML file events:
|
||||
|
||||
- On new file (`rename` event for a file that exists): delete `.events` file if present (`unlinkSync`), log `screen-added` to stdout as JSON
|
||||
- On file change (`change` event): log `screen-updated` to stdout as JSON (do NOT clear `.events`)
|
||||
- Both events: send `{ type: 'reload' }` to all connected WebSocket clients
|
||||
|
||||
Debounce per-filename with ~100ms timeout to prevent duplicate events (common on macOS and Linux).
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Malformed JSON from WebSocket clients: log to stderr, continue
|
||||
- Unhandled opcodes: close with status 1003
|
||||
- Client disconnects: remove from broadcast set
|
||||
- `fs.watch` errors: log to stderr, continue
|
||||
- No graceful shutdown logic — shell scripts handle process lifecycle via SIGTERM
|
||||
|
||||
## What Changes
|
||||
|
||||
| Before | After |
|
||||
|---|---|
|
||||
| `index.js` + `package.json` + `package-lock.json` + 714 `node_modules` files | `server.js` (single file) |
|
||||
| express, ws, chokidar dependencies | none |
|
||||
| No static file serving | `/files/*` serves from screen directory |
|
||||
|
||||
## What Stays the Same
|
||||
|
||||
- `helper.js` — no changes
|
||||
- `frame-template.html` — no changes
|
||||
- `start-server.sh` — one-line update: `index.js` to `server.js`
|
||||
- `stop-server.sh` — no changes
|
||||
- `visual-companion.md` — no changes
|
||||
- All existing server behavior and external contract
|
||||
|
||||
## Platform Compatibility
|
||||
|
||||
- `server.js` uses only cross-platform Node built-ins
|
||||
- `fs.watch` is reliable for single flat directories on macOS, Linux, and Windows
|
||||
- Shell scripts require bash (Git Bash on Windows, which is required for Claude Code)
|
||||
|
||||
## Testing
|
||||
|
||||
**Unit tests** (`ws-protocol.test.js`): Test WebSocket frame encoding/decoding, handshake computation, and protocol edge cases directly by requiring `server.js` exports.
|
||||
|
||||
**Integration tests** (`server.test.js`): Test full server behavior — HTTP serving, WebSocket communication, file watching, brainstorming workflow. Uses `ws` npm package as a test-only client dependency (not shipped to end users).
|
||||
@@ -0,0 +1,244 @@
|
||||
# Codex App Compatibility: Worktree and Finishing Skill Adaptation
|
||||
|
||||
Make superpowers skills work in the Codex App's sandboxed worktree environment without breaking existing Claude Code or Codex CLI behavior.
|
||||
|
||||
**Ticket:** PRI-823
|
||||
|
||||
## Motivation
|
||||
|
||||
The Codex App runs agents inside git worktrees it manages — detached HEAD, located under `$CODEX_HOME/worktrees/`, with a Seatbelt sandbox that blocks `git checkout -b`, `git push`, and network access. Three superpowers skills assume unrestricted git access: `using-git-worktrees` creates manual worktrees with named branches, `finishing-a-development-branch` merges/pushes/PRs by branch name, and `subagent-driven-development` requires both.
|
||||
|
||||
The Codex CLI (open source terminal tool) does NOT have this conflict — it has no built-in worktree management. Our manual worktree approach fills an isolation gap there. The problem is specifically with the Codex App.
|
||||
|
||||
## Empirical Findings
|
||||
|
||||
Tested in the Codex App on 2026-03-23:
|
||||
|
||||
| Operation | workspace-write sandbox | Full access sandbox |
|
||||
|---|---|---|
|
||||
| `git add` | Works | Works |
|
||||
| `git commit` | Works | Works |
|
||||
| `git checkout -b` | **Blocked** (can't write `.git/refs/heads/`) | Works |
|
||||
| `git push` | **Blocked** (network + `.git/refs/remotes/`) | Works |
|
||||
| `gh pr create` | **Blocked** (network) | Works |
|
||||
| `git status/diff/log` | Works | Works |
|
||||
|
||||
Additional findings:
|
||||
- `spawn_agent` subagents **share** the parent thread's filesystem (confirmed via marker file test)
|
||||
- "Create branch" button appears in the App header regardless of which branch the worktree was started from
|
||||
- The App's native finishing flow: Create branch → Commit modal → Commit and push / Commit and create PR
|
||||
- `network_access = true` config is silently broken on macOS (issue #10390)
|
||||
|
||||
## Design: Read-Only Environment Detection
|
||||
|
||||
Three read-only git commands detect the environment without side effects:
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
Two signals derived:
|
||||
|
||||
- **IN_LINKED_WORKTREE:** `GIT_DIR != GIT_COMMON` — the agent is in a worktree created by something else (Codex App, Claude Code Agent tool, previous skill run, or the user)
|
||||
- **ON_DETACHED_HEAD:** `BRANCH` is empty — no named branch exists
|
||||
|
||||
Why `git-dir != git-common-dir` instead of checking `show-toplevel`:
|
||||
- In a normal repo, both resolve to the same `.git` directory
|
||||
- In a linked worktree, `git-dir` is `.git/worktrees/<name>` while `git-common-dir` is `.git`
|
||||
- In a submodule, both are equal — avoiding a false positive that `show-toplevel` would produce
|
||||
- Resolving via `cd && pwd -P` handles the relative-path problem (`git-common-dir` returns `.git` relative in normal repos but absolute in worktrees) and symlinks (macOS `/tmp` → `/private/tmp`)
|
||||
|
||||
### Decision Matrix
|
||||
|
||||
| Linked Worktree? | Detached HEAD? | Environment | Action |
|
||||
|---|---|---|---|
|
||||
| No | No | Claude Code / Codex CLI / normal git | Full skill behavior (unchanged) |
|
||||
| Yes | Yes | Codex App worktree (workspace-write) | Skip worktree creation; handoff payload at finish |
|
||||
| Yes | No | Codex App (Full access) or manual worktree | Skip worktree creation; full finishing flow |
|
||||
| No | Yes | Unusual (manual detached HEAD) | Create worktree normally; warn at finish |
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. `using-git-worktrees/SKILL.md` — Add Step 0 (~12 lines)
|
||||
|
||||
New section between "Overview" and "Directory Selection Process":
|
||||
|
||||
**Step 0: Check if Already in an Isolated Workspace**
|
||||
|
||||
Run the detection commands. If `GIT_DIR != GIT_COMMON`, skip worktree creation entirely. Instead:
|
||||
1. Skip to "Run Project Setup" subsection under Creation Steps — `npm install` etc. is idempotent, worth running for safety
|
||||
2. Then "Verify Clean Baseline" — run tests
|
||||
3. Report with branch state:
|
||||
- On a branch: "Already in an isolated workspace at `<path>` on branch `<name>`. Tests passing. Ready to implement."
|
||||
- Detached HEAD: "Already in an isolated workspace at `<path>` (detached HEAD, externally managed). Tests passing. Note: branch creation needed at finish time. Ready to implement."
|
||||
|
||||
If `GIT_DIR == GIT_COMMON`, proceed with the full worktree creation flow (unchanged).
|
||||
|
||||
Safety verification (.gitignore check) is skipped when Step 0 fires — irrelevant for externally-created worktrees.
|
||||
|
||||
Update the Integration section's "Called by" entries. Change the description on each from context-specific text to: "Ensures isolated workspace (creates one or verifies existing)". For example, the `subagent-driven-development` entry changes from "REQUIRED: Set up isolated workspace before starting" to "REQUIRED: Ensures isolated workspace (creates one or verifies existing)".
|
||||
|
||||
**Sandbox fallback:** If `GIT_DIR == GIT_COMMON` and the skill proceeds to Creation Steps, but `git worktree add -b` fails with a permission error (e.g., Seatbelt sandbox denial), treat this as a late-detected restricted environment. Fall back to the Step 0 "already in workspace" behavior — skip creation, run setup and baseline tests in the current directory, report accordingly.
|
||||
|
||||
After reporting in Step 0, STOP. Do not continue to Directory Selection or Creation Steps.
|
||||
|
||||
**Everything else unchanged:** Directory Selection, Safety Verification, Creation Steps, Project Setup, Baseline Tests, Quick Reference, Common Mistakes, Red Flags.
|
||||
|
||||
### 2. `finishing-a-development-branch/SKILL.md` — Add Step 1.5 + cleanup guard (~20 lines)
|
||||
|
||||
**Step 1.5: Detect Environment** (after Step 1 "Verify Tests", before Step 2 "Determine Base Branch")
|
||||
|
||||
Run the detection commands. Three paths:
|
||||
|
||||
- **Path A** skips Steps 2 and 3 entirely (no base branch or options needed).
|
||||
- **Paths B and C** proceed through Step 2 (Determine Base Branch) and Step 3 (Present Options) as normal.
|
||||
|
||||
**Path A — Externally managed worktree + detached HEAD** (`GIT_DIR != GIT_COMMON` AND `BRANCH` empty):
|
||||
|
||||
First, ensure all work is staged and committed (`git add` + `git commit`). The Codex App's finishing controls operate on committed work.
|
||||
|
||||
Then present this to the user (do NOT present the 4-option menu):
|
||||
|
||||
```
|
||||
Implementation complete. All tests passing.
|
||||
Current HEAD: <full-commit-sha>
|
||||
|
||||
This workspace is externally managed (detached HEAD).
|
||||
I cannot create branches, push, or open PRs from here.
|
||||
|
||||
⚠ These commits are on a detached HEAD. If you do not create a branch,
|
||||
they may be lost when this workspace is cleaned up.
|
||||
|
||||
If your host application provides these controls:
|
||||
- "Create branch" — to name a branch, then commit/push/PR
|
||||
- "Hand off to local" — to move changes to your local checkout
|
||||
|
||||
Suggested branch name: <ticket-id/short-description>
|
||||
Suggested commit message: <summary-of-work>
|
||||
```
|
||||
|
||||
Branch name derivation: use the ticket ID if available (e.g., `pri-823/codex-compat`), otherwise slugify the first 5 words of the plan title, otherwise omit the suggestion. Avoid including sensitive content (vulnerability descriptions, customer names) in branch names.
|
||||
|
||||
Skip to Step 5 (cleanup is a no-op for externally managed worktrees).
|
||||
|
||||
**Path B — Externally managed worktree + named branch** (`GIT_DIR != GIT_COMMON` AND `BRANCH` exists):
|
||||
|
||||
Present the 4-option menu as normal. (The Step 5 cleanup guard will re-detect the externally managed state independently.)
|
||||
|
||||
**Path C — Normal environment** (`GIT_DIR == GIT_COMMON`):
|
||||
|
||||
Present the 4-option menu as today (unchanged).
|
||||
|
||||
**Step 5 cleanup guard:**
|
||||
|
||||
Re-run the `GIT_DIR` vs `GIT_COMMON` detection at cleanup time (do not rely on earlier skill output — the finishing skill may run in a different session). If `GIT_DIR != GIT_COMMON`, skip `git worktree remove` — the host environment owns this workspace.
|
||||
|
||||
Otherwise, check and remove as today. Note: the existing Step 5 text says "For Options 1, 2, 4" but the Quick Reference table and Common Mistakes section say "Options 1 & 4 only." The new guard is added before this existing logic and does not change which options trigger cleanup.
|
||||
|
||||
**Everything else unchanged:** Options 1-4 logic, Quick Reference, Common Mistakes, Red Flags.
|
||||
|
||||
### 3. `subagent-driven-development/SKILL.md` and `executing-plans/SKILL.md` — 1 line edit each
|
||||
|
||||
Both skills have an identical Integration section line. Change from:
|
||||
```
|
||||
- superpowers:using-git-worktrees - REQUIRED: Set up isolated workspace before starting
|
||||
```
|
||||
To:
|
||||
```
|
||||
- superpowers:using-git-worktrees - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
|
||||
```
|
||||
|
||||
**Everything else unchanged:** Dispatch/review loop, prompt templates, model selection, status handling, red flags.
|
||||
|
||||
### 4. `codex-tools.md` — Add environment detection docs (~15 lines)
|
||||
|
||||
Two new sections at the end:
|
||||
|
||||
**Environment Detection:**
|
||||
|
||||
```markdown
|
||||
## Environment Detection
|
||||
|
||||
Skills that create worktrees or finish branches should detect their
|
||||
environment with read-only git commands before proceeding:
|
||||
|
||||
\```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
\```
|
||||
|
||||
- `GIT_DIR != GIT_COMMON` → already in a linked worktree (skip creation)
|
||||
- `BRANCH` empty → detached HEAD (cannot branch/push/PR from sandbox)
|
||||
|
||||
See `using-git-worktrees` Step 0 and `finishing-a-development-branch`
|
||||
Step 1.5 for how each skill uses these signals.
|
||||
```
|
||||
|
||||
**Codex App Finishing:**
|
||||
|
||||
```markdown
|
||||
## Codex App Finishing
|
||||
|
||||
When the sandbox blocks branch/push operations (detached HEAD in an
|
||||
externally managed worktree), the agent commits all work and informs
|
||||
the user to use the App's native controls:
|
||||
|
||||
- **"Create branch"** — names the branch, then commit/push/PR via App UI
|
||||
- **"Hand off to local"** — transfers work to the user's local checkout
|
||||
|
||||
The agent can still run tests, stage files, and output suggested branch
|
||||
names, commit messages, and PR descriptions for the user to copy.
|
||||
```
|
||||
|
||||
## What Does NOT Change
|
||||
|
||||
- `implementer-prompt.md`, `spec-reviewer-prompt.md`, `code-quality-reviewer-prompt.md` — subagent prompts untouched
|
||||
- `executing-plans/SKILL.md` — only the 1-line Integration description changes (same as `subagent-driven-development`); all runtime behavior is unchanged
|
||||
- `dispatching-parallel-agents/SKILL.md` — no worktree or finishing operations
|
||||
- `.codex/INSTALL.md` — installation process unchanged
|
||||
- The 4-option finishing menu — preserved exactly for Claude Code and Codex CLI
|
||||
- The full worktree creation flow — preserved exactly for non-worktree environments
|
||||
- Subagent dispatch/review/iterate loop — unchanged (filesystem sharing confirmed)
|
||||
|
||||
## Scope Summary
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `skills/using-git-worktrees/SKILL.md` | +12 lines (Step 0) |
|
||||
| `skills/finishing-a-development-branch/SKILL.md` | +20 lines (Step 1.5 + cleanup guard) |
|
||||
| `skills/subagent-driven-development/SKILL.md` | 1 line edit |
|
||||
| `skills/executing-plans/SKILL.md` | 1 line edit |
|
||||
| `skills/using-superpowers/references/codex-tools.md` | +15 lines |
|
||||
|
||||
~50 lines added/changed across 5 files. Zero new files. Zero breaking changes.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
If a third skill needs the same detection pattern, extract it into a shared `references/environment-detection.md` file (Approach B). Not needed now — only 2 skills use it.
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Automated (run in Claude Code after implementation)
|
||||
|
||||
1. Normal repo detection — assert IN_LINKED_WORKTREE=false
|
||||
2. Linked worktree detection — `git worktree add` test worktree, assert IN_LINKED_WORKTREE=true
|
||||
3. Detached HEAD detection — `git checkout --detach`, assert ON_DETACHED_HEAD=true
|
||||
4. Finishing skill handoff output — verify handoff message (not 4-option menu) in restricted environment
|
||||
5. **Step 5 cleanup guard** — create a linked worktree (`git worktree add /tmp/test-cleanup -b test-cleanup`), `cd` into it, run the Step 5 cleanup detection (`GIT_DIR` vs `GIT_COMMON`), assert it would NOT call `git worktree remove`. Then `cd` back to main repo, run the same detection, assert it WOULD call `git worktree remove`. Clean up test worktree afterward.
|
||||
|
||||
### Manual Codex App Tests (5 tests)
|
||||
|
||||
1. Detection in Worktree thread (workspace-write) — verify GIT_DIR != GIT_COMMON, empty branch
|
||||
2. Detection in Worktree thread (Full access) — same detection, different sandbox behavior
|
||||
3. Finishing skill handoff format — verify agent emits handoff payload, not 4-option menu
|
||||
4. Full lifecycle — detection → commit → finishing detection → correct behavior → cleanup
|
||||
5. **Sandbox fallback in Local thread** — Start a Codex App **Local thread** (workspace-write sandbox). Prompt: "Use the superpowers skill `using-git-worktrees` to set up an isolated workspace for implementing a small change." Pre-check: `git checkout -b test-sandbox-check` should fail with `Operation not permitted`. Expected: the skill detects `GIT_DIR == GIT_COMMON` (normal repo), attempts `git worktree add -b`, hits Seatbelt denial, falls back to Step 0 "already in workspace" behavior — runs setup, baseline tests, reports ready from current directory. Pass: agent recovers gracefully without cryptic error messages. Fail: agent prints raw Seatbelt error, retries, or gives up with confusing output.
|
||||
|
||||
### Regression
|
||||
|
||||
- Existing Claude Code skill-triggering tests still pass
|
||||
- Existing subagent-driven-development integration tests still pass
|
||||
- Normal Claude Code session: full worktree creation + 4-option finishing still works
|
||||
341
docs/superpowers/specs/2026-04-06-worktree-rototill-design.md
Normal file
341
docs/superpowers/specs/2026-04-06-worktree-rototill-design.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# Worktree Rototill: Detect-and-Defer
|
||||
|
||||
**Date:** 2026-04-06
|
||||
**Status:** Draft
|
||||
**Ticket:** PRI-974
|
||||
**Subsumes:** PRI-823 (Codex App compatibility)
|
||||
|
||||
## Problem
|
||||
|
||||
Superpowers is opinionated about worktree management — specific paths (`.worktrees/<branch>`), specific commands (`git worktree add`), specific cleanup (`git worktree remove`). Meanwhile, Claude Code, Codex App, Gemini CLI, and Cursor all provide native worktree support with their own paths, lifecycle management, and cleanup.
|
||||
|
||||
This creates three failure modes:
|
||||
|
||||
1. **Duplication** — on Claude Code, the skill does what `EnterWorktree`/`ExitWorktree` already does
|
||||
2. **Conflict** — on Codex App, the skill tries to create worktrees inside an already-managed worktree
|
||||
3. **Phantom state** — skill-created worktrees at `.worktrees/` are invisible to the harness; harness-created worktrees at `.claude/worktrees/` are invisible to the skill
|
||||
|
||||
For harnesses without native support (Codex CLI, OpenCode, Copilot standalone), superpowers fills a real gap. The skill shouldn't go away — it should get out of the way when native support exists.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Defer to native harness worktree systems when they exist
|
||||
2. Continue providing worktree support for harnesses that lack it
|
||||
3. Fix three known bugs in finishing-a-development-branch (#940, #999, #238)
|
||||
4. Make worktree creation opt-in rather than mandatory (#991)
|
||||
5. Replace hardcoded `CLAUDE.md` references with platform-neutral language (#1049)
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Per-worktree environment conventions (`.worktree-env.sh`, port offsetting) — Phase 4
|
||||
- PreToolUse hooks for path enforcement — Phase 4
|
||||
- Multi-repo worktree documentation — Phase 4
|
||||
- Brainstorming checklist changes for worktrees — Phase 4
|
||||
- `.superpowers-session.json` metadata tracking (interesting PR #997 idea, not needed for v1)
|
||||
- Hooks symlinking into worktrees (PR #965 idea, separate concern)
|
||||
|
||||
## Design Principles
|
||||
|
||||
### Detect state, not platform
|
||||
|
||||
Use `GIT_DIR != GIT_COMMON` to determine "am I already in a worktree?" rather than sniffing environment variables to identify the harness. This is a stable git primitive (since git 2.5, 2015), works universally across all harnesses, and requires zero maintenance as new harnesses appear.
|
||||
|
||||
### Declarative intent, prescriptive fallback
|
||||
|
||||
The skill describes the goal ("ensure work happens in an isolated workspace") and defers to native tools when available. It prescribes specific git commands only as a fallback for harnesses without native worktree support. Step 1a comes first and names native tools explicitly (`EnterWorktree`, `WorktreeCreate`, `/worktree`, `--worktree`); Step 1b comes second with the git fallback. The original spec kept Step 1a abstract ("you know your own toolkit"), but TDD proved that agents anchor on Step 1b's concrete commands when Step 1a is too vague. Explicit tool naming and a consent-authorization bridge were required to make the preference reliable.
|
||||
|
||||
### Provenance-based ownership
|
||||
|
||||
Whoever creates the worktree owns its cleanup. If the harness created it, superpowers doesn't touch it. If superpowers created it (via git fallback), superpowers cleans it up. The heuristic: if the worktree lives under `.worktrees/` or `worktrees/`, superpowers owns it. Anything else (`.claude/worktrees/`, `~/.codex/worktrees/`, `.gemini/worktrees/`, or old user-global Superpowers paths) belongs to the harness or user and is left alone.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. `using-git-worktrees` SKILL.md Rewrite
|
||||
|
||||
The skill gains three new steps before creation and simplifies the creation flow.
|
||||
|
||||
#### Step 0: Detect Existing Isolation
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
Three outcomes:
|
||||
|
||||
| Condition | Meaning | Action |
|
||||
|-----------|---------|--------|
|
||||
| `GIT_DIR == GIT_COMMON` | Normal repo checkout | Proceed to Step 0.5 |
|
||||
| `GIT_DIR != GIT_COMMON`, named branch | Already in a linked worktree | Skip to Step 3 (project setup). Report: "Already in isolated workspace at `<path>` on branch `<name>`." |
|
||||
| `GIT_DIR != GIT_COMMON`, detached HEAD | Externally managed worktree (e.g., Codex App sandbox) | Skip to Step 3. Report: "Already in isolated workspace at `<path>` (detached HEAD, externally managed)." |
|
||||
|
||||
Step 0 does not care who created the worktree or which harness is running. A worktree is a worktree regardless of origin.
|
||||
|
||||
**Submodule guard:** `GIT_DIR != GIT_COMMON` is also true inside git submodules. Before concluding "already in a worktree," check that we're not in a submodule:
|
||||
|
||||
```bash
|
||||
# If this returns a path, we're in a submodule, not a worktree
|
||||
git rev-parse --show-superproject-working-tree 2>/dev/null
|
||||
```
|
||||
|
||||
If in a submodule, treat as `GIT_DIR == GIT_COMMON` (proceed to Step 0.5).
|
||||
|
||||
#### Step 0.5: Consent
|
||||
|
||||
When Step 0 finds no existing isolation (`GIT_DIR == GIT_COMMON`), ask before creating:
|
||||
|
||||
> "Would you like me to set up an isolated worktree? This protects your current branch from changes. (y/n)"
|
||||
|
||||
If yes, proceed to Step 1. If no, work in place — skip to Step 3 with no worktree.
|
||||
|
||||
This step is skipped entirely when Step 0 detects existing isolation (no point asking about what already exists).
|
||||
|
||||
#### Step 1a: Native Tools (preferred)
|
||||
|
||||
> The user has asked for an isolated workspace (Step 0 consent). Check your available tools — do you have `EnterWorktree`, `WorktreeCreate`, a `/worktree` command, or a `--worktree` flag? If YES: the user's consent to create a worktree is your authorization to use it. Use it now and skip to Step 3.
|
||||
|
||||
After using a native tool, skip to Step 3 (project setup).
|
||||
|
||||
**Design note — TDD revision:** The original spec used a deliberately short, abstract Step 1a ("You know your own toolkit — the skill does not need to name specific tools"). TDD validation disproved this: agents anchored on Step 1b's concrete git commands and ignored the abstract guidance (2/6 pass rate). Three changes fixed it (50/50 pass rate across GREEN and PRESSURE tests):
|
||||
|
||||
1. **Explicit tool naming** — listing `EnterWorktree`, `WorktreeCreate`, `/worktree`, `--worktree` by name transforms the decision from interpretation ("do I have a native tool?") into factual lookup ("is `EnterWorktree` in my tool list?"). Agents on platforms without these tools simply check, find nothing, and fall through to Step 1b. No false positives observed.
|
||||
2. **Consent bridge** — "the user's consent to create a worktree is your authorization to use it" directly addresses `EnterWorktree`'s tool-level guardrail ("ONLY when user explicitly asks"). Tool descriptions override skill instructions (Claude Code #29950), so the skill must frame user consent as the authorization the tool requires.
|
||||
3. **Red Flag entry** — naming the specific anti-pattern ("Use `git worktree add` when you have a native worktree tool — this is the #1 mistake") in the Red Flags section.
|
||||
|
||||
File splitting (Step 1b in a separate skill) was tested and proven unnecessary. The anchoring problem is solved by the quality of Step 1a's text, not by physical separation of git commands. Control tests with the full 240-line skill (all git commands visible) passed 20/20.
|
||||
|
||||
#### Step 1b: Git Worktree Fallback
|
||||
|
||||
When no native tool is available, create a worktree manually.
|
||||
|
||||
**Directory selection** (priority order):
|
||||
1. Check the project's agent instruction file (CLAUDE.md, GEMINI.md, AGENTS.md, .cursorrules, or equivalent) for a worktree directory preference.
|
||||
2. Check for existing `.worktrees/` or `worktrees/` directory — if found, use it. If both exist, `.worktrees/` wins.
|
||||
3. Default to `.worktrees/`.
|
||||
|
||||
No interactive directory selection prompt. Old user-global Superpowers worktree paths are not detected or offered; new manual worktrees are project-local unless the user explicitly specifies another location.
|
||||
|
||||
**Safety verification** (project-local directories only):
|
||||
|
||||
```bash
|
||||
git check-ignore -q .worktrees 2>/dev/null
|
||||
```
|
||||
|
||||
If not ignored, add to `.gitignore` and commit before proceeding.
|
||||
|
||||
**Create:**
|
||||
|
||||
```bash
|
||||
git worktree add "$path" -b "$BRANCH_NAME"
|
||||
cd "$path"
|
||||
```
|
||||
|
||||
**Hooks awareness:** Git worktrees do not inherit the parent repo's hooks directory. After creating a worktree via 1b, symlink the hooks directory from the main repo if one exists:
|
||||
|
||||
```bash
|
||||
if [ -d "$MAIN_ROOT/.git/hooks" ]; then
|
||||
ln -sf "$MAIN_ROOT/.git/hooks" "$path/.git/hooks"
|
||||
fi
|
||||
```
|
||||
|
||||
This prevents pre-commit checks, linters, and other hooks from silently stopping when work moves to a worktree. (Idea from PR #965.)
|
||||
|
||||
**Sandbox fallback:** If `git worktree add` fails with a permission error, treat as a restricted environment. Skip creation, work in current directory, proceed to Step 3.
|
||||
|
||||
**Step numbering note:** The current skill has Steps 1-4 as a flat list. This redesign uses 0, 0.5, 1a, 1b, 3, 4. There is no Step 2 — it was the old monolithic "Create Isolated Workspace" which is now split into the 1a/1b structure. The implementation should renumber cleanly (e.g., 0 → "Step 0: Detect", 0.5 → within Step 0's flow, 1a/1b → "Step 1", 3 → "Step 2", 4 → "Step 3") or keep the current numbering with a note. Implementer's choice.
|
||||
|
||||
#### Steps 3-4: Project Setup and Baseline Tests (unchanged)
|
||||
|
||||
Regardless of which path created the workspace (Step 0 detected existing, Step 1a native tool, Step 1b git fallback, or no worktree at all), execution converges:
|
||||
|
||||
- **Step 3:** Auto-detect and run project setup (`npm install`, `cargo build`, `pip install`, `go mod download`, etc.)
|
||||
- **Step 4:** Run the test suite. If tests fail, report failures and ask whether to proceed.
|
||||
|
||||
### 2. `finishing-a-development-branch` SKILL.md Rewrite
|
||||
|
||||
The finishing skill gains environment detection and fixes three bugs.
|
||||
|
||||
#### Step 1: Verify Tests (unchanged)
|
||||
|
||||
Run the project's test suite. If tests fail, stop. Don't offer completion options.
|
||||
|
||||
#### Step 1.5: Detect Environment (new)
|
||||
|
||||
Re-run the same detection as Step 0 in creation:
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
```
|
||||
|
||||
Three paths:
|
||||
|
||||
| State | Menu | Cleanup |
|
||||
|-------|------|---------|
|
||||
| `GIT_DIR == GIT_COMMON` (normal repo) | Standard 4 options | No worktree to clean up |
|
||||
| `GIT_DIR != GIT_COMMON`, named branch | Standard 4 options | Provenance-based (see Step 5) |
|
||||
| `GIT_DIR != GIT_COMMON`, detached HEAD | Reduced menu: push as new branch + PR, keep as-is, discard | No merge options (can't merge from detached HEAD) |
|
||||
|
||||
#### Step 2: Determine Base Branch (unchanged)
|
||||
|
||||
#### Step 3: Present Options
|
||||
|
||||
**Normal repo and named-branch worktree:**
|
||||
|
||||
1. Merge back to `<base-branch>` locally
|
||||
2. Push and create a Pull Request
|
||||
3. Keep the branch as-is (I'll handle it later)
|
||||
4. Discard this work
|
||||
|
||||
**Detached HEAD:**
|
||||
|
||||
1. Push as new branch and create a Pull Request
|
||||
2. Keep as-is (I'll handle it later)
|
||||
3. Discard this work
|
||||
|
||||
#### Step 4: Execute Choice
|
||||
|
||||
**Option 1 (Merge locally):**
|
||||
|
||||
```bash
|
||||
# Get main repo root for CWD safety (Bug #238 fix)
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
|
||||
# Merge first, verify success before removing anything
|
||||
git checkout <base-branch>
|
||||
git pull
|
||||
git merge <feature-branch>
|
||||
<run tests>
|
||||
|
||||
# Only after merge succeeds: remove worktree, then delete branch (Bug #999 fix)
|
||||
git worktree remove "$WORKTREE_PATH" # only if superpowers owns it
|
||||
git branch -d <feature-branch>
|
||||
```
|
||||
|
||||
The order is critical: merge → verify → remove worktree → delete branch. The old skill deleted the branch before removing the worktree (which fails because the worktree still references the branch). The naive fix of removing the worktree first is also wrong — if the merge then fails, the working directory is gone and changes are lost.
|
||||
|
||||
**Option 2 (Create PR):**
|
||||
|
||||
Push branch, create PR. Do NOT clean up worktree — user needs it for PR iteration. (Bug #940 fix: remove contradictory "Then: Cleanup worktree" prose.)
|
||||
|
||||
**Option 3 (Keep as-is):** No action.
|
||||
|
||||
**Option 4 (Discard):** Require typed "discard" confirmation. Then remove worktree (if superpowers owns it), force-delete branch.
|
||||
|
||||
#### Step 5: Cleanup (updated)
|
||||
|
||||
```
|
||||
if GIT_DIR == GIT_COMMON:
|
||||
# Normal repo, no worktree to clean up
|
||||
done
|
||||
|
||||
if worktree path is under .worktrees/ or worktrees/:
|
||||
# Superpowers created it — we own cleanup
|
||||
cd to main repo root # Bug #238 fix
|
||||
git worktree remove <path>
|
||||
|
||||
else:
|
||||
# Harness created it — hands off
|
||||
# If platform provides a workspace-exit tool, use it
|
||||
# Otherwise, leave the worktree in place
|
||||
```
|
||||
|
||||
Cleanup only runs for Options 1 and 4. Options 2 and 3 always preserve the worktree. (Bug #940 fix.)
|
||||
|
||||
**Stale worktree pruning:** After any `git worktree remove`, run `git worktree prune` as a self-healing step. Worktree directories can get deleted out-of-band (e.g., by harness cleanup, manual `rm`, or `.claude/` cleanup), leaving stale registrations that cause confusing errors. One line, prevents silent rot. (Idea from PR #1072.)
|
||||
|
||||
### 3. Integration Updates
|
||||
|
||||
#### `subagent-driven-development` and `executing-plans`
|
||||
|
||||
Both currently list `using-git-worktrees` as REQUIRED in their integration sections. Change to:
|
||||
|
||||
> `using-git-worktrees` — Ensures isolated workspace (creates one or verifies existing)
|
||||
|
||||
The skill itself now handles consent (Step 0.5) and detection (Step 0), so calling skills don't need to gate or prompt.
|
||||
|
||||
#### `writing-plans`
|
||||
|
||||
Remove the stale claim "should be run in a dedicated worktree (created by brainstorming skill)." Brainstorming is a design skill and does not create worktrees. The worktree prompt happens at execution time via `using-git-worktrees`.
|
||||
|
||||
### 4. Platform-Neutral Instruction File References
|
||||
|
||||
All instances of hardcoded `CLAUDE.md` in worktree-related skills are replaced with:
|
||||
|
||||
> "your project's agent instruction file (CLAUDE.md, GEMINI.md, AGENTS.md, .cursorrules, or equivalent)"
|
||||
|
||||
This applies to directory preference checks in Step 1b.
|
||||
|
||||
## Bug Fixes (bundled)
|
||||
|
||||
| Bug | Problem | Fix | Location |
|
||||
|-----|---------|-----|----------|
|
||||
| #940 | Option 2 prose says "Then: Cleanup worktree (Step 5)" but quick reference says keep it. Step 5 says "For Options 1, 2, 4" but Common Mistakes says "Options 1 and 4 only." | Remove cleanup from Option 2. Step 5 applies to Options 1 and 4 only. | finishing SKILL.md |
|
||||
| #999 | Option 1 deletes branch before removing worktree. `git branch -d` can fail because worktree still references the branch. | Reorder to: merge → verify tests → remove worktree → delete branch. Merge must succeed before anything is removed. | finishing SKILL.md |
|
||||
| #238 | `git worktree remove` fails silently if CWD is inside the worktree being removed. | Add CWD guard: `cd` to main repo root before `git worktree remove`. | finishing SKILL.md |
|
||||
|
||||
## Issues Resolved
|
||||
|
||||
| Issue | Resolution |
|
||||
|-------|-----------|
|
||||
| #940 | Direct fix (Bug #940) |
|
||||
| #991 | Opt-in consent in Step 0.5 |
|
||||
| #918 | Step 0 detection + Step 1.5 finishing detection |
|
||||
| #1009 | Resolved by Step 1a — agents use native tools (e.g., `EnterWorktree`) which create at harness-native paths. Depends on Step 1a working; see Risks. |
|
||||
| #999 | Direct fix (Bug #999) |
|
||||
| #238 | Direct fix (Bug #238) |
|
||||
| #1049 | Platform-neutral instruction file references |
|
||||
| #279 | Solved by detect-and-defer — native paths respected because we don't override them |
|
||||
| #574 | **Deferred.** Nothing in this spec touches the brainstorming skill where the bug lives. Full fix (adding a worktree step to brainstorming's checklist) is Phase 4. |
|
||||
|
||||
## Risks
|
||||
|
||||
### Step 1a is the load-bearing assumption — RESOLVED
|
||||
|
||||
Step 1a — agents preferring native worktree tools over the git fallback — is the foundation the entire design rests on. If agents ignore Step 1a and fall through to Step 1b on harnesses with native support, detect-and-defer fails entirely.
|
||||
|
||||
**Status:** This risk materialized during implementation. The original abstract Step 1a ("You know your own toolkit") failed at 2/6 on Claude Code. The TDD gate worked as designed — it caught the failure before any skill files were modified, preventing a broken release. Three REFACTOR iterations identified the root causes (agent anchoring on concrete commands, tool-description guardrail overriding skill instructions) and produced a fix validated at 50/50 across GREEN and PRESSURE tests. See Step 1a design note above for details.
|
||||
|
||||
**Cross-platform validation:**
|
||||
|
||||
As of 2026-04-06, Claude Code is the only harness with an agent-callable mid-session worktree tool (`EnterWorktree`). All others either create worktrees before the agent starts (Codex App, Gemini CLI, Cursor) or have no native worktree support (Codex CLI, OpenCode). Step 1a is forward-compatible: when other harnesses add agent-callable worktree tools, agents will match them against the named examples and use them without skill changes.
|
||||
|
||||
| Harness | Current worktree model | Skill mechanism | Tested |
|
||||
|---------|----------------------|-----------------|--------|
|
||||
| Claude Code | Agent-callable `EnterWorktree` | Step 1a | 50/50 (GREEN + PRESSURE) |
|
||||
| Codex CLI | No native tool (shell only) | Step 1b git fallback | 6/6 (`codex exec`) |
|
||||
| Gemini CLI | Launch-time `--worktree` flag, no agent tool | Step 0 if launched with flag, Step 1b if not | Step 0: 1/1, Step 1b: 1/1 (`gemini -p`) |
|
||||
| Cursor Agent | User-facing `/worktree`, no agent tool | Step 0 if user activated, Step 1b if not | Step 0: 1/1, Step 1b: 1/1 (`cursor-agent -p`) |
|
||||
| Codex App | Platform-managed, detached HEAD, no agent tool | Step 0 detects existing | 1/1 simulated |
|
||||
| OpenCode | Detection only (`ctx.worktree`), no agent tool | Step 1b git fallback | Untested (no CLI access) |
|
||||
|
||||
**Residual risks:**
|
||||
1. If Anthropic changes `EnterWorktree`'s tool description to be more restrictive (e.g., "Do not use based on skill instructions"), the consent bridge breaks. Worth filing an issue requesting that the tool description accommodate skill-driven invocation.
|
||||
2. When other harnesses add agent-callable worktree tools, they may use names not in Step 1a's list. The list should be updated as new tools appear. The generic phrasing ("a worktree or workspace-isolation tool") provides some forward coverage.
|
||||
|
||||
### Provenance heuristic
|
||||
|
||||
The `.worktrees/` or `worktrees/` = ours, anything else = hands off` heuristic works for every current harness. If a future harness adopts one of those project-local directories as its convention, we'd have a false positive (superpowers tries to clean up a harness-owned worktree). Similarly, if a user manually runs `git worktree add .worktrees/experiment` without superpowers, we'd incorrectly claim ownership. Both are low risk — every harness uses branded paths, and manual `.worktrees/` creation is unlikely — but worth noting.
|
||||
|
||||
### Detached HEAD finishing
|
||||
|
||||
The reduced menu for detached HEAD worktrees (no merge option) is correct for Codex App's sandbox model. If a user is in detached HEAD for another reason, the reduced menu still makes sense — you genuinely can't merge from detached HEAD without creating a branch first.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Both skill files contain sections beyond the core steps that need updating during implementation:
|
||||
|
||||
- **Frontmatter** (`name`, `description`): Update to reflect detect-and-defer behavior
|
||||
- **Quick Reference tables**: Rewrite to match new step structure and bug fixes
|
||||
- **Common Mistakes sections**: Update or remove items that reference old behavior (e.g., "Skip CLAUDE.md check" is now wrong)
|
||||
- **Red Flags sections**: Update to reflect new priorities (e.g., "Never create a worktree when Step 0 detects existing isolation")
|
||||
- **Integration sections**: Update cross-references between skills
|
||||
|
||||
The spec describes *what changes*; the implementation plan will specify exact edits to these secondary sections.
|
||||
|
||||
## Future Work (not in this spec)
|
||||
|
||||
- **Phase 3 remainder:** `$TMPDIR` directory option (#666), setup docs for caching and env inheritance (#299)
|
||||
- **Phase 4:** PreToolUse hooks for path enforcement (#1040), per-worktree env conventions (#597), brainstorming checklist worktree step (#574), multi-repo documentation (#710)
|
||||
@@ -0,0 +1,247 @@
|
||||
# Lift drill into superpowers as `evals/` — design
|
||||
|
||||
## Background
|
||||
|
||||
Drill is a Python skill-compliance benchmark that lives in its own repo at `obra/drill`. It drives real tmux sessions, runs an LLM actor as a simulated user, runs an LLM verifier on the resulting transcript, and reports pass/fail per scenario. It supports Claude Code, Codex, Gemini CLI, and (per recent commits) OpenCode and Copilot CLI.
|
||||
|
||||
Drill is already the *de facto* eval harness for superpowers. The PRI-1397 commit series in the drill repo lifted ~22 superpowers bash tests into drill scenarios, and the most recent superpowers commit (`a2292c5`) explicitly removed a redundant bash test with the message *"replaced by drill behavioral coverage"*. Migration momentum exists; this spec completes it.
|
||||
|
||||
This work moves drill into superpowers under `evals/`, deletes the redundant bash tests after per-file verification of drill scenario coverage, and updates docs so contributors land on the new structure.
|
||||
|
||||
## Goals
|
||||
|
||||
1. `evals/` is the canonical eval harness in superpowers — full drill source, scenarios, fixtures, prompts, backend configs, and tests.
|
||||
2. Bash tests in `superpowers/tests/` that have been individually verified as 100% covered by drill scenarios are deleted; the rest are preserved.
|
||||
3. The split between `tests/` (plugin infrastructure: bash + node + python integration tests) and `evals/` (LLM behavior with actor + verifier) is meaningful and documented.
|
||||
4. Top-level docs (`README.md`, `CLAUDE.md`, `docs/testing.md`) point contributors at the right place.
|
||||
5. The standalone `obra/drill` repo continues to exist (this PR does not touch it) and gets archived as a separate manual step after this PR merges.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- **CI integration.** Manual-only here. The natural follow-up is "tiered": fast subset on every PR, full sweep nightly + on-demand. That requires API budget decisions, GitHub Actions secrets, and a runner image with `tmux` + `node` + `python` + `claude` / `codex` / `gemini` CLIs installed. Out of scope.
|
||||
- **Scenario co-location with skills.** Scenarios stay centralized at `evals/scenarios/`. If we later decide each skill should own its scenarios, that's a path-find-and-rename operation; the YAML format does not change.
|
||||
- **Renaming the internal Python package** (`drill` → `evals`). The directory is `evals/` (user-facing); the Python package keeps its `drill` name to keep the diff small. A short note in `evals/README.md` explains.
|
||||
- **Drill repo archival.** This PR does not touch `obra/drill`. After merge, the drill repo is archived manually (read-only on GitHub, README pointer to `obra/superpowers/evals/`).
|
||||
- **Lifting `tests/claude-code/analyze-token-usage.py` into `evals/bin/`.** Useful utility, not test code. Can move later; not required by this PR.
|
||||
|
||||
## Branching
|
||||
|
||||
Branch off `dev` as `f/evals-lift`. This work is independent of the open `f/cross-platform` PR — no shared file changes besides possibly `README.md`, which is small enough to resolve at merge time if it conflicts.
|
||||
|
||||
## Architecture after the move
|
||||
|
||||
```
|
||||
superpowers/
|
||||
evals/ ← NEW (full drill copy)
|
||||
pyproject.toml (Python 3.11, uv-managed)
|
||||
uv.lock
|
||||
.gitignore (drill's own; results/, .venv/, .env)
|
||||
README.md (was drill's README; install instructions updated)
|
||||
CLAUDE.md (was drill's CLAUDE.md; paths updated)
|
||||
docs/
|
||||
design.md (drill's design — preserved verbatim, cross-linked from this spec)
|
||||
manual-testing.md
|
||||
pressure-and-red-testing.md
|
||||
drill/ (Python package; name kept; cli, engine, actor, verifier, etc.)
|
||||
backends/ (claude-*.yaml, codex.yaml, gemini.yaml)
|
||||
scenarios/ (32+ YAML scenarios)
|
||||
setup_helpers/ (15 Python helpers; create_base_repo, sdd_*, spec_*, worktree, etc.)
|
||||
fixtures/ (template-repo, sdd-go-fractals, sdd-svelte-todo)
|
||||
prompts/ (actor.md, verifier.md)
|
||||
bin/ (assertion helper scripts: tool-called, tool-count, etc.)
|
||||
tests/ (drill's own pytest suite)
|
||||
|
||||
tests/ ← bash tests preserved by default
|
||||
brainstorm-server/ ← KEEP (node tests for brainstorm-server JS code)
|
||||
opencode/ ← KEEP (plugin loading tests)
|
||||
codex-plugin-sync/ ← KEEP (sync verification)
|
||||
claude-code/ ← MOSTLY KEEP — see deletion gate
|
||||
explicit-skill-requests/ ← KEEP unless verified replaced
|
||||
skill-triggering/ ← KEEP unless verified replaced
|
||||
subagent-driven-dev/ ← KEEP unless verified replaced
|
||||
|
||||
docs/
|
||||
testing.md ← UPDATED (split into "Plugin tests" + "Skill behavior evals")
|
||||
superpowers/
|
||||
specs/
|
||||
2026-05-06-lift-drill-into-evals-design.md ← THIS SPEC
|
||||
|
||||
README.md ← small Contributing-section pointer to evals/
|
||||
CLAUDE.md ← one-line "Eval harness lives at evals/" pointer
|
||||
```
|
||||
|
||||
The `tests/` and `evals/` directories serve clearly distinct roles after this PR:
|
||||
|
||||
- **`tests/`** — does the plugin's non-LLM code work? Unit and integration tests for the brainstorm-server JS code, OpenCode plugin loading, codex-plugin-sync sync verification. Bash + node + python.
|
||||
- **`evals/`** — do agents behave correctly on real LLM sessions? Drill scenarios with actor + verifier. Python-only, runs real tmux sessions.
|
||||
|
||||
## Deletion gate (per bash test)
|
||||
|
||||
A bash test is deleted *only if* a drill scenario verifiably covers every assertion it makes. The implementation plan documents this verification per file: read the bash test, list its checks, find the drill scenario, confirm each check has a matching `verify.assertions` or `verify.criteria` entry. If even one check is missing, the option is to either extend the drill scenario or keep the bash test. Default keeps it.
|
||||
|
||||
**Tentative coverage map** (commit-message-based; needs per-file verification before any deletion):
|
||||
|
||||
| Bash test | Claimed drill replacement | Coverage status |
|
||||
|-----------|---------------------------|-----------------|
|
||||
| `tests/skill-triggering/prompts/*` (6 prompt files) | `triggering-*.yaml` (6 scenarios) | candidate — verify per-prompt before deleting |
|
||||
| `tests/skill-triggering/run-test.sh`, `run-all.sh` | n/a (runners, not tests) | **keep** — runner scripts |
|
||||
| `tests/explicit-skill-requests/prompts/please-use-brainstorming.txt` | needs verification — drill has no obvious counterpart yet | likely **keep** unless drill scenario added |
|
||||
| `tests/explicit-skill-requests/prompts/use-systematic-debugging.txt` | needs verification — drill has no obvious counterpart | likely **keep** unless drill scenario added |
|
||||
| `tests/explicit-skill-requests/run-claude-describes-sdd.sh` | partially → `mid-conversation-skill-invocation.yaml` | candidate — verify per-script |
|
||||
| `tests/explicit-skill-requests/run-haiku-test.sh` | no drill scenario covers Haiku-specific behavior | **keep** |
|
||||
| `tests/explicit-skill-requests/run-multiturn-test.sh`, `run-extended-multiturn-test.sh` | no drill scenario covers multi-turn build-up | **keep** unless drill scenarios added |
|
||||
| `tests/explicit-skill-requests/run-test.sh`, `run-all.sh` | n/a (runners) | **keep** |
|
||||
| `tests/subagent-driven-dev/go-fractals/`, `tests/subagent-driven-dev/svelte-todo/` | `sdd-go-fractals.yaml`, `sdd-svelte-todo.yaml` | candidate — verify before deleting (these include real assertions about test suites passing) |
|
||||
| `tests/claude-code/test-document-review-system.sh` | `spec-reviewer-catches-planted-flaws.yaml` | candidate — verify before deleting |
|
||||
| `tests/claude-code/test-requesting-code-review.sh` | `code-review-catches-planted-bugs.yaml` | candidate — verify before deleting |
|
||||
| `tests/claude-code/test-subagent-driven-development-integration.sh` | `sdd-rejects-extra-features.yaml` (YAGNI subset) | **partial** — bash test also asserts ≥3 commits / `npm test` passes / runs `analyze-token-usage.py`. Drill scenario asserts forbidden-exports + reviewer-as-gate. Mostly disjoint — almost certainly **keep + extend drill scenario**. |
|
||||
| `tests/claude-code/test-subagent-driven-development.sh` | meta/documentation test (asks agent to *describe* SDD); no drill scenario covers description tests | **keep** unless drill scenario added |
|
||||
| `tests/claude-code/test-worktree-native-preference.sh` | `worktree-creation-under-pressure.yaml` | candidate — verify before deleting |
|
||||
| `tests/claude-code/test-helpers.sh`, `run-skill-tests.sh`, `analyze-token-usage.py` | n/a (utilities, not tests) | **keep** — libraries/tools |
|
||||
|
||||
## Verification protocol (subagent-gated)
|
||||
|
||||
Every change in the implementation plan gets cross-checked by an independent subagent before commit.
|
||||
|
||||
| Change category | Subagent verification |
|
||||
|----------------|----------------------|
|
||||
| Each bash-test deletion | Dispatch a subagent with: (a) the bash test file content, (b) the candidate drill scenario YAML, (c) the prompt: *"List every assertion the bash test makes. List every verify entry in the drill scenario. For each bash assertion, find a matching drill check or report it as unmatched. Output a per-assertion table."* The subagent's output is the gate — only delete if every bash assertion has a match. |
|
||||
| Initial `evals/` copy | Subagent verifies: (a) drill SHA being copied is recorded in the lift commit message so provenance is auditable; (b) **per-file SHA-256 checksum** matches drill repo for every file (not just file count); (c) excluded paths (`.git/`, `.venv/`, `results/`, `.env`, `__pycache__/`, `*.egg-info/`, any `.private-journal/`) are absent from `evals/`; (d) all backend YAMLs reference paths that exist post-move; (e) `pyproject.toml`, `uv.lock`, `.gitignore` are intact. |
|
||||
| Drill's own pytest suite | Subagent runs `cd evals && uv run pytest` after the path-default change. Drill ships its own pytest suite at `evals/tests/` including `test_backend.py` which exercises `SUPERPOWERS_ROOT` env-var behavior — these tests must update to match the helper and continue to pass. |
|
||||
| Reference scrubbing after deletion | Subagent greps the entire superpowers tree (excluding `node_modules/`, `.venv/`, and `evals/`) for references to deleted bash test paths. Search targets: `docs/`, `docs/superpowers/plans/`, `RELEASE-NOTES.md`, `CLAUDE.md`, `GEMINI.md`, `AGENTS.md`, `README.md`, `.github/`, `scripts/`, `.opencode/INSTALL.md`, `.codex-plugin/INSTALL.md`, `lefthook.yml`. Any hit is either updated or surfaces a missed dependency. |
|
||||
| Path defaults change (`SUPERPOWERS_ROOT` default) | Subagent runs at least one cheap drill scenario after the path changes (e.g., `triggering-test-driven-development`) and confirms it still passes. Real validation, not just code review. |
|
||||
| Final pre-PR adversarial review | Two subagents in parallel, "5 points to whoever finds the most legitimate issues" framing — same protocol used on the cross-platform PR. Verify both source code and behavior. |
|
||||
|
||||
Each subagent task gets its own bullet in the implementation plan with explicit inputs and pass criteria. The subagent's output is summarized in the relevant commit message ("Subagent verification: …") so the trail is auditable.
|
||||
|
||||
## Concrete path/config edits
|
||||
|
||||
**Verified prior to writing this spec.** `drill/cli.py` defines `PROJECT_ROOT = Path(__file__).parent.parent`. After the move, `cli.py` lives at `evals/drill/cli.py`, so `PROJECT_ROOT` resolves to `evals/` and `PROJECT_ROOT.parent` resolves to the superpowers repo root. That's the value `SUPERPOWERS_ROOT` should take by default.
|
||||
|
||||
**YAML substitution audit.** Only the four `claude*.yaml` backend configs interpolate `${SUPERPOWERS_ROOT}` into `args` (for the `--plugin-dir` flag); `codex.yaml` and `gemini.yaml` only list `SUPERPOWERS_ROOT` in `required_env` (consumed by `engine.py:233` / `setup.py:25`'s `os.environ["SUPERPOWERS_ROOT"]` lookups in pre/post-run hooks). The helper's `os.environ` mutation covers both code paths.
|
||||
|
||||
| File | Current | After |
|
||||
|------|---------|-------|
|
||||
| `drill/cli.py` | `load_dotenv(PROJECT_ROOT / ".env")` at module import; nothing about `SUPERPOWERS_ROOT` | After `load_dotenv`, call new helper `_set_superpowers_root_default()` that sets `os.environ["SUPERPOWERS_ROOT"]` to `str(PROJECT_ROOT.parent)` if and only if not already set. Order: `load_dotenv` → set default → click group definitions. |
|
||||
| `drill/engine.py:233`, `drill/setup.py:25` | Direct `os.environ["SUPERPOWERS_ROOT"]` access (KeyError if unset) | Unchanged. The CLI startup hook guarantees the env var is set by the time the engine/setup execute. |
|
||||
| `backends/claude*.yaml` (5 files) | `${SUPERPOWERS_ROOT}` substituted in `args` for `--plugin-dir` | Unchanged. YAML substitution reads `os.environ` at backend-load time, which is after CLI startup. |
|
||||
| `backends/codex.yaml`, `backends/gemini.yaml` | `SUPERPOWERS_ROOT` in `required_env` only | Drop from `required_env` (the helper supplies it). `claude*.yaml` keep `required_env` for backward compat (env var works as override). |
|
||||
| `evals/tests/test_backend.py` | Tests assert `SUPERPOWERS_ROOT` is in `required_env` lists, plus path-resolution tests | Update tests to match the new contract: helper-supplied default, env override still works, `required_env` no longer required for codex/gemini. |
|
||||
| `evals/README.md` | "export SUPERPOWERS_ROOT=/path/to/superpowers" | Drop the export line; note that the env var auto-defaults to the parent of `evals/`; mention the only required setup is `ANTHROPIC_API_KEY` (or `OPENAI_API_KEY` / Gemini auth). |
|
||||
| `evals/CLAUDE.md` | Same | Same |
|
||||
| `evals/.gitignore` | drill's existing patterns (`results/`, `.venv/`, `__pycache__/`, `.env`, `*.pyc`, `*.egg-info/`, `dist/`, `build/`, `.claude/`) | Copied verbatim. Patterns are relative to file location, so they apply correctly under `evals/`. |
|
||||
| `evals/lefthook.yml` | drill ships `lefthook.yml` defining `pre-commit: uv run ruff check && uv run ty check` | Move to `evals/lefthook.yml`. Either (a) install lefthook at the superpowers root and have it federate to `evals/lefthook.yml`, or (b) document that contributors run `cd evals && lefthook run pre-commit` manually. **Decision in implementation: option (b) for simplicity** — superpowers' top-level workflow doesn't change. |
|
||||
|
||||
`.env` placement: keep `evals/.env` (gitignored). Contributors source it from there or set `ANTHROPIC_API_KEY` in their shell environment.
|
||||
|
||||
**Top-level superpowers files needing small additions:**
|
||||
|
||||
- `superpowers/.gitignore`: add `evals/results/`, `evals/.venv/`, `evals/.env` (belt-and-suspenders; evals/.gitignore already covers these locally).
|
||||
- `superpowers/CLAUDE.md`: add a one-line pointer "Eval harness lives at `evals/` — see `evals/README.md`" so agents discover it.
|
||||
- `superpowers/docs/testing.md`: split into "## Plugin tests" (existing tests/ content, with the deleted-test references trimmed) and "## Skill behavior evals" (one-paragraph summary + pointer to `evals/`).
|
||||
- `superpowers/README.md`: add a single line in the Contributing section pointing at `evals/` for skill-behavior testing.
|
||||
|
||||
## Migration ordering
|
||||
|
||||
Each step is a separate commit (or small group of commits). Step 2 is the biggest single commit (the verbatim drill copy); subsequent steps are small and atomic.
|
||||
|
||||
```
|
||||
1. Branch off `dev` (f/evals-lift)
|
||||
|
||||
2. Copy drill repo into evals/ (single commit, easy to revert)
|
||||
├─ Record drill SHA at copy time → commit message
|
||||
├─ Use `rsync -a --exclude=.git --exclude=.venv --exclude=results
|
||||
│ --exclude=.env --exclude=__pycache__ --exclude='*.egg-info'
|
||||
│ --exclude=.private-journal /path/to/drill/ evals/`
|
||||
│ (rsync chosen over `cp -r` for explicit excludes; verify with
|
||||
│ `find evals -name '.git' -type d` returns nothing)
|
||||
├─ Subagent gate: per-file SHA-256 checksum matches drill repo for every
|
||||
│ non-excluded file; excluded paths absent from evals/
|
||||
└─ Smoke check: `cd evals && uv sync` succeeds (proves install only;
|
||||
not a behavioral test)
|
||||
|
||||
3. Update path defaults
|
||||
├─ Add _set_superpowers_root_default() helper to drill/cli.py
|
||||
├─ Wire it after load_dotenv, before click group definition
|
||||
├─ Update evals/README.md and evals/CLAUDE.md (drop SUPERPOWERS_ROOT install step)
|
||||
├─ Drop SUPERPOWERS_ROOT from required_env in codex.yaml/gemini.yaml
|
||||
│ (keep in claude*.yaml as override)
|
||||
└─ Update evals/tests/test_backend.py to match new contract
|
||||
|
||||
4. Validate from new location (TWO checks)
|
||||
├─ Run drill's own pytest: `cd evals && uv run pytest` — must pass
|
||||
└─ Run cheap drill scenario: `cd evals && uv run drill run
|
||||
triggering-test-driven-development -b claude` — must pass.
|
||||
Real behavioral validation, not just code review.
|
||||
|
||||
5. Bash test deletion phase — per-file with subagent gate
|
||||
For each file in the candidate-deletion list:
|
||||
a. Subagent compares bash test assertions vs drill scenario verify block
|
||||
b. Pass criterion: every bash assertion has a matching drill check
|
||||
c. If pass → delete the bash test file (one commit per file or per
|
||||
coherent group)
|
||||
d. If fail → either extend drill scenario (separate commit + verify) or
|
||||
keep the bash test (no commit)
|
||||
|
||||
6. Stale-reference scrub
|
||||
├─ Subagent greps the superpowers tree (excluding node_modules/, .venv/,
|
||||
│ evals/) for deleted file paths
|
||||
├─ Search targets: docs/, docs/superpowers/plans/, RELEASE-NOTES.md,
|
||||
│ CLAUDE.md, GEMINI.md, AGENTS.md, README.md, .github/, scripts/,
|
||||
│ .opencode/INSTALL.md, .codex-plugin/INSTALL.md, lefthook.yml
|
||||
├─ Update active references (e.g., docs/testing.md, README.md install)
|
||||
└─ Historical references in docs/superpowers/plans/*.md and
|
||||
RELEASE-NOTES.md are PRESERVED with a brief annotation
|
||||
("(test removed; behavior covered by drill scenario X)") rather
|
||||
than rewritten — these are dated artifacts, not living docs.
|
||||
|
||||
7. Top-level docs
|
||||
├─ docs/testing.md split
|
||||
├─ CLAUDE.md pointer
|
||||
└─ README.md Contributing section
|
||||
|
||||
8. Re-run smoke checks (regression gate)
|
||||
├─ `cd evals && uv run pytest`
|
||||
└─ `cd evals && uv run drill run triggering-test-driven-development -b claude`
|
||||
|
||||
9. Final adversarial review
|
||||
└─ Two parallel subagents, full diff, "5 points to whoever finds the
|
||||
most legitimate issues" framing. Address findings before push.
|
||||
|
||||
10. Push branch + open PR against dev
|
||||
└─ PR description includes: drill SHA pinned at copy, archival action
|
||||
item ("after merge: archive obra/drill, add README pointer to
|
||||
obra/superpowers/evals/"), per-deleted-file coverage receipts.
|
||||
```
|
||||
|
||||
## Verification (post-implementation)
|
||||
|
||||
The implementation plan must show:
|
||||
|
||||
- All non-excluded drill source files present at `evals/` after step 2 (subagent **per-file SHA-256 checksum diff** vs `obra/drill@<recorded-sha>`).
|
||||
- Excluded paths (`.git/`, `.venv/`, `results/`, `.env`, `__pycache__/`, `*.egg-info/`, `.private-journal/`) absent from `evals/`.
|
||||
- The step-2 commit message records the drill source SHA.
|
||||
- `cd evals && uv sync` succeeds without `SUPERPOWERS_ROOT` set.
|
||||
- `cd evals && uv run pytest` passes (drill's own pytest suite).
|
||||
- `cd evals && uv run drill list` returns the same scenario count as the standalone drill repo at the recorded SHA.
|
||||
- `cd evals && uv run drill run triggering-test-driven-development -b claude` passes (proves path defaults work end-to-end).
|
||||
- For each deleted bash test: subagent verification table in the commit message showing every assertion mapped to a drill check.
|
||||
- Grep for deleted file paths returns zero hits across living superpowers docs (post step 6); historical refs in `docs/superpowers/plans/*.md` and `RELEASE-NOTES.md` are annotated, not rewritten.
|
||||
- `docs/testing.md` has both "Plugin tests" and "Skill behavior evals" sections.
|
||||
- The drill repo's history is untouched; `obra/drill` is unaffected by this PR.
|
||||
- PR description names the action item to archive `obra/drill` after merge.
|
||||
|
||||
## Open questions
|
||||
|
||||
None. All clarifying decisions have been made:
|
||||
|
||||
| Question | Decision |
|
||||
|----------|----------|
|
||||
| Where does drill live in superpowers? | `evals/` (rename from drill); standalone repo archived as separate step |
|
||||
| Fate of redundant bash tests? | Delete per-file with subagent verification of coverage; default keep |
|
||||
| Scenarios layout? | Centralized at `evals/scenarios/` |
|
||||
| Python toolchain placement? | Self-contained at `evals/` |
|
||||
| CI integration? | Manual-only this PR; documented future path |
|
||||
| Migration mechanics? | Plain copy; drill repo's history preserved in archived repo, not in-tree |
|
||||
| Internal Python package name? | Keep as `drill` (directory is `evals/`) |
|
||||
| Branching strategy? | Independent off `dev` (not stacked on `f/cross-platform`) |
|
||||
34
docs/testing.md
Normal file
34
docs/testing.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Testing Superpowers
|
||||
|
||||
Superpowers has two distinct kinds of tests, each in its own directory:
|
||||
|
||||
- **`tests/`** — does the plugin's non-LLM code work? Bash + node + python integration tests for brainstorm-server JS, OpenCode plugin loading, codex-plugin sync, and analysis utilities.
|
||||
- **`evals/`** — do agents behave correctly on real LLM sessions? Python harness driving real tmux sessions of Claude Code / Codex / Gemini CLI, with an LLM actor and verifier judging skill compliance.
|
||||
|
||||
## Plugin tests
|
||||
|
||||
Live in `tests/`. Currently:
|
||||
|
||||
- `tests/brainstorm-server/` — node test suite for the brainstorm server JS code.
|
||||
- `tests/opencode/` — bash tests for OpenCode plugin loading, bootstrap caching, and tool registration.
|
||||
- `tests/codex-plugin-sync/` — bash sync verification.
|
||||
- `tests/claude-code/test-helpers.sh`, `analyze-token-usage.py` — utilities used by remaining bash tests.
|
||||
- `tests/claude-code/test-subagent-driven-development.sh` — agent-can-describe-SDD test (no drill counterpart; tests description-recall, not behavior).
|
||||
- `tests/claude-code/test-subagent-driven-development-integration.sh` — extended SDD integration with token analysis (drill covers the YAGNI subset; bash adds commit-count, TodoWrite, and token telemetry assertions).
|
||||
- `tests/claude-code/test-worktree-native-preference.sh` — RED-GREEN-REFACTOR validation for worktree skill (drill covers the PRESSURE phase; bash also covers RED/GREEN baselines).
|
||||
- `tests/explicit-skill-requests/` — Haiku-specific, multi-turn, and skill-name-prompted tests not covered by drill.
|
||||
|
||||
Run plugin tests via the relevant directory's `run-*.sh` or `npm test`.
|
||||
|
||||
## Skill behavior evals
|
||||
|
||||
Live in `evals/`. Drill is the harness; scenarios live at `evals/scenarios/*.yaml`. See `evals/README.md` for setup. Quick start:
|
||||
|
||||
```bash
|
||||
cd evals
|
||||
uv sync --extra dev
|
||||
export ANTHROPIC_API_KEY=sk-...
|
||||
uv run drill run triggering-test-driven-development -b claude
|
||||
```
|
||||
|
||||
Drill scenarios are slow (3-30+ minutes each) and run real LLM sessions. They are not part of CI today; the natural follow-up is a tiered model (fast subset on PR, full sweep nightly + on-demand).
|
||||
212
docs/windows/polyglot-hooks.md
Normal file
212
docs/windows/polyglot-hooks.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Cross-Platform Polyglot Hooks for Claude Code
|
||||
|
||||
Claude Code plugins need hooks that work on Windows, macOS, and Linux. This document explains the polyglot wrapper technique that makes this possible.
|
||||
|
||||
## The Problem
|
||||
|
||||
Claude Code runs hook commands through the system's default shell:
|
||||
- **Windows**: CMD.exe
|
||||
- **macOS/Linux**: bash or sh
|
||||
|
||||
This creates several challenges:
|
||||
|
||||
1. **Script execution**: Windows CMD can't execute `.sh` files directly - it tries to open them in a text editor
|
||||
2. **Path format**: Windows uses backslashes (`C:\path`), Unix uses forward slashes (`/path`)
|
||||
3. **Environment variables**: `$VAR` syntax doesn't work in CMD
|
||||
4. **No `bash` in PATH**: Even with Git Bash installed, `bash` isn't in the PATH when CMD runs
|
||||
|
||||
## The Solution: Polyglot `.cmd` Wrapper
|
||||
|
||||
A polyglot script is valid syntax in multiple languages simultaneously. Our wrapper is valid in both CMD and bash:
|
||||
|
||||
```cmd
|
||||
: << 'CMDBLOCK'
|
||||
@echo off
|
||||
"C:\Program Files\Git\bin\bash.exe" -l -c "\"$(cygpath -u \"$CLAUDE_PLUGIN_ROOT\")/hooks/session-start.sh\""
|
||||
exit /b
|
||||
CMDBLOCK
|
||||
|
||||
# Unix shell runs from here
|
||||
"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
#### On Windows (CMD.exe)
|
||||
|
||||
1. `: << 'CMDBLOCK'` - CMD sees `:` as a label (like `:label`) and ignores `<< 'CMDBLOCK'`
|
||||
2. `@echo off` - Suppresses command echoing
|
||||
3. The bash.exe command runs with:
|
||||
- `-l` (login shell) to get proper PATH with Unix utilities
|
||||
- `cygpath -u` converts Windows path to Unix format (`C:\foo` → `/c/foo`)
|
||||
4. `exit /b` - Exits the batch script, stopping CMD here
|
||||
5. Everything after `CMDBLOCK` is never reached by CMD
|
||||
|
||||
#### On Unix (bash/sh)
|
||||
|
||||
1. `: << 'CMDBLOCK'` - `:` is a no-op, `<< 'CMDBLOCK'` starts a heredoc
|
||||
2. Everything until `CMDBLOCK` is consumed by the heredoc (ignored)
|
||||
3. `# Unix shell runs from here` - Comment
|
||||
4. The script runs directly with the Unix path
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
hooks/
|
||||
├── hooks.json # Points to the .cmd wrapper
|
||||
├── session-start.cmd # Polyglot wrapper (cross-platform entry point)
|
||||
└── session-start.sh # Actual hook logic (bash script)
|
||||
```
|
||||
|
||||
### hooks.json
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|clear|compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.cmd\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The path must be quoted because `${CLAUDE_PLUGIN_ROOT}` may contain spaces on Windows (e.g., `C:\Program Files\...`).
|
||||
|
||||
## Requirements
|
||||
|
||||
### Windows
|
||||
- **Git for Windows** must be installed (provides `bash.exe` and `cygpath`)
|
||||
- Default installation path: `C:\Program Files\Git\bin\bash.exe`
|
||||
- If Git is installed elsewhere, the wrapper needs modification
|
||||
|
||||
### Unix (macOS/Linux)
|
||||
- Standard bash or sh shell
|
||||
- The `.cmd` file must have execute permission (`chmod +x`)
|
||||
|
||||
## Writing Cross-Platform Hook Scripts
|
||||
|
||||
Your actual hook logic goes in the `.sh` file. To ensure it works on Windows (via Git Bash):
|
||||
|
||||
### Do:
|
||||
- Use pure bash builtins when possible
|
||||
- Use `$(command)` instead of backticks
|
||||
- Quote all variable expansions: `"$VAR"`
|
||||
- Use `printf` or here-docs for output
|
||||
|
||||
### Avoid:
|
||||
- External commands that may not be in PATH (sed, awk, grep)
|
||||
- If you must use them, they're available in Git Bash but ensure PATH is set up (use `bash -l`)
|
||||
|
||||
### Example: JSON Escaping Without sed/awk
|
||||
|
||||
Instead of:
|
||||
```bash
|
||||
escaped=$(echo "$content" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
|
||||
```
|
||||
|
||||
Use pure bash:
|
||||
```bash
|
||||
escape_for_json() {
|
||||
local input="$1"
|
||||
local output=""
|
||||
local i char
|
||||
for (( i=0; i<${#input}; i++ )); do
|
||||
char="${input:$i:1}"
|
||||
case "$char" in
|
||||
$'\\') output+='\\' ;;
|
||||
'"') output+='\"' ;;
|
||||
$'\n') output+='\n' ;;
|
||||
$'\r') output+='\r' ;;
|
||||
$'\t') output+='\t' ;;
|
||||
*) output+="$char" ;;
|
||||
esac
|
||||
done
|
||||
printf '%s' "$output"
|
||||
}
|
||||
```
|
||||
|
||||
## Reusable Wrapper Pattern
|
||||
|
||||
For plugins with multiple hooks, you can create a generic wrapper that takes the script name as an argument:
|
||||
|
||||
### run-hook.cmd
|
||||
```cmd
|
||||
: << 'CMDBLOCK'
|
||||
@echo off
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "SCRIPT_NAME=%~1"
|
||||
"C:\Program Files\Git\bin\bash.exe" -l -c "cd \"$(cygpath -u \"%SCRIPT_DIR%\")\" && \"./%SCRIPT_NAME%\""
|
||||
exit /b
|
||||
CMDBLOCK
|
||||
|
||||
# Unix shell runs from here
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SCRIPT_NAME="$1"
|
||||
shift
|
||||
"${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
|
||||
```
|
||||
|
||||
### hooks.json using the reusable wrapper
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" validate-bash.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "bash is not recognized"
|
||||
CMD can't find bash. The wrapper uses the full path `C:\Program Files\Git\bin\bash.exe`. If Git is installed elsewhere, update the path.
|
||||
|
||||
### "cygpath: command not found" or "dirname: command not found"
|
||||
Bash isn't running as a login shell. Ensure `-l` flag is used.
|
||||
|
||||
### Path has weird `\/` in it
|
||||
`${CLAUDE_PLUGIN_ROOT}` expanded to a Windows path ending with backslash, then `/hooks/...` was appended. Use `cygpath` to convert the entire path.
|
||||
|
||||
### Script opens in text editor instead of running
|
||||
The hooks.json is pointing directly to the `.sh` file. Point to the `.cmd` wrapper instead.
|
||||
|
||||
### Works in terminal but not as hook
|
||||
Claude Code may run hooks differently. Test by simulating the hook environment:
|
||||
```powershell
|
||||
$env:CLAUDE_PLUGIN_ROOT = "C:\path\to\plugin"
|
||||
cmd /c "C:\path\to\plugin\hooks\session-start.cmd"
|
||||
```
|
||||
|
||||
## Related Issues
|
||||
|
||||
- [anthropics/claude-code#9758](https://github.com/anthropics/claude-code/issues/9758) - .sh scripts open in editor on Windows
|
||||
- [anthropics/claude-code#3417](https://github.com/anthropics/claude-code/issues/3417) - Hooks don't work on Windows
|
||||
- [anthropics/claude-code#6023](https://github.com/anthropics/claude-code/issues/6023) - CLAUDE_PROJECT_DIR not found
|
||||
1
evals
Submodule
1
evals
Submodule
Submodule evals added at f88b0e8ab9
6
gemini-extension.json
Normal file
6
gemini-extension.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "5.1.0",
|
||||
"contextFileName": "GEMINI.md"
|
||||
}
|
||||
10
hooks/hooks-cursor.json
Normal file
10
hooks/hooks-cursor.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 1,
|
||||
"hooks": {
|
||||
"sessionStart": [
|
||||
{
|
||||
"command": "./hooks/run-hook.cmd session-start"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|clear|compact",
|
||||
"matcher": "startup|clear|compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
|
||||
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
|
||||
"async": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
46
hooks/run-hook.cmd
Executable file
46
hooks/run-hook.cmd
Executable file
@@ -0,0 +1,46 @@
|
||||
: << 'CMDBLOCK'
|
||||
@echo off
|
||||
REM Cross-platform polyglot wrapper for hook scripts.
|
||||
REM On Windows: cmd.exe runs the batch portion, which finds and calls bash.
|
||||
REM On Unix: the shell interprets this as a script (: is a no-op in bash).
|
||||
REM
|
||||
REM Hook scripts use extensionless filenames (e.g. "session-start" not
|
||||
REM "session-start.sh") so Claude Code's Windows auto-detection -- which
|
||||
REM prepends "bash" to any command containing .sh -- doesn't interfere.
|
||||
REM
|
||||
REM Usage: run-hook.cmd <script-name> [args...]
|
||||
|
||||
if "%~1"=="" (
|
||||
echo run-hook.cmd: missing script name >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "HOOK_DIR=%~dp0"
|
||||
|
||||
REM Try Git for Windows bash in standard locations
|
||||
if exist "C:\Program Files\Git\bin\bash.exe" (
|
||||
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
|
||||
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
|
||||
REM Try bash on PATH (e.g. user-installed Git Bash, MSYS2, Cygwin)
|
||||
where bash >nul 2>nul
|
||||
if %ERRORLEVEL% equ 0 (
|
||||
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
|
||||
REM No bash found - exit silently rather than error
|
||||
REM (plugin still works, just without SessionStart context injection)
|
||||
exit /b 0
|
||||
CMDBLOCK
|
||||
|
||||
# Unix: run the named script directly
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SCRIPT_NAME="$1"
|
||||
shift
|
||||
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
|
||||
57
hooks/session-start
Executable file
57
hooks/session-start
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# SessionStart hook for superpowers plugin
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Determine plugin root directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Check if legacy skills directory exists and build warning
|
||||
warning_message=""
|
||||
legacy_skills_dir="${HOME}/.config/superpowers/skills"
|
||||
if [ -d "$legacy_skills_dir" ]; then
|
||||
warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
|
||||
fi
|
||||
|
||||
# Read using-superpowers content
|
||||
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
|
||||
|
||||
# Escape string for JSON embedding using bash parameter substitution.
|
||||
# Each ${s//old/new} is a single C-level pass - orders of magnitude
|
||||
# faster than the character-by-character loop this replaces.
|
||||
escape_for_json() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
s="${s//$'\r'/\\r}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
|
||||
warning_escaped=$(escape_for_json "$warning_message")
|
||||
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
|
||||
|
||||
# Output context injection as JSON.
|
||||
# Cursor hooks expect additional_context (snake_case).
|
||||
# Claude Code hooks expect hookSpecificOutput.additionalContext (nested).
|
||||
# Copilot CLI (v1.0.11+) and others expect additionalContext (top-level, SDK standard).
|
||||
# Claude Code reads BOTH additional_context and hookSpecificOutput without
|
||||
# deduplication, so we must emit only the field the current platform consumes.
|
||||
#
|
||||
# Uses printf instead of heredoc to work around bash 5.3+ heredoc hang.
|
||||
# See: https://github.com/obra/superpowers/issues/571
|
||||
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
|
||||
# Cursor sets CURSOR_PLUGIN_ROOT (may also set CLAUDE_PLUGIN_ROOT)
|
||||
printf '{\n "additional_context": "%s"\n}\n' "$session_context"
|
||||
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
|
||||
# Claude Code sets CLAUDE_PLUGIN_ROOT without COPILOT_CLI
|
||||
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
|
||||
else
|
||||
# Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format
|
||||
printf '{\n "additionalContext": "%s"\n}\n' "$session_context"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# SessionStart hook for superpowers plugin
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Run personal superpowers setup
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
setup_output=$("${SCRIPT_DIR}/setup-personal-superpowers.sh" 2>&1 || echo "setup_failed=true")
|
||||
|
||||
# Use same directory resolution as setup script
|
||||
SUPERPOWERS_DIR="${PERSONAL_SUPERPOWERS_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/superpowers}"
|
||||
|
||||
# Check if GitHub CLI is available and setup succeeded
|
||||
github_recommendation=""
|
||||
if echo "$setup_output" | grep -q "github_cli_available=true"; then
|
||||
if [[ ! -d "$SUPERPOWERS_DIR/.git" ]]; then
|
||||
# This should not happen, but handle gracefully
|
||||
github_recommendation=""
|
||||
else
|
||||
# Check if remote already exists
|
||||
if ! (cd "$SUPERPOWERS_DIR" && git remote get-url origin &>/dev/null); then
|
||||
github_recommendation="\n\n💡 Want to share your personal skills on GitHub? Superpowers are best when everyone can learn from them! I can create a 'personal-superpowers' repo for you."
|
||||
fi
|
||||
fi
|
||||
elif echo "$setup_output" | grep -q "setup_failed=true"; then
|
||||
github_recommendation="\n\n⚠️ Personal superpowers setup encountered an issue. Please file a bug at https://github.com/obra/superpowers/issues"
|
||||
fi
|
||||
|
||||
# Output context injection as JSON
|
||||
cat <<EOF
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Skill tools are at:**\n- find-skills: ${CLAUDE_PLUGIN_ROOT}/scripts/find-skills\n- skill-run: ${CLAUDE_PLUGIN_ROOT}/scripts/skill-run\n\n**RIGHT NOW, go read**: @${CLAUDE_PLUGIN_ROOT}/skills/getting-started/SKILL.md${github_recommendation}\n</EXTREMELY_IMPORTANT>"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Setup script for personal superpowers directory
|
||||
# Creates personal superpowers directory with git repo for personal skills
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Use PERSONAL_SUPERPOWERS_DIR if set, otherwise XDG_CONFIG_HOME/superpowers, otherwise ~/.config/superpowers
|
||||
SUPERPOWERS_DIR="${PERSONAL_SUPERPOWERS_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/superpowers}"
|
||||
SKILLS_DIR="${SUPERPOWERS_DIR}/skills"
|
||||
|
||||
# Check if already set up
|
||||
if [[ -d "${SUPERPOWERS_DIR}/.git" ]] && [[ -d "${SKILLS_DIR}" ]]; then
|
||||
# Already set up, nothing to do
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "${SKILLS_DIR}"
|
||||
|
||||
# Create .gitignore
|
||||
cat > "${SUPERPOWERS_DIR}/.gitignore" <<'EOF'
|
||||
# Superpowers local data
|
||||
search-log.jsonl
|
||||
conversation-index/
|
||||
conversation-archive/
|
||||
EOF
|
||||
|
||||
# Create README
|
||||
cat > "${SUPERPOWERS_DIR}/README.md" <<'EOF'
|
||||
# My Personal Superpowers
|
||||
|
||||
Personal skills and techniques for Claude Code.
|
||||
|
||||
Learn more about Superpowers: https://github.com/obra/superpowers
|
||||
EOF
|
||||
|
||||
# Initialize git repo if not already initialized
|
||||
if [[ ! -d "${SUPERPOWERS_DIR}/.git" ]]; then
|
||||
cd "${SUPERPOWERS_DIR}"
|
||||
git init -q
|
||||
git add .gitignore README.md
|
||||
git commit -q -m "Initial commit: Personal superpowers setup"
|
||||
fi
|
||||
|
||||
# Check for gh and recommend GitHub setup
|
||||
if command -v gh &> /dev/null; then
|
||||
echo "github_cli_available=true"
|
||||
else
|
||||
echo "github_cli_available=false"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
6
package.json
Normal file
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"version": "5.1.0",
|
||||
"type": "module",
|
||||
"main": ".opencode/plugins/superpowers.js"
|
||||
}
|
||||
220
scripts/bump-version.sh
Executable file
220
scripts/bump-version.sh
Executable file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# bump-version.sh — bump version numbers across all declared files,
|
||||
# with drift detection and repo-wide audit for missed files.
|
||||
#
|
||||
# Usage:
|
||||
# bump-version.sh <new-version> Bump all declared files to new version
|
||||
# bump-version.sh --check Report current versions (detect drift)
|
||||
# bump-version.sh --audit Check + grep repo for old version strings
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
CONFIG="$REPO_ROOT/.version-bump.json"
|
||||
|
||||
if [[ ! -f "$CONFIG" ]]; then
|
||||
echo "error: .version-bump.json not found at $CONFIG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- helpers ---
|
||||
|
||||
# Read a dotted field path from a JSON file.
|
||||
# Handles both simple ("version") and nested ("plugins.0.version") paths.
|
||||
read_json_field() {
|
||||
local file="$1" field="$2"
|
||||
# Convert dot-path to jq path: "plugins.0.version" -> .plugins[0].version
|
||||
local jq_path
|
||||
jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g')
|
||||
jq -r "$jq_path" "$file"
|
||||
}
|
||||
|
||||
# Write a dotted field path in a JSON file, preserving formatting.
|
||||
write_json_field() {
|
||||
local file="$1" field="$2" value="$3"
|
||||
local jq_path
|
||||
jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g')
|
||||
local tmp="${file}.tmp"
|
||||
jq "$jq_path = \"$value\"" "$file" > "$tmp" && mv "$tmp" "$file"
|
||||
}
|
||||
|
||||
# Read the list of declared files from config.
|
||||
# Outputs lines of "path<TAB>field"
|
||||
declared_files() {
|
||||
jq -r '.files[] | "\(.path)\t\(.field)"' "$CONFIG"
|
||||
}
|
||||
|
||||
# Read the audit exclude patterns from config.
|
||||
audit_excludes() {
|
||||
jq -r '.audit.exclude[]' "$CONFIG" 2>/dev/null
|
||||
}
|
||||
|
||||
# --- commands ---
|
||||
|
||||
cmd_check() {
|
||||
local has_drift=0
|
||||
local versions=()
|
||||
|
||||
echo "Version check:"
|
||||
echo ""
|
||||
|
||||
while IFS=$'\t' read -r path field; do
|
||||
local fullpath="$REPO_ROOT/$path"
|
||||
if [[ ! -f "$fullpath" ]]; then
|
||||
printf " %-45s MISSING\n" "$path ($field)"
|
||||
has_drift=1
|
||||
continue
|
||||
fi
|
||||
local ver
|
||||
ver=$(read_json_field "$fullpath" "$field")
|
||||
printf " %-45s %s\n" "$path ($field)" "$ver"
|
||||
versions+=("$ver")
|
||||
done < <(declared_files)
|
||||
|
||||
echo ""
|
||||
|
||||
# Check if all versions match
|
||||
local unique
|
||||
unique=$(printf '%s\n' "${versions[@]}" | sort -u | wc -l | tr -d ' ')
|
||||
if [[ "$unique" -gt 1 ]]; then
|
||||
echo "DRIFT DETECTED — versions are not in sync:"
|
||||
printf '%s\n' "${versions[@]}" | sort | uniq -c | sort -rn | while read -r count ver; do
|
||||
echo " $ver ($count files)"
|
||||
done
|
||||
has_drift=1
|
||||
else
|
||||
echo "All declared files are in sync at ${versions[0]}"
|
||||
fi
|
||||
|
||||
return $has_drift
|
||||
}
|
||||
|
||||
cmd_audit() {
|
||||
# First run check
|
||||
cmd_check || true
|
||||
echo ""
|
||||
|
||||
# Determine the current version (most common across declared files)
|
||||
local current_version
|
||||
current_version=$(
|
||||
while IFS=$'\t' read -r path field; do
|
||||
local fullpath="$REPO_ROOT/$path"
|
||||
[[ -f "$fullpath" ]] && read_json_field "$fullpath" "$field"
|
||||
done < <(declared_files) | sort | uniq -c | sort -rn | head -1 | awk '{print $2}'
|
||||
)
|
||||
|
||||
if [[ -z "$current_version" ]]; then
|
||||
echo "error: could not determine current version" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Audit: scanning repo for version string '$current_version'..."
|
||||
echo ""
|
||||
|
||||
# Build grep exclude args
|
||||
local -a exclude_args=()
|
||||
while IFS= read -r pattern; do
|
||||
exclude_args+=("--exclude=$pattern" "--exclude-dir=$pattern")
|
||||
done < <(audit_excludes)
|
||||
|
||||
# Also always exclude binary files and .git
|
||||
exclude_args+=("--exclude-dir=.git" "--exclude-dir=node_modules" "--binary-files=without-match")
|
||||
|
||||
# Get list of declared paths for comparison
|
||||
local -a declared_paths=()
|
||||
while IFS=$'\t' read -r path _field; do
|
||||
declared_paths+=("$path")
|
||||
done < <(declared_files)
|
||||
|
||||
# Grep for the version string
|
||||
local found_undeclared=0
|
||||
while IFS= read -r match; do
|
||||
local match_file
|
||||
match_file=$(echo "$match" | cut -d: -f1)
|
||||
# Make path relative to repo root
|
||||
local rel_path="${match_file#$REPO_ROOT/}"
|
||||
|
||||
# Check if this file is in the declared list
|
||||
local is_declared=0
|
||||
for dp in "${declared_paths[@]}"; do
|
||||
if [[ "$rel_path" == "$dp" ]]; then
|
||||
is_declared=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$is_declared" -eq 0 ]]; then
|
||||
if [[ "$found_undeclared" -eq 0 ]]; then
|
||||
echo "UNDECLARED files containing '$current_version':"
|
||||
found_undeclared=1
|
||||
fi
|
||||
echo " $match"
|
||||
fi
|
||||
done < <(grep -rn "${exclude_args[@]}" -F "$current_version" "$REPO_ROOT" 2>/dev/null || true)
|
||||
|
||||
if [[ "$found_undeclared" -eq 0 ]]; then
|
||||
echo "No undeclared files contain the version string. All clear."
|
||||
else
|
||||
echo ""
|
||||
echo "Review the above files — if they should be bumped, add them to .version-bump.json"
|
||||
echo "If they should be skipped, add them to the audit.exclude list."
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_bump() {
|
||||
local new_version="$1"
|
||||
|
||||
# Validate semver-ish format
|
||||
if ! echo "$new_version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
|
||||
echo "error: '$new_version' doesn't look like a version (expected X.Y.Z)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Bumping all declared files to $new_version..."
|
||||
echo ""
|
||||
|
||||
while IFS=$'\t' read -r path field; do
|
||||
local fullpath="$REPO_ROOT/$path"
|
||||
if [[ ! -f "$fullpath" ]]; then
|
||||
echo " SKIP (missing): $path"
|
||||
continue
|
||||
fi
|
||||
local old_ver
|
||||
old_ver=$(read_json_field "$fullpath" "$field")
|
||||
write_json_field "$fullpath" "$field" "$new_version"
|
||||
printf " %-45s %s -> %s\n" "$path ($field)" "$old_ver" "$new_version"
|
||||
done < <(declared_files)
|
||||
|
||||
echo ""
|
||||
echo "Done. Running audit to check for missed files..."
|
||||
echo ""
|
||||
cmd_audit
|
||||
}
|
||||
|
||||
# --- main ---
|
||||
|
||||
case "${1:-}" in
|
||||
--check)
|
||||
cmd_check
|
||||
;;
|
||||
--audit)
|
||||
cmd_audit
|
||||
;;
|
||||
--help|-h|"")
|
||||
echo "Usage: bump-version.sh <new-version> | --check | --audit"
|
||||
echo ""
|
||||
echo " <new-version> Bump all declared files to the given version"
|
||||
echo " --check Show current versions, detect drift"
|
||||
echo " --audit Check + scan repo for undeclared version references"
|
||||
exit 0
|
||||
;;
|
||||
--*)
|
||||
echo "error: unknown flag '$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
cmd_bump "$1"
|
||||
;;
|
||||
esac
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# find-skills - Find and list skills with descriptions
|
||||
# Shows all skills by default, filters by pattern if provided
|
||||
# Searches personal superpowers first, then core (personal shadows core)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Determine directories
|
||||
PERSONAL_SUPERPOWERS_DIR="${PERSONAL_SUPERPOWERS_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/superpowers}"
|
||||
PERSONAL_SKILLS_DIR="${PERSONAL_SUPERPOWERS_DIR}/skills"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
CORE_SKILLS_DIR="${PLUGIN_ROOT}/skills"
|
||||
|
||||
LOG_FILE="${PERSONAL_SUPERPOWERS_DIR}/search-log.jsonl"
|
||||
|
||||
# Show help
|
||||
if [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "-h" ]]; then
|
||||
cat <<'EOF'
|
||||
find-skills - Find and list skills with descriptions
|
||||
|
||||
USAGE:
|
||||
find-skills Show all skills with descriptions
|
||||
find-skills PATTERN Filter skills by grep pattern
|
||||
find-skills --help Show this help
|
||||
|
||||
EXAMPLES:
|
||||
find-skills # All skills
|
||||
find-skills test # Skills matching "test"
|
||||
find-skills 'test.*driven|TDD' # Regex pattern
|
||||
|
||||
OUTPUT:
|
||||
Each line shows: skill-path - description
|
||||
Personal skills listed first, then core skills
|
||||
Personal skills shadow core skills when paths match
|
||||
|
||||
SEARCH:
|
||||
Searches both skill content AND path names.
|
||||
Personal skills at: ~/.config/superpowers/skills/
|
||||
Core skills at: plugin installation directory
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get pattern (optional)
|
||||
PATTERN="${1:-}"
|
||||
|
||||
# Function to extract description from SKILL.md
|
||||
get_description() {
|
||||
local file="$1"
|
||||
grep "^description:" "$file" 2>/dev/null | sed 's/description: *//' || echo ""
|
||||
}
|
||||
|
||||
# Function to get relative skill path
|
||||
get_skill_path() {
|
||||
local file="$1"
|
||||
local base_dir="$2"
|
||||
local rel_path="${file#$base_dir/}"
|
||||
echo "${rel_path%/SKILL.md}"
|
||||
}
|
||||
|
||||
# Collect all matching skills (use simple list for bash 3.2 compatibility)
|
||||
seen_skills_list=""
|
||||
results=()
|
||||
|
||||
# If pattern provided, log the search
|
||||
if [[ -n "$PATTERN" ]]; then
|
||||
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
echo "{\"timestamp\":\"$timestamp\",\"query\":\"$PATTERN\"}" >> "$LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Search personal skills first
|
||||
if [[ -d "$PERSONAL_SKILLS_DIR" ]]; then
|
||||
while IFS= read -r file; do
|
||||
[[ -z "$file" ]] && continue
|
||||
|
||||
skill_path=$(get_skill_path "$file" "$PERSONAL_SKILLS_DIR")
|
||||
description=$(get_description "$file")
|
||||
|
||||
seen_skills_list="${seen_skills_list}${skill_path}"$'\n'
|
||||
results+=("$skill_path|$description")
|
||||
done < <(
|
||||
if [[ -n "$PATTERN" ]]; then
|
||||
# Pattern mode: search content and paths
|
||||
{
|
||||
grep -E -r "$PATTERN" "$PERSONAL_SKILLS_DIR/" --include="SKILL.md" -l 2>/dev/null || true
|
||||
find "$PERSONAL_SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null | grep -E "$PATTERN" 2>/dev/null || true
|
||||
} | sort -u
|
||||
else
|
||||
# Show all
|
||||
find "$PERSONAL_SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null || true
|
||||
fi
|
||||
)
|
||||
fi
|
||||
|
||||
# Search core skills (only if not shadowed)
|
||||
while IFS= read -r file; do
|
||||
[[ -z "$file" ]] && continue
|
||||
|
||||
skill_path=$(get_skill_path "$file" "$CORE_SKILLS_DIR")
|
||||
|
||||
# Skip if shadowed by personal skill
|
||||
echo "$seen_skills_list" | grep -q "^${skill_path}$" && continue
|
||||
|
||||
description=$(get_description "$file")
|
||||
results+=("$skill_path|$description")
|
||||
done < <(
|
||||
if [[ -n "$PATTERN" ]]; then
|
||||
# Pattern mode: search content and paths
|
||||
{
|
||||
grep -E -r "$PATTERN" "$CORE_SKILLS_DIR/" --include="SKILL.md" -l 2>/dev/null || true
|
||||
find "$CORE_SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null | grep -E "$PATTERN" 2>/dev/null || true
|
||||
} | sort -u
|
||||
else
|
||||
# Show all
|
||||
find "$CORE_SKILLS_DIR/" -name "SKILL.md" -type f 2>/dev/null || true
|
||||
fi
|
||||
)
|
||||
|
||||
# Check if we found anything
|
||||
if [[ ${#results[@]} -eq 0 ]]; then
|
||||
if [[ -n "$PATTERN" ]]; then
|
||||
echo "❌ No skills found matching: $PATTERN"
|
||||
echo ""
|
||||
echo "Search logged. If a skill should exist, consider writing it!"
|
||||
else
|
||||
echo "❌ No skills found"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Sort and display results
|
||||
printf "%s\n" "${results[@]}" | sort | while IFS='|' read -r skill_path description; do
|
||||
if [[ -n "$description" ]]; then
|
||||
echo "skills/$skill_path - $description"
|
||||
else
|
||||
echo "skills/$skill_path"
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generic runner for skill scripts
|
||||
# Searches personal superpowers first, then core plugin
|
||||
#
|
||||
# Usage: scripts/skill-run <skill-relative-path> [args...]
|
||||
# Example: scripts/skill-run skills/collaboration/remembering-conversations/tool/search-conversations "query"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
cat <<'EOF'
|
||||
Usage: scripts/skill-run <skill-relative-path> [args...]
|
||||
|
||||
Runs scripts from skills, checking personal superpowers first, then core.
|
||||
|
||||
Examples:
|
||||
scripts/skill-run skills/collaboration/remembering-conversations/tool/search-conversations "query"
|
||||
scripts/skill-run skills/collaboration/remembering-conversations/tool/index-conversations --cleanup
|
||||
|
||||
The script will be found at:
|
||||
1. ~/.config/superpowers/<skill-relative-path> (personal, if exists)
|
||||
2. ${CLAUDE_PLUGIN_ROOT}/<skill-relative-path> (core plugin)
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the script path to run
|
||||
SCRIPT_PATH="$1"
|
||||
shift # Remove script path from args, leaving remaining args
|
||||
|
||||
# Determine directories
|
||||
PERSONAL_SUPERPOWERS_DIR="${PERSONAL_SUPERPOWERS_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/superpowers}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Try personal superpowers first
|
||||
PERSONAL_SCRIPT="${PERSONAL_SUPERPOWERS_DIR}/${SCRIPT_PATH}"
|
||||
if [[ -x "$PERSONAL_SCRIPT" ]]; then
|
||||
exec "$PERSONAL_SCRIPT" "$@"
|
||||
fi
|
||||
|
||||
# Fall back to core plugin
|
||||
CORE_SCRIPT="${PLUGIN_ROOT}/${SCRIPT_PATH}"
|
||||
if [[ -x "$CORE_SCRIPT" ]]; then
|
||||
exec "$CORE_SCRIPT" "$@"
|
||||
fi
|
||||
|
||||
# Not found
|
||||
echo "Error: Script not found: $SCRIPT_PATH" >&2
|
||||
echo "" >&2
|
||||
echo "Searched:" >&2
|
||||
echo " $PERSONAL_SCRIPT (personal)" >&2
|
||||
echo " $CORE_SCRIPT (core)" >&2
|
||||
exit 1
|
||||
463
scripts/sync-to-codex-plugin.sh
Executable file
463
scripts/sync-to-codex-plugin.sh
Executable file
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# sync-to-codex-plugin.sh
|
||||
#
|
||||
# Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins.
|
||||
# Clones the fork fresh into a temp dir, rsyncs tracked upstream plugin content
|
||||
# (including committed Codex files under .codex-plugin/ and assets/), preserves
|
||||
# OpenAI-owned marketplace metadata already in the destination plugin, commits,
|
||||
# pushes a sync branch, and opens a PR.
|
||||
# Path/user agnostic — auto-detects upstream from script location.
|
||||
#
|
||||
# Deterministic: running twice against the same upstream SHA produces PRs with
|
||||
# identical diffs, so two back-to-back runs can verify the tool itself.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/sync-to-codex-plugin.sh # full run
|
||||
# ./scripts/sync-to-codex-plugin.sh -n # dry run
|
||||
# ./scripts/sync-to-codex-plugin.sh -y # skip confirm
|
||||
# ./scripts/sync-to-codex-plugin.sh --local PATH # existing checkout
|
||||
# ./scripts/sync-to-codex-plugin.sh --base BRANCH # default: main
|
||||
# ./scripts/sync-to-codex-plugin.sh --bootstrap # create plugin dir if missing
|
||||
#
|
||||
# Bootstrap mode: skips the "plugin must exist on base" requirement and creates
|
||||
# plugins/superpowers/ when absent, then copies the tracked plugin files from
|
||||
# upstream just like a normal sync.
|
||||
#
|
||||
# Requires: bash, rsync, git, gh (authenticated), python3.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# Config — edit as upstream or canonical plugin shape evolves
|
||||
# =============================================================================
|
||||
|
||||
FORK="prime-radiant-inc/openai-codex-plugins"
|
||||
DEFAULT_BASE="main"
|
||||
DEST_REL="plugins/superpowers"
|
||||
|
||||
# Paths in upstream that should NOT land in the embedded plugin.
|
||||
# All patterns use a leading "/" to anchor them to the source root.
|
||||
# Unanchored patterns like "scripts/" would match any directory named
|
||||
# "scripts" at any depth — including legitimate nested dirs like
|
||||
# skills/brainstorming/scripts/. Anchoring prevents that.
|
||||
# (.DS_Store is intentionally unanchored — Finder creates them everywhere.)
|
||||
EXCLUDES=(
|
||||
# Dotfiles and infra — top-level only
|
||||
"/.claude/"
|
||||
"/.claude-plugin/"
|
||||
"/.codex/"
|
||||
"/.cursor-plugin/"
|
||||
"/.git/"
|
||||
"/.gitattributes"
|
||||
"/.github/"
|
||||
"/.gitignore"
|
||||
"/.opencode/"
|
||||
"/.version-bump.json"
|
||||
"/.worktrees/"
|
||||
".DS_Store"
|
||||
|
||||
# Root ceremony files
|
||||
"/AGENTS.md"
|
||||
"/CHANGELOG.md"
|
||||
"/CLAUDE.md"
|
||||
"/GEMINI.md"
|
||||
"/RELEASE-NOTES.md"
|
||||
"/gemini-extension.json"
|
||||
"/package.json"
|
||||
|
||||
# Directories not shipped by canonical Codex plugins
|
||||
"/commands/"
|
||||
"/docs/"
|
||||
"/evals/"
|
||||
"/hooks/"
|
||||
"/lib/"
|
||||
"/scripts/"
|
||||
"/tests/"
|
||||
"/tmp/"
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Ignored-path helpers
|
||||
# =============================================================================
|
||||
|
||||
IGNORED_DIR_EXCLUDES=()
|
||||
|
||||
path_has_directory_exclude() {
|
||||
local path="$1"
|
||||
local dir
|
||||
|
||||
if [[ ${#IGNORED_DIR_EXCLUDES[@]} -eq 0 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
for dir in "${IGNORED_DIR_EXCLUDES[@]}"; do
|
||||
[[ "$path" == "$dir"* ]] && return 0
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
ignored_directory_has_tracked_descendants() {
|
||||
local path="$1"
|
||||
|
||||
[[ -n "$(git -C "$UPSTREAM" ls-files --cached -- "$path/")" ]]
|
||||
}
|
||||
|
||||
append_git_ignored_directory_excludes() {
|
||||
local path
|
||||
local lookup_path
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
[[ "$path" == */ ]] || continue
|
||||
|
||||
lookup_path="${path%/}"
|
||||
if ! ignored_directory_has_tracked_descendants "$lookup_path"; then
|
||||
IGNORED_DIR_EXCLUDES+=("$path")
|
||||
RSYNC_ARGS+=(--exclude="/$path")
|
||||
fi
|
||||
done < <(git -C "$UPSTREAM" ls-files --others --ignored --exclude-standard --directory -z)
|
||||
}
|
||||
|
||||
append_git_ignored_file_excludes() {
|
||||
local path
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
path_has_directory_exclude "$path" && continue
|
||||
RSYNC_ARGS+=(--exclude="/$path")
|
||||
done < <(git -C "$UPSTREAM" ls-files --others --ignored --exclude-standard -z)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Args
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UPSTREAM="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BASE="$DEFAULT_BASE"
|
||||
DRY_RUN=0
|
||||
YES=0
|
||||
LOCAL_CHECKOUT=""
|
||||
BOOTSTRAP=0
|
||||
|
||||
usage() {
|
||||
sed -n '/^# Usage:/,/^# Requires:/s/^# \{0,1\}//p' "$0"
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n|--dry-run) DRY_RUN=1; shift ;;
|
||||
-y|--yes) YES=1; shift ;;
|
||||
--local) LOCAL_CHECKOUT="$2"; shift 2 ;;
|
||||
--base) BASE="$2"; shift 2 ;;
|
||||
--bootstrap) BOOTSTRAP=1; shift ;;
|
||||
-h|--help) usage 0 ;;
|
||||
*) echo "Unknown arg: $1" >&2; usage 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
# Preflight
|
||||
# =============================================================================
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
command -v rsync >/dev/null || die "rsync not found in PATH"
|
||||
command -v git >/dev/null || die "git not found in PATH"
|
||||
command -v gh >/dev/null || die "gh not found — install GitHub CLI"
|
||||
command -v python3 >/dev/null || die "python3 not found in PATH"
|
||||
|
||||
gh auth status >/dev/null 2>&1 || die "gh not authenticated — run 'gh auth login'"
|
||||
|
||||
[[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout"
|
||||
[[ -f "$UPSTREAM/.codex-plugin/plugin.json" ]] || die "committed Codex manifest missing at $UPSTREAM/.codex-plugin/plugin.json"
|
||||
|
||||
# Read the upstream version from the committed Codex manifest.
|
||||
UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/.codex-plugin/plugin.json")"
|
||||
[[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from committed Codex manifest"
|
||||
|
||||
UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)"
|
||||
UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)"
|
||||
UPSTREAM_SHORT="$(cd "$UPSTREAM" && git rev-parse --short HEAD)"
|
||||
|
||||
confirm() {
|
||||
[[ $YES -eq 1 ]] && return 0
|
||||
read -rp "$1 [y/N] " ans
|
||||
[[ "$ans" == "y" || "$ans" == "Y" ]]
|
||||
}
|
||||
|
||||
if [[ "$UPSTREAM_BRANCH" != "main" ]]; then
|
||||
echo "WARNING: upstream is on '$UPSTREAM_BRANCH', not 'main'"
|
||||
confirm "Sync from '$UPSTREAM_BRANCH' anyway?" || exit 1
|
||||
fi
|
||||
|
||||
UPSTREAM_STATUS="$(cd "$UPSTREAM" && git status --porcelain)"
|
||||
if [[ -n "$UPSTREAM_STATUS" ]]; then
|
||||
echo "WARNING: upstream has uncommitted changes:"
|
||||
echo "$UPSTREAM_STATUS" | sed 's/^/ /'
|
||||
echo "Sync will use working-tree state, not HEAD ($UPSTREAM_SHORT)."
|
||||
confirm "Continue anyway?" || exit 1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Prepare destination (clone fork fresh, or use --local)
|
||||
# =============================================================================
|
||||
|
||||
CLEANUP_DIR=""
|
||||
cleanup() {
|
||||
if [[ -n "$CLEANUP_DIR" ]]; then
|
||||
rm -rf "$CLEANUP_DIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -n "$LOCAL_CHECKOUT" ]]; then
|
||||
DEST_REPO="$(cd "$LOCAL_CHECKOUT" && pwd)"
|
||||
[[ -d "$DEST_REPO/.git" ]] || die "--local path '$DEST_REPO' is not a git checkout"
|
||||
else
|
||||
echo "Cloning $FORK..."
|
||||
CLEANUP_DIR="$(mktemp -d)"
|
||||
DEST_REPO="$CLEANUP_DIR/openai-codex-plugins"
|
||||
gh repo clone "$FORK" "$DEST_REPO" >/dev/null
|
||||
fi
|
||||
|
||||
DEST="$DEST_REPO/$DEST_REL"
|
||||
PREVIEW_REPO="$DEST_REPO"
|
||||
PREVIEW_DEST="$DEST"
|
||||
SYNC_SOURCE=""
|
||||
|
||||
overlay_destination_paths() {
|
||||
local repo="$1"
|
||||
local path
|
||||
local source_path
|
||||
local preview_path
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
source_path="$repo/$path"
|
||||
preview_path="$PREVIEW_REPO/$path"
|
||||
|
||||
if [[ -e "$source_path" ]]; then
|
||||
mkdir -p "$(dirname "$preview_path")"
|
||||
cp -R "$source_path" "$preview_path"
|
||||
else
|
||||
rm -rf "$preview_path"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
copy_local_destination_overlay() {
|
||||
overlay_destination_paths "$DEST_REPO" < <(
|
||||
git -C "$DEST_REPO" diff --name-only -z -- "$DEST_REL"
|
||||
)
|
||||
overlay_destination_paths "$DEST_REPO" < <(
|
||||
git -C "$DEST_REPO" diff --cached --name-only -z -- "$DEST_REL"
|
||||
)
|
||||
overlay_destination_paths "$DEST_REPO" < <(
|
||||
git -C "$DEST_REPO" ls-files --others --exclude-standard -z -- "$DEST_REL"
|
||||
)
|
||||
overlay_destination_paths "$DEST_REPO" < <(
|
||||
git -C "$DEST_REPO" ls-files --others --ignored --exclude-standard -z -- "$DEST_REL"
|
||||
)
|
||||
}
|
||||
|
||||
local_checkout_has_uncommitted_destination_changes() {
|
||||
[[ -n "$(git -C "$DEST_REPO" status --porcelain=1 --untracked-files=all --ignored=matching -- "$DEST_REL")" ]]
|
||||
}
|
||||
|
||||
prepare_preview_checkout() {
|
||||
if [[ -n "$LOCAL_CHECKOUT" ]]; then
|
||||
[[ -n "$CLEANUP_DIR" ]] || CLEANUP_DIR="$(mktemp -d)"
|
||||
PREVIEW_REPO="$CLEANUP_DIR/preview"
|
||||
git clone -q --no-local "$DEST_REPO" "$PREVIEW_REPO"
|
||||
PREVIEW_DEST="$PREVIEW_REPO/$DEST_REL"
|
||||
fi
|
||||
|
||||
git -C "$PREVIEW_REPO" checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
|
||||
if [[ -n "$LOCAL_CHECKOUT" ]]; then
|
||||
copy_local_destination_overlay
|
||||
fi
|
||||
if [[ $BOOTSTRAP -ne 1 ]]; then
|
||||
[[ -d "$PREVIEW_DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap, or pass --base <branch>"
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_apply_checkout() {
|
||||
git -C "$DEST_REPO" checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
|
||||
if [[ $BOOTSTRAP -ne 1 ]]; then
|
||||
[[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap, or pass --base <branch>"
|
||||
fi
|
||||
}
|
||||
|
||||
apply_to_preview_checkout() {
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
mkdir -p "$PREVIEW_DEST"
|
||||
fi
|
||||
|
||||
rsync "${RSYNC_ARGS[@]}" "$SYNC_SOURCE/" "$PREVIEW_DEST/"
|
||||
}
|
||||
|
||||
preview_checkout_has_changes() {
|
||||
[[ -n "$(git -C "$PREVIEW_REPO" status --porcelain "$DEST_REL")" ]]
|
||||
}
|
||||
|
||||
prepare_preview_checkout
|
||||
|
||||
TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)"
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
SYNC_BRANCH="bootstrap/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
|
||||
else
|
||||
SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Build rsync args
|
||||
# =============================================================================
|
||||
|
||||
RSYNC_ARGS=(-av --delete --delete-excluded)
|
||||
for pat in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$pat"); done
|
||||
append_git_ignored_directory_excludes
|
||||
append_git_ignored_file_excludes
|
||||
|
||||
copy_preserved_destination_metadata() {
|
||||
local destination="$1"
|
||||
local source="$2"
|
||||
local path
|
||||
local rel
|
||||
|
||||
[[ -d "$destination/skills" ]] || return 0
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
rel="${path#"$destination"/}"
|
||||
mkdir -p "$source/$(dirname "$rel")"
|
||||
cp -p "$path" "$source/$rel"
|
||||
done < <(find "$destination/skills" -path '*/agents/openai.yaml' -type f -print0)
|
||||
}
|
||||
|
||||
prepare_sync_source() {
|
||||
local destination="$1"
|
||||
|
||||
[[ -n "$CLEANUP_DIR" ]] || CLEANUP_DIR="$(mktemp -d)"
|
||||
|
||||
SYNC_SOURCE="$CLEANUP_DIR/source-overlay"
|
||||
rm -rf "$SYNC_SOURCE"
|
||||
mkdir -p "$SYNC_SOURCE"
|
||||
|
||||
rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$SYNC_SOURCE/" >/dev/null
|
||||
copy_preserved_destination_metadata "$destination" "$SYNC_SOURCE"
|
||||
}
|
||||
|
||||
prepare_sync_source "$PREVIEW_DEST"
|
||||
|
||||
# =============================================================================
|
||||
# Dry run preview (always shown)
|
||||
# =============================================================================
|
||||
|
||||
echo ""
|
||||
echo "Upstream: $UPSTREAM ($UPSTREAM_BRANCH @ $UPSTREAM_SHORT)"
|
||||
echo "Version: $UPSTREAM_VERSION"
|
||||
echo "Fork: $FORK"
|
||||
echo "Base: $BASE"
|
||||
echo "Branch: $SYNC_BRANCH"
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
echo "Mode: BOOTSTRAP (creating plugins/superpowers/ when absent)"
|
||||
fi
|
||||
echo ""
|
||||
echo "=== Preview (rsync --dry-run) ==="
|
||||
rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$SYNC_SOURCE/" "$PREVIEW_DEST/"
|
||||
echo "=== End preview ==="
|
||||
echo ""
|
||||
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "Dry run only. Nothing was changed or pushed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Apply
|
||||
# =============================================================================
|
||||
|
||||
echo ""
|
||||
confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; }
|
||||
|
||||
echo ""
|
||||
if [[ -n "$LOCAL_CHECKOUT" ]]; then
|
||||
if local_checkout_has_uncommitted_destination_changes; then
|
||||
die "local checkout has uncommitted changes under '$DEST_REL' — commit, stash, or discard them before syncing"
|
||||
fi
|
||||
|
||||
apply_to_preview_checkout
|
||||
if ! preview_checkout_has_changes; then
|
||||
echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
prepare_apply_checkout
|
||||
cd "$DEST_REPO"
|
||||
git checkout -q -b "$SYNC_BRANCH"
|
||||
echo "Syncing upstream content..."
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
mkdir -p "$DEST"
|
||||
fi
|
||||
rsync "${RSYNC_ARGS[@]}" "$SYNC_SOURCE/" "$DEST/"
|
||||
|
||||
# Bail early if nothing actually changed
|
||||
cd "$DEST_REPO"
|
||||
if [[ -z "$(git status --porcelain "$DEST_REL")" ]]; then
|
||||
echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Commit, push, open PR
|
||||
# =============================================================================
|
||||
|
||||
git add "$DEST_REL"
|
||||
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
|
||||
PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
|
||||
|
||||
Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\` and \`assets/\`.
|
||||
|
||||
Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\`
|
||||
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
|
||||
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files."
|
||||
else
|
||||
COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
|
||||
PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
|
||||
|
||||
Copies the tracked plugin files from upstream, including the committed Codex manifest and assets.
|
||||
|
||||
Run via: \`scripts/sync-to-codex-plugin.sh\`
|
||||
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
|
||||
Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving."
|
||||
fi
|
||||
|
||||
git commit --quiet -m "$COMMIT_TITLE
|
||||
|
||||
Automated sync via scripts/sync-to-codex-plugin.sh
|
||||
Upstream: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
Branch: $SYNC_BRANCH"
|
||||
|
||||
echo "Pushing $SYNC_BRANCH to $FORK..."
|
||||
git push -u origin "$SYNC_BRANCH" --quiet
|
||||
|
||||
echo "Opening PR..."
|
||||
PR_URL="$(gh pr create \
|
||||
--repo "$FORK" \
|
||||
--base "$BASE" \
|
||||
--head "$SYNC_BRANCH" \
|
||||
--title "$COMMIT_TITLE" \
|
||||
--body "$PR_BODY")"
|
||||
|
||||
PR_NUM="${PR_URL##*/}"
|
||||
DIFF_URL="https://github.com/$FORK/pull/$PR_NUM/files"
|
||||
|
||||
echo ""
|
||||
echo "PR opened: $PR_URL"
|
||||
echo "Diff view: $DIFF_URL"
|
||||
@@ -1,36 +0,0 @@
|
||||
# Skill Requests
|
||||
|
||||
Use this page to document skills you wish existed. Add requests here when you encounter situations where a skill would have helped.
|
||||
|
||||
## Format
|
||||
|
||||
```markdown
|
||||
## [Short Descriptive Name]
|
||||
**What I need:** One-line description
|
||||
**When I'd use it:** Specific situations/symptoms
|
||||
**Why I need this:** What makes this non-obvious or worth capturing
|
||||
**Added:** YYYY-MM-DD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Requests
|
||||
|
||||
(None yet - add requests below as you discover needs)
|
||||
|
||||
---
|
||||
|
||||
## Completed Requests
|
||||
|
||||
Skills that have been created from this list will move here with links.
|
||||
|
||||
---
|
||||
|
||||
## Guidelines
|
||||
|
||||
- **Be specific** - "Flaky test debugging" not "testing help"
|
||||
- **Include symptoms** - Error messages, behavior patterns
|
||||
- **Explain non-obvious** - Why can't you just figure this out?
|
||||
- **One skill per request** - Keep them focused
|
||||
|
||||
your human partner reviews this periodically and we create skills together.
|
||||
164
skills/brainstorming/SKILL.md
Normal file
164
skills/brainstorming/SKILL.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
name: brainstorming
|
||||
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
|
||||
---
|
||||
|
||||
# Brainstorming Ideas Into Designs
|
||||
|
||||
Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
|
||||
|
||||
Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.
|
||||
|
||||
<HARD-GATE>
|
||||
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
|
||||
</HARD-GATE>
|
||||
|
||||
## Anti-Pattern: "This Is Too Simple To Need A Design"
|
||||
|
||||
Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.
|
||||
|
||||
## Checklist
|
||||
|
||||
You MUST create a task for each of these items and complete them in order:
|
||||
|
||||
1. **Explore project context** — check files, docs, recent commits
|
||||
2. **Offer visual companion** (if topic will involve visual questions) — this is its own message, not combined with a clarifying question. See the Visual Companion section below.
|
||||
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
|
||||
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
|
||||
5. **Present design** — in sections scaled to their complexity, get user approval after each section
|
||||
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
|
||||
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
|
||||
8. **User reviews written spec** — ask user to review the spec file before proceeding
|
||||
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan
|
||||
|
||||
## Process Flow
|
||||
|
||||
```dot
|
||||
digraph brainstorming {
|
||||
"Explore project context" [shape=box];
|
||||
"Visual questions ahead?" [shape=diamond];
|
||||
"Offer Visual Companion\n(own message, no other content)" [shape=box];
|
||||
"Ask clarifying questions" [shape=box];
|
||||
"Propose 2-3 approaches" [shape=box];
|
||||
"Present design sections" [shape=box];
|
||||
"User approves design?" [shape=diamond];
|
||||
"Write design doc" [shape=box];
|
||||
"Spec self-review\n(fix inline)" [shape=box];
|
||||
"User reviews spec?" [shape=diamond];
|
||||
"Invoke writing-plans skill" [shape=doublecircle];
|
||||
|
||||
"Explore project context" -> "Visual questions ahead?";
|
||||
"Visual questions ahead?" -> "Offer Visual Companion\n(own message, no other content)" [label="yes"];
|
||||
"Visual questions ahead?" -> "Ask clarifying questions" [label="no"];
|
||||
"Offer Visual Companion\n(own message, no other content)" -> "Ask clarifying questions";
|
||||
"Ask clarifying questions" -> "Propose 2-3 approaches";
|
||||
"Propose 2-3 approaches" -> "Present design sections";
|
||||
"Present design sections" -> "User approves design?";
|
||||
"User approves design?" -> "Present design sections" [label="no, revise"];
|
||||
"User approves design?" -> "Write design doc" [label="yes"];
|
||||
"Write design doc" -> "Spec self-review\n(fix inline)";
|
||||
"Spec self-review\n(fix inline)" -> "User reviews spec?";
|
||||
"User reviews spec?" -> "Write design doc" [label="changes requested"];
|
||||
"User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
|
||||
}
|
||||
```
|
||||
|
||||
**The terminal state is invoking writing-plans.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans.
|
||||
|
||||
## The Process
|
||||
|
||||
**Understanding the idea:**
|
||||
|
||||
- Check out the current project state first (files, docs, recent commits)
|
||||
- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
|
||||
- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
|
||||
- For appropriately-scoped projects, ask questions one at a time to refine the idea
|
||||
- Prefer multiple choice questions when possible, but open-ended is fine too
|
||||
- Only one question per message - if a topic needs more exploration, break it into multiple questions
|
||||
- Focus on understanding: purpose, constraints, success criteria
|
||||
|
||||
**Exploring approaches:**
|
||||
|
||||
- Propose 2-3 different approaches with trade-offs
|
||||
- Present options conversationally with your recommendation and reasoning
|
||||
- Lead with your recommended option and explain why
|
||||
|
||||
**Presenting the design:**
|
||||
|
||||
- Once you believe you understand what you're building, present the design
|
||||
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
|
||||
- Ask after each section whether it looks right so far
|
||||
- Cover: architecture, components, data flow, error handling, testing
|
||||
- Be ready to go back and clarify if something doesn't make sense
|
||||
|
||||
**Design for isolation and clarity:**
|
||||
|
||||
- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
|
||||
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
|
||||
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
|
||||
- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.
|
||||
|
||||
**Working in existing codebases:**
|
||||
|
||||
- Explore the current structure before proposing changes. Follow existing patterns.
|
||||
- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
|
||||
- Don't propose unrelated refactoring. Stay focused on what serves the current goal.
|
||||
|
||||
## After the Design
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
|
||||
- (User preferences for spec location override this default)
|
||||
- Use elements-of-style:writing-clearly-and-concisely skill if available
|
||||
- Commit the design document to git
|
||||
|
||||
**Spec Self-Review:**
|
||||
After writing the spec document, look at it with fresh eyes:
|
||||
|
||||
1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
|
||||
2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
|
||||
3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
|
||||
4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.
|
||||
|
||||
Fix any issues inline. No need to re-review — just fix and move on.
|
||||
|
||||
**User Review Gate:**
|
||||
After the spec review loop passes, ask the user to review the written spec before proceeding:
|
||||
|
||||
> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."
|
||||
|
||||
Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Invoke the writing-plans skill to create a detailed implementation plan
|
||||
- Do NOT invoke any other skill. writing-plans is the next step.
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **One question at a time** - Don't overwhelm with multiple questions
|
||||
- **Multiple choice preferred** - Easier to answer than open-ended when possible
|
||||
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
|
||||
- **Explore alternatives** - Always propose 2-3 approaches before settling
|
||||
- **Incremental validation** - Present design, get approval before moving on
|
||||
- **Be flexible** - Go back and clarify when something doesn't make sense
|
||||
|
||||
## Visual Companion
|
||||
|
||||
A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.
|
||||
|
||||
**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent:
|
||||
> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)"
|
||||
|
||||
**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming.
|
||||
|
||||
**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?**
|
||||
|
||||
- **Use the browser** for content that IS visual — mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs
|
||||
- **Use the terminal** for content that is text — requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions
|
||||
|
||||
A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.
|
||||
|
||||
If they agree to the companion, read the detailed guide before proceeding:
|
||||
`skills/brainstorming/visual-companion.md`
|
||||
214
skills/brainstorming/scripts/frame-template.html
Normal file
214
skills/brainstorming/scripts/frame-template.html
Normal file
@@ -0,0 +1,214 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Superpowers Brainstorming</title>
|
||||
<style>
|
||||
/*
|
||||
* BRAINSTORM COMPANION FRAME TEMPLATE
|
||||
*
|
||||
* This template provides a consistent frame with:
|
||||
* - OS-aware light/dark theming
|
||||
* - Fixed header and selection indicator bar
|
||||
* - Scrollable main content area
|
||||
* - CSS helpers for common UI patterns
|
||||
*
|
||||
* Content is injected via placeholder comment in #claude-content.
|
||||
*/
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
|
||||
/* ===== THEME VARIABLES ===== */
|
||||
:root {
|
||||
--bg-primary: #f5f5f7;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #e5e5e7;
|
||||
--border: #d1d1d6;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #86868b;
|
||||
--text-tertiary: #aeaeb2;
|
||||
--accent: #0071e3;
|
||||
--accent-hover: #0077ed;
|
||||
--success: #34c759;
|
||||
--warning: #ff9f0a;
|
||||
--error: #ff3b30;
|
||||
--selected-bg: #e8f4fd;
|
||||
--selected-border: #0071e3;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-primary: #1d1d1f;
|
||||
--bg-secondary: #2d2d2f;
|
||||
--bg-tertiary: #3d3d3f;
|
||||
--border: #424245;
|
||||
--text-primary: #f5f5f7;
|
||||
--text-secondary: #86868b;
|
||||
--text-tertiary: #636366;
|
||||
--accent: #0a84ff;
|
||||
--accent-hover: #409cff;
|
||||
--selected-bg: rgba(10, 132, 255, 0.15);
|
||||
--selected-border: #0a84ff;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ===== FRAME STRUCTURE ===== */
|
||||
.header {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.5rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header h1 { font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
|
||||
.header .status { font-size: 0.7rem; color: var(--success); display: flex; align-items: center; gap: 0.4rem; }
|
||||
.header .status::before { content: ''; width: 6px; height: 6px; background: var(--success); border-radius: 50%; }
|
||||
|
||||
.main { flex: 1; overflow-y: auto; }
|
||||
#claude-content { padding: 2rem; min-height: 100%; }
|
||||
|
||||
.indicator-bar {
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 0.5rem 1.5rem;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.indicator-bar span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.indicator-bar .selected-text {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== TYPOGRAPHY ===== */
|
||||
h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
h3 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.25rem; }
|
||||
.subtitle { color: var(--text-secondary); margin-bottom: 1.5rem; }
|
||||
.section { margin-bottom: 2rem; }
|
||||
.label { font-size: 0.7rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
||||
|
||||
/* ===== OPTIONS (for A/B/C choices) ===== */
|
||||
.options { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.option {
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
.option:hover { border-color: var(--accent); }
|
||||
.option.selected { background: var(--selected-bg); border-color: var(--selected-border); }
|
||||
.option .letter {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
width: 1.75rem; height: 1.75rem;
|
||||
border-radius: 6px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 600; font-size: 0.85rem; flex-shrink: 0;
|
||||
}
|
||||
.option.selected .letter { background: var(--accent); color: white; }
|
||||
.option .content { flex: 1; }
|
||||
.option .content h3 { font-size: 0.95rem; margin-bottom: 0.15rem; }
|
||||
.option .content p { color: var(--text-secondary); font-size: 0.85rem; margin: 0; }
|
||||
|
||||
/* ===== CARDS (for showing designs/mockups) ===== */
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
|
||||
.card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||
.card.selected { border-color: var(--selected-border); border-width: 2px; }
|
||||
.card-image { background: var(--bg-tertiary); aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center; }
|
||||
.card-body { padding: 1rem; }
|
||||
.card-body h3 { margin-bottom: 0.25rem; }
|
||||
.card-body p { color: var(--text-secondary); font-size: 0.85rem; }
|
||||
|
||||
/* ===== MOCKUP CONTAINER ===== */
|
||||
.mockup {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.mockup-header {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.mockup-body { padding: 1.5rem; }
|
||||
|
||||
/* ===== SPLIT VIEW (side-by-side comparison) ===== */
|
||||
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
|
||||
@media (max-width: 700px) { .split { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ===== PROS/CONS ===== */
|
||||
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1rem 0; }
|
||||
.pros, .cons { background: var(--bg-secondary); border-radius: 8px; padding: 1rem; }
|
||||
.pros h4 { color: var(--success); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.cons h4 { color: var(--error); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.pros ul, .cons ul { margin-left: 1.25rem; font-size: 0.85rem; color: var(--text-secondary); }
|
||||
.pros li, .cons li { margin-bottom: 0.25rem; }
|
||||
|
||||
/* ===== PLACEHOLDER (for mockup areas) ===== */
|
||||
.placeholder {
|
||||
background: var(--bg-tertiary);
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== INLINE MOCKUP ELEMENTS ===== */
|
||||
.mock-nav { background: var(--accent); color: white; padding: 0.75rem 1rem; display: flex; gap: 1.5rem; font-size: 0.9rem; }
|
||||
.mock-sidebar { background: var(--bg-tertiary); padding: 1rem; min-width: 180px; }
|
||||
.mock-content { padding: 1.5rem; flex: 1; }
|
||||
.mock-button { background: var(--accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; }
|
||||
.mock-input { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem; width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers Brainstorming</a></h1>
|
||||
<div class="status">Connected</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div id="claude-content">
|
||||
<!-- CONTENT -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="indicator-bar">
|
||||
<span id="indicator-text">Click an option above, then return to the terminal</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
88
skills/brainstorming/scripts/helper.js
Normal file
88
skills/brainstorming/scripts/helper.js
Normal file
@@ -0,0 +1,88 @@
|
||||
(function() {
|
||||
const WS_URL = 'ws://' + window.location.host;
|
||||
let ws = null;
|
||||
let eventQueue = [];
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(WS_URL);
|
||||
|
||||
ws.onopen = () => {
|
||||
eventQueue.forEach(e => ws.send(JSON.stringify(e)));
|
||||
eventQueue = [];
|
||||
};
|
||||
|
||||
ws.onmessage = (msg) => {
|
||||
const data = JSON.parse(msg.data);
|
||||
if (data.type === 'reload') {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setTimeout(connect, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
function sendEvent(event) {
|
||||
event.timestamp = Date.now();
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(event));
|
||||
} else {
|
||||
eventQueue.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture clicks on choice elements
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-choice]');
|
||||
if (!target) return;
|
||||
|
||||
sendEvent({
|
||||
type: 'click',
|
||||
text: target.textContent.trim(),
|
||||
choice: target.dataset.choice,
|
||||
id: target.id || null
|
||||
});
|
||||
|
||||
// Update indicator bar (defer so toggleSelect runs first)
|
||||
setTimeout(() => {
|
||||
const indicator = document.getElementById('indicator-text');
|
||||
if (!indicator) return;
|
||||
const container = target.closest('.options') || target.closest('.cards');
|
||||
const selected = container ? container.querySelectorAll('.selected') : [];
|
||||
if (selected.length === 0) {
|
||||
indicator.textContent = 'Click an option above, then return to the terminal';
|
||||
} else if (selected.length === 1) {
|
||||
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
|
||||
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
|
||||
} else {
|
||||
indicator.innerHTML = '<span class="selected-text">' + selected.length + ' selected</span> — return to terminal to continue';
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Frame UI: selection tracking
|
||||
window.selectedChoice = null;
|
||||
|
||||
window.toggleSelect = function(el) {
|
||||
const container = el.closest('.options') || el.closest('.cards');
|
||||
const multi = container && container.dataset.multiselect !== undefined;
|
||||
if (container && !multi) {
|
||||
container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));
|
||||
}
|
||||
if (multi) {
|
||||
el.classList.toggle('selected');
|
||||
} else {
|
||||
el.classList.add('selected');
|
||||
}
|
||||
window.selectedChoice = el.dataset.choice;
|
||||
};
|
||||
|
||||
// Expose API for explicit use
|
||||
window.brainstorm = {
|
||||
send: sendEvent,
|
||||
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
|
||||
};
|
||||
|
||||
connect();
|
||||
})();
|
||||
354
skills/brainstorming/scripts/server.cjs
Normal file
354
skills/brainstorming/scripts/server.cjs
Normal file
@@ -0,0 +1,354 @@
|
||||
const crypto = require('crypto');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ========== WebSocket Protocol (RFC 6455) ==========
|
||||
|
||||
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
|
||||
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
|
||||
function computeAcceptKey(clientKey) {
|
||||
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
|
||||
}
|
||||
|
||||
function encodeFrame(opcode, payload) {
|
||||
const fin = 0x80;
|
||||
const len = payload.length;
|
||||
let header;
|
||||
|
||||
if (len < 126) {
|
||||
header = Buffer.alloc(2);
|
||||
header[0] = fin | opcode;
|
||||
header[1] = len;
|
||||
} else if (len < 65536) {
|
||||
header = Buffer.alloc(4);
|
||||
header[0] = fin | opcode;
|
||||
header[1] = 126;
|
||||
header.writeUInt16BE(len, 2);
|
||||
} else {
|
||||
header = Buffer.alloc(10);
|
||||
header[0] = fin | opcode;
|
||||
header[1] = 127;
|
||||
header.writeBigUInt64BE(BigInt(len), 2);
|
||||
}
|
||||
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
function decodeFrame(buffer) {
|
||||
if (buffer.length < 2) return null;
|
||||
|
||||
const secondByte = buffer[1];
|
||||
const opcode = buffer[0] & 0x0F;
|
||||
const masked = (secondByte & 0x80) !== 0;
|
||||
let payloadLen = secondByte & 0x7F;
|
||||
let offset = 2;
|
||||
|
||||
if (!masked) throw new Error('Client frames must be masked');
|
||||
|
||||
if (payloadLen === 126) {
|
||||
if (buffer.length < 4) return null;
|
||||
payloadLen = buffer.readUInt16BE(2);
|
||||
offset = 4;
|
||||
} else if (payloadLen === 127) {
|
||||
if (buffer.length < 10) return null;
|
||||
payloadLen = Number(buffer.readBigUInt64BE(2));
|
||||
offset = 10;
|
||||
}
|
||||
|
||||
const maskOffset = offset;
|
||||
const dataOffset = offset + 4;
|
||||
const totalLen = dataOffset + payloadLen;
|
||||
if (buffer.length < totalLen) return null;
|
||||
|
||||
const mask = buffer.slice(maskOffset, dataOffset);
|
||||
const data = Buffer.alloc(payloadLen);
|
||||
for (let i = 0; i < payloadLen; i++) {
|
||||
data[i] = buffer[dataOffset + i] ^ mask[i % 4];
|
||||
}
|
||||
|
||||
return { opcode, payload: data, bytesConsumed: totalLen };
|
||||
}
|
||||
|
||||
// ========== Configuration ==========
|
||||
|
||||
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
|
||||
const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
|
||||
const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
|
||||
const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
|
||||
const CONTENT_DIR = path.join(SESSION_DIR, 'content');
|
||||
const STATE_DIR = path.join(SESSION_DIR, 'state');
|
||||
let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
|
||||
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'
|
||||
};
|
||||
|
||||
// ========== Templates and Constants ==========
|
||||
|
||||
const WAITING_PAGE = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>Brainstorm Companion</title>
|
||||
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; } p { color: #666; }</style>
|
||||
</head>
|
||||
<body><h1>Brainstorm Companion</h1>
|
||||
<p>Waiting for the agent to push a screen...</p></body></html>`;
|
||||
|
||||
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
|
||||
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
|
||||
const helperInjection = '<script>\n' + helperScript + '\n</script>';
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function isFullDocument(html) {
|
||||
const trimmed = html.trimStart().toLowerCase();
|
||||
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
|
||||
}
|
||||
|
||||
function wrapInFrame(content) {
|
||||
return frameTemplate.replace('<!-- CONTENT -->', content);
|
||||
}
|
||||
|
||||
function getNewestScreen() {
|
||||
const files = fs.readdirSync(CONTENT_DIR)
|
||||
.filter(f => f.endsWith('.html'))
|
||||
.map(f => {
|
||||
const fp = path.join(CONTENT_DIR, f);
|
||||
return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
|
||||
})
|
||||
.sort((a, b) => b.mtime - a.mtime);
|
||||
return files.length > 0 ? files[0].path : null;
|
||||
}
|
||||
|
||||
// ========== HTTP Request Handler ==========
|
||||
|
||||
function handleRequest(req, res) {
|
||||
touchActivity();
|
||||
if (req.method === 'GET' && req.url === '/') {
|
||||
const screenFile = getNewestScreen();
|
||||
let html = screenFile
|
||||
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
|
||||
: WAITING_PAGE;
|
||||
|
||||
if (html.includes('</body>')) {
|
||||
html = html.replace('</body>', helperInjection + '\n</body>');
|
||||
} else {
|
||||
html += helperInjection;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
|
||||
const fileName = req.url.slice(7);
|
||||
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
|
||||
if (!fs.existsSync(filePath)) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(fs.readFileSync(filePath));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== WebSocket Connection Handling ==========
|
||||
|
||||
const clients = new Set();
|
||||
|
||||
function handleUpgrade(req, socket) {
|
||||
const key = req.headers['sec-websocket-key'];
|
||||
if (!key) { socket.destroy(); return; }
|
||||
|
||||
const accept = computeAcceptKey(key);
|
||||
socket.write(
|
||||
'HTTP/1.1 101 Switching Protocols\r\n' +
|
||||
'Upgrade: websocket\r\n' +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'
|
||||
);
|
||||
|
||||
let buffer = Buffer.alloc(0);
|
||||
clients.add(socket);
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
while (buffer.length > 0) {
|
||||
let result;
|
||||
try {
|
||||
result = decodeFrame(buffer);
|
||||
} catch (e) {
|
||||
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
|
||||
clients.delete(socket);
|
||||
return;
|
||||
}
|
||||
if (!result) break;
|
||||
buffer = buffer.slice(result.bytesConsumed);
|
||||
|
||||
switch (result.opcode) {
|
||||
case OPCODES.TEXT:
|
||||
handleMessage(result.payload.toString());
|
||||
break;
|
||||
case OPCODES.CLOSE:
|
||||
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
|
||||
clients.delete(socket);
|
||||
return;
|
||||
case OPCODES.PING:
|
||||
socket.write(encodeFrame(OPCODES.PONG, result.payload));
|
||||
break;
|
||||
case OPCODES.PONG:
|
||||
break;
|
||||
default: {
|
||||
const closeBuf = Buffer.alloc(2);
|
||||
closeBuf.writeUInt16BE(1003);
|
||||
socket.end(encodeFrame(OPCODES.CLOSE, closeBuf));
|
||||
clients.delete(socket);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', () => clients.delete(socket));
|
||||
socket.on('error', () => clients.delete(socket));
|
||||
}
|
||||
|
||||
function handleMessage(text) {
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse WebSocket message:', e.message);
|
||||
return;
|
||||
}
|
||||
touchActivity();
|
||||
console.log(JSON.stringify({ source: 'user-event', ...event }));
|
||||
if (event.choice) {
|
||||
const eventsFile = path.join(STATE_DIR, 'events');
|
||||
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
function broadcast(msg) {
|
||||
const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg)));
|
||||
for (const socket of clients) {
|
||||
try { socket.write(frame); } catch (e) { clients.delete(socket); }
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Activity Tracking ==========
|
||||
|
||||
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
||||
let lastActivity = Date.now();
|
||||
|
||||
function touchActivity() {
|
||||
lastActivity = Date.now();
|
||||
}
|
||||
|
||||
// ========== File Watching ==========
|
||||
|
||||
const debounceTimers = new Map();
|
||||
|
||||
// ========== Server Startup ==========
|
||||
|
||||
function startServer() {
|
||||
if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true });
|
||||
if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||
|
||||
// Track known files to distinguish new screens from updates.
|
||||
// macOS fs.watch reports 'rename' for both new files and overwrites,
|
||||
// so we can't rely on eventType alone.
|
||||
const knownFiles = new Set(
|
||||
fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html'))
|
||||
);
|
||||
|
||||
const server = http.createServer(handleRequest);
|
||||
server.on('upgrade', handleUpgrade);
|
||||
|
||||
const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => {
|
||||
if (!filename || !filename.endsWith('.html')) return;
|
||||
|
||||
if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename));
|
||||
debounceTimers.set(filename, setTimeout(() => {
|
||||
debounceTimers.delete(filename);
|
||||
const filePath = path.join(CONTENT_DIR, filename);
|
||||
|
||||
if (!fs.existsSync(filePath)) return; // file was deleted
|
||||
touchActivity();
|
||||
|
||||
if (!knownFiles.has(filename)) {
|
||||
knownFiles.add(filename);
|
||||
const eventsFile = path.join(STATE_DIR, 'events');
|
||||
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
|
||||
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
|
||||
} else {
|
||||
console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
|
||||
}
|
||||
|
||||
broadcast({ type: 'reload' });
|
||||
}, 100));
|
||||
});
|
||||
watcher.on('error', (err) => console.error('fs.watch error:', err.message));
|
||||
|
||||
function shutdown(reason) {
|
||||
console.log(JSON.stringify({ type: 'server-stopped', reason }));
|
||||
const infoFile = path.join(STATE_DIR, 'server-info');
|
||||
if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
|
||||
fs.writeFileSync(
|
||||
path.join(STATE_DIR, 'server-stopped'),
|
||||
JSON.stringify({ reason, timestamp: Date.now() }) + '\n'
|
||||
);
|
||||
watcher.close();
|
||||
clearInterval(lifecycleCheck);
|
||||
server.close(() => process.exit(0));
|
||||
}
|
||||
|
||||
function ownerAlive() {
|
||||
if (!ownerPid) return true;
|
||||
try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
||||
}
|
||||
|
||||
// Check every 60s: exit if owner process died or idle for 30 minutes
|
||||
const lifecycleCheck = setInterval(() => {
|
||||
if (!ownerAlive()) shutdown('owner process exited');
|
||||
else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout');
|
||||
}, 60 * 1000);
|
||||
lifecycleCheck.unref();
|
||||
|
||||
// Validate owner PID at startup. If it's already dead, the PID resolution
|
||||
// was wrong (common on WSL, Tailscale SSH, and cross-user scenarios).
|
||||
// Disable monitoring and rely on the idle timeout instead.
|
||||
if (ownerPid) {
|
||||
try { process.kill(ownerPid, 0); }
|
||||
catch (e) {
|
||||
if (e.code !== 'EPERM') {
|
||||
console.log(JSON.stringify({ type: 'owner-pid-invalid', pid: ownerPid, reason: 'dead at startup' }));
|
||||
ownerPid = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
const info = JSON.stringify({
|
||||
type: 'server-started', port: Number(PORT), host: HOST,
|
||||
url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT,
|
||||
screen_dir: CONTENT_DIR, state_dir: STATE_DIR
|
||||
});
|
||||
console.log(info);
|
||||
fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n');
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
startServer();
|
||||
}
|
||||
|
||||
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };
|
||||
148
skills/brainstorming/scripts/start-server.sh
Executable file
148
skills/brainstorming/scripts/start-server.sh
Executable file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the brainstorm server and output connection info
|
||||
# Usage: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
|
||||
#
|
||||
# Starts server on a random high port, outputs JSON with URL.
|
||||
# Each session gets its own directory to avoid conflicts.
|
||||
#
|
||||
# Options:
|
||||
# --project-dir <path> Store session files under <path>/.superpowers/brainstorm/
|
||||
# instead of /tmp. Files persist after server stops.
|
||||
# --host <bind-host> Host/interface to bind (default: 127.0.0.1).
|
||||
# Use 0.0.0.0 in remote/containerized environments.
|
||||
# --url-host <host> Hostname shown in returned URL JSON.
|
||||
# --foreground Run server in the current terminal (no backgrounding).
|
||||
# --background Force background mode (overrides Codex auto-foreground).
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Parse arguments
|
||||
PROJECT_DIR=""
|
||||
FOREGROUND="false"
|
||||
FORCE_BACKGROUND="false"
|
||||
BIND_HOST="127.0.0.1"
|
||||
URL_HOST=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project-dir)
|
||||
PROJECT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--host)
|
||||
BIND_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--url-host)
|
||||
URL_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--foreground|--no-daemon)
|
||||
FOREGROUND="true"
|
||||
shift
|
||||
;;
|
||||
--background|--daemon)
|
||||
FORCE_BACKGROUND="true"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "{\"error\": \"Unknown argument: $1\"}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$URL_HOST" ]]; then
|
||||
if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then
|
||||
URL_HOST="localhost"
|
||||
else
|
||||
URL_HOST="$BIND_HOST"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Some environments reap detached/background processes. Auto-foreground when detected.
|
||||
if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
|
||||
FOREGROUND="true"
|
||||
fi
|
||||
|
||||
# Windows/Git Bash reaps nohup background processes. Auto-foreground when detected.
|
||||
if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
|
||||
case "${OSTYPE:-}" in
|
||||
msys*|cygwin*|mingw*) FOREGROUND="true" ;;
|
||||
esac
|
||||
if [[ -n "${MSYSTEM:-}" ]]; then
|
||||
FOREGROUND="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate unique session directory
|
||||
SESSION_ID="$$-$(date +%s)"
|
||||
|
||||
if [[ -n "$PROJECT_DIR" ]]; then
|
||||
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
|
||||
else
|
||||
SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
|
||||
fi
|
||||
|
||||
STATE_DIR="${SESSION_DIR}/state"
|
||||
PID_FILE="${STATE_DIR}/server.pid"
|
||||
LOG_FILE="${STATE_DIR}/server.log"
|
||||
|
||||
# Create fresh session directory with content and state peers
|
||||
mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"
|
||||
|
||||
# Kill any existing server
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
old_pid=$(cat "$PID_FILE")
|
||||
kill "$old_pid" 2>/dev/null
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Resolve the harness PID (grandparent of this script).
|
||||
# $PPID is the ephemeral shell the harness spawned to run us — it dies
|
||||
# when this script exits. The harness itself is $PPID's parent.
|
||||
OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')"
|
||||
if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
|
||||
OWNER_PID="$PPID"
|
||||
fi
|
||||
|
||||
# Foreground mode for environments that reap detached/background processes.
|
||||
if [[ "$FOREGROUND" == "true" ]]; then
|
||||
echo "$$" > "$PID_FILE"
|
||||
env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Start server, capturing output to log file
|
||||
# Use nohup to survive shell exit; disown to remove from job table
|
||||
nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs > "$LOG_FILE" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
disown "$SERVER_PID" 2>/dev/null
|
||||
echo "$SERVER_PID" > "$PID_FILE"
|
||||
|
||||
# Wait for server-started message (check log file)
|
||||
for i in {1..50}; do
|
||||
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
|
||||
# Verify server is still alive after a short window (catches process reapers)
|
||||
alive="true"
|
||||
for _ in {1..20}; do
|
||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||
alive="false"
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
if [[ "$alive" != "true" ]]; then
|
||||
echo "{\"error\": \"Server started but was killed. Retry in a persistent terminal with: $SCRIPT_DIR/start-server.sh${PROJECT_DIR:+ --project-dir $PROJECT_DIR} --host $BIND_HOST --url-host $URL_HOST --foreground\"}"
|
||||
exit 1
|
||||
fi
|
||||
grep "server-started" "$LOG_FILE" | head -1
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# Timeout - server didn't start
|
||||
echo '{"error": "Server failed to start within 5 seconds"}'
|
||||
exit 1
|
||||
56
skills/brainstorming/scripts/stop-server.sh
Executable file
56
skills/brainstorming/scripts/stop-server.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop the brainstorm server and clean up
|
||||
# Usage: stop-server.sh <session_dir>
|
||||
#
|
||||
# Kills the server process. Only deletes session directory if it's
|
||||
# under /tmp (ephemeral). Persistent directories (.superpowers/) are
|
||||
# kept so mockups can be reviewed later.
|
||||
|
||||
SESSION_DIR="$1"
|
||||
|
||||
if [[ -z "$SESSION_DIR" ]]; then
|
||||
echo '{"error": "Usage: stop-server.sh <session_dir>"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STATE_DIR="${SESSION_DIR}/state"
|
||||
PID_FILE="${STATE_DIR}/server.pid"
|
||||
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
pid=$(cat "$PID_FILE")
|
||||
|
||||
# Try to stop gracefully, fallback to force if still alive
|
||||
kill "$pid" 2>/dev/null || true
|
||||
|
||||
# Wait for graceful shutdown (up to ~2s)
|
||||
for i in {1..20}; do
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# If still running, escalate to SIGKILL
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
|
||||
# Give SIGKILL a moment to take effect
|
||||
sleep 0.1
|
||||
fi
|
||||
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo '{"status": "failed", "error": "process still running"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$PID_FILE" "${STATE_DIR}/server.log"
|
||||
|
||||
# Only delete ephemeral /tmp directories
|
||||
if [[ "$SESSION_DIR" == /tmp/* ]]; then
|
||||
rm -rf "$SESSION_DIR"
|
||||
fi
|
||||
|
||||
echo '{"status": "stopped"}'
|
||||
else
|
||||
echo '{"status": "not_running"}'
|
||||
fi
|
||||
49
skills/brainstorming/spec-document-reviewer-prompt.md
Normal file
49
skills/brainstorming/spec-document-reviewer-prompt.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Spec Document Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a spec document reviewer subagent.
|
||||
|
||||
**Purpose:** Verify the spec is complete, consistent, and ready for implementation planning.
|
||||
|
||||
**Dispatch after:** Spec document is written to docs/superpowers/specs/
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Review spec document"
|
||||
prompt: |
|
||||
You are a spec document reviewer. Verify this spec is complete and ready for planning.
|
||||
|
||||
**Spec to review:** [SPEC_FILE_PATH]
|
||||
|
||||
## What to Check
|
||||
|
||||
| Category | What to Look For |
|
||||
|----------|------------------|
|
||||
| Completeness | TODOs, placeholders, "TBD", incomplete sections |
|
||||
| Consistency | Internal contradictions, conflicting requirements |
|
||||
| Clarity | Requirements ambiguous enough to cause someone to build the wrong thing |
|
||||
| Scope | Focused enough for a single plan — not covering multiple independent subsystems |
|
||||
| YAGNI | Unrequested features, over-engineering |
|
||||
|
||||
## Calibration
|
||||
|
||||
**Only flag issues that would cause real problems during implementation planning.**
|
||||
A missing section, a contradiction, or a requirement so ambiguous it could be
|
||||
interpreted two different ways — those are issues. Minor wording improvements,
|
||||
stylistic preferences, and "sections less detailed than others" are not.
|
||||
|
||||
Approve unless there are serious gaps that would lead to a flawed plan.
|
||||
|
||||
## Output Format
|
||||
|
||||
## Spec Review
|
||||
|
||||
**Status:** Approved | Issues Found
|
||||
|
||||
**Issues (if any):**
|
||||
- [Section X]: [specific issue] - [why it matters for planning]
|
||||
|
||||
**Recommendations (advisory, do not block approval):**
|
||||
- [suggestions for improvement]
|
||||
```
|
||||
|
||||
**Reviewer returns:** Status, Issues (if any), Recommendations
|
||||
287
skills/brainstorming/visual-companion.md
Normal file
287
skills/brainstorming/visual-companion.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Visual Companion Guide
|
||||
|
||||
Browser-based visual brainstorming companion for showing mockups, diagrams, and options.
|
||||
|
||||
## When to Use
|
||||
|
||||
Decide per-question, not per-session. The test: **would the user understand this better by seeing it than reading it?**
|
||||
|
||||
**Use the browser** when the content itself is visual:
|
||||
|
||||
- **UI mockups** — wireframes, layouts, navigation structures, component designs
|
||||
- **Architecture diagrams** — system components, data flow, relationship maps
|
||||
- **Side-by-side visual comparisons** — comparing two layouts, two color schemes, two design directions
|
||||
- **Design polish** — when the question is about look and feel, spacing, visual hierarchy
|
||||
- **Spatial relationships** — state machines, flowcharts, entity relationships rendered as diagrams
|
||||
|
||||
**Use the terminal** when the content is text or tabular:
|
||||
|
||||
- **Requirements and scope questions** — "what does X mean?", "which features are in scope?"
|
||||
- **Conceptual A/B/C choices** — picking between approaches described in words
|
||||
- **Tradeoff lists** — pros/cons, comparison tables
|
||||
- **Technical decisions** — API design, data modeling, architectural approach selection
|
||||
- **Clarifying questions** — anything where the answer is words, not a visual preference
|
||||
|
||||
A question *about* a UI topic is not automatically a visual question. "What kind of wizard do you want?" is conceptual — use the terminal. "Which of these wizard layouts feels right?" is visual — use the browser.
|
||||
|
||||
## How It Works
|
||||
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
|
||||
|
||||
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
|
||||
|
||||
## Starting a Session
|
||||
|
||||
```bash
|
||||
# Start server with persistence (mockups saved to project)
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
|
||||
# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341",
|
||||
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/content",
|
||||
# "state_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/state"}
|
||||
```
|
||||
|
||||
Save `screen_dir` and `state_dir` from the response. Tell user to open the URL.
|
||||
|
||||
**Finding connection info:** The server writes its startup JSON to `$STATE_DIR/server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `<project>/.superpowers/brainstorm/` for the session directory.
|
||||
|
||||
**Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there.
|
||||
|
||||
**Launching the server by platform:**
|
||||
|
||||
**Claude Code (macOS / Linux):**
|
||||
```bash
|
||||
# Default mode works — the script backgrounds the server itself
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
```
|
||||
|
||||
**Claude Code (Windows):**
|
||||
```bash
|
||||
# Windows auto-detects and uses foreground mode, which blocks the tool call.
|
||||
# Use run_in_background: true on the Bash tool call so the server survives
|
||||
# across conversation turns.
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
```
|
||||
When calling this via the Bash tool, set `run_in_background: true`. Then read `$STATE_DIR/server-info` on the next turn to get the URL and port.
|
||||
|
||||
**Codex:**
|
||||
```bash
|
||||
# Codex reaps background processes. The script auto-detects CODEX_CI and
|
||||
# switches to foreground mode. Run it normally — no extra flags needed.
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
```
|
||||
|
||||
**Gemini CLI:**
|
||||
```bash
|
||||
# Use --foreground and set is_background: true on your shell tool call
|
||||
# so the process survives across turns
|
||||
scripts/start-server.sh --project-dir /path/to/project --foreground
|
||||
```
|
||||
|
||||
**Other environments:** The server must keep running in the background across conversation turns. If your environment reaps detached processes, use `--foreground` and launch the command with your platform's background execution mechanism.
|
||||
|
||||
If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host:
|
||||
|
||||
```bash
|
||||
scripts/start-server.sh \
|
||||
--project-dir /path/to/project \
|
||||
--host 0.0.0.0 \
|
||||
--url-host localhost
|
||||
```
|
||||
|
||||
Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
||||
|
||||
## The Loop
|
||||
|
||||
1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`:
|
||||
- Before each write, check that `$STATE_DIR/server-info` exists. If it doesn't (or `$STATE_DIR/server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity.
|
||||
- Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html`
|
||||
- **Never reuse filenames** — each screen gets a fresh file
|
||||
- Use Write tool — **never use cat/heredoc** (dumps noise into terminal)
|
||||
- Server automatically serves the newest file
|
||||
|
||||
2. **Tell user what to expect and end your turn:**
|
||||
- Remind them of the URL (every step, not just first)
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
|
||||
- Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like."
|
||||
|
||||
3. **On your next turn** — after the user responds in the terminal:
|
||||
- Read `$STATE_DIR/events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines
|
||||
- Merge with the user's terminal text to get the full picture
|
||||
- The terminal message is the primary feedback; `state_dir/events` provides structured interaction data
|
||||
|
||||
4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated.
|
||||
|
||||
5. **Unload when returning to terminal** — when the next step doesn't need the browser (e.g., a clarifying question, a tradeoff discussion), push a waiting screen to clear the stale content:
|
||||
|
||||
```html
|
||||
<!-- filename: waiting.html (or waiting-2.html, etc.) -->
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
This prevents the user from staring at a resolved choice while the conversation has moved on. When the next visual question comes up, push a new content file as usual.
|
||||
|
||||
6. Repeat until done.
|
||||
|
||||
## Writing Content Fragments
|
||||
|
||||
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
|
||||
|
||||
**Minimal example:**
|
||||
|
||||
```html
|
||||
<h2>Which layout works better?</h2>
|
||||
<p class="subtitle">Consider readability and visual hierarchy</p>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Single Column</h3>
|
||||
<p>Clean, focused reading experience</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content">
|
||||
<h3>Two Column</h3>
|
||||
<p>Sidebar navigation with main content</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
That's it. No `<html>`, no CSS, no `<script>` tags needed. The server provides all of that.
|
||||
|
||||
## CSS Classes Available
|
||||
|
||||
The frame template provides these CSS classes for your content:
|
||||
|
||||
### Options (A/B/C choices)
|
||||
|
||||
```html
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Title</h3>
|
||||
<p>Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Multi-select:** Add `data-multiselect` to the container to let users select multiple options. Each click toggles the item. The indicator bar shows the count.
|
||||
|
||||
```html
|
||||
<div class="options" data-multiselect>
|
||||
<!-- same option markup — users can select/deselect multiple -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Cards (visual designs)
|
||||
|
||||
```html
|
||||
<div class="cards">
|
||||
<div class="card" data-choice="design1" onclick="toggleSelect(this)">
|
||||
<div class="card-image"><!-- mockup content --></div>
|
||||
<div class="card-body">
|
||||
<h3>Name</h3>
|
||||
<p>Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mockup container
|
||||
|
||||
```html
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Preview: Dashboard Layout</div>
|
||||
<div class="mockup-body"><!-- your mockup HTML --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Split view (side-by-side)
|
||||
|
||||
```html
|
||||
<div class="split">
|
||||
<div class="mockup"><!-- left --></div>
|
||||
<div class="mockup"><!-- right --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pros/Cons
|
||||
|
||||
```html
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Benefit</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>Drawback</li></ul></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mock elements (wireframe building blocks)
|
||||
|
||||
```html
|
||||
<div class="mock-nav">Logo | Home | About | Contact</div>
|
||||
<div style="display: flex;">
|
||||
<div class="mock-sidebar">Navigation</div>
|
||||
<div class="mock-content">Main content area</div>
|
||||
</div>
|
||||
<button class="mock-button">Action Button</button>
|
||||
<input class="mock-input" placeholder="Input field">
|
||||
<div class="placeholder">Placeholder area</div>
|
||||
```
|
||||
|
||||
### Typography and sections
|
||||
|
||||
- `h2` — page title
|
||||
- `h3` — section heading
|
||||
- `.subtitle` — secondary text below title
|
||||
- `.section` — content block with bottom margin
|
||||
- `.label` — small uppercase label text
|
||||
|
||||
## Browser Events Format
|
||||
|
||||
When the user clicks options in the browser, their interactions are recorded to `$STATE_DIR/events` (one JSON object per line). The file is cleared automatically when you push a new screen.
|
||||
|
||||
```jsonl
|
||||
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
|
||||
{"type":"click","choice":"c","text":"Option C - Complex Grid","timestamp":1706000108}
|
||||
{"type":"click","choice":"b","text":"Option B - Hybrid","timestamp":1706000115}
|
||||
```
|
||||
|
||||
The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about.
|
||||
|
||||
If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser — use only their terminal text.
|
||||
|
||||
## Design Tips
|
||||
|
||||
- **Scale fidelity to the question** — wireframes for layout, polish for polish questions
|
||||
- **Explain the question on each page** — "Which layout feels more professional?" not just "Pick one"
|
||||
- **Iterate before advancing** — if feedback changes current screen, write a new version
|
||||
- **2-4 options max** per screen
|
||||
- **Use real content when it matters** — for a photography portfolio, use actual images (Unsplash). Placeholder content obscures design issues.
|
||||
- **Keep mockups simple** — focus on layout and structure, not pixel-perfect design
|
||||
|
||||
## File Naming
|
||||
|
||||
- Use semantic names: `platform.html`, `visual-style.html`, `layout.html`
|
||||
- Never reuse filenames — each screen must be a new file
|
||||
- For iterations: append version suffix like `layout-v2.html`, `layout-v3.html`
|
||||
- Server serves newest file by modification time
|
||||
|
||||
## Cleaning Up
|
||||
|
||||
```bash
|
||||
scripts/stop-server.sh $SESSION_DIR
|
||||
```
|
||||
|
||||
If the session used `--project-dir`, mockup files persist in `.superpowers/brainstorm/` for later reference. Only `/tmp` sessions get deleted on stop.
|
||||
|
||||
## Reference
|
||||
|
||||
- Frame template (CSS reference): `scripts/frame-template.html`
|
||||
- Helper script (client-side): `scripts/helper.js`
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
name: Brainstorming Ideas Into Designs
|
||||
description: Interactive idea refinement using Socratic method to develop fully-formed designs
|
||||
when_to_use: When your human partner says "I've got an idea", "Let's make/build/create", "I want to implement/add", "What if we". When starting design for complex feature. Before writing implementation plans. When idea needs refinement and exploration. ACTIVATE THIS AUTOMATICALLY when your human partner describes a feature or project idea - don't wait for /brainstorm command.
|
||||
version: 2.0.0
|
||||
---
|
||||
|
||||
# Brainstorming Ideas Into Designs
|
||||
|
||||
## Overview
|
||||
|
||||
Transform rough ideas into fully-formed designs through structured questioning and alternative exploration.
|
||||
|
||||
**Core principle:** Ask questions to understand, explore alternatives, present design incrementally for validation.
|
||||
|
||||
**Announce at start:** "I'm using the Brainstorming skill to refine your idea into a design."
|
||||
|
||||
## The Process
|
||||
|
||||
### Phase 1: Understanding
|
||||
- Check current project state in working directory
|
||||
- Ask ONE question at a time to refine the idea
|
||||
- Prefer multiple choice when possible
|
||||
- Gather: Purpose, constraints, success criteria
|
||||
|
||||
### Phase 2: Exploration
|
||||
- Propose 2-3 different approaches (reference skills/coding/exploring-alternatives)
|
||||
- For each: Core architecture, trade-offs, complexity assessment
|
||||
- Ask your human partner which approach resonates
|
||||
|
||||
### Phase 3: Design Presentation
|
||||
- Present in 200-300 word sections
|
||||
- Cover: Architecture, components, data flow, error handling, testing
|
||||
- Ask after each section: "Does this look right so far?"
|
||||
|
||||
### Phase 4: Worktree Setup (for implementation)
|
||||
When design is approved and implementation will follow:
|
||||
- Announce: "I'm using the Using Git Worktrees skill to set up an isolated workspace."
|
||||
- Switch to skills/collaboration/using-git-worktrees
|
||||
- Follow that skill's process for directory selection, safety verification, and setup
|
||||
- Return here when worktree ready
|
||||
|
||||
### Phase 5: Planning Handoff
|
||||
Ask: "Ready to create the implementation plan?"
|
||||
|
||||
When your human partner confirms (any affirmative response):
|
||||
- Announce: "I'm using the Writing Plans skill to create the implementation plan."
|
||||
- Switch to skills/collaboration/writing-plans skill
|
||||
- Create detailed plan in the worktree
|
||||
|
||||
## Remember
|
||||
- One question per message during Phase 1
|
||||
- Apply YAGNI ruthlessly (reference skills/architecture/reducing-complexity)
|
||||
- Explore 2-3 alternatives before settling
|
||||
- Present incrementally, validate as you go
|
||||
- Announce skill usage at start
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
name: Executing Plans
|
||||
description: Execute detailed plans in batches with review checkpoints
|
||||
when_to_use: When have a complete implementation plan to execute. When implementing in separate session from planning. When your human partner points you to a plan file to implement.
|
||||
version: 2.0.0
|
||||
---
|
||||
|
||||
# Executing Plans
|
||||
|
||||
## Overview
|
||||
|
||||
Load plan, review critically, execute tasks in batches, report for review between batches.
|
||||
|
||||
**Core principle:** Batch execution with checkpoints for architect review.
|
||||
|
||||
**Announce at start:** "I'm using the Executing Plans skill to implement this plan."
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Load and Review Plan
|
||||
1. Read plan file
|
||||
2. Review critically - identify any questions or concerns about the plan
|
||||
3. If concerns: Raise them with your human partner before starting
|
||||
4. If no concerns: Create TodoWrite and proceed
|
||||
|
||||
### Step 2: Execute Batch
|
||||
**Default: First 3 tasks**
|
||||
|
||||
For each task:
|
||||
1. Mark as in_progress
|
||||
2. Follow each step exactly (plan has bite-sized steps)
|
||||
3. Run verifications as specified
|
||||
4. Mark as completed
|
||||
|
||||
### Step 3: Report
|
||||
When batch complete:
|
||||
- Show what was implemented
|
||||
- Show verification output
|
||||
- Say: "Ready for feedback."
|
||||
|
||||
### Step 4: Continue
|
||||
Based on feedback:
|
||||
- Apply changes if needed
|
||||
- Execute next batch
|
||||
- Repeat until complete
|
||||
|
||||
### Step 5: Complete Development
|
||||
|
||||
After all tasks complete and verified:
|
||||
- Announce: "I'm using the Finishing a Development Branch skill to complete this work."
|
||||
- Switch to skills/collaboration/finishing-a-development-branch
|
||||
- Follow that skill to verify tests, present options, execute choice
|
||||
|
||||
## Remember
|
||||
- Review plan critically first
|
||||
- Follow plan steps exactly
|
||||
- Don't skip verifications
|
||||
- Reference skills when plan says to
|
||||
- Between batches: just report and wait
|
||||
@@ -1,202 +0,0 @@
|
||||
---
|
||||
name: Finishing a Development Branch
|
||||
description: Complete feature development with structured options for merge, PR, or cleanup
|
||||
when_to_use: After completing implementation. When all tests passing. At end of executing-plans or subagent-driven-development. When feature work is done.
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Finishing a Development Branch
|
||||
|
||||
## Overview
|
||||
|
||||
Guide completion of development work by presenting clear options and handling chosen workflow.
|
||||
|
||||
**Core principle:** Verify tests → Present options → Execute choice → Clean up.
|
||||
|
||||
**Announce at start:** "I'm using the Finishing a Development Branch skill to complete this work."
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Verify Tests
|
||||
|
||||
**Before presenting options, verify tests pass:**
|
||||
|
||||
```bash
|
||||
# Run project's test suite
|
||||
npm test / cargo test / pytest / go test ./...
|
||||
```
|
||||
|
||||
**If tests fail:**
|
||||
```
|
||||
Tests failing (<N> failures). Must fix before completing:
|
||||
|
||||
[Show failures]
|
||||
|
||||
Cannot proceed with merge/PR until tests pass.
|
||||
```
|
||||
|
||||
Stop. Don't proceed to Step 2.
|
||||
|
||||
**If tests pass:** Continue to Step 2.
|
||||
|
||||
### Step 2: Determine Base Branch
|
||||
|
||||
```bash
|
||||
# Try common base branches
|
||||
git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null
|
||||
```
|
||||
|
||||
Or ask: "This branch split from main - is that correct?"
|
||||
|
||||
### Step 3: Present Options
|
||||
|
||||
Present exactly these 4 options:
|
||||
|
||||
```
|
||||
Implementation complete. What would you like to do?
|
||||
|
||||
1. Merge back to <base-branch> locally
|
||||
2. Push and create a Pull Request
|
||||
3. Keep the branch as-is (I'll handle it later)
|
||||
4. Discard this work
|
||||
|
||||
Which option?
|
||||
```
|
||||
|
||||
**Don't add explanation** - keep options concise.
|
||||
|
||||
### Step 4: Execute Choice
|
||||
|
||||
#### Option 1: Merge Locally
|
||||
|
||||
```bash
|
||||
# Switch to base branch
|
||||
git checkout <base-branch>
|
||||
|
||||
# Pull latest
|
||||
git pull
|
||||
|
||||
# Merge feature branch
|
||||
git merge <feature-branch>
|
||||
|
||||
# Verify tests on merged result
|
||||
<test command>
|
||||
|
||||
# If tests pass
|
||||
git branch -d <feature-branch>
|
||||
```
|
||||
|
||||
Then: Cleanup worktree (Step 5)
|
||||
|
||||
#### Option 2: Push and Create PR
|
||||
|
||||
```bash
|
||||
# Push branch
|
||||
git push -u origin <feature-branch>
|
||||
|
||||
# Create PR
|
||||
gh pr create --title "<title>" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
<2-3 bullets of what changed>
|
||||
|
||||
## Test Plan
|
||||
- [ ] <verification steps>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
Then: Cleanup worktree (Step 5)
|
||||
|
||||
#### Option 3: Keep As-Is
|
||||
|
||||
Report: "Keeping branch <name>. Worktree preserved at <path>."
|
||||
|
||||
**Don't cleanup worktree.**
|
||||
|
||||
#### Option 4: Discard
|
||||
|
||||
**Confirm first:**
|
||||
```
|
||||
This will permanently delete:
|
||||
- Branch <name>
|
||||
- All commits: <commit-list>
|
||||
- Worktree at <path>
|
||||
|
||||
Type 'discard' to confirm.
|
||||
```
|
||||
|
||||
Wait for exact confirmation.
|
||||
|
||||
If confirmed:
|
||||
```bash
|
||||
git checkout <base-branch>
|
||||
git branch -D <feature-branch>
|
||||
```
|
||||
|
||||
Then: Cleanup worktree (Step 5)
|
||||
|
||||
### Step 5: Cleanup Worktree
|
||||
|
||||
**For Options 1, 2, 4:**
|
||||
|
||||
Check if in worktree:
|
||||
```bash
|
||||
git worktree list | grep $(git branch --show-current)
|
||||
```
|
||||
|
||||
If yes:
|
||||
```bash
|
||||
git worktree remove <worktree-path>
|
||||
```
|
||||
|
||||
**For Option 3:** Keep worktree.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Option | Merge | Push | Keep Worktree | Cleanup Branch |
|
||||
|--------|-------|------|---------------|----------------|
|
||||
| 1. Merge locally | ✓ | - | - | ✓ |
|
||||
| 2. Create PR | - | ✓ | ✓ | - |
|
||||
| 3. Keep as-is | - | - | ✓ | - |
|
||||
| 4. Discard | - | - | - | ✓ (force) |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Skipping test verification**
|
||||
- **Problem:** Merge broken code, create failing PR
|
||||
- **Fix:** Always verify tests before offering options
|
||||
|
||||
**Open-ended questions**
|
||||
- **Problem:** "What should I do next?" → ambiguous
|
||||
- **Fix:** Present exactly 4 structured options
|
||||
|
||||
**Automatic worktree cleanup**
|
||||
- **Problem:** Remove worktree when might need it (Option 2, 3)
|
||||
- **Fix:** Only cleanup for Options 1 and 4
|
||||
|
||||
**No confirmation for discard**
|
||||
- **Problem:** Accidentally delete work
|
||||
- **Fix:** Require typed "discard" confirmation
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Proceed with failing tests
|
||||
- Merge without verifying tests on result
|
||||
- Delete work without confirmation
|
||||
- Force-push without explicit request
|
||||
|
||||
**Always:**
|
||||
- Verify tests before offering options
|
||||
- Present exactly 4 options
|
||||
- Get typed confirmation for Option 4
|
||||
- Clean up worktree for Options 1 & 4 only
|
||||
|
||||
## Integration
|
||||
|
||||
**Called by:**
|
||||
- skills/collaboration/subagent-driven-development (Step 7)
|
||||
- skills/collaboration/executing-plans (Step 5)
|
||||
|
||||
**Pairs with:**
|
||||
- skills/collaboration/using-git-worktrees (created the worktree)
|
||||
@@ -1,329 +0,0 @@
|
||||
# Conversation Search Deployment Guide
|
||||
|
||||
Quick reference for deploying and maintaining the conversation indexing system.
|
||||
|
||||
## Initial Deployment
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/collaboration/remembering-conversations/tool
|
||||
|
||||
# 1. Install hook
|
||||
./install-hook
|
||||
|
||||
# 2. Index existing conversations (with parallel summarization)
|
||||
./index-conversations --cleanup --concurrency 8
|
||||
|
||||
# 3. Verify index health
|
||||
./index-conversations --verify
|
||||
|
||||
# 4. Test search
|
||||
./search-conversations "test query"
|
||||
```
|
||||
|
||||
**Expected results:**
|
||||
- Hook installed at `~/.claude/hooks/sessionEnd`
|
||||
- Summaries created for all conversations (50-120 words each)
|
||||
- Search returns relevant results in <1 second
|
||||
- No verification errors
|
||||
|
||||
**Performance tip:** Use `--concurrency 8` or `--concurrency 16` for 8-16x faster summarization on initial indexing. Hook uses concurrency=1 (safe for background).
|
||||
|
||||
## Ongoing Maintenance
|
||||
|
||||
### Automatic (No Action Required)
|
||||
|
||||
- Hook runs after every session ends
|
||||
- New conversations indexed in background (<30 sec per conversation)
|
||||
- Summaries generated automatically
|
||||
|
||||
### Weekly Health Check
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/collaboration/remembering-conversations/tool
|
||||
./index-conversations --verify
|
||||
```
|
||||
|
||||
If issues found:
|
||||
```bash
|
||||
./index-conversations --repair
|
||||
```
|
||||
|
||||
### After System Changes
|
||||
|
||||
| Change | Action |
|
||||
|--------|--------|
|
||||
| Moved conversation archive | Update paths in code, run `--rebuild` |
|
||||
| Updated CLAUDE.md | Run `--verify` to check for issues |
|
||||
| Changed database schema | Backup DB, run `--rebuild` |
|
||||
| Hook not running | Check executable: `chmod +x ~/.claude/hooks/sessionEnd` |
|
||||
|
||||
## Recovery Scenarios
|
||||
|
||||
| Issue | Diagnosis | Fix |
|
||||
|-------|-----------|-----|
|
||||
| **Missing summaries** | `--verify` shows "Missing summaries: N" | `--repair` regenerates missing summaries |
|
||||
| **Orphaned DB entries** | `--verify` shows "Orphaned entries: N" | `--repair` removes orphaned entries |
|
||||
| **Outdated indexes** | `--verify` shows "Outdated files: N" | `--repair` re-indexes modified files |
|
||||
| **Corrupted database** | Errors during search/verify | `--rebuild` (re-indexes everything, requires confirmation) |
|
||||
| **Hook not running** | No summaries for new conversations | See Troubleshooting below |
|
||||
| **Slow indexing** | Takes >30 sec per conversation | Check API key, network, Haiku fallback in logs |
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Check hook installed and executable
|
||||
ls -l ~/.claude/hooks/sessionEnd
|
||||
|
||||
# Check recent conversations
|
||||
ls -lt ~/.config/superpowers/conversation-archive/*/*.jsonl | head -5
|
||||
|
||||
# Check database size
|
||||
ls -lh ~/.config/superpowers/conversation-index/db.sqlite
|
||||
|
||||
# Full verification
|
||||
./index-conversations --verify
|
||||
```
|
||||
|
||||
### Expected Behavior Metrics
|
||||
|
||||
- **Hook execution:** Within seconds of session end
|
||||
- **Indexing speed:** <30 seconds per conversation
|
||||
- **Summary length:** 50-120 words
|
||||
- **Search latency:** <1 second
|
||||
- **Verification:** 0 errors when healthy
|
||||
|
||||
### Log Output
|
||||
|
||||
Normal indexing:
|
||||
```
|
||||
Initializing database...
|
||||
Loading embedding model...
|
||||
Processing project: my-project (3 conversations)
|
||||
Summary: 87 words
|
||||
Indexed conversation.jsonl: 5 exchanges
|
||||
✅ Indexing complete! Conversations: 3, Exchanges: 15
|
||||
```
|
||||
|
||||
Verification with issues:
|
||||
```
|
||||
Verifying conversation index...
|
||||
Verified 100 conversations.
|
||||
|
||||
=== Verification Results ===
|
||||
Missing summaries: 2
|
||||
Orphaned entries: 0
|
||||
Outdated files: 1
|
||||
Corrupted files: 0
|
||||
|
||||
Run with --repair to fix these issues.
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hook Not Running
|
||||
|
||||
**Symptoms:** New conversations not indexed automatically
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# 1. Check hook exists and is executable
|
||||
ls -l ~/.claude/hooks/sessionEnd
|
||||
# Should show: -rwxr-xr-x ... sessionEnd
|
||||
|
||||
# 2. Check $SESSION_ID is set during sessions
|
||||
echo $SESSION_ID
|
||||
# Should show: session ID when in active session
|
||||
|
||||
# 3. Check indexer exists
|
||||
ls -l ~/.claude/skills/collaboration/remembering-conversations/tool/index-conversations
|
||||
# Should show: -rwxr-xr-x ... index-conversations
|
||||
|
||||
# 4. Test hook manually
|
||||
SESSION_ID=test-$(date +%s) ~/.claude/hooks/sessionEnd
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Make hook executable
|
||||
chmod +x ~/.claude/hooks/sessionEnd
|
||||
|
||||
# Reinstall if needed
|
||||
./install-hook
|
||||
```
|
||||
|
||||
### Summaries Failing
|
||||
|
||||
**Symptoms:** Verify shows missing summaries, repair fails
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check API key
|
||||
echo $ANTHROPIC_API_KEY
|
||||
# Should show: sk-ant-...
|
||||
|
||||
# Try manual indexing with logging
|
||||
./index-conversations 2>&1 | tee index.log
|
||||
grep -i error index.log
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Set API key if missing
|
||||
export ANTHROPIC_API_KEY="your-key-here"
|
||||
|
||||
# Check for rate limits (wait and retry)
|
||||
sleep 60 && ./index-conversations --repair
|
||||
|
||||
# Fallback uses claude-3-haiku-20240307 (cheaper)
|
||||
# Check logs for: "Summary: N words" to confirm success
|
||||
```
|
||||
|
||||
### Search Not Finding Results
|
||||
|
||||
**Symptoms:** `./search-conversations "query"` returns no results
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# 1. Verify conversations indexed
|
||||
./index-conversations --verify
|
||||
|
||||
# 2. Check database exists and has data
|
||||
ls -lh ~/.config/superpowers/conversation-index/db.sqlite
|
||||
# Should be > 100KB if conversations indexed
|
||||
|
||||
# 3. Try text search (exact match)
|
||||
./search-conversations --text "exact phrase from conversation"
|
||||
|
||||
# 4. Check for corruption
|
||||
sqlite3 ~/.config/superpowers/conversation-index/db.sqlite "SELECT COUNT(*) FROM exchanges;"
|
||||
# Should show number > 0
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# If database missing or corrupt
|
||||
./index-conversations --rebuild
|
||||
|
||||
# If specific conversations missing
|
||||
./index-conversations --repair
|
||||
|
||||
# If still failing, check embedding model
|
||||
rm -rf ~/.cache/transformers # Force re-download
|
||||
./index-conversations
|
||||
```
|
||||
|
||||
### Database Corruption
|
||||
|
||||
**Symptoms:** Errors like "database disk image is malformed"
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# 1. Backup current database
|
||||
cp ~/.config/superpowers/conversation-index/db.sqlite ~/.config/superpowers/conversation-index/db.sqlite.backup
|
||||
|
||||
# 2. Rebuild from scratch
|
||||
./index-conversations --rebuild
|
||||
# Confirms with: "Are you sure? [yes/NO]:"
|
||||
# Type: yes
|
||||
|
||||
# 3. Verify rebuild
|
||||
./index-conversations --verify
|
||||
```
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Index all conversations
|
||||
./index-conversations
|
||||
|
||||
# Index specific session (called by hook)
|
||||
./index-conversations --session <session-id>
|
||||
|
||||
# Index only unprocessed conversations
|
||||
./index-conversations --cleanup
|
||||
|
||||
# Verify index health
|
||||
./index-conversations --verify
|
||||
|
||||
# Repair issues found by verify
|
||||
./index-conversations --repair
|
||||
|
||||
# Rebuild everything (with confirmation)
|
||||
./index-conversations --rebuild
|
||||
|
||||
# Search conversations (semantic)
|
||||
./search-conversations "query"
|
||||
|
||||
# Search conversations (text match)
|
||||
./search-conversations --text "exact phrase"
|
||||
|
||||
# Install/reinstall hook
|
||||
./install-hook
|
||||
```
|
||||
|
||||
## Subagent Workflow
|
||||
|
||||
**For searching conversations from within Claude Code sessions**, use the subagent pattern (see `skills/getting-started` for complete workflow).
|
||||
|
||||
**Template:** `tool/prompts/search-agent.md`
|
||||
|
||||
**Key requirements:**
|
||||
- Synthesis must be 200-1000 words (Summary section)
|
||||
- All sources must include: project, date, file path, status
|
||||
- No raw conversation excerpts (synthesize instead)
|
||||
- Follow-up via subagent (not direct file reads)
|
||||
|
||||
**Manual test checklist:**
|
||||
1. ✓ Dispatch subagent with search template
|
||||
2. ✓ Verify synthesis 200-1000 words
|
||||
3. ✓ Verify all sources have metadata (project, date, path, status)
|
||||
4. ✓ Ask follow-up → dispatch second subagent to dig deeper
|
||||
5. ✓ Confirm no raw conversations in main context
|
||||
|
||||
## Files and Directories
|
||||
|
||||
```
|
||||
~/.claude/
|
||||
├── hooks/
|
||||
│ └── sessionEnd # Hook that triggers indexing
|
||||
└── skills/collaboration/remembering-conversations/
|
||||
├── SKILL.md # Main documentation
|
||||
├── DEPLOYMENT.md # This file
|
||||
└── tool/
|
||||
├── index-conversations # Main indexer
|
||||
├── search-conversations # Search interface
|
||||
├── install-hook # Hook installer
|
||||
├── test-deployment.sh # End-to-end tests
|
||||
├── src/ # TypeScript source
|
||||
└── prompts/
|
||||
└── search-agent.md # Subagent template
|
||||
|
||||
~/.config/superpowers/
|
||||
├── conversation-archive/ # Archived conversations
|
||||
│ └── <project>/
|
||||
│ ├── <uuid>.jsonl # Conversation file
|
||||
│ └── <uuid>-summary.txt # AI summary (50-120 words)
|
||||
└── conversation-index/
|
||||
└── db.sqlite # SQLite database with embeddings
|
||||
```
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### Initial Setup
|
||||
- [ ] Hook installed: `./install-hook`
|
||||
- [ ] Existing conversations indexed: `./index-conversations`
|
||||
- [ ] Verification clean: `./index-conversations --verify`
|
||||
- [ ] Search working: `./search-conversations "test"`
|
||||
- [ ] Subagent template exists: `ls tool/prompts/search-agent.md`
|
||||
|
||||
### Ongoing
|
||||
- [ ] Weekly: Run `--verify` and `--repair` if needed
|
||||
- [ ] After system changes: Re-verify
|
||||
- [ ] Monitor: Check hook runs (summaries appear for new conversations)
|
||||
|
||||
### Testing
|
||||
- [ ] Run end-to-end tests: `./test-deployment.sh`
|
||||
- [ ] All 5 scenarios pass
|
||||
- [ ] Manual subagent test (see scenario 5 in test output)
|
||||
@@ -1,133 +0,0 @@
|
||||
# Managing Conversation Index
|
||||
|
||||
Index, archive, and maintain conversations for search.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Install auto-indexing hook:**
|
||||
```bash
|
||||
~/.claude/skills/collaboration/remembering-conversations/tool/install-hook
|
||||
```
|
||||
|
||||
**Index all conversations:**
|
||||
```bash
|
||||
~/.claude/skills/collaboration/remembering-conversations/tool/index-conversations
|
||||
```
|
||||
|
||||
**Process unindexed only:**
|
||||
```bash
|
||||
~/.claude/skills/collaboration/remembering-conversations/tool/index-conversations --cleanup
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic indexing** via sessionEnd hook (install once, forget)
|
||||
- **Semantic search** across all past conversations
|
||||
- **AI summaries** (Claude Haiku with Sonnet fallback)
|
||||
- **Recovery modes** (verify, repair, rebuild)
|
||||
- **Permanent archive** at `~/.config/superpowers/conversation-archive/`
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Hook (One-Time)
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/collaboration/remembering-conversations/tool
|
||||
./install-hook
|
||||
```
|
||||
|
||||
Handles existing hooks gracefully (merge or replace). Runs in background after each session.
|
||||
|
||||
### 2. Index Existing Conversations
|
||||
|
||||
```bash
|
||||
# Index everything
|
||||
./index-conversations
|
||||
|
||||
# Or just unindexed (faster, cheaper)
|
||||
./index-conversations --cleanup
|
||||
```
|
||||
|
||||
## Index Modes
|
||||
|
||||
```bash
|
||||
# Index all (first run or full rebuild)
|
||||
./index-conversations
|
||||
|
||||
# Index specific session (used by hook)
|
||||
./index-conversations --session <uuid>
|
||||
|
||||
# Process only unindexed (missing summaries)
|
||||
./index-conversations --cleanup
|
||||
|
||||
# Check index health
|
||||
./index-conversations --verify
|
||||
|
||||
# Fix detected issues
|
||||
./index-conversations --repair
|
||||
|
||||
# Nuclear option (deletes DB, re-indexes everything)
|
||||
./index-conversations --rebuild
|
||||
```
|
||||
|
||||
## Recovery Scenarios
|
||||
|
||||
| Situation | Command |
|
||||
|-----------|---------|
|
||||
| Missed conversations | `--cleanup` |
|
||||
| Hook didn't run | `--cleanup` |
|
||||
| Updated conversation | `--verify` then `--repair` |
|
||||
| Corrupted database | `--rebuild` |
|
||||
| Index health check | `--verify` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Hook not running:**
|
||||
- Check: `ls -l ~/.claude/hooks/sessionEnd` (should be executable)
|
||||
- Test: `SESSION_ID=test-$(date +%s) ~/.claude/hooks/sessionEnd`
|
||||
- Re-install: `./install-hook`
|
||||
|
||||
**Summaries failing:**
|
||||
- Check API key: `echo $ANTHROPIC_API_KEY`
|
||||
- Check logs in ~/.config/superpowers/conversation-index/
|
||||
- Try manual: `./index-conversations --session <uuid>`
|
||||
|
||||
**Search not finding results:**
|
||||
- Verify indexed: `./index-conversations --verify`
|
||||
- Try text search: `./search-conversations --text "exact phrase"`
|
||||
- Rebuild if needed: `./index-conversations --rebuild`
|
||||
|
||||
## Excluding Projects
|
||||
|
||||
To exclude specific projects from indexing (e.g., meta-conversations), create:
|
||||
|
||||
`~/.config/superpowers/conversation-index/exclude.txt`
|
||||
```
|
||||
# One project name per line
|
||||
# Lines starting with # are comments
|
||||
-Users-yourname-Documents-some-project
|
||||
```
|
||||
|
||||
Or set env variable:
|
||||
```bash
|
||||
export CONVERSATION_SEARCH_EXCLUDE_PROJECTS="project1,project2"
|
||||
```
|
||||
|
||||
## Storage
|
||||
|
||||
- **Archive:** `~/.config/superpowers/conversation-archive/<project>/<uuid>.jsonl`
|
||||
- **Summaries:** `~/.config/superpowers/conversation-archive/<project>/<uuid>-summary.txt`
|
||||
- **Database:** `~/.config/superpowers/conversation-index/db.sqlite`
|
||||
- **Exclusions:** `~/.config/superpowers/conversation-index/exclude.txt` (optional)
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Embeddings:** @xenova/transformers (all-MiniLM-L6-v2, 384 dimensions, local/free)
|
||||
- **Vector search:** sqlite-vec (local/free)
|
||||
- **Summaries:** Claude Haiku with Sonnet fallback (~$0.01-0.02/conversation)
|
||||
- **Parser:** Handles multi-message exchanges and sidechains
|
||||
|
||||
## See Also
|
||||
|
||||
- **Searching:** See SKILL.md for search modes (vector, text, time filtering)
|
||||
- **Deployment:** See DEPLOYMENT.md for production runbook
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
name: Remembering Conversations
|
||||
description: Search previous Claude Code conversations for facts, patterns, decisions, and context using semantic or text search
|
||||
when_to_use: When your human partner mentions "we discussed this before". When debugging similar issues. When looking for architectural decisions or code patterns from past work. Before reinventing solutions. When you need to find a specific git SHA or error message.
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Remembering Conversations
|
||||
|
||||
Search archived conversations using semantic similarity or exact text matching.
|
||||
|
||||
**Core principle:** Search before reinventing.
|
||||
|
||||
**Announce:** "I'm searching previous conversations for [topic]."
|
||||
|
||||
**Setup:** See INDEXING.md
|
||||
|
||||
## When to Use
|
||||
|
||||
**Search when:**
|
||||
- Your human partner mentions "we discussed this before"
|
||||
- Debugging similar issues
|
||||
- Looking for architectural decisions or patterns
|
||||
- Before implementing something familiar
|
||||
|
||||
**Don't search when:**
|
||||
- Info in current conversation
|
||||
- Question about current codebase (use Grep/Read)
|
||||
|
||||
## In-Session Use
|
||||
|
||||
**Always use subagents** (50-100x context savings). See skills/getting-started for workflow.
|
||||
|
||||
**Manual/CLI use:** Direct search (below) for humans outside Claude Code sessions.
|
||||
|
||||
## Direct Search (Manual/CLI)
|
||||
|
||||
**Tool:** `${CLAUDE_PLUGIN_ROOT}/skills/collaboration/remembering-conversations/tool/search-conversations`
|
||||
|
||||
**Modes:**
|
||||
```bash
|
||||
search-conversations "query" # Vector similarity (default)
|
||||
search-conversations --text "exact" # Exact string match
|
||||
search-conversations --both "query" # Both modes
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
```bash
|
||||
--after YYYY-MM-DD # Filter by date
|
||||
--before YYYY-MM-DD # Filter by date
|
||||
--limit N # Max results (default: 10)
|
||||
--help # Full usage
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Semantic search
|
||||
search-conversations "React Router authentication errors"
|
||||
|
||||
# Find git SHA
|
||||
search-conversations --text "a1b2c3d4"
|
||||
|
||||
# Time range
|
||||
search-conversations --after 2025-09-01 "refactoring"
|
||||
```
|
||||
|
||||
Returns: project, date, conversation summary, matched exchange, similarity %, file path.
|
||||
|
||||
**For details:** Run `search-conversations --help`
|
||||
@@ -1,8 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# Local data (database and archives are at ~/.clank/, not in repo)
|
||||
*.sqlite*
|
||||
.cache/
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Auto-index conversation after session ends
|
||||
# Copy to ~/.claude/hooks/sessionEnd to enable
|
||||
|
||||
INDEXER="$HOME/.claude/skills/collaboration/remembering-conversations/tool/index-conversations"
|
||||
|
||||
if [ -n "$SESSION_ID" ] && [ -x "$INDEXER" ]; then
|
||||
# Run in background, suppress output
|
||||
"$INDEXER" --session "$SESSION_ID" > /dev/null 2>&1 &
|
||||
fi
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
SCRIPT_DIR="$(pwd)"
|
||||
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
cat <<'EOF'
|
||||
index-conversations - Index and manage conversation archives
|
||||
|
||||
USAGE:
|
||||
index-conversations [COMMAND] [OPTIONS]
|
||||
|
||||
COMMANDS:
|
||||
(default) Index all conversations
|
||||
--cleanup Process only unindexed conversations (fast, cheap)
|
||||
--session ID Index specific session (used by hook)
|
||||
--verify Check index health
|
||||
--repair Fix detected issues
|
||||
--rebuild Delete DB and re-index everything (requires confirmation)
|
||||
|
||||
OPTIONS:
|
||||
--concurrency N Parallel summarization (1-16, default: 1)
|
||||
-c N Short form of --concurrency
|
||||
--no-summaries Skip AI summary generation (free, but no summaries in results)
|
||||
--help, -h Show this help
|
||||
|
||||
EXAMPLES:
|
||||
# Index all unprocessed (recommended for backfill)
|
||||
index-conversations --cleanup
|
||||
|
||||
# Index with 8 parallel summarizations (8x faster)
|
||||
index-conversations --cleanup --concurrency 8
|
||||
|
||||
# Index without AI summaries (free, fast)
|
||||
index-conversations --cleanup --no-summaries
|
||||
|
||||
# Check index health
|
||||
index-conversations --verify
|
||||
|
||||
# Fix any issues found
|
||||
index-conversations --repair
|
||||
|
||||
# Nuclear option (deletes everything, re-indexes)
|
||||
index-conversations --rebuild
|
||||
|
||||
WORKFLOW:
|
||||
1. Initial setup: index-conversations --cleanup
|
||||
2. Ongoing: Auto-indexed by sessionEnd hook
|
||||
3. Health check: index-conversations --verify (weekly)
|
||||
4. Recovery: index-conversations --repair (if issues found)
|
||||
|
||||
SEE ALSO:
|
||||
INDEXING.md - Setup and maintenance guide
|
||||
DEPLOYMENT.md - Production runbook
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
--session)
|
||||
npx tsx "$SCRIPT_DIR/src/index-cli.ts" index-session "$@"
|
||||
;;
|
||||
--cleanup)
|
||||
npx tsx "$SCRIPT_DIR/src/index-cli.ts" index-cleanup "$@"
|
||||
;;
|
||||
--verify)
|
||||
npx tsx "$SCRIPT_DIR/src/index-cli.ts" verify "$@"
|
||||
;;
|
||||
--repair)
|
||||
npx tsx "$SCRIPT_DIR/src/index-cli.ts" repair "$@"
|
||||
;;
|
||||
--rebuild)
|
||||
echo "⚠️ This will DELETE the entire database and re-index everything."
|
||||
read -p "Are you sure? [yes/NO]: " confirm
|
||||
if [ "$confirm" = "yes" ]; then
|
||||
npx tsx "$SCRIPT_DIR/src/index-cli.ts" rebuild "$@"
|
||||
else
|
||||
echo "Cancelled"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
npx tsx "$SCRIPT_DIR/src/index-cli.ts" index-all "$@"
|
||||
;;
|
||||
esac
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Install sessionEnd hook with merge support
|
||||
|
||||
HOOK_DIR="$HOME/.claude/hooks"
|
||||
HOOK_FILE="$HOOK_DIR/sessionEnd"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SOURCE_HOOK="$SCRIPT_DIR/hooks/sessionEnd"
|
||||
|
||||
echo "Installing conversation indexing hook..."
|
||||
|
||||
# Create hooks directory
|
||||
mkdir -p "$HOOK_DIR"
|
||||
|
||||
# Handle existing hook
|
||||
if [ -f "$HOOK_FILE" ]; then
|
||||
echo "⚠️ Existing sessionEnd hook found"
|
||||
|
||||
# Check if our indexer is already installed
|
||||
if grep -q "remembering-conversations.*index-conversations" "$HOOK_FILE"; then
|
||||
echo "✓ Indexer already installed in existing hook"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create backup
|
||||
BACKUP="$HOOK_FILE.backup.$(date +%s)"
|
||||
cp "$HOOK_FILE" "$BACKUP"
|
||||
echo "Created backup: $BACKUP"
|
||||
|
||||
# Offer merge or replace
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " (m) Merge - Add indexer to existing hook"
|
||||
echo " (r) Replace - Overwrite with our hook"
|
||||
echo " (c) Cancel - Exit without changes"
|
||||
echo ""
|
||||
read -p "Choose [m/r/c]: " choice
|
||||
|
||||
case "$choice" in
|
||||
m|M)
|
||||
# Append our indexer
|
||||
cat >> "$HOOK_FILE" <<'EOF'
|
||||
|
||||
# Auto-index conversations (remembering-conversations skill)
|
||||
INDEXER="$HOME/.claude/skills/collaboration/remembering-conversations/tool/index-conversations"
|
||||
if [ -n "$SESSION_ID" ] && [ -x "$INDEXER" ]; then
|
||||
"$INDEXER" --session "$SESSION_ID" > /dev/null 2>&1 &
|
||||
fi
|
||||
EOF
|
||||
echo "✓ Merged indexer into existing hook"
|
||||
;;
|
||||
r|R)
|
||||
cp "$SOURCE_HOOK" "$HOOK_FILE"
|
||||
chmod +x "$HOOK_FILE"
|
||||
echo "✓ Replaced hook with our version"
|
||||
;;
|
||||
c|C)
|
||||
echo "Installation cancelled"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice. Exiting."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# No existing hook, install fresh
|
||||
cp "$SOURCE_HOOK" "$HOOK_FILE"
|
||||
chmod +x "$HOOK_FILE"
|
||||
echo "✓ Installed sessionEnd hook"
|
||||
fi
|
||||
|
||||
# Verify executable
|
||||
if [ ! -x "$HOOK_FILE" ]; then
|
||||
chmod +x "$HOOK_FILE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Hook installed successfully!"
|
||||
echo "Location: $HOOK_FILE"
|
||||
echo ""
|
||||
echo "Test it:"
|
||||
echo " SESSION_ID=test-\$(date +%s) $HOOK_FILE"
|
||||
@@ -1,124 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migrate conversation archive and index from ~/.clank to ~/.config/superpowers
|
||||
#
|
||||
# IMPORTANT: This preserves all data. The old ~/.clank directory is not deleted,
|
||||
# allowing you to verify the migration before removing it manually.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Determine target directory
|
||||
SUPERPOWERS_DIR="${PERSONAL_SUPERPOWERS_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/superpowers}"
|
||||
|
||||
OLD_ARCHIVE="$HOME/.clank/conversation-archive"
|
||||
OLD_INDEX="$HOME/.clank/conversation-index"
|
||||
|
||||
NEW_ARCHIVE="${SUPERPOWERS_DIR}/conversation-archive"
|
||||
NEW_INDEX="${SUPERPOWERS_DIR}/conversation-index"
|
||||
|
||||
echo "Migration: ~/.clank → ${SUPERPOWERS_DIR}"
|
||||
echo ""
|
||||
|
||||
# Check if source exists
|
||||
if [[ ! -d "$HOME/.clank" ]]; then
|
||||
echo "✅ No ~/.clank directory found. Nothing to migrate."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if already migrated
|
||||
if [[ -d "$NEW_ARCHIVE" ]] || [[ -d "$NEW_INDEX" ]]; then
|
||||
echo "⚠️ Destination already exists:"
|
||||
[[ -d "$NEW_ARCHIVE" ]] && echo " - ${NEW_ARCHIVE}"
|
||||
[[ -d "$NEW_INDEX" ]] && echo " - ${NEW_INDEX}"
|
||||
echo ""
|
||||
echo "Migration appears to have already run."
|
||||
echo "To re-run migration, manually remove destination directories first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show what will be migrated
|
||||
echo "Source directories:"
|
||||
if [[ -d "$OLD_ARCHIVE" ]]; then
|
||||
archive_size=$(du -sh "$OLD_ARCHIVE" | cut -f1)
|
||||
archive_count=$(find "$OLD_ARCHIVE" -name "*.jsonl" | wc -l | tr -d ' ')
|
||||
echo " Archive: ${OLD_ARCHIVE} (${archive_count} conversations, ${archive_size})"
|
||||
else
|
||||
echo " Archive: Not found"
|
||||
fi
|
||||
|
||||
if [[ -d "$OLD_INDEX" ]]; then
|
||||
index_size=$(du -sh "$OLD_INDEX" | cut -f1)
|
||||
echo " Index: ${OLD_INDEX} (${index_size})"
|
||||
else
|
||||
echo " Index: Not found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Destination: ${SUPERPOWERS_DIR}"
|
||||
echo ""
|
||||
|
||||
# Confirm
|
||||
read -p "Proceed with migration? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Migration cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ensure destination base exists
|
||||
mkdir -p "${SUPERPOWERS_DIR}"
|
||||
|
||||
# Migrate archive
|
||||
if [[ -d "$OLD_ARCHIVE" ]]; then
|
||||
echo "Copying conversation archive..."
|
||||
cp -r "$OLD_ARCHIVE" "$NEW_ARCHIVE"
|
||||
echo " ✓ Archive migrated"
|
||||
fi
|
||||
|
||||
# Migrate index
|
||||
if [[ -d "$OLD_INDEX" ]]; then
|
||||
echo "Copying conversation index..."
|
||||
cp -r "$OLD_INDEX" "$NEW_INDEX"
|
||||
echo " ✓ Index migrated"
|
||||
fi
|
||||
|
||||
# Update database paths to point to new location
|
||||
if [[ -f "$NEW_INDEX/db.sqlite" ]]; then
|
||||
echo "Updating database paths..."
|
||||
sqlite3 "$NEW_INDEX/db.sqlite" "UPDATE exchanges SET archive_path = REPLACE(archive_path, '/.clank/', '/.config/superpowers/') WHERE archive_path LIKE '%/.clank/%';"
|
||||
echo " ✓ Database paths updated"
|
||||
fi
|
||||
|
||||
# Verify migration
|
||||
echo ""
|
||||
echo "Verifying migration..."
|
||||
|
||||
if [[ -d "$OLD_ARCHIVE" ]]; then
|
||||
old_count=$(find "$OLD_ARCHIVE" -name "*.jsonl" | wc -l | tr -d ' ')
|
||||
new_count=$(find "$NEW_ARCHIVE" -name "*.jsonl" | wc -l | tr -d ' ')
|
||||
|
||||
if [[ "$old_count" -eq "$new_count" ]]; then
|
||||
echo " ✓ All $new_count conversations migrated"
|
||||
else
|
||||
echo " ⚠️ Conversation count mismatch: old=$old_count, new=$new_count"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "$OLD_INDEX/db.sqlite" ]]; then
|
||||
old_size=$(stat -f%z "$OLD_INDEX/db.sqlite" 2>/dev/null || stat --format=%s "$OLD_INDEX/db.sqlite" 2>/dev/null)
|
||||
new_size=$(stat -f%z "$NEW_INDEX/db.sqlite" 2>/dev/null || stat --format=%s "$NEW_INDEX/db.sqlite" 2>/dev/null)
|
||||
echo " ✓ Database migrated (${new_size} bytes)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Migration complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Test search: ./search-conversations 'test query'"
|
||||
echo " 2. Verify results look correct"
|
||||
echo " 3. Once verified, manually remove old directory:"
|
||||
echo " rm -rf ~/.clank"
|
||||
echo ""
|
||||
echo "The old ~/.clank directory is preserved for safety."
|
||||
|
||||
exit 0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "conversation-search",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"index": "./index-conversations",
|
||||
"search": "./search-conversations",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.9",
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"sqlite-vec": "^0.1.7-alpha.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^24.7.0",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
# Conversation Search Agent
|
||||
|
||||
You are searching historical Claude Code conversations for relevant context.
|
||||
|
||||
**Your task:**
|
||||
1. Search conversations for: {TOPIC}
|
||||
2. Read the top 2-5 most relevant results
|
||||
3. Synthesize key findings (max 1000 words)
|
||||
4. Return synthesis + source pointers (so main agent can dig deeper)
|
||||
|
||||
## Search Query
|
||||
|
||||
{SEARCH_QUERY}
|
||||
|
||||
## What to Look For
|
||||
|
||||
{FOCUS_AREAS}
|
||||
|
||||
Example focus areas:
|
||||
- What was the problem or question?
|
||||
- What solution was chosen and why?
|
||||
- What alternatives were considered and rejected?
|
||||
- Any gotchas, edge cases, or lessons learned?
|
||||
- Relevant code patterns, APIs, or approaches used
|
||||
- Architectural decisions and rationale
|
||||
|
||||
## How to Search
|
||||
|
||||
Run:
|
||||
```bash
|
||||
~/.claude/skills/collaboration/remembering-conversations/tool/search-conversations "{SEARCH_QUERY}"
|
||||
```
|
||||
|
||||
This returns:
|
||||
- Project name and date
|
||||
- Conversation summary (AI-generated)
|
||||
- Matched exchange with similarity score
|
||||
- File path and line numbers
|
||||
|
||||
Read the full conversations for top 2-5 results to get complete context.
|
||||
|
||||
## Output Format
|
||||
|
||||
**Required structure:**
|
||||
|
||||
### Summary
|
||||
[Synthesize findings in 200-1000 words. Adapt structure to what you found:
|
||||
- Quick answer? 1-2 paragraphs.
|
||||
- Complex topic? Use sections (Context/Solution/Rationale/Lessons/Code).
|
||||
- Multiple approaches? Compare and contrast.
|
||||
- Historical evolution? Show progression chronologically.
|
||||
|
||||
Focus on actionable insights for the current task.]
|
||||
|
||||
### Sources
|
||||
[List ALL conversations examined, in order of relevance:]
|
||||
|
||||
**1. [project-name, YYYY-MM-DD]** - X% match
|
||||
Conversation summary: [One sentence - what was this conversation about?]
|
||||
File: ~/.clank/conversation-archive/.../uuid.jsonl:start-end
|
||||
Status: [Read in detail | Reviewed summary only | Skimmed]
|
||||
|
||||
**2. [project-name, YYYY-MM-DD]** - X% match
|
||||
Conversation summary: ...
|
||||
File: ...
|
||||
Status: ...
|
||||
|
||||
[Continue for all examined sources...]
|
||||
|
||||
### For Follow-Up
|
||||
|
||||
Main agent can:
|
||||
- Ask you to dig deeper into specific source (#1, #2, etc.)
|
||||
- Ask you to read adjacent exchanges in a conversation
|
||||
- Ask you to search with refined query
|
||||
- Read sources directly (discouraged - risks context bloat)
|
||||
|
||||
## Critical Rules
|
||||
|
||||
**DO:**
|
||||
- Search using the provided query
|
||||
- Read full conversations for top results
|
||||
- Synthesize into actionable insights (200-1000 words)
|
||||
- Include ALL sources with metadata (project, date, summary, file, status)
|
||||
- Focus on what will help the current task
|
||||
- Include specific details (function names, error messages, line numbers)
|
||||
|
||||
**DO NOT:**
|
||||
- Include raw conversation excerpts (synthesize instead)
|
||||
- Paste full file contents
|
||||
- Add meta-commentary ("I searched and found...")
|
||||
- Exceed 1000 words in Summary section
|
||||
- Return search results verbatim
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
### Summary
|
||||
|
||||
developer needed to handle authentication errors in React Router 7 data loaders
|
||||
without crashing the app. The solution uses RR7's errorElement + useRouteError()
|
||||
to catch 401s and redirect to login.
|
||||
|
||||
**Key implementation:**
|
||||
Protected route wrapper catches loader errors, checks error.status === 401.
|
||||
If 401, redirects to /login with return URL. Otherwise shows error boundary.
|
||||
|
||||
**Why this works:**
|
||||
Loaders can't use hooks (tried useNavigate, failed). Throwing redirect()
|
||||
bypasses error handling. Final approach lets errors bubble to errorElement
|
||||
where component context is available.
|
||||
|
||||
**Critical gotchas:**
|
||||
- Test with expired tokens, not just missing tokens
|
||||
- Error boundaries need unique keys per route or won't reset
|
||||
- Always include return URL in redirect
|
||||
- Loaders execute before components, no hook access
|
||||
|
||||
**Code pattern:**
|
||||
```typescript
|
||||
// In loader
|
||||
if (!response.ok) throw { status: response.status, message: 'Failed' };
|
||||
|
||||
// In ErrorBoundary
|
||||
const error = useRouteError();
|
||||
if (error.status === 401) navigate('/login?return=' + location.pathname);
|
||||
```
|
||||
|
||||
### Sources
|
||||
|
||||
**1. [react-router-7-starter, 2024-09-17]** - 92% match
|
||||
Conversation summary: Built authentication system with JWT, implemented protected routes
|
||||
File: ~/.clank/conversation-archive/react-router-7-starter/19df92b9.jsonl:145-289
|
||||
Status: Read in detail (multiple exchanges on error handling evolution)
|
||||
|
||||
**2. [react-router-docs-reading, 2024-09-10]** - 78% match
|
||||
Conversation summary: Read RR7 docs, discussed new loader patterns and errorElement
|
||||
File: ~/.clank/conversation-archive/react-router-docs-reading/a3c871f2.jsonl:56-98
|
||||
Status: Reviewed summary only (confirmed errorElement usage)
|
||||
|
||||
**3. [auth-debugging, 2024-09-18]** - 73% match
|
||||
Conversation summary: Fixed token expiration handling and error boundary reset issues
|
||||
File: ~/.clank/conversation-archive/react-router-7-starter/7b2e8d91.jsonl:201-345
|
||||
Status: Read in detail (discovered gotchas about keys and expired tokens)
|
||||
|
||||
### For Follow-Up
|
||||
|
||||
Main agent can ask me to:
|
||||
- Dig deeper into source #1 (full error handling evolution)
|
||||
- Read adjacent exchanges in #3 (more debugging context)
|
||||
- Search for "React Router error boundary patterns" more broadly
|
||||
```
|
||||
|
||||
This output:
|
||||
- Synthesis: ~350 words (actionable, specific)
|
||||
- Sources: Full metadata for 3 conversations
|
||||
- Enables iteration without context bloat
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Parse arguments
|
||||
MODE="vector"
|
||||
AFTER=""
|
||||
BEFORE=""
|
||||
LIMIT="10"
|
||||
QUERY=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--help|-h)
|
||||
cat <<'EOF'
|
||||
search-conversations - Search previous Claude Code conversations
|
||||
|
||||
USAGE:
|
||||
search-conversations [OPTIONS] <query>
|
||||
|
||||
MODES:
|
||||
(default) Vector similarity search (semantic)
|
||||
--text Exact string matching (for git SHAs, error codes)
|
||||
--both Combine vector + text search
|
||||
|
||||
OPTIONS:
|
||||
--after DATE Only conversations after YYYY-MM-DD
|
||||
--before DATE Only conversations before YYYY-MM-DD
|
||||
--limit N Max results (default: 10)
|
||||
--help, -h Show this help
|
||||
|
||||
EXAMPLES:
|
||||
# Semantic search
|
||||
search-conversations "React Router authentication errors"
|
||||
|
||||
# Find exact string (git SHA, error message)
|
||||
search-conversations --text "a1b2c3d4e5f6"
|
||||
|
||||
# Time filtering
|
||||
search-conversations --after 2025-09-01 "refactoring"
|
||||
search-conversations --before 2025-10-01 --limit 20 "bug fix"
|
||||
|
||||
# Combine modes
|
||||
search-conversations --both "React Router data loading"
|
||||
|
||||
OUTPUT FORMAT:
|
||||
For each result:
|
||||
- Project name and date
|
||||
- Conversation summary (AI-generated)
|
||||
- Matched exchange with similarity % (vector mode)
|
||||
- File path with line numbers
|
||||
|
||||
Example:
|
||||
1. [react-router-7-starter, 2025-09-17]
|
||||
Built authentication with JWT, implemented protected routes.
|
||||
|
||||
92% match: "How do I handle auth errors in loaders?"
|
||||
~/.clank/conversation-archive/.../uuid.jsonl:145-167
|
||||
|
||||
QUERY TIPS:
|
||||
- Use natural language: "How did we handle X?"
|
||||
- Be specific: "React Router data loading" not "routing"
|
||||
- Include context: "TypeScript type narrowing in guards"
|
||||
|
||||
SEE ALSO:
|
||||
skills/collaboration/remembering-conversations/INDEXING.md - Manage index
|
||||
skills/collaboration/remembering-conversations/SKILL.md - Usage guide
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
--text)
|
||||
MODE="text"
|
||||
shift
|
||||
;;
|
||||
--both)
|
||||
MODE="both"
|
||||
shift
|
||||
;;
|
||||
--after)
|
||||
AFTER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--before)
|
||||
BEFORE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--limit)
|
||||
LIMIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
QUERY="$QUERY $1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
QUERY=$(echo "$QUERY" | sed 's/^ *//')
|
||||
|
||||
if [ -z "$QUERY" ]; then
|
||||
echo "Usage: search-conversations [options] <query>"
|
||||
echo "Try: search-conversations --help"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npx tsx src/search-cli.ts "$QUERY" "$MODE" "$LIMIT" "$AFTER" "$BEFORE"
|
||||
@@ -1,112 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { initDatabase, migrateSchema, insertExchange } from './db.js';
|
||||
import { ConversationExchange } from './types.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
describe('database migration', () => {
|
||||
const testDir = path.join(os.tmpdir(), 'db-migration-test-' + Date.now());
|
||||
const dbPath = path.join(testDir, 'test.db');
|
||||
|
||||
beforeEach(() => {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
process.env.TEST_DB_PATH = dbPath;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.TEST_DB_PATH;
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('adds last_indexed column to existing database', () => {
|
||||
// Create a database with old schema (no last_indexed)
|
||||
const db = new Database(dbPath);
|
||||
db.exec(`
|
||||
CREATE TABLE exchanges (
|
||||
id TEXT PRIMARY KEY,
|
||||
project TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
user_message TEXT NOT NULL,
|
||||
assistant_message TEXT NOT NULL,
|
||||
archive_path TEXT NOT NULL,
|
||||
line_start INTEGER NOT NULL,
|
||||
line_end INTEGER NOT NULL,
|
||||
embedding BLOB
|
||||
)
|
||||
`);
|
||||
|
||||
// Verify column doesn't exist
|
||||
const columnsBefore = db.prepare(`PRAGMA table_info(exchanges)`).all();
|
||||
const hasLastIndexedBefore = columnsBefore.some((col: any) => col.name === 'last_indexed');
|
||||
expect(hasLastIndexedBefore).toBe(false);
|
||||
|
||||
db.close();
|
||||
|
||||
// Run migration
|
||||
const migratedDb = initDatabase();
|
||||
|
||||
// Verify column now exists
|
||||
const columnsAfter = migratedDb.prepare(`PRAGMA table_info(exchanges)`).all();
|
||||
const hasLastIndexedAfter = columnsAfter.some((col: any) => col.name === 'last_indexed');
|
||||
expect(hasLastIndexedAfter).toBe(true);
|
||||
|
||||
migratedDb.close();
|
||||
});
|
||||
|
||||
it('handles existing last_indexed column gracefully', () => {
|
||||
// Create database with migration already applied
|
||||
const db = initDatabase();
|
||||
|
||||
// Run migration again - should not error
|
||||
expect(() => migrateSchema(db)).not.toThrow();
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertExchange with last_indexed', () => {
|
||||
const testDir = path.join(os.tmpdir(), 'insert-test-' + Date.now());
|
||||
const dbPath = path.join(testDir, 'test.db');
|
||||
|
||||
beforeEach(() => {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
process.env.TEST_DB_PATH = dbPath;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.TEST_DB_PATH;
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('sets last_indexed timestamp when inserting exchange', () => {
|
||||
const db = initDatabase();
|
||||
|
||||
const exchange: ConversationExchange = {
|
||||
id: 'test-id-1',
|
||||
project: 'test-project',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
userMessage: 'Hello',
|
||||
assistantMessage: 'Hi there!',
|
||||
archivePath: '/test/path.jsonl',
|
||||
lineStart: 1,
|
||||
lineEnd: 2
|
||||
};
|
||||
|
||||
const beforeInsert = Date.now();
|
||||
// Create proper 384-dimensional embedding
|
||||
const embedding = new Array(384).fill(0.1);
|
||||
insertExchange(db, exchange, embedding);
|
||||
const afterInsert = Date.now();
|
||||
|
||||
// Query the exchange
|
||||
const row = db.prepare(`SELECT last_indexed FROM exchanges WHERE id = ?`).get('test-id-1') as any;
|
||||
|
||||
expect(row.last_indexed).toBeDefined();
|
||||
expect(row.last_indexed).toBeGreaterThanOrEqual(beforeInsert);
|
||||
expect(row.last_indexed).toBeLessThanOrEqual(afterInsert);
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { ConversationExchange } from './types.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import * as sqliteVec from 'sqlite-vec';
|
||||
import { getDbPath } from './paths.js';
|
||||
|
||||
export function migrateSchema(db: Database.Database): void {
|
||||
const hasColumn = db.prepare(`
|
||||
SELECT COUNT(*) as count FROM pragma_table_info('exchanges')
|
||||
WHERE name='last_indexed'
|
||||
`).get() as { count: number };
|
||||
|
||||
if (hasColumn.count === 0) {
|
||||
console.log('Migrating schema: adding last_indexed column...');
|
||||
db.prepare('ALTER TABLE exchanges ADD COLUMN last_indexed INTEGER').run();
|
||||
console.log('Migration complete.');
|
||||
}
|
||||
}
|
||||
|
||||
export function initDatabase(): Database.Database {
|
||||
const dbPath = getDbPath();
|
||||
|
||||
// Ensure directory exists
|
||||
const dbDir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Load sqlite-vec extension
|
||||
sqliteVec.load(db);
|
||||
|
||||
// Enable WAL mode for better concurrency
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// Create exchanges table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS exchanges (
|
||||
id TEXT PRIMARY KEY,
|
||||
project TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
user_message TEXT NOT NULL,
|
||||
assistant_message TEXT NOT NULL,
|
||||
archive_path TEXT NOT NULL,
|
||||
line_start INTEGER NOT NULL,
|
||||
line_end INTEGER NOT NULL,
|
||||
embedding BLOB
|
||||
)
|
||||
`);
|
||||
|
||||
// Create vector search index
|
||||
db.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS vec_exchanges USING vec0(
|
||||
id TEXT PRIMARY KEY,
|
||||
embedding FLOAT[384]
|
||||
)
|
||||
`);
|
||||
|
||||
// Create index on timestamp for sorting
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_timestamp ON exchanges(timestamp DESC)
|
||||
`);
|
||||
|
||||
// Run migrations
|
||||
migrateSchema(db);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
export function insertExchange(
|
||||
db: Database.Database,
|
||||
exchange: ConversationExchange,
|
||||
embedding: number[]
|
||||
): void {
|
||||
const now = Date.now();
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO exchanges
|
||||
(id, project, timestamp, user_message, assistant_message, archive_path, line_start, line_end, last_indexed)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
exchange.id,
|
||||
exchange.project,
|
||||
exchange.timestamp,
|
||||
exchange.userMessage,
|
||||
exchange.assistantMessage,
|
||||
exchange.archivePath,
|
||||
exchange.lineStart,
|
||||
exchange.lineEnd,
|
||||
now
|
||||
);
|
||||
|
||||
// Insert into vector table (delete first since virtual tables don't support REPLACE)
|
||||
const delStmt = db.prepare(`DELETE FROM vec_exchanges WHERE id = ?`);
|
||||
delStmt.run(exchange.id);
|
||||
|
||||
const vecStmt = db.prepare(`
|
||||
INSERT INTO vec_exchanges (id, embedding)
|
||||
VALUES (?, ?)
|
||||
`);
|
||||
|
||||
vecStmt.run(exchange.id, Buffer.from(new Float32Array(embedding).buffer));
|
||||
}
|
||||
|
||||
export function getAllExchanges(db: Database.Database): Array<{ id: string; archivePath: string }> {
|
||||
const stmt = db.prepare(`SELECT id, archive_path as archivePath FROM exchanges`);
|
||||
return stmt.all() as Array<{ id: string; archivePath: string }>;
|
||||
}
|
||||
|
||||
export function getFileLastIndexed(db: Database.Database, archivePath: string): number | null {
|
||||
const stmt = db.prepare(`
|
||||
SELECT MAX(last_indexed) as lastIndexed
|
||||
FROM exchanges
|
||||
WHERE archive_path = ?
|
||||
`);
|
||||
const row = stmt.get(archivePath) as { lastIndexed: number | null };
|
||||
return row.lastIndexed;
|
||||
}
|
||||
|
||||
export function deleteExchange(db: Database.Database, id: string): void {
|
||||
// Delete from vector table
|
||||
db.prepare(`DELETE FROM vec_exchanges WHERE id = ?`).run(id);
|
||||
|
||||
// Delete from main table
|
||||
db.prepare(`DELETE FROM exchanges WHERE id = ?`).run(id);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { pipeline, Pipeline } from '@xenova/transformers';
|
||||
|
||||
let embeddingPipeline: Pipeline | null = null;
|
||||
|
||||
export async function initEmbeddings(): Promise<void> {
|
||||
if (!embeddingPipeline) {
|
||||
console.log('Loading embedding model (first run may take time)...');
|
||||
embeddingPipeline = await pipeline(
|
||||
'feature-extraction',
|
||||
'Xenova/all-MiniLM-L6-v2'
|
||||
);
|
||||
console.log('Embedding model loaded');
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateEmbedding(text: string): Promise<number[]> {
|
||||
if (!embeddingPipeline) {
|
||||
await initEmbeddings();
|
||||
}
|
||||
|
||||
// Truncate text to avoid token limits (512 tokens max for this model)
|
||||
const truncated = text.substring(0, 2000);
|
||||
|
||||
const output = await embeddingPipeline!(truncated, {
|
||||
pooling: 'mean',
|
||||
normalize: true
|
||||
});
|
||||
|
||||
return Array.from(output.data);
|
||||
}
|
||||
|
||||
export async function generateExchangeEmbedding(
|
||||
userMessage: string,
|
||||
assistantMessage: string
|
||||
): Promise<number[]> {
|
||||
// Combine user question and assistant answer for better searchability
|
||||
const combined = `User: ${userMessage}\n\nAssistant: ${assistantMessage}`;
|
||||
return generateEmbedding(combined);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { verifyIndex, repairIndex } from './verify.js';
|
||||
import { indexSession, indexUnprocessed, indexConversations } from './indexer.js';
|
||||
import { initDatabase } from './db.js';
|
||||
import { getDbPath, getArchiveDir } from './paths.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const command = process.argv[2];
|
||||
|
||||
// Parse --concurrency flag from remaining args
|
||||
function getConcurrency(): number {
|
||||
const concurrencyIndex = process.argv.findIndex(arg => arg === '--concurrency' || arg === '-c');
|
||||
if (concurrencyIndex !== -1 && process.argv[concurrencyIndex + 1]) {
|
||||
const value = parseInt(process.argv[concurrencyIndex + 1], 10);
|
||||
if (value >= 1 && value <= 16) return value;
|
||||
}
|
||||
return 1; // default
|
||||
}
|
||||
|
||||
// Parse --no-summaries flag
|
||||
function getNoSummaries(): boolean {
|
||||
return process.argv.includes('--no-summaries');
|
||||
}
|
||||
|
||||
const concurrency = getConcurrency();
|
||||
const noSummaries = getNoSummaries();
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
switch (command) {
|
||||
case 'index-session':
|
||||
const sessionId = process.argv[3];
|
||||
if (!sessionId) {
|
||||
console.error('Usage: index-cli index-session <session-id>');
|
||||
process.exit(1);
|
||||
}
|
||||
await indexSession(sessionId, concurrency, noSummaries);
|
||||
break;
|
||||
|
||||
case 'index-cleanup':
|
||||
await indexUnprocessed(concurrency, noSummaries);
|
||||
break;
|
||||
|
||||
case 'verify':
|
||||
console.log('Verifying conversation index...');
|
||||
const issues = await verifyIndex();
|
||||
|
||||
console.log('\n=== Verification Results ===');
|
||||
console.log(`Missing summaries: ${issues.missing.length}`);
|
||||
console.log(`Orphaned entries: ${issues.orphaned.length}`);
|
||||
console.log(`Outdated files: ${issues.outdated.length}`);
|
||||
console.log(`Corrupted files: ${issues.corrupted.length}`);
|
||||
|
||||
if (issues.missing.length > 0) {
|
||||
console.log('\nMissing summaries:');
|
||||
issues.missing.forEach(m => console.log(` ${m.path}`));
|
||||
}
|
||||
|
||||
if (issues.missing.length + issues.orphaned.length + issues.outdated.length + issues.corrupted.length > 0) {
|
||||
console.log('\nRun with --repair to fix these issues.');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\n✅ Index is healthy!');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'repair':
|
||||
console.log('Verifying conversation index...');
|
||||
const repairIssues = await verifyIndex();
|
||||
|
||||
if (repairIssues.missing.length + repairIssues.orphaned.length + repairIssues.outdated.length > 0) {
|
||||
await repairIndex(repairIssues);
|
||||
} else {
|
||||
console.log('✅ No issues to repair!');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rebuild':
|
||||
console.log('Rebuilding entire index...');
|
||||
|
||||
// Delete database
|
||||
const dbPath = getDbPath();
|
||||
if (fs.existsSync(dbPath)) {
|
||||
fs.unlinkSync(dbPath);
|
||||
console.log('Deleted existing database');
|
||||
}
|
||||
|
||||
// Delete all summary files
|
||||
const archiveDir = getArchiveDir();
|
||||
if (fs.existsSync(archiveDir)) {
|
||||
const projects = fs.readdirSync(archiveDir);
|
||||
for (const project of projects) {
|
||||
const projectPath = path.join(archiveDir, project);
|
||||
if (!fs.statSync(projectPath).isDirectory()) continue;
|
||||
|
||||
const summaries = fs.readdirSync(projectPath).filter(f => f.endsWith('-summary.txt'));
|
||||
for (const summary of summaries) {
|
||||
fs.unlinkSync(path.join(projectPath, summary));
|
||||
}
|
||||
}
|
||||
console.log('Deleted all summary files');
|
||||
}
|
||||
|
||||
// Re-index everything
|
||||
console.log('Re-indexing all conversations...');
|
||||
await indexConversations(undefined, undefined, concurrency, noSummaries);
|
||||
break;
|
||||
|
||||
case 'index-all':
|
||||
default:
|
||||
await indexConversations(undefined, undefined, concurrency, noSummaries);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,374 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { initDatabase, insertExchange } from './db.js';
|
||||
import { parseConversation } from './parser.js';
|
||||
import { initEmbeddings, generateExchangeEmbedding } from './embeddings.js';
|
||||
import { summarizeConversation } from './summarizer.js';
|
||||
import { ConversationExchange } from './types.js';
|
||||
import { getArchiveDir, getExcludeConfigPath } from './paths.js';
|
||||
|
||||
// Set max output tokens for Claude SDK (used by summarizer)
|
||||
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '20000';
|
||||
|
||||
// Increase max listeners for concurrent API calls
|
||||
import { EventEmitter } from 'events';
|
||||
EventEmitter.defaultMaxListeners = 20;
|
||||
|
||||
// Allow overriding paths for testing
|
||||
function getProjectsDir(): string {
|
||||
return process.env.TEST_PROJECTS_DIR || path.join(os.homedir(), '.claude', 'projects');
|
||||
}
|
||||
|
||||
// Projects to exclude from indexing (configurable via env or config file)
|
||||
function getExcludedProjects(): string[] {
|
||||
// Check env variable first
|
||||
if (process.env.CONVERSATION_SEARCH_EXCLUDE_PROJECTS) {
|
||||
return process.env.CONVERSATION_SEARCH_EXCLUDE_PROJECTS.split(',').map(p => p.trim());
|
||||
}
|
||||
|
||||
// Check for config file
|
||||
const configPath = getExcludeConfigPath();
|
||||
if (fs.existsSync(configPath)) {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
return content.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('#'));
|
||||
}
|
||||
|
||||
// Default: no exclusions
|
||||
return [];
|
||||
}
|
||||
|
||||
// Process items in batches with limited concurrency
|
||||
async function processBatch<T, R>(
|
||||
items: T[],
|
||||
processor: (item: T) => Promise<R>,
|
||||
concurrency: number
|
||||
): Promise<R[]> {
|
||||
const results: R[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i += concurrency) {
|
||||
const batch = items.slice(i, i + concurrency);
|
||||
const batchResults = await Promise.all(batch.map(processor));
|
||||
results.push(...batchResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function indexConversations(
|
||||
limitToProject?: string,
|
||||
maxConversations?: number,
|
||||
concurrency: number = 1,
|
||||
noSummaries: boolean = false
|
||||
): Promise<void> {
|
||||
console.log('Initializing database...');
|
||||
const db = initDatabase();
|
||||
|
||||
console.log('Loading embedding model...');
|
||||
await initEmbeddings();
|
||||
|
||||
if (noSummaries) {
|
||||
console.log('⚠️ Running in no-summaries mode (skipping AI summaries)');
|
||||
}
|
||||
|
||||
console.log('Scanning for conversation files...');
|
||||
const PROJECTS_DIR = getProjectsDir();
|
||||
const ARCHIVE_DIR = getArchiveDir(); // Now uses paths.ts
|
||||
const projects = fs.readdirSync(PROJECTS_DIR);
|
||||
|
||||
let totalExchanges = 0;
|
||||
let conversationsProcessed = 0;
|
||||
|
||||
const excludedProjects = getExcludedProjects();
|
||||
|
||||
for (const project of projects) {
|
||||
// Skip excluded projects
|
||||
if (excludedProjects.includes(project)) {
|
||||
console.log(`\nSkipping excluded project: ${project}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if limiting to specific project
|
||||
if (limitToProject && project !== limitToProject) continue;
|
||||
const projectPath = path.join(PROJECTS_DIR, project);
|
||||
const stat = fs.statSync(projectPath);
|
||||
|
||||
if (!stat.isDirectory()) continue;
|
||||
|
||||
const files = fs.readdirSync(projectPath).filter(f => f.endsWith('.jsonl'));
|
||||
|
||||
if (files.length === 0) continue;
|
||||
|
||||
console.log(`\nProcessing project: ${project} (${files.length} conversations)`);
|
||||
if (concurrency > 1) console.log(` Concurrency: ${concurrency}`);
|
||||
|
||||
// Create archive directory for this project
|
||||
const projectArchive = path.join(ARCHIVE_DIR, project);
|
||||
fs.mkdirSync(projectArchive, { recursive: true });
|
||||
|
||||
// Prepare all conversations first
|
||||
type ConvToProcess = {
|
||||
file: string;
|
||||
sourcePath: string;
|
||||
archivePath: string;
|
||||
summaryPath: string;
|
||||
exchanges: ConversationExchange[];
|
||||
};
|
||||
|
||||
const toProcess: ConvToProcess[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(projectPath, file);
|
||||
const archivePath = path.join(projectArchive, file);
|
||||
|
||||
// Copy to archive
|
||||
if (!fs.existsSync(archivePath)) {
|
||||
fs.copyFileSync(sourcePath, archivePath);
|
||||
console.log(` Archived: ${file}`);
|
||||
}
|
||||
|
||||
// Parse conversation
|
||||
const exchanges = await parseConversation(sourcePath, project, archivePath);
|
||||
|
||||
if (exchanges.length === 0) {
|
||||
console.log(` Skipped ${file} (no exchanges)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
toProcess.push({
|
||||
file,
|
||||
sourcePath,
|
||||
archivePath,
|
||||
summaryPath: archivePath.replace('.jsonl', '-summary.txt'),
|
||||
exchanges
|
||||
});
|
||||
}
|
||||
|
||||
// Batch summarize conversations in parallel (unless --no-summaries)
|
||||
if (!noSummaries) {
|
||||
const needsSummary = toProcess.filter(c => !fs.existsSync(c.summaryPath));
|
||||
|
||||
if (needsSummary.length > 0) {
|
||||
console.log(` Generating ${needsSummary.length} summaries (concurrency: ${concurrency})...`);
|
||||
|
||||
await processBatch(needsSummary, async (conv) => {
|
||||
try {
|
||||
const summary = await summarizeConversation(conv.exchanges);
|
||||
fs.writeFileSync(conv.summaryPath, summary, 'utf-8');
|
||||
const wordCount = summary.split(/\s+/).length;
|
||||
console.log(` ✓ ${conv.file}: ${wordCount} words`);
|
||||
return summary;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${conv.file}: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}, concurrency);
|
||||
}
|
||||
} else {
|
||||
console.log(` Skipping ${toProcess.length} summaries (--no-summaries mode)`);
|
||||
}
|
||||
|
||||
// Now process embeddings and DB inserts (fast, sequential is fine)
|
||||
for (const conv of toProcess) {
|
||||
for (const exchange of conv.exchanges) {
|
||||
const embedding = await generateExchangeEmbedding(
|
||||
exchange.userMessage,
|
||||
exchange.assistantMessage
|
||||
);
|
||||
|
||||
insertExchange(db, exchange, embedding);
|
||||
}
|
||||
|
||||
totalExchanges += conv.exchanges.length;
|
||||
conversationsProcessed++;
|
||||
|
||||
// Check if we hit the limit
|
||||
if (maxConversations && conversationsProcessed >= maxConversations) {
|
||||
console.log(`\nReached limit of ${maxConversations} conversations`);
|
||||
db.close();
|
||||
console.log(`✅ Indexing complete! Conversations: ${conversationsProcessed}, Exchanges: ${totalExchanges}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log(`\n✅ Indexing complete! Conversations: ${conversationsProcessed}, Exchanges: ${totalExchanges}`);
|
||||
}
|
||||
|
||||
export async function indexSession(sessionId: string, concurrency: number = 1, noSummaries: boolean = false): Promise<void> {
|
||||
console.log(`Indexing session: ${sessionId}`);
|
||||
|
||||
// Find the conversation file for this session
|
||||
const PROJECTS_DIR = getProjectsDir();
|
||||
const ARCHIVE_DIR = getArchiveDir(); // Now uses paths.ts
|
||||
const projects = fs.readdirSync(PROJECTS_DIR);
|
||||
const excludedProjects = getExcludedProjects();
|
||||
let found = false;
|
||||
|
||||
for (const project of projects) {
|
||||
if (excludedProjects.includes(project)) continue;
|
||||
|
||||
const projectPath = path.join(PROJECTS_DIR, project);
|
||||
if (!fs.statSync(projectPath).isDirectory()) continue;
|
||||
|
||||
const files = fs.readdirSync(projectPath).filter(f => f.includes(sessionId) && f.endsWith('.jsonl'));
|
||||
|
||||
if (files.length > 0) {
|
||||
found = true;
|
||||
const file = files[0];
|
||||
const sourcePath = path.join(projectPath, file);
|
||||
|
||||
const db = initDatabase();
|
||||
await initEmbeddings();
|
||||
|
||||
const projectArchive = path.join(ARCHIVE_DIR, project);
|
||||
fs.mkdirSync(projectArchive, { recursive: true });
|
||||
|
||||
const archivePath = path.join(projectArchive, file);
|
||||
|
||||
// Archive
|
||||
if (!fs.existsSync(archivePath)) {
|
||||
fs.copyFileSync(sourcePath, archivePath);
|
||||
}
|
||||
|
||||
// Parse and summarize
|
||||
const exchanges = await parseConversation(sourcePath, project, archivePath);
|
||||
|
||||
if (exchanges.length > 0) {
|
||||
// Generate summary (unless --no-summaries)
|
||||
const summaryPath = archivePath.replace('.jsonl', '-summary.txt');
|
||||
if (!noSummaries && !fs.existsSync(summaryPath)) {
|
||||
const summary = await summarizeConversation(exchanges);
|
||||
fs.writeFileSync(summaryPath, summary, 'utf-8');
|
||||
console.log(`Summary: ${summary.split(/\s+/).length} words`);
|
||||
}
|
||||
|
||||
// Index
|
||||
for (const exchange of exchanges) {
|
||||
const embedding = await generateExchangeEmbedding(
|
||||
exchange.userMessage,
|
||||
exchange.assistantMessage
|
||||
);
|
||||
insertExchange(db, exchange, embedding);
|
||||
}
|
||||
|
||||
console.log(`✅ Indexed session ${sessionId}: ${exchanges.length} exchanges`);
|
||||
}
|
||||
|
||||
db.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
console.log(`Session ${sessionId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function indexUnprocessed(concurrency: number = 1, noSummaries: boolean = false): Promise<void> {
|
||||
console.log('Finding unprocessed conversations...');
|
||||
if (concurrency > 1) console.log(`Concurrency: ${concurrency}`);
|
||||
if (noSummaries) console.log('⚠️ Running in no-summaries mode (skipping AI summaries)');
|
||||
|
||||
const db = initDatabase();
|
||||
await initEmbeddings();
|
||||
|
||||
const PROJECTS_DIR = getProjectsDir();
|
||||
const ARCHIVE_DIR = getArchiveDir(); // Now uses paths.ts
|
||||
const projects = fs.readdirSync(PROJECTS_DIR);
|
||||
const excludedProjects = getExcludedProjects();
|
||||
|
||||
type UnprocessedConv = {
|
||||
project: string;
|
||||
file: string;
|
||||
sourcePath: string;
|
||||
archivePath: string;
|
||||
summaryPath: string;
|
||||
exchanges: ConversationExchange[];
|
||||
};
|
||||
|
||||
const unprocessed: UnprocessedConv[] = [];
|
||||
|
||||
// Collect all unprocessed conversations
|
||||
for (const project of projects) {
|
||||
if (excludedProjects.includes(project)) continue;
|
||||
|
||||
const projectPath = path.join(PROJECTS_DIR, project);
|
||||
if (!fs.statSync(projectPath).isDirectory()) continue;
|
||||
|
||||
const files = fs.readdirSync(projectPath).filter(f => f.endsWith('.jsonl'));
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(projectPath, file);
|
||||
const projectArchive = path.join(ARCHIVE_DIR, project);
|
||||
const archivePath = path.join(projectArchive, file);
|
||||
const summaryPath = archivePath.replace('.jsonl', '-summary.txt');
|
||||
|
||||
// Check if already indexed in database
|
||||
const alreadyIndexed = db.prepare('SELECT COUNT(*) as count FROM exchanges WHERE archive_path = ?')
|
||||
.get(archivePath) as { count: number };
|
||||
|
||||
if (alreadyIndexed.count > 0) continue;
|
||||
|
||||
fs.mkdirSync(projectArchive, { recursive: true });
|
||||
|
||||
// Archive if needed
|
||||
if (!fs.existsSync(archivePath)) {
|
||||
fs.copyFileSync(sourcePath, archivePath);
|
||||
}
|
||||
|
||||
// Parse and check
|
||||
const exchanges = await parseConversation(sourcePath, project, archivePath);
|
||||
if (exchanges.length === 0) continue;
|
||||
|
||||
unprocessed.push({ project, file, sourcePath, archivePath, summaryPath, exchanges });
|
||||
}
|
||||
}
|
||||
|
||||
if (unprocessed.length === 0) {
|
||||
console.log('✅ All conversations are already processed!');
|
||||
db.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${unprocessed.length} unprocessed conversations`);
|
||||
|
||||
// Batch process summaries (unless --no-summaries)
|
||||
if (!noSummaries) {
|
||||
const needsSummary = unprocessed.filter(c => !fs.existsSync(c.summaryPath));
|
||||
if (needsSummary.length > 0) {
|
||||
console.log(`Generating ${needsSummary.length} summaries (concurrency: ${concurrency})...\n`);
|
||||
|
||||
await processBatch(needsSummary, async (conv) => {
|
||||
try {
|
||||
const summary = await summarizeConversation(conv.exchanges);
|
||||
fs.writeFileSync(conv.summaryPath, summary, 'utf-8');
|
||||
const wordCount = summary.split(/\s+/).length;
|
||||
console.log(` ✓ ${conv.project}/${conv.file}: ${wordCount} words`);
|
||||
return summary;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${conv.project}/${conv.file}: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}, concurrency);
|
||||
}
|
||||
} else {
|
||||
console.log(`Skipping summaries for ${unprocessed.length} conversations (--no-summaries mode)\n`);
|
||||
}
|
||||
|
||||
// Now index embeddings
|
||||
console.log(`\nIndexing embeddings...`);
|
||||
for (const conv of unprocessed) {
|
||||
for (const exchange of conv.exchanges) {
|
||||
const embedding = await generateExchangeEmbedding(
|
||||
exchange.userMessage,
|
||||
exchange.assistantMessage
|
||||
);
|
||||
insertExchange(db, exchange, embedding);
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log(`\n✅ Processed ${unprocessed.length} conversations`);
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import readline from 'readline';
|
||||
import { ConversationExchange } from './types.js';
|
||||
import crypto from 'crypto';
|
||||
|
||||
interface JSONLMessage {
|
||||
type: string;
|
||||
message?: {
|
||||
role: 'user' | 'assistant';
|
||||
content: string | Array<{ type: string; text?: string }>;
|
||||
};
|
||||
timestamp?: string;
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
export async function parseConversation(
|
||||
filePath: string,
|
||||
projectName: string,
|
||||
archivePath: string
|
||||
): Promise<ConversationExchange[]> {
|
||||
const exchanges: ConversationExchange[] = [];
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
let lineNumber = 0;
|
||||
let currentExchange: {
|
||||
userMessage: string;
|
||||
userLine: number;
|
||||
assistantMessages: string[];
|
||||
lastAssistantLine: number;
|
||||
timestamp: string;
|
||||
} | null = null;
|
||||
|
||||
const finalizeExchange = () => {
|
||||
if (currentExchange && currentExchange.assistantMessages.length > 0) {
|
||||
const exchange: ConversationExchange = {
|
||||
id: crypto
|
||||
.createHash('md5')
|
||||
.update(`${archivePath}:${currentExchange.userLine}-${currentExchange.lastAssistantLine}`)
|
||||
.digest('hex'),
|
||||
project: projectName,
|
||||
timestamp: currentExchange.timestamp,
|
||||
userMessage: currentExchange.userMessage,
|
||||
assistantMessage: currentExchange.assistantMessages.join('\n\n'),
|
||||
archivePath,
|
||||
lineStart: currentExchange.userLine,
|
||||
lineEnd: currentExchange.lastAssistantLine
|
||||
};
|
||||
exchanges.push(exchange);
|
||||
}
|
||||
};
|
||||
|
||||
for await (const line of rl) {
|
||||
lineNumber++;
|
||||
|
||||
try {
|
||||
const parsed: JSONLMessage = JSON.parse(line);
|
||||
|
||||
// Skip non-message types
|
||||
if (parsed.type !== 'user' && parsed.type !== 'assistant') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parsed.message) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract text from message content
|
||||
let text = '';
|
||||
if (typeof parsed.message.content === 'string') {
|
||||
text = parsed.message.content;
|
||||
} else if (Array.isArray(parsed.message.content)) {
|
||||
text = parsed.message.content
|
||||
.filter(block => block.type === 'text' && block.text)
|
||||
.map(block => block.text)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
// Skip empty messages
|
||||
if (!text.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.message.role === 'user') {
|
||||
// Finalize previous exchange before starting new one
|
||||
finalizeExchange();
|
||||
|
||||
// Start new exchange
|
||||
currentExchange = {
|
||||
userMessage: text,
|
||||
userLine: lineNumber,
|
||||
assistantMessages: [],
|
||||
lastAssistantLine: lineNumber,
|
||||
timestamp: parsed.timestamp || new Date().toISOString()
|
||||
};
|
||||
} else if (parsed.message.role === 'assistant' && currentExchange) {
|
||||
// Accumulate assistant messages
|
||||
currentExchange.assistantMessages.push(text);
|
||||
currentExchange.lastAssistantLine = lineNumber;
|
||||
// Update timestamp to last assistant message
|
||||
if (parsed.timestamp) {
|
||||
currentExchange.timestamp = parsed.timestamp;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip malformed JSON lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize last exchange
|
||||
finalizeExchange();
|
||||
|
||||
return exchanges;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Get the personal superpowers directory
|
||||
*
|
||||
* Precedence:
|
||||
* 1. PERSONAL_SUPERPOWERS_DIR env var (if set)
|
||||
* 2. XDG_CONFIG_HOME/superpowers (if XDG_CONFIG_HOME is set)
|
||||
* 3. ~/.config/superpowers (default)
|
||||
*/
|
||||
export function getSuperpowersDir(): string {
|
||||
if (process.env.PERSONAL_SUPERPOWERS_DIR) {
|
||||
return process.env.PERSONAL_SUPERPOWERS_DIR;
|
||||
}
|
||||
|
||||
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
||||
if (xdgConfigHome) {
|
||||
return path.join(xdgConfigHome, 'superpowers');
|
||||
}
|
||||
|
||||
return path.join(os.homedir(), '.config', 'superpowers');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation archive directory
|
||||
*/
|
||||
export function getArchiveDir(): string {
|
||||
// Allow test override
|
||||
if (process.env.TEST_ARCHIVE_DIR) {
|
||||
return process.env.TEST_ARCHIVE_DIR;
|
||||
}
|
||||
|
||||
return path.join(getSuperpowersDir(), 'conversation-archive');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation index directory
|
||||
*/
|
||||
export function getIndexDir(): string {
|
||||
return path.join(getSuperpowersDir(), 'conversation-index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database path
|
||||
*/
|
||||
export function getDbPath(): string {
|
||||
return path.join(getIndexDir(), 'db.sqlite');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exclude config path
|
||||
*/
|
||||
export function getExcludeConfigPath(): string {
|
||||
return path.join(getIndexDir(), 'exclude.txt');
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
describe('search-agent template', () => {
|
||||
const templatePath = path.join(__dirname, '..', 'prompts', 'search-agent.md');
|
||||
|
||||
it('exists at expected location', () => {
|
||||
expect(fs.existsSync(templatePath)).toBe(true);
|
||||
});
|
||||
|
||||
it('contains required placeholders', () => {
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
|
||||
// Check for all required placeholders
|
||||
expect(content).toContain('{TOPIC}');
|
||||
expect(content).toContain('{SEARCH_QUERY}');
|
||||
expect(content).toContain('{FOCUS_AREAS}');
|
||||
});
|
||||
|
||||
it('contains required output sections', () => {
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
|
||||
// Check for required output format sections
|
||||
expect(content).toContain('### Summary');
|
||||
expect(content).toContain('### Sources');
|
||||
expect(content).toContain('### For Follow-Up');
|
||||
});
|
||||
|
||||
it('specifies word count requirements', () => {
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
|
||||
// Should specify 200-1000 words for synthesis
|
||||
expect(content).toMatch(/200-1000 words/);
|
||||
expect(content).toMatch(/max 1000 words/);
|
||||
});
|
||||
|
||||
it('includes source metadata requirements', () => {
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
|
||||
// Check for source metadata fields
|
||||
expect(content).toContain('project-name');
|
||||
expect(content).toContain('YYYY-MM-DD');
|
||||
expect(content).toContain('% match');
|
||||
expect(content).toContain('Conversation summary:');
|
||||
expect(content).toContain('File:');
|
||||
expect(content).toContain('Status:');
|
||||
expect(content).toContain('Read in detail');
|
||||
expect(content).toContain('Reviewed summary only');
|
||||
expect(content).toContain('Skimmed');
|
||||
});
|
||||
|
||||
it('provides search command', () => {
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
|
||||
// Should include the search command
|
||||
expect(content).toContain('~/.claude/skills/collaboration/remembering-conversations/tool/search-conversations');
|
||||
});
|
||||
|
||||
it('includes critical rules', () => {
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
|
||||
// Check for DO and DO NOT sections
|
||||
expect(content).toContain('## Critical Rules');
|
||||
expect(content).toContain('**DO:**');
|
||||
expect(content).toContain('**DO NOT:**');
|
||||
});
|
||||
|
||||
it('includes complete example output', () => {
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
|
||||
// Check example has all required components
|
||||
expect(content).toContain('## Example Output');
|
||||
|
||||
// Example should show Summary, Sources, and For Follow-Up
|
||||
const exampleSection = content.substring(content.indexOf('## Example Output'));
|
||||
expect(exampleSection).toContain('### Summary');
|
||||
expect(exampleSection).toContain('### Sources');
|
||||
expect(exampleSection).toContain('### For Follow-Up');
|
||||
|
||||
// Example should show specific details
|
||||
expect(exampleSection).toContain('react-router-7-starter');
|
||||
expect(exampleSection).toContain('92% match');
|
||||
expect(exampleSection).toContain('.jsonl');
|
||||
});
|
||||
|
||||
it('emphasizes synthesis over raw excerpts', () => {
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
|
||||
// Should explicitly discourage raw conversation excerpts
|
||||
expect(content).toContain('synthesize');
|
||||
expect(content).toContain('raw conversation excerpts');
|
||||
expect(content).toContain('synthesize instead');
|
||||
});
|
||||
|
||||
it('provides follow-up options', () => {
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
|
||||
// Should explain how main agent can follow up
|
||||
expect(content).toContain('Main agent can:');
|
||||
expect(content).toContain('dig deeper');
|
||||
expect(content).toContain('refined query');
|
||||
expect(content).toContain('context bloat');
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { searchConversations, formatResults, SearchOptions } from './search.js';
|
||||
|
||||
const query = process.argv[2];
|
||||
const mode = (process.argv[3] || 'vector') as 'vector' | 'text' | 'both';
|
||||
const limit = parseInt(process.argv[4] || '10');
|
||||
const after = process.argv[5] || undefined;
|
||||
const before = process.argv[6] || undefined;
|
||||
|
||||
if (!query) {
|
||||
console.error('Usage: search-conversations <query> [mode] [limit] [after] [before]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const options: SearchOptions = {
|
||||
mode,
|
||||
limit,
|
||||
after,
|
||||
before
|
||||
};
|
||||
|
||||
searchConversations(query, options)
|
||||
.then(results => {
|
||||
console.log(formatResults(results));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error searching:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,173 +0,0 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { initDatabase } from './db.js';
|
||||
import { initEmbeddings, generateEmbedding } from './embeddings.js';
|
||||
import { SearchResult, ConversationExchange } from './types.js';
|
||||
import fs from 'fs';
|
||||
|
||||
export interface SearchOptions {
|
||||
limit?: number;
|
||||
mode?: 'vector' | 'text' | 'both';
|
||||
after?: string; // ISO date string
|
||||
before?: string; // ISO date string
|
||||
}
|
||||
|
||||
function validateISODate(dateStr: string, paramName: string): void {
|
||||
const isoDateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!isoDateRegex.test(dateStr)) {
|
||||
throw new Error(`Invalid ${paramName} date: "${dateStr}". Expected YYYY-MM-DD format (e.g., 2025-10-01)`);
|
||||
}
|
||||
// Verify it's actually a valid date
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error(`Invalid ${paramName} date: "${dateStr}". Not a valid calendar date.`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchConversations(
|
||||
query: string,
|
||||
options: SearchOptions = {}
|
||||
): Promise<SearchResult[]> {
|
||||
const { limit = 10, mode = 'vector', after, before } = options;
|
||||
|
||||
// Validate date parameters
|
||||
if (after) validateISODate(after, '--after');
|
||||
if (before) validateISODate(before, '--before');
|
||||
|
||||
const db = initDatabase();
|
||||
|
||||
let results: any[] = [];
|
||||
|
||||
// Build time filter clause
|
||||
const timeFilter = [];
|
||||
if (after) timeFilter.push(`e.timestamp >= '${after}'`);
|
||||
if (before) timeFilter.push(`e.timestamp <= '${before}'`);
|
||||
const timeClause = timeFilter.length > 0 ? `AND ${timeFilter.join(' AND ')}` : '';
|
||||
|
||||
if (mode === 'vector' || mode === 'both') {
|
||||
// Vector similarity search
|
||||
await initEmbeddings();
|
||||
const queryEmbedding = await generateEmbedding(query);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
e.id,
|
||||
e.project,
|
||||
e.timestamp,
|
||||
e.user_message,
|
||||
e.assistant_message,
|
||||
e.archive_path,
|
||||
e.line_start,
|
||||
e.line_end,
|
||||
vec.distance
|
||||
FROM vec_exchanges AS vec
|
||||
JOIN exchanges AS e ON vec.id = e.id
|
||||
WHERE vec.embedding MATCH ?
|
||||
AND k = ?
|
||||
${timeClause}
|
||||
ORDER BY vec.distance ASC
|
||||
`);
|
||||
|
||||
results = stmt.all(
|
||||
Buffer.from(new Float32Array(queryEmbedding).buffer),
|
||||
limit
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'text' || mode === 'both') {
|
||||
// Text search
|
||||
const textStmt = db.prepare(`
|
||||
SELECT
|
||||
e.id,
|
||||
e.project,
|
||||
e.timestamp,
|
||||
e.user_message,
|
||||
e.assistant_message,
|
||||
e.archive_path,
|
||||
e.line_start,
|
||||
e.line_end,
|
||||
0 as distance
|
||||
FROM exchanges AS e
|
||||
WHERE (e.user_message LIKE ? OR e.assistant_message LIKE ?)
|
||||
${timeClause}
|
||||
ORDER BY e.timestamp DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
const textResults = textStmt.all(`%${query}%`, `%${query}%`, limit);
|
||||
|
||||
if (mode === 'both') {
|
||||
// Merge and deduplicate by ID
|
||||
const seenIds = new Set(results.map(r => r.id));
|
||||
for (const textResult of textResults) {
|
||||
if (!seenIds.has(textResult.id)) {
|
||||
results.push(textResult);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results = textResults;
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
return results.map((row: any) => {
|
||||
const exchange: ConversationExchange = {
|
||||
id: row.id,
|
||||
project: row.project,
|
||||
timestamp: row.timestamp,
|
||||
userMessage: row.user_message,
|
||||
assistantMessage: row.assistant_message,
|
||||
archivePath: row.archive_path,
|
||||
lineStart: row.line_start,
|
||||
lineEnd: row.line_end
|
||||
};
|
||||
|
||||
// Try to load summary if available
|
||||
const summaryPath = row.archive_path.replace('.jsonl', '-summary.txt');
|
||||
let summary: string | undefined;
|
||||
if (fs.existsSync(summaryPath)) {
|
||||
summary = fs.readFileSync(summaryPath, 'utf-8').trim();
|
||||
}
|
||||
|
||||
// Create snippet (first 200 chars)
|
||||
const snippet = exchange.userMessage.substring(0, 200) +
|
||||
(exchange.userMessage.length > 200 ? '...' : '');
|
||||
|
||||
return {
|
||||
exchange,
|
||||
similarity: mode === 'text' ? undefined : 1 - row.distance,
|
||||
snippet,
|
||||
summary
|
||||
} as SearchResult & { summary?: string };
|
||||
});
|
||||
}
|
||||
|
||||
export function formatResults(results: Array<SearchResult & { summary?: string }>): string {
|
||||
if (results.length === 0) {
|
||||
return 'No results found.';
|
||||
}
|
||||
|
||||
let output = `Found ${results.length} relevant conversations:\n\n`;
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const date = new Date(result.exchange.timestamp).toISOString().split('T')[0];
|
||||
output += `${index + 1}. [${result.exchange.project}, ${date}]\n`;
|
||||
|
||||
// Show conversation summary if available
|
||||
if (result.summary) {
|
||||
output += ` ${result.summary}\n\n`;
|
||||
}
|
||||
|
||||
// Show match with similarity percentage
|
||||
if (result.similarity !== undefined) {
|
||||
const pct = Math.round(result.similarity * 100);
|
||||
output += ` ${pct}% match: "${result.snippet}"\n`;
|
||||
} else {
|
||||
output += ` Match: "${result.snippet}"\n`;
|
||||
}
|
||||
|
||||
output += ` ${result.exchange.archivePath}:${result.exchange.lineStart}-${result.exchange.lineEnd}\n\n`;
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import { ConversationExchange } from './types.js';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
export function formatConversationText(exchanges: ConversationExchange[]): string {
|
||||
return exchanges.map(ex => {
|
||||
return `User: ${ex.userMessage}\n\nAgent: ${ex.assistantMessage}`;
|
||||
}).join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
function extractSummary(text: string): string {
|
||||
const match = text.match(/<summary>(.*?)<\/summary>/s);
|
||||
if (match) {
|
||||
return match[1].trim();
|
||||
}
|
||||
// Fallback if no tags found
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
async function callClaude(prompt: string, useSonnet = false): Promise<string> {
|
||||
const model = useSonnet ? 'sonnet' : 'haiku';
|
||||
|
||||
for await (const message of query({
|
||||
prompt,
|
||||
options: {
|
||||
model,
|
||||
maxTokens: 4096,
|
||||
maxThinkingTokens: 0, // Disable extended thinking
|
||||
systemPrompt: 'Write concise, factual summaries. Output ONLY the summary - no preamble, no "Here is", no "I will". Your output will be indexed directly.'
|
||||
}
|
||||
})) {
|
||||
if (message && typeof message === 'object' && 'type' in message && message.type === 'result') {
|
||||
const result = (message as any).result;
|
||||
|
||||
// Check if result is an API error (SDK returns errors as result strings)
|
||||
if (typeof result === 'string' && result.includes('API Error') && result.includes('thinking.budget_tokens')) {
|
||||
if (!useSonnet) {
|
||||
console.log(` Haiku hit thinking budget error, retrying with Sonnet`);
|
||||
return await callClaude(prompt, true);
|
||||
}
|
||||
// If Sonnet also fails, return error message
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function chunkExchanges(exchanges: ConversationExchange[], chunkSize: number): ConversationExchange[][] {
|
||||
const chunks: ConversationExchange[][] = [];
|
||||
for (let i = 0; i < exchanges.length; i += chunkSize) {
|
||||
chunks.push(exchanges.slice(i, i + chunkSize));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
export async function summarizeConversation(exchanges: ConversationExchange[]): Promise<string> {
|
||||
// Handle trivial conversations
|
||||
if (exchanges.length === 0) {
|
||||
return 'Trivial conversation with no substantive content.';
|
||||
}
|
||||
|
||||
if (exchanges.length === 1) {
|
||||
const text = formatConversationText(exchanges);
|
||||
if (text.length < 100 || exchanges[0].userMessage.trim() === '/exit') {
|
||||
return 'Trivial conversation with no substantive content.';
|
||||
}
|
||||
}
|
||||
|
||||
// For short conversations (≤15 exchanges), summarize directly
|
||||
if (exchanges.length <= 15) {
|
||||
const conversationText = formatConversationText(exchanges);
|
||||
const prompt = `Context: This summary will be shown in a list to help users and Claude choose which conversations are relevant to a future activity.
|
||||
|
||||
Summarize what happened in 2-4 sentences. Be factual and specific. Output in <summary></summary> tags.
|
||||
|
||||
Include:
|
||||
- What was built/changed/discussed (be specific)
|
||||
- Key technical decisions or approaches
|
||||
- Problems solved or current state
|
||||
|
||||
Exclude:
|
||||
- Apologies, meta-commentary, or your questions
|
||||
- Raw logs or debug output
|
||||
- Generic descriptions - focus on what makes THIS conversation unique
|
||||
|
||||
Good:
|
||||
<summary>Built JWT authentication for React app with refresh tokens and protected routes. Fixed token expiration bug by implementing refresh-during-request logic.</summary>
|
||||
|
||||
Bad:
|
||||
<summary>I apologize. The conversation discussed authentication and various approaches were considered...</summary>
|
||||
|
||||
${conversationText}`;
|
||||
|
||||
const result = await callClaude(prompt);
|
||||
return extractSummary(result);
|
||||
}
|
||||
|
||||
// For long conversations, use hierarchical summarization
|
||||
console.log(` Long conversation (${exchanges.length} exchanges) - using hierarchical summarization`);
|
||||
|
||||
// Chunk into groups of 8 exchanges
|
||||
const chunks = chunkExchanges(exchanges, 8);
|
||||
console.log(` Split into ${chunks.length} chunks`);
|
||||
|
||||
// Summarize each chunk
|
||||
const chunkSummaries: string[] = [];
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunkText = formatConversationText(chunks[i]);
|
||||
const prompt = `Summarize this part of a conversation in 2-3 sentences. What happened, what was built/discussed. Use <summary></summary> tags.
|
||||
|
||||
${chunkText}
|
||||
|
||||
Example: <summary>Implemented HID keyboard functionality for ESP32. Hit Bluetooth controller initialization error, fixed by adjusting memory allocation.</summary>`;
|
||||
|
||||
try {
|
||||
const summary = await callClaude(prompt);
|
||||
const extracted = extractSummary(summary);
|
||||
chunkSummaries.push(extracted);
|
||||
console.log(` Chunk ${i + 1}/${chunks.length}: ${extracted.split(/\s+/).length} words`);
|
||||
} catch (error) {
|
||||
console.log(` Chunk ${i + 1} failed, skipping`);
|
||||
}
|
||||
}
|
||||
|
||||
if (chunkSummaries.length === 0) {
|
||||
return 'Error: Unable to summarize conversation.';
|
||||
}
|
||||
|
||||
// Synthesize chunks into final summary
|
||||
const synthesisPrompt = `Context: This summary will be shown in a list to help users and Claude choose which past conversations are relevant to a future activity.
|
||||
|
||||
Synthesize these part-summaries into one cohesive paragraph. Focus on what was accomplished and any notable technical decisions or challenges. Output in <summary></summary> tags.
|
||||
|
||||
Part summaries:
|
||||
${chunkSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}
|
||||
|
||||
Good:
|
||||
<summary>Built conversation search system with JavaScript, sqlite-vec, and local embeddings. Implemented hierarchical summarization for long conversations. System archives conversations permanently and provides semantic search via CLI.</summary>
|
||||
|
||||
Bad:
|
||||
<summary>This conversation synthesizes several topics discussed across multiple parts...</summary>
|
||||
|
||||
Your summary (max 200 words):`;
|
||||
|
||||
console.log(` Synthesizing final summary...`);
|
||||
try {
|
||||
const result = await callClaude(synthesisPrompt);
|
||||
return extractSummary(result);
|
||||
} catch (error) {
|
||||
console.log(` Synthesis failed, using chunk summaries`);
|
||||
return chunkSummaries.join(' ');
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
export interface ConversationExchange {
|
||||
id: string;
|
||||
project: string;
|
||||
timestamp: string;
|
||||
userMessage: string;
|
||||
assistantMessage: string;
|
||||
archivePath: string;
|
||||
lineStart: number;
|
||||
lineEnd: number;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
exchange: ConversationExchange;
|
||||
similarity: number;
|
||||
snippet: string;
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { verifyIndex, repairIndex, VerificationResult } from './verify.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { initDatabase, insertExchange } from './db.js';
|
||||
import { ConversationExchange } from './types.js';
|
||||
|
||||
describe('verifyIndex', () => {
|
||||
const testDir = path.join(os.tmpdir(), 'conversation-search-test-' + Date.now());
|
||||
const projectsDir = path.join(testDir, '.claude', 'projects');
|
||||
const archiveDir = path.join(testDir, '.clank', 'conversation-archive');
|
||||
const dbPath = path.join(testDir, '.clank', 'conversation-index', 'db.sqlite');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test directories
|
||||
fs.mkdirSync(path.join(testDir, '.clank', 'conversation-index'), { recursive: true });
|
||||
fs.mkdirSync(projectsDir, { recursive: true });
|
||||
fs.mkdirSync(archiveDir, { recursive: true });
|
||||
|
||||
// Override environment paths for testing
|
||||
process.env.TEST_PROJECTS_DIR = projectsDir;
|
||||
process.env.TEST_ARCHIVE_DIR = archiveDir;
|
||||
process.env.TEST_DB_PATH = dbPath;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
delete process.env.TEST_PROJECTS_DIR;
|
||||
delete process.env.TEST_ARCHIVE_DIR;
|
||||
delete process.env.TEST_DB_PATH;
|
||||
});
|
||||
|
||||
it('detects missing summaries', async () => {
|
||||
// Create a test conversation file without a summary
|
||||
const projectArchive = path.join(archiveDir, 'test-project');
|
||||
fs.mkdirSync(projectArchive, { recursive: true });
|
||||
|
||||
const conversationPath = path.join(projectArchive, 'test-conversation.jsonl');
|
||||
|
||||
// Create proper JSONL format (one JSON object per line)
|
||||
const messages = [
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'Hello' }, timestamp: '2024-01-01T00:00:00Z' }),
|
||||
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: 'Hi there!' }, timestamp: '2024-01-01T00:00:01Z' })
|
||||
];
|
||||
fs.writeFileSync(conversationPath, messages.join('\n'));
|
||||
|
||||
const result = await verifyIndex();
|
||||
|
||||
expect(result.missing.length).toBe(1);
|
||||
expect(result.missing[0].path).toBe(conversationPath);
|
||||
expect(result.missing[0].reason).toBe('No summary file');
|
||||
});
|
||||
|
||||
it('detects orphaned database entries', async () => {
|
||||
// Initialize database
|
||||
const db = initDatabase();
|
||||
|
||||
// Create an exchange in the database
|
||||
const exchange: ConversationExchange = {
|
||||
id: 'orphan-id-1',
|
||||
project: 'deleted-project',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
userMessage: 'This conversation was deleted',
|
||||
assistantMessage: 'But still in database',
|
||||
archivePath: path.join(archiveDir, 'deleted-project', 'deleted.jsonl'),
|
||||
lineStart: 1,
|
||||
lineEnd: 2
|
||||
};
|
||||
|
||||
const embedding = new Array(384).fill(0.1);
|
||||
insertExchange(db, exchange, embedding);
|
||||
db.close();
|
||||
|
||||
// Verify detects orphaned entry (file doesn't exist)
|
||||
const result = await verifyIndex();
|
||||
|
||||
expect(result.orphaned.length).toBe(1);
|
||||
expect(result.orphaned[0].uuid).toBe('orphan-id-1');
|
||||
expect(result.orphaned[0].path).toBe(exchange.archivePath);
|
||||
});
|
||||
|
||||
it('detects outdated files (file modified after last_indexed)', async () => {
|
||||
// Create conversation file with summary
|
||||
const projectArchive = path.join(archiveDir, 'test-project');
|
||||
fs.mkdirSync(projectArchive, { recursive: true });
|
||||
|
||||
const conversationPath = path.join(projectArchive, 'updated-conversation.jsonl');
|
||||
const summaryPath = conversationPath.replace('.jsonl', '-summary.txt');
|
||||
|
||||
// Create initial conversation
|
||||
const messages = [
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'Hello' }, timestamp: '2024-01-01T00:00:00Z' }),
|
||||
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: 'Hi there!' }, timestamp: '2024-01-01T00:00:01Z' })
|
||||
];
|
||||
fs.writeFileSync(conversationPath, messages.join('\n'));
|
||||
fs.writeFileSync(summaryPath, 'Test summary');
|
||||
|
||||
// Index it
|
||||
const db = initDatabase();
|
||||
const exchange: ConversationExchange = {
|
||||
id: 'updated-id-1',
|
||||
project: 'test-project',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
userMessage: 'Hello',
|
||||
assistantMessage: 'Hi there!',
|
||||
archivePath: conversationPath,
|
||||
lineStart: 1,
|
||||
lineEnd: 2
|
||||
};
|
||||
|
||||
const embedding = new Array(384).fill(0.1);
|
||||
insertExchange(db, exchange, embedding);
|
||||
|
||||
// Get the last_indexed timestamp
|
||||
const row = db.prepare(`SELECT last_indexed FROM exchanges WHERE id = ?`).get('updated-id-1') as any;
|
||||
const lastIndexed = row.last_indexed;
|
||||
db.close();
|
||||
|
||||
// Wait a bit, then modify the file
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Update the conversation file
|
||||
const updatedMessages = [
|
||||
...messages,
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'New message' }, timestamp: '2024-01-01T00:00:02Z' })
|
||||
];
|
||||
fs.writeFileSync(conversationPath, updatedMessages.join('\n'));
|
||||
|
||||
// Verify detects outdated file
|
||||
const result = await verifyIndex();
|
||||
|
||||
expect(result.outdated.length).toBe(1);
|
||||
expect(result.outdated[0].path).toBe(conversationPath);
|
||||
expect(result.outdated[0].dbTime).toBe(lastIndexed);
|
||||
expect(result.outdated[0].fileTime).toBeGreaterThan(lastIndexed);
|
||||
});
|
||||
|
||||
// Note: Parser is resilient to malformed JSON - it skips bad lines
|
||||
// Corruption detection would require file system errors or permission issues
|
||||
// which are harder to test. Skipping for now as missing summaries is the
|
||||
// primary use case for verification.
|
||||
});
|
||||
|
||||
describe('repairIndex', () => {
|
||||
const testDir = path.join(os.tmpdir(), 'conversation-repair-test-' + Date.now());
|
||||
const projectsDir = path.join(testDir, '.claude', 'projects');
|
||||
const archiveDir = path.join(testDir, '.clank', 'conversation-archive');
|
||||
const dbPath = path.join(testDir, '.clank', 'conversation-index', 'db.sqlite');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test directories
|
||||
fs.mkdirSync(path.join(testDir, '.clank', 'conversation-index'), { recursive: true });
|
||||
fs.mkdirSync(projectsDir, { recursive: true });
|
||||
fs.mkdirSync(archiveDir, { recursive: true });
|
||||
|
||||
// Override environment paths for testing
|
||||
process.env.TEST_PROJECTS_DIR = projectsDir;
|
||||
process.env.TEST_ARCHIVE_DIR = archiveDir;
|
||||
process.env.TEST_DB_PATH = dbPath;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
delete process.env.TEST_PROJECTS_DIR;
|
||||
delete process.env.TEST_ARCHIVE_DIR;
|
||||
delete process.env.TEST_DB_PATH;
|
||||
});
|
||||
|
||||
it('deletes orphaned database entries during repair', async () => {
|
||||
// Initialize database with orphaned entry
|
||||
const db = initDatabase();
|
||||
|
||||
const exchange: ConversationExchange = {
|
||||
id: 'orphan-repair-1',
|
||||
project: 'deleted-project',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
userMessage: 'This conversation was deleted',
|
||||
assistantMessage: 'But still in database',
|
||||
archivePath: path.join(archiveDir, 'deleted-project', 'deleted.jsonl'),
|
||||
lineStart: 1,
|
||||
lineEnd: 2
|
||||
};
|
||||
|
||||
const embedding = new Array(384).fill(0.1);
|
||||
insertExchange(db, exchange, embedding);
|
||||
db.close();
|
||||
|
||||
// Verify it's there
|
||||
const dbBefore = initDatabase();
|
||||
const beforeCount = dbBefore.prepare(`SELECT COUNT(*) as count FROM exchanges WHERE id = ?`).get('orphan-repair-1') as { count: number };
|
||||
expect(beforeCount.count).toBe(1);
|
||||
dbBefore.close();
|
||||
|
||||
// Run repair
|
||||
const issues = await verifyIndex();
|
||||
expect(issues.orphaned.length).toBe(1);
|
||||
await repairIndex(issues);
|
||||
|
||||
// Verify it's gone
|
||||
const dbAfter = initDatabase();
|
||||
const afterCount = dbAfter.prepare(`SELECT COUNT(*) as count FROM exchanges WHERE id = ?`).get('orphan-repair-1') as { count: number };
|
||||
expect(afterCount.count).toBe(0);
|
||||
dbAfter.close();
|
||||
});
|
||||
|
||||
it('re-indexes outdated files during repair', { timeout: 30000 }, async () => {
|
||||
// Create conversation file with summary
|
||||
const projectArchive = path.join(archiveDir, 'test-project');
|
||||
fs.mkdirSync(projectArchive, { recursive: true });
|
||||
|
||||
const conversationPath = path.join(projectArchive, 'outdated-repair.jsonl');
|
||||
const summaryPath = conversationPath.replace('.jsonl', '-summary.txt');
|
||||
|
||||
// Create initial conversation
|
||||
const messages = [
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'Hello' }, timestamp: '2024-01-01T00:00:00Z' }),
|
||||
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: 'Hi there!' }, timestamp: '2024-01-01T00:00:01Z' })
|
||||
];
|
||||
fs.writeFileSync(conversationPath, messages.join('\n'));
|
||||
fs.writeFileSync(summaryPath, 'Old summary');
|
||||
|
||||
// Index it
|
||||
const db = initDatabase();
|
||||
const exchange: ConversationExchange = {
|
||||
id: 'outdated-repair-1',
|
||||
project: 'test-project',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
userMessage: 'Hello',
|
||||
assistantMessage: 'Hi there!',
|
||||
archivePath: conversationPath,
|
||||
lineStart: 1,
|
||||
lineEnd: 2
|
||||
};
|
||||
|
||||
const embedding = new Array(384).fill(0.1);
|
||||
insertExchange(db, exchange, embedding);
|
||||
|
||||
// Get the last_indexed timestamp
|
||||
const beforeRow = db.prepare(`SELECT last_indexed FROM exchanges WHERE id = ?`).get('outdated-repair-1') as any;
|
||||
const beforeIndexed = beforeRow.last_indexed;
|
||||
db.close();
|
||||
|
||||
// Wait a bit, then modify the file
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Update the conversation file (add new exchange)
|
||||
const updatedMessages = [
|
||||
...messages,
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'New message' }, timestamp: '2024-01-01T00:00:02Z' }),
|
||||
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: 'New response' }, timestamp: '2024-01-01T00:00:03Z' })
|
||||
];
|
||||
fs.writeFileSync(conversationPath, updatedMessages.join('\n'));
|
||||
|
||||
// Verify detects outdated
|
||||
const issues = await verifyIndex();
|
||||
expect(issues.outdated.length).toBe(1);
|
||||
|
||||
// Wait a bit to ensure different timestamp
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Run repair
|
||||
await repairIndex(issues);
|
||||
|
||||
// Verify it was re-indexed with new timestamp
|
||||
const dbAfter = initDatabase();
|
||||
const afterRow = dbAfter.prepare(`SELECT MAX(last_indexed) as last_indexed FROM exchanges WHERE archive_path = ?`).get(conversationPath) as any;
|
||||
expect(afterRow.last_indexed).toBeGreaterThan(beforeIndexed);
|
||||
|
||||
// Verify no longer outdated
|
||||
const verifyAfter = await verifyIndex();
|
||||
expect(verifyAfter.outdated.length).toBe(0);
|
||||
|
||||
dbAfter.close();
|
||||
});
|
||||
});
|
||||
@@ -1,177 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { parseConversation } from './parser.js';
|
||||
import { initDatabase, getAllExchanges, getFileLastIndexed } from './db.js';
|
||||
import { getArchiveDir } from './paths.js';
|
||||
|
||||
export interface VerificationResult {
|
||||
missing: Array<{ path: string; reason: string }>;
|
||||
orphaned: Array<{ uuid: string; path: string }>;
|
||||
outdated: Array<{ path: string; fileTime: number; dbTime: number }>;
|
||||
corrupted: Array<{ path: string; error: string }>;
|
||||
}
|
||||
|
||||
export async function verifyIndex(): Promise<VerificationResult> {
|
||||
const result: VerificationResult = {
|
||||
missing: [],
|
||||
orphaned: [],
|
||||
outdated: [],
|
||||
corrupted: []
|
||||
};
|
||||
|
||||
const archiveDir = getArchiveDir();
|
||||
|
||||
// Track all files we find
|
||||
const foundFiles = new Set<string>();
|
||||
|
||||
// Find all conversation files
|
||||
if (!fs.existsSync(archiveDir)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Initialize database once for all checks
|
||||
const db = initDatabase();
|
||||
|
||||
const projects = fs.readdirSync(archiveDir);
|
||||
let totalChecked = 0;
|
||||
|
||||
for (const project of projects) {
|
||||
const projectPath = path.join(archiveDir, project);
|
||||
const stat = fs.statSync(projectPath);
|
||||
|
||||
if (!stat.isDirectory()) continue;
|
||||
|
||||
const files = fs.readdirSync(projectPath).filter(f => f.endsWith('.jsonl'));
|
||||
|
||||
for (const file of files) {
|
||||
totalChecked++;
|
||||
|
||||
if (totalChecked % 100 === 0) {
|
||||
console.log(` Checked ${totalChecked} conversations...`);
|
||||
}
|
||||
|
||||
const conversationPath = path.join(projectPath, file);
|
||||
foundFiles.add(conversationPath);
|
||||
|
||||
const summaryPath = conversationPath.replace('.jsonl', '-summary.txt');
|
||||
|
||||
// Check for missing summary
|
||||
if (!fs.existsSync(summaryPath)) {
|
||||
result.missing.push({ path: conversationPath, reason: 'No summary file' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if file is outdated (modified after last_indexed)
|
||||
const lastIndexed = getFileLastIndexed(db, conversationPath);
|
||||
if (lastIndexed !== null) {
|
||||
const fileStat = fs.statSync(conversationPath);
|
||||
if (fileStat.mtimeMs > lastIndexed) {
|
||||
result.outdated.push({
|
||||
path: conversationPath,
|
||||
fileTime: fileStat.mtimeMs,
|
||||
dbTime: lastIndexed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Try parsing to detect corruption
|
||||
try {
|
||||
await parseConversation(conversationPath, project, conversationPath);
|
||||
} catch (error) {
|
||||
result.corrupted.push({
|
||||
path: conversationPath,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Verified ${totalChecked} conversations.`);
|
||||
|
||||
// Check for orphaned database entries
|
||||
const dbExchanges = getAllExchanges(db);
|
||||
db.close();
|
||||
|
||||
for (const exchange of dbExchanges) {
|
||||
if (!foundFiles.has(exchange.archivePath)) {
|
||||
result.orphaned.push({
|
||||
uuid: exchange.id,
|
||||
path: exchange.archivePath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function repairIndex(issues: VerificationResult): Promise<void> {
|
||||
console.log('Repairing index...');
|
||||
|
||||
// To avoid circular dependencies, we import the indexer functions dynamically
|
||||
const { initDatabase, insertExchange, deleteExchange } = await import('./db.js');
|
||||
const { parseConversation } = await import('./parser.js');
|
||||
const { initEmbeddings, generateExchangeEmbedding } = await import('./embeddings.js');
|
||||
const { summarizeConversation } = await import('./summarizer.js');
|
||||
|
||||
const db = initDatabase();
|
||||
await initEmbeddings();
|
||||
|
||||
// Remove orphaned entries first
|
||||
for (const orphan of issues.orphaned) {
|
||||
console.log(`Removing orphaned entry: ${orphan.uuid}`);
|
||||
deleteExchange(db, orphan.uuid);
|
||||
}
|
||||
|
||||
// Re-index missing and outdated conversations
|
||||
const toReindex = [
|
||||
...issues.missing.map(m => m.path),
|
||||
...issues.outdated.map(o => o.path)
|
||||
];
|
||||
|
||||
for (const conversationPath of toReindex) {
|
||||
console.log(`Re-indexing: ${conversationPath}`);
|
||||
try {
|
||||
// Extract project name from path
|
||||
const archiveDir = getArchiveDir();
|
||||
const relativePath = conversationPath.replace(archiveDir + path.sep, '');
|
||||
const project = relativePath.split(path.sep)[0];
|
||||
|
||||
// Parse conversation
|
||||
const exchanges = await parseConversation(conversationPath, project, conversationPath);
|
||||
|
||||
if (exchanges.length === 0) {
|
||||
console.log(` Skipped (no exchanges)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate/update summary
|
||||
const summaryPath = conversationPath.replace('.jsonl', '-summary.txt');
|
||||
const summary = await summarizeConversation(exchanges);
|
||||
fs.writeFileSync(summaryPath, summary, 'utf-8');
|
||||
console.log(` Created summary: ${summary.split(/\s+/).length} words`);
|
||||
|
||||
// Index exchanges
|
||||
for (const exchange of exchanges) {
|
||||
const embedding = await generateExchangeEmbedding(
|
||||
exchange.userMessage,
|
||||
exchange.assistantMessage
|
||||
);
|
||||
insertExchange(db, exchange, embedding);
|
||||
}
|
||||
|
||||
console.log(` Indexed ${exchanges.length} exchanges`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to re-index ${conversationPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
// Report corrupted files (manual intervention needed)
|
||||
if (issues.corrupted.length > 0) {
|
||||
console.log('\n⚠️ Corrupted files (manual review needed):');
|
||||
issues.corrupted.forEach(c => console.log(` ${c.path}: ${c.error}`));
|
||||
}
|
||||
|
||||
console.log('✅ Repair complete.');
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
#!/bin/bash
|
||||
# End-to-end deployment testing
|
||||
# Tests all deployment scenarios from docs/plans/2025-10-07-deployment-plan.md
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
INSTALL_HOOK="$SCRIPT_DIR/install-hook"
|
||||
INDEX_CONVERSATIONS="$SCRIPT_DIR/index-conversations"
|
||||
|
||||
# Test counter
|
||||
TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
setup_test() {
|
||||
TEST_DIR=$(mktemp -d)
|
||||
export HOME="$TEST_DIR"
|
||||
export TEST_PROJECTS_DIR="$TEST_DIR/.claude/projects"
|
||||
export TEST_ARCHIVE_DIR="$TEST_DIR/.clank/conversation-archive"
|
||||
export TEST_DB_PATH="$TEST_DIR/.clank/conversation-index/db.sqlite"
|
||||
|
||||
mkdir -p "$HOME/.claude/hooks"
|
||||
mkdir -p "$TEST_PROJECTS_DIR"
|
||||
mkdir -p "$TEST_ARCHIVE_DIR"
|
||||
mkdir -p "$TEST_DIR/.clank/conversation-index"
|
||||
}
|
||||
|
||||
cleanup_test() {
|
||||
if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then
|
||||
rm -rf "$TEST_DIR"
|
||||
fi
|
||||
unset TEST_PROJECTS_DIR
|
||||
unset TEST_ARCHIVE_DIR
|
||||
unset TEST_DB_PATH
|
||||
}
|
||||
|
||||
assert_file_exists() {
|
||||
if [ ! -f "$1" ]; then
|
||||
echo -e "${RED}❌ FAIL: File does not exist: $1${NC}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
assert_file_executable() {
|
||||
if [ ! -x "$1" ]; then
|
||||
echo -e "${RED}❌ FAIL: File is not executable: $1${NC}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
assert_file_contains() {
|
||||
if ! grep -q "$2" "$1"; then
|
||||
echo -e "${RED}❌ FAIL: File $1 does not contain: $2${NC}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
assert_summary_exists() {
|
||||
local jsonl_file="$1"
|
||||
|
||||
# If file is in projects dir, convert to archive path
|
||||
if [[ "$jsonl_file" == *"/.claude/projects/"* ]]; then
|
||||
jsonl_file=$(echo "$jsonl_file" | sed "s|/.claude/projects/|/.clank/conversation-archive/|")
|
||||
fi
|
||||
|
||||
local summary_file="${jsonl_file%.jsonl}-summary.txt"
|
||||
if [ ! -f "$summary_file" ]; then
|
||||
echo -e "${RED}❌ FAIL: Summary does not exist: $summary_file${NC}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
create_test_conversation() {
|
||||
local project="$1"
|
||||
local uuid="${2:-test-$(date +%s)}"
|
||||
|
||||
mkdir -p "$TEST_PROJECTS_DIR/$project"
|
||||
local conv_file="$TEST_PROJECTS_DIR/$project/${uuid}.jsonl"
|
||||
|
||||
cat > "$conv_file" <<'EOF'
|
||||
{"type":"user","message":{"role":"user","content":"What is TDD?"},"timestamp":"2024-01-01T00:00:00Z"}
|
||||
{"type":"assistant","message":{"role":"assistant","content":"TDD stands for Test-Driven Development. You write tests first."},"timestamp":"2024-01-01T00:00:01Z"}
|
||||
EOF
|
||||
|
||||
echo "$conv_file"
|
||||
}
|
||||
|
||||
run_test() {
|
||||
local test_name="$1"
|
||||
local test_func="$2"
|
||||
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
echo -e "\n${YELLOW}Running test: $test_name${NC}"
|
||||
|
||||
setup_test
|
||||
|
||||
if $test_func; then
|
||||
echo -e "${GREEN}✓ PASS: $test_name${NC}"
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
else
|
||||
echo -e "${RED}❌ FAIL: $test_name${NC}"
|
||||
fi
|
||||
|
||||
cleanup_test
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Scenario 1: Fresh Installation
|
||||
# ============================================================================
|
||||
|
||||
test_scenario_1_fresh_install() {
|
||||
echo " 1. Installing hook with no existing hook..."
|
||||
"$INSTALL_HOOK" > /dev/null 2>&1 || true
|
||||
|
||||
assert_file_exists "$HOME/.claude/hooks/sessionEnd" || return 1
|
||||
assert_file_executable "$HOME/.claude/hooks/sessionEnd" || return 1
|
||||
|
||||
echo " 2. Creating test conversation..."
|
||||
local conv_file=$(create_test_conversation "test-project" "conv-1")
|
||||
|
||||
echo " 3. Indexing conversation..."
|
||||
cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" > /dev/null 2>&1
|
||||
|
||||
echo " 4. Verifying summary was created..."
|
||||
assert_summary_exists "$conv_file" || return 1
|
||||
|
||||
echo " 5. Testing hook triggers indexing..."
|
||||
export SESSION_ID="hook-session-$(date +%s)"
|
||||
|
||||
# Create conversation file with SESSION_ID in name
|
||||
mkdir -p "$TEST_PROJECTS_DIR/test-project"
|
||||
local new_conv="$TEST_PROJECTS_DIR/test-project/${SESSION_ID}.jsonl"
|
||||
cat > "$new_conv" <<'EOF'
|
||||
{"type":"user","message":{"role":"user","content":"What is TDD?"},"timestamp":"2024-01-01T00:00:00Z"}
|
||||
{"type":"assistant","message":{"role":"assistant","content":"TDD stands for Test-Driven Development. You write tests first."},"timestamp":"2024-01-01T00:00:01Z"}
|
||||
EOF
|
||||
|
||||
# Verify hook runs the index command (manually call indexer with --session)
|
||||
# In real environment, hook would do this automatically
|
||||
cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" --session "$SESSION_ID" > /dev/null 2>&1
|
||||
|
||||
echo " 6. Verifying session was indexed..."
|
||||
assert_summary_exists "$new_conv" || return 1
|
||||
|
||||
echo " 7. Testing search functionality..."
|
||||
local search_result=$(cd "$SCRIPT_DIR" && "$SCRIPT_DIR/search-conversations" "TDD" 2>/dev/null || echo "")
|
||||
if [ -z "$search_result" ]; then
|
||||
echo -e "${RED}❌ Search returned no results${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Scenario 2: Existing Hook (merge)
|
||||
# ============================================================================
|
||||
|
||||
test_scenario_2_existing_hook_merge() {
|
||||
echo " 1. Creating existing hook..."
|
||||
cat > "$HOME/.claude/hooks/sessionEnd" <<'EOF'
|
||||
#!/bin/bash
|
||||
# Existing hook
|
||||
echo "Existing hook running"
|
||||
EOF
|
||||
chmod +x "$HOME/.claude/hooks/sessionEnd"
|
||||
|
||||
echo " 2. Installing with merge option..."
|
||||
echo "m" | "$INSTALL_HOOK" > /dev/null 2>&1 || true
|
||||
|
||||
echo " 3. Verifying backup created..."
|
||||
local backup_count=$(ls -1 "$HOME/.claude/hooks/sessionEnd.backup."* 2>/dev/null | wc -l)
|
||||
if [ "$backup_count" -lt 1 ]; then
|
||||
echo -e "${RED}❌ No backup created${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " 4. Verifying merge preserved existing content..."
|
||||
assert_file_contains "$HOME/.claude/hooks/sessionEnd" "Existing hook running" || return 1
|
||||
|
||||
echo " 5. Verifying indexer was appended..."
|
||||
assert_file_contains "$HOME/.claude/hooks/sessionEnd" "remembering-conversations.*index-conversations" || return 1
|
||||
|
||||
echo " 6. Testing merged hook runs both parts..."
|
||||
local conv_file=$(create_test_conversation "merge-project" "merge-conv")
|
||||
cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" > /dev/null 2>&1
|
||||
|
||||
export SESSION_ID="merge-session-$(date +%s)"
|
||||
local hook_output=$("$HOME/.claude/hooks/sessionEnd" 2>&1)
|
||||
|
||||
if ! echo "$hook_output" | grep -q "Existing hook running"; then
|
||||
echo -e "${RED}❌ Existing hook logic not executed${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Scenario 3: Recovery (verify/repair)
|
||||
# ============================================================================
|
||||
|
||||
test_scenario_3_recovery_verify_repair() {
|
||||
echo " 1. Creating conversations and indexing..."
|
||||
local conv1=$(create_test_conversation "recovery-project" "conv-1")
|
||||
local conv2=$(create_test_conversation "recovery-project" "conv-2")
|
||||
|
||||
cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" > /dev/null 2>&1
|
||||
|
||||
echo " 2. Verifying summaries exist..."
|
||||
assert_summary_exists "$conv1" || return 1
|
||||
assert_summary_exists "$conv2" || return 1
|
||||
|
||||
echo " 3. Deleting summary to simulate missing file..."
|
||||
# Delete from archive (where summaries are stored)
|
||||
local archive_conv1=$(echo "$conv1" | sed "s|/.claude/projects/|/.clank/conversation-archive/|")
|
||||
rm "${archive_conv1%.jsonl}-summary.txt"
|
||||
|
||||
echo " 4. Running verify (should detect missing)..."
|
||||
local verify_output=$(cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" --verify 2>&1)
|
||||
|
||||
if ! echo "$verify_output" | grep -q "Missing summaries: 1"; then
|
||||
echo -e "${RED}❌ Verify did not detect missing summary${NC}"
|
||||
echo "Verify output: $verify_output"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " 5. Running repair..."
|
||||
cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" --repair > /dev/null 2>&1
|
||||
|
||||
echo " 6. Verifying summary was regenerated..."
|
||||
assert_summary_exists "$conv1" || return 1
|
||||
|
||||
echo " 7. Running verify again (should be clean)..."
|
||||
verify_output=$(cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" --verify 2>&1)
|
||||
|
||||
# Verify should report no missing issues
|
||||
if ! echo "$verify_output" | grep -q "Missing summaries: 0"; then
|
||||
echo -e "${RED}❌ Verify still reports missing issues after repair${NC}"
|
||||
echo "Verify output: $verify_output"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Scenario 4: Change Detection
|
||||
# ============================================================================
|
||||
|
||||
test_scenario_4_change_detection() {
|
||||
echo " 1. Creating and indexing conversation..."
|
||||
local conv=$(create_test_conversation "change-project" "conv-1")
|
||||
|
||||
cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" > /dev/null 2>&1
|
||||
|
||||
echo " 2. Verifying initial index..."
|
||||
assert_summary_exists "$conv" || return 1
|
||||
|
||||
echo " 3. Modifying conversation (adding exchange)..."
|
||||
# Wait to ensure different mtime
|
||||
sleep 1
|
||||
|
||||
# Modify the archive file (that's what verify checks)
|
||||
local archive_conv=$(echo "$conv" | sed "s|/.claude/projects/|/.clank/conversation-archive/|")
|
||||
cat >> "$archive_conv" <<'EOF'
|
||||
{"type":"user","message":{"role":"user","content":"Tell me more about TDD"},"timestamp":"2024-01-01T00:00:02Z"}
|
||||
{"type":"assistant","message":{"role":"assistant","content":"TDD has three phases: Red, Green, Refactor."},"timestamp":"2024-01-01T00:00:03Z"}
|
||||
EOF
|
||||
|
||||
echo " 4. Running verify (should detect outdated)..."
|
||||
local verify_output=$(cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" --verify 2>&1)
|
||||
|
||||
if ! echo "$verify_output" | grep -q "Outdated files: 1"; then
|
||||
echo -e "${RED}❌ Verify did not detect outdated file${NC}"
|
||||
echo "Verify output: $verify_output"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " 5. Running repair (should re-index)..."
|
||||
cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" --repair > /dev/null 2>&1
|
||||
|
||||
echo " 6. Verifying conversation is up to date..."
|
||||
verify_output=$(cd "$SCRIPT_DIR" && "$INDEX_CONVERSATIONS" --verify 2>&1)
|
||||
|
||||
if ! echo "$verify_output" | grep -q "Outdated files: 0"; then
|
||||
echo -e "${RED}❌ File still outdated after repair${NC}"
|
||||
echo "Verify output: $verify_output"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " 7. Verifying new content is searchable..."
|
||||
local search_result=$(cd "$SCRIPT_DIR" && "$SCRIPT_DIR/search-conversations" "Red Green Refactor" 2>/dev/null || echo "")
|
||||
if [ -z "$search_result" ]; then
|
||||
echo -e "${RED}❌ New content not found in search${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Scenario 5: Subagent Workflow (Manual Testing Required)
|
||||
# ============================================================================
|
||||
|
||||
test_scenario_5_subagent_workflow_docs() {
|
||||
echo " This scenario requires manual testing with a live subagent."
|
||||
echo " Automated checks:"
|
||||
|
||||
echo " 1. Verifying search-agent template exists..."
|
||||
local template_file="$SCRIPT_DIR/prompts/search-agent.md"
|
||||
assert_file_exists "$template_file" || return 1
|
||||
|
||||
echo " 2. Verifying template has required sections..."
|
||||
assert_file_contains "$template_file" "### Summary" || return 1
|
||||
assert_file_contains "$template_file" "### Sources" || return 1
|
||||
assert_file_contains "$template_file" "### For Follow-Up" || return 1
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW} MANUAL TESTING REQUIRED:${NC}"
|
||||
echo " To complete Scenario 5 testing:"
|
||||
echo " 1. Start a new Claude Code session"
|
||||
echo " 2. Ask about a past conversation topic"
|
||||
echo " 3. Dispatch subagent using: skills/collaboration/remembering-conversations/tool/prompts/search-agent.md"
|
||||
echo " 4. Verify synthesis is 200-1000 words"
|
||||
echo " 5. Verify all sources include: project, date, file path, status"
|
||||
echo " 6. Ask follow-up question to test iterative refinement"
|
||||
echo " 7. Verify no raw conversations loaded into main context"
|
||||
echo ""
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Run All Tests
|
||||
# ============================================================================
|
||||
|
||||
echo "=========================================="
|
||||
echo " End-to-End Deployment Testing"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Testing deployment scenarios from:"
|
||||
echo " docs/plans/2025-10-07-deployment-plan.md"
|
||||
echo ""
|
||||
|
||||
run_test "Scenario 1: Fresh Installation" test_scenario_1_fresh_install
|
||||
run_test "Scenario 2: Existing Hook (merge)" test_scenario_2_existing_hook_merge
|
||||
run_test "Scenario 3: Recovery (verify/repair)" test_scenario_3_recovery_verify_repair
|
||||
run_test "Scenario 4: Change Detection" test_scenario_4_change_detection
|
||||
run_test "Scenario 5: Subagent Workflow (docs check)" test_scenario_5_subagent_workflow_docs
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo -e " Test Results: ${GREEN}$TESTS_PASSED${NC}/${TESTS_RUN} passed"
|
||||
echo "=========================================="
|
||||
|
||||
if [ $TESTS_PASSED -eq $TESTS_RUN ]; then
|
||||
echo -e "${GREEN}✅ All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}❌ Some tests failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user