learnship 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +172 -155
- package/SKILL.md +23 -2
- package/bin/install.js +305 -3
- package/commands/learnship/diagnose-issues.md +1 -0
- package/commands/learnship/discuss-phase.md +1 -0
- package/commands/learnship/ideate.md +1 -0
- package/commands/learnship/list-phase-assumptions.md +1 -0
- package/commands/learnship/quick.md +1 -0
- package/commands/learnship/research-phase.md +1 -0
- package/commands/learnship/secure-phase.md +1 -0
- package/commands/learnship/validate-phase.md +2 -0
- package/commands/learnship/verify-work.md +1 -0
- package/cursor-rules/learnship.mdc +14 -4
- package/gemini-extension.json +1 -1
- package/hooks/learnship-context-monitor.js +120 -0
- package/hooks/learnship-prompt-guard.js +75 -0
- package/hooks/learnship-session-state.js +136 -0
- package/hooks/learnship-statusline.js +179 -0
- package/learnship/contexts/dev.md +21 -0
- package/learnship/contexts/research.md +22 -0
- package/learnship/contexts/review.md +22 -0
- package/learnship/templates/research-project/ARCHITECTURE.md +140 -0
- package/learnship/templates/research-project/FEATURES.md +130 -0
- package/learnship/templates/research-project/PITFALLS.md +102 -0
- package/learnship/templates/research-project/STACK.md +105 -0
- package/learnship/templates/research-project/SUMMARY.md +111 -0
- package/learnship/workflows/challenge.md +16 -4
- package/learnship/workflows/debug.md +30 -6
- package/learnship/workflows/diagnose-issues.md +14 -1
- package/learnship/workflows/discuss-milestone.md +15 -1
- package/learnship/workflows/discuss-phase.md +83 -10
- package/learnship/workflows/ideate.md +25 -5
- package/learnship/workflows/list-phase-assumptions.md +12 -5
- package/learnship/workflows/new-milestone.md +12 -6
- package/learnship/workflows/new-project.md +229 -85
- package/learnship/workflows/quick.md +18 -4
- package/learnship/workflows/research-phase.md +43 -8
- package/learnship/workflows/secure-phase.md +57 -15
- package/learnship/workflows/settings.md +142 -142
- package/learnship/workflows/validate-phase.md +39 -12
- package/learnship/workflows/verify-work.md +27 -0
- package/package.json +1 -1
- package/templates/config.json +1 -0
package/bin/install.js
CHANGED
|
@@ -67,6 +67,8 @@ const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
|
67
67
|
const hasLocal = args.includes('--local') || args.includes('-l');
|
|
68
68
|
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
69
69
|
const hasHelp = args.includes('--help') || args.includes('-h');
|
|
70
|
+
const targetIdx = args.indexOf('--target');
|
|
71
|
+
const targetOverride = targetIdx !== -1 && args[targetIdx + 1] ? path.resolve(args[targetIdx + 1]) : null;
|
|
70
72
|
|
|
71
73
|
let selectedPlatforms = [];
|
|
72
74
|
if (hasAll) {
|
|
@@ -111,6 +113,7 @@ const helpText = `
|
|
|
111
113
|
${cyan}-l, --local${reset} Install to current project directory
|
|
112
114
|
|
|
113
115
|
${yellow}Options:${reset}
|
|
116
|
+
${cyan}--target <dir>${reset} Install to a custom directory instead of the platform default
|
|
114
117
|
${cyan}-u, --uninstall${reset} Remove learnship files
|
|
115
118
|
${cyan}-h, --help${reset} Show this help
|
|
116
119
|
|
|
@@ -533,6 +536,16 @@ function replacePaths(content, pathPrefix, platform) {
|
|
|
533
536
|
} else if (platform === 'codex') {
|
|
534
537
|
c = c.replace(/~\/\.codex\//g, pathPrefix);
|
|
535
538
|
}
|
|
539
|
+
// Rewrite AskUserQuestion to platform-native interactive question tool name.
|
|
540
|
+
// Source files use AskUserQuestion (Claude Code syntax). Each platform has its own tool name.
|
|
541
|
+
// OpenCode is handled separately in convertToOpencode(). Claude keeps AskUserQuestion as-is.
|
|
542
|
+
if (platform === 'windsurf') {
|
|
543
|
+
c = c.replace(/\bAskUserQuestion\b/g, 'ask_user_question');
|
|
544
|
+
} else if (platform === 'gemini') {
|
|
545
|
+
c = c.replace(/\bAskUserQuestion\b/g, 'ask_user');
|
|
546
|
+
} else if (platform === 'codex') {
|
|
547
|
+
c = c.replace(/\bAskUserQuestion\b/g, 'request_user_input');
|
|
548
|
+
}
|
|
536
549
|
// Replace @mention skill syntax — @mention dispatch is Windsurf-native only
|
|
537
550
|
if (platform === 'claude') {
|
|
538
551
|
c = c.replace(/@agentic-learning\b/g, '/agentic-learning');
|
|
@@ -1084,6 +1097,266 @@ function scanForLeakedPaths(targetDir, platform) {
|
|
|
1084
1097
|
}
|
|
1085
1098
|
}
|
|
1086
1099
|
|
|
1100
|
+
// ─── Hook installation ────────────────────────────────────────────────────
|
|
1101
|
+
|
|
1102
|
+
/** List of learnship hook files managed by the installer */
|
|
1103
|
+
const LEARNSHIP_MANAGED_HOOKS = [
|
|
1104
|
+
'learnship-statusline.js',
|
|
1105
|
+
'learnship-context-monitor.js',
|
|
1106
|
+
'learnship-prompt-guard.js',
|
|
1107
|
+
'learnship-session-state.js',
|
|
1108
|
+
];
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Install Claude Code / Gemini native hooks into settings.json.
|
|
1112
|
+
* Copies hook .js files to target/hooks/ and registers them in settings.json.
|
|
1113
|
+
* Preserves existing non-learnship entries (read-modify-write).
|
|
1114
|
+
*/
|
|
1115
|
+
function installClaudeHooks(targetDir, isGlobal, platform) {
|
|
1116
|
+
const hooksSrc = path.join(__dirname, '..', 'hooks');
|
|
1117
|
+
const hooksDest = path.join(targetDir, 'hooks');
|
|
1118
|
+
fs.mkdirSync(hooksDest, { recursive: true });
|
|
1119
|
+
|
|
1120
|
+
// Copy hook .js files (skip session-start bash script — replaced by learnship-session-state.js)
|
|
1121
|
+
let copied = 0;
|
|
1122
|
+
for (const file of LEARNSHIP_MANAGED_HOOKS) {
|
|
1123
|
+
const src = path.join(hooksSrc, file);
|
|
1124
|
+
if (fs.existsSync(src)) {
|
|
1125
|
+
// Stamp version header
|
|
1126
|
+
let content = fs.readFileSync(src, 'utf8');
|
|
1127
|
+
content = content.replace(/learnship-hook-version:\s*[\d.]+/, `learnship-hook-version: ${pkg.version}`);
|
|
1128
|
+
fs.writeFileSync(path.join(hooksDest, file), content);
|
|
1129
|
+
copied++;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (copied === 0) return 0;
|
|
1134
|
+
|
|
1135
|
+
// Write package.json for CJS require() support in hooks
|
|
1136
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
1137
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
1138
|
+
fs.writeFileSync(pkgJsonPath, '{"type":"commonjs"}\n');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Build hook commands — use $CLAUDE_PROJECT_DIR for local installs
|
|
1142
|
+
const dirName = getDirName(platform);
|
|
1143
|
+
const localPrefix = '"$CLAUDE_PROJECT_DIR"/' + dirName;
|
|
1144
|
+
const buildCmd = (file) => {
|
|
1145
|
+
if (isGlobal) {
|
|
1146
|
+
const resolved = path.resolve(targetDir).replace(/\\/g, '/');
|
|
1147
|
+
return `node "${resolved}/hooks/${file}"`;
|
|
1148
|
+
}
|
|
1149
|
+
return `node ${localPrefix}/hooks/${file}`;
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// Read-modify-write settings.json
|
|
1153
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
1154
|
+
const settings = readSettings(settingsPath);
|
|
1155
|
+
if (!settings.hooks) settings.hooks = {};
|
|
1156
|
+
|
|
1157
|
+
// Gemini uses AfterTool/BeforeTool instead of PostToolUse/PreToolUse
|
|
1158
|
+
const isGemini = platform === 'gemini';
|
|
1159
|
+
const postToolEvent = isGemini ? 'AfterTool' : 'PostToolUse';
|
|
1160
|
+
const preToolEvent = isGemini ? 'BeforeTool' : 'PreToolUse';
|
|
1161
|
+
|
|
1162
|
+
// --- SessionStart: learnship-session-state.js ---
|
|
1163
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
1164
|
+
const hasSessionHook = settings.hooks.SessionStart.some(entry =>
|
|
1165
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('learnship-session-state'))
|
|
1166
|
+
);
|
|
1167
|
+
if (!hasSessionHook && fs.existsSync(path.join(hooksDest, 'learnship-session-state.js'))) {
|
|
1168
|
+
settings.hooks.SessionStart.push({
|
|
1169
|
+
hooks: [{ type: 'command', command: buildCmd('learnship-session-state.js') }]
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// --- PostToolUse: learnship-context-monitor.js ---
|
|
1174
|
+
if (!settings.hooks[postToolEvent]) settings.hooks[postToolEvent] = [];
|
|
1175
|
+
const hasContextHook = settings.hooks[postToolEvent].some(entry =>
|
|
1176
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('learnship-context-monitor'))
|
|
1177
|
+
);
|
|
1178
|
+
if (!hasContextHook && fs.existsSync(path.join(hooksDest, 'learnship-context-monitor.js'))) {
|
|
1179
|
+
settings.hooks[postToolEvent].push({
|
|
1180
|
+
matcher: 'Bash|Edit|Write|MultiEdit',
|
|
1181
|
+
hooks: [{ type: 'command', command: buildCmd('learnship-context-monitor.js'), timeout: 10 }]
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// --- PreToolUse: learnship-prompt-guard.js ---
|
|
1186
|
+
if (!settings.hooks[preToolEvent]) settings.hooks[preToolEvent] = [];
|
|
1187
|
+
const hasPromptGuard = settings.hooks[preToolEvent].some(entry =>
|
|
1188
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('learnship-prompt-guard'))
|
|
1189
|
+
);
|
|
1190
|
+
if (!hasPromptGuard && fs.existsSync(path.join(hooksDest, 'learnship-prompt-guard.js'))) {
|
|
1191
|
+
settings.hooks[preToolEvent].push({
|
|
1192
|
+
matcher: 'Write|Edit',
|
|
1193
|
+
hooks: [{ type: 'command', command: buildCmd('learnship-prompt-guard.js'), timeout: 5 }]
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// --- statusLine: learnship-statusline.js ---
|
|
1198
|
+
if (!settings.statusLine && fs.existsSync(path.join(hooksDest, 'learnship-statusline.js'))) {
|
|
1199
|
+
settings.statusLine = {
|
|
1200
|
+
type: 'command',
|
|
1201
|
+
command: buildCmd('learnship-statusline.js')
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
writeSettings(settingsPath, settings);
|
|
1206
|
+
return copied;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Remove learnship hooks from settings.json and delete hook files.
|
|
1211
|
+
*/
|
|
1212
|
+
function uninstallClaudeHooks(targetDir) {
|
|
1213
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
1214
|
+
if (fs.existsSync(settingsPath)) {
|
|
1215
|
+
try {
|
|
1216
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1217
|
+
let modified = false;
|
|
1218
|
+
|
|
1219
|
+
// Remove learnship entries from hook arrays
|
|
1220
|
+
for (const event of ['SessionStart', 'PostToolUse', 'AfterTool', 'PreToolUse', 'BeforeTool']) {
|
|
1221
|
+
if (Array.isArray(settings.hooks?.[event])) {
|
|
1222
|
+
const before = settings.hooks[event].length;
|
|
1223
|
+
settings.hooks[event] = settings.hooks[event].filter(entry =>
|
|
1224
|
+
!entry.hooks || !entry.hooks.some(h => h.command && h.command.includes('learnship-'))
|
|
1225
|
+
);
|
|
1226
|
+
if (settings.hooks[event].length !== before) modified = true;
|
|
1227
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Remove statusLine if it's ours
|
|
1232
|
+
if (settings.statusLine?.command?.includes('learnship-')) {
|
|
1233
|
+
delete settings.statusLine;
|
|
1234
|
+
modified = true;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Clean empty hooks object
|
|
1238
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
1239
|
+
|
|
1240
|
+
if (modified) {
|
|
1241
|
+
writeSettings(settingsPath, settings);
|
|
1242
|
+
console.log(` ${green}✓${reset} Removed learnship hooks from settings.json`);
|
|
1243
|
+
}
|
|
1244
|
+
} catch (e) { /* ignore parse errors */ }
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Remove hook files
|
|
1248
|
+
const hooksDir = path.join(targetDir, 'hooks');
|
|
1249
|
+
if (fs.existsSync(hooksDir)) {
|
|
1250
|
+
let n = 0;
|
|
1251
|
+
for (const file of LEARNSHIP_MANAGED_HOOKS) {
|
|
1252
|
+
const fp = path.join(hooksDir, file);
|
|
1253
|
+
if (fs.existsSync(fp)) { fs.unlinkSync(fp); n++; }
|
|
1254
|
+
}
|
|
1255
|
+
if (n > 0) console.log(` ${green}✓${reset} Removed ${n} learnship hook files`);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Remove package.json if it's our minimal one
|
|
1259
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
1260
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
1261
|
+
try {
|
|
1262
|
+
const content = fs.readFileSync(pkgJsonPath, 'utf8').trim();
|
|
1263
|
+
if (content === '{"type":"commonjs"}') {
|
|
1264
|
+
fs.unlinkSync(pkgJsonPath);
|
|
1265
|
+
}
|
|
1266
|
+
} catch (e) { /* ignore */ }
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// ─── File manifest ────────────────────────────────────────────────────────
|
|
1271
|
+
|
|
1272
|
+
const crypto = require('crypto');
|
|
1273
|
+
|
|
1274
|
+
function fileHash(filePath) {
|
|
1275
|
+
const content = fs.readFileSync(filePath);
|
|
1276
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Generate install manifest with SHA-256 hashes for all installed files.
|
|
1281
|
+
*/
|
|
1282
|
+
function generateManifest(targetDir) {
|
|
1283
|
+
const manifest = {
|
|
1284
|
+
version: pkg.version,
|
|
1285
|
+
timestamp: new Date().toISOString(),
|
|
1286
|
+
files: {}
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
function scanDir(dir, prefix) {
|
|
1290
|
+
if (!fs.existsSync(dir)) return;
|
|
1291
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1292
|
+
const full = path.join(dir, entry.name);
|
|
1293
|
+
const rel = prefix ? prefix + '/' + entry.name : entry.name;
|
|
1294
|
+
if (entry.isDirectory()) {
|
|
1295
|
+
scanDir(full, rel);
|
|
1296
|
+
} else {
|
|
1297
|
+
manifest.files[rel] = fileHash(full);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Scan learnship/ payload
|
|
1303
|
+
const learnshipDir = path.join(targetDir, 'learnship');
|
|
1304
|
+
if (fs.existsSync(learnshipDir)) scanDir(learnshipDir, 'learnship');
|
|
1305
|
+
|
|
1306
|
+
// Scan hooks/
|
|
1307
|
+
const hooksDir = path.join(targetDir, 'hooks');
|
|
1308
|
+
if (fs.existsSync(hooksDir)) {
|
|
1309
|
+
for (const file of fs.readdirSync(hooksDir)) {
|
|
1310
|
+
if (file.startsWith('learnship-')) {
|
|
1311
|
+
manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
fs.writeFileSync(path.join(targetDir, 'learnship-file-manifest.json'), JSON.stringify(manifest, null, 2));
|
|
1317
|
+
return manifest;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Detect user-modified files by comparing against install manifest.
|
|
1322
|
+
* Backs up modified files to learnship-local-patches/.
|
|
1323
|
+
*/
|
|
1324
|
+
function saveLocalPatches(targetDir) {
|
|
1325
|
+
const manifestPath = path.join(targetDir, 'learnship-file-manifest.json');
|
|
1326
|
+
if (!fs.existsSync(manifestPath)) return [];
|
|
1327
|
+
|
|
1328
|
+
let manifest;
|
|
1329
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return []; }
|
|
1330
|
+
|
|
1331
|
+
const patchesDir = path.join(targetDir, 'learnship-local-patches');
|
|
1332
|
+
const modified = [];
|
|
1333
|
+
|
|
1334
|
+
for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
|
|
1335
|
+
const fullPath = path.join(targetDir, relPath);
|
|
1336
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
1337
|
+
const currentHash = fileHash(fullPath);
|
|
1338
|
+
if (currentHash !== originalHash) {
|
|
1339
|
+
const backupPath = path.join(patchesDir, relPath);
|
|
1340
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
1341
|
+
fs.copyFileSync(fullPath, backupPath);
|
|
1342
|
+
modified.push(relPath);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (modified.length > 0) {
|
|
1347
|
+
const meta = {
|
|
1348
|
+
backed_up_at: new Date().toISOString(),
|
|
1349
|
+
from_version: manifest.version,
|
|
1350
|
+
files: modified
|
|
1351
|
+
};
|
|
1352
|
+
fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify(meta, null, 2));
|
|
1353
|
+
console.log(`\n ${yellow}i${reset} Found ${modified.length} locally modified learnship file(s) — backed up to learnship-local-patches/`);
|
|
1354
|
+
for (const f of modified.slice(0, 5)) console.log(` ${dim}${f}${reset}`);
|
|
1355
|
+
if (modified.length > 5) console.log(` ${dim}... and ${modified.length - 5} more${reset}`);
|
|
1356
|
+
}
|
|
1357
|
+
return modified;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1087
1360
|
// ─── Main install function ─────────────────────────────────────────────────
|
|
1088
1361
|
function install(platform, isGlobal) {
|
|
1089
1362
|
// Cursor installs via the marketplace plugin, not this CLI.
|
|
@@ -1100,7 +1373,7 @@ function install(platform, isGlobal) {
|
|
|
1100
1373
|
}
|
|
1101
1374
|
|
|
1102
1375
|
const src = path.join(__dirname, '..');
|
|
1103
|
-
const targetDir = isGlobal ? getGlobalDir(platform) : path.join(process.cwd(), getDirName(platform));
|
|
1376
|
+
const targetDir = targetOverride || (isGlobal ? getGlobalDir(platform) : path.join(process.cwd(), getDirName(platform)));
|
|
1104
1377
|
const pathPrefix = `${targetDir.replace(/\\/g, '/')}/learnship/`;
|
|
1105
1378
|
const label = getPlatformLabel(platform);
|
|
1106
1379
|
const locationLabel = targetDir.replace(os.homedir(), '~');
|
|
@@ -1109,6 +1382,9 @@ function install(platform, isGlobal) {
|
|
|
1109
1382
|
|
|
1110
1383
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
1111
1384
|
|
|
1385
|
+
// Save locally modified files before overwriting
|
|
1386
|
+
saveLocalPatches(targetDir);
|
|
1387
|
+
|
|
1112
1388
|
const learnshipSrc = path.join(src, 'learnship');
|
|
1113
1389
|
const commandsSrc = path.join(src, 'commands', 'learnship');
|
|
1114
1390
|
const agentsSrc = path.join(src, 'agents');
|
|
@@ -1200,6 +1476,9 @@ function install(platform, isGlobal) {
|
|
|
1200
1476
|
} else {
|
|
1201
1477
|
failures.push('skills/');
|
|
1202
1478
|
}
|
|
1479
|
+
// Install native Claude Code hooks (settings.json + hook files)
|
|
1480
|
+
const hCount = installClaudeHooks(targetDir, isGlobal, 'claude');
|
|
1481
|
+
if (hCount > 0) console.log(` ${green}✓${reset} Installed ${hCount} hooks + settings.json (statusline, context monitor, prompt guard, session state)`);
|
|
1203
1482
|
} else if (platform === 'opencode') {
|
|
1204
1483
|
const count = installOpencodeCommands(commandsSrc, targetDir, pathPrefix);
|
|
1205
1484
|
console.log(` ${green}✓${reset} Installed ${count} commands to command/ (flat)`);
|
|
@@ -1222,6 +1501,9 @@ function install(platform, isGlobal) {
|
|
|
1222
1501
|
writeSettings(settingsPath, settings);
|
|
1223
1502
|
console.log(` ${green}✓${reset} Enabled experimental.enableAgents in settings.json`);
|
|
1224
1503
|
}
|
|
1504
|
+
// Install native Gemini hooks (settings.json + hook files)
|
|
1505
|
+
const hCount = installClaudeHooks(targetDir, isGlobal, 'gemini');
|
|
1506
|
+
if (hCount > 0) console.log(` ${green}✓${reset} Installed ${hCount} hooks + settings.json (statusline, context monitor, prompt guard, session state)`);
|
|
1225
1507
|
} else if (platform === 'codex') {
|
|
1226
1508
|
const count = installCodexSkills(commandsSrc, targetDir, pathPrefix);
|
|
1227
1509
|
console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
|
|
@@ -1237,7 +1519,11 @@ function install(platform, isGlobal) {
|
|
|
1237
1519
|
// 4. Scan for leaked .claude paths
|
|
1238
1520
|
scanForLeakedPaths(targetDir, platform);
|
|
1239
1521
|
|
|
1240
|
-
// 5.
|
|
1522
|
+
// 5. Generate file manifest for upgrade safety
|
|
1523
|
+
generateManifest(targetDir);
|
|
1524
|
+
console.log(` ${green}✓${reset} Generated learnship-file-manifest.json`);
|
|
1525
|
+
|
|
1526
|
+
// 6. Post-install tips
|
|
1241
1527
|
const firstCmd = platform === 'windsurf' ? '/ls' :
|
|
1242
1528
|
platform === 'claude' ? '/learnship:ls' :
|
|
1243
1529
|
platform === 'opencode' ? '/learnship-ls' :
|
|
@@ -1251,7 +1537,7 @@ function install(platform, isGlobal) {
|
|
|
1251
1537
|
|
|
1252
1538
|
// ─── Uninstall function ────────────────────────────────────────────────────
|
|
1253
1539
|
function uninstall(platform, isGlobal) {
|
|
1254
|
-
const targetDir = isGlobal ? getGlobalDir(platform) : path.join(process.cwd(), getDirName(platform));
|
|
1540
|
+
const targetDir = targetOverride || (isGlobal ? getGlobalDir(platform) : path.join(process.cwd(), getDirName(platform)));
|
|
1255
1541
|
const label = getPlatformLabel(platform);
|
|
1256
1542
|
const locationLabel = targetDir.replace(os.homedir(), '~');
|
|
1257
1543
|
console.log(`\n Uninstalling learnship from ${cyan}${label}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
@@ -1374,6 +1660,18 @@ function uninstall(platform, isGlobal) {
|
|
|
1374
1660
|
if (n > 0) { removed++; console.log(` ${green}✓${reset} Removed ${n} learnship agent files`); }
|
|
1375
1661
|
}
|
|
1376
1662
|
|
|
1663
|
+
// 4. Remove hooks and settings.json entries (Claude Code / Gemini)
|
|
1664
|
+
if (platform === 'claude' || platform === 'gemini') {
|
|
1665
|
+
uninstallClaudeHooks(targetDir);
|
|
1666
|
+
removed++;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// 5. Remove file manifest and local patches
|
|
1670
|
+
const manifestPath = path.join(targetDir, 'learnship-file-manifest.json');
|
|
1671
|
+
if (fs.existsSync(manifestPath)) { fs.unlinkSync(manifestPath); removed++; console.log(` ${green}✓${reset} Removed learnship-file-manifest.json`); }
|
|
1672
|
+
const patchesDir = path.join(targetDir, 'learnship-local-patches');
|
|
1673
|
+
if (fs.existsSync(patchesDir)) { fs.rmSync(patchesDir, { recursive: true }); removed++; console.log(` ${green}✓${reset} Removed learnship-local-patches/`); }
|
|
1674
|
+
|
|
1377
1675
|
if (removed === 0) console.log(` ${yellow}⚠${reset} No learnship files found.`);
|
|
1378
1676
|
else console.log(`\n ${green}Done!${reset} learnship uninstalled from ${label}. Your other files and settings were preserved.`);
|
|
1379
1677
|
}
|
|
@@ -1455,6 +1753,10 @@ if (process.env.LEARNSHIP_TEST_MODE) {
|
|
|
1455
1753
|
rewriteNewProject,
|
|
1456
1754
|
rewriteAgentsMd,
|
|
1457
1755
|
installClaudeSkills,
|
|
1756
|
+
installClaudeHooks,
|
|
1757
|
+
uninstallClaudeHooks,
|
|
1758
|
+
generateManifest,
|
|
1759
|
+
saveLocalPatches,
|
|
1458
1760
|
toHomePrefix,
|
|
1459
1761
|
LEARNSHIP_CODEX_MARKER,
|
|
1460
1762
|
CODEX_AGENT_SANDBOX,
|
|
@@ -47,7 +47,7 @@ When the user runs `/new-project`, execute these **9 mandatory steps in order**.
|
|
|
47
47
|
|
|
48
48
|
2. **Research decision = always ask the user.** After PROJECT.md is confirmed, ask: "Do you want me to research the domain ecosystem first?" and WAIT for the user's reply. You are FORBIDDEN from deciding this yourself — even if the tech stack is defined in PROJECT.md, the domain seems trivial, or the user gave detailed answers. Never say "no research needed" or "skipping research" on your own.
|
|
49
49
|
|
|
50
|
-
3. **Research = 5
|
|
50
|
+
3. **Research = WRITE 5 FILES TO DISK.** "Research" means creating files on the filesystem, not thinking or browsing. If the user chooses research, write exactly 5 files to `.planning/research/`: `STACK.md`, `FEATURES.md`, `ARCHITECTURE.md`, `PITFALLS.md`, `SUMMARY.md`. Do NOT do web searches or domain analysis and then say "I have enough research data" without writing the files — that is a workflow failure. Run the `node -e` verification command — it must print `RESEARCH VERIFIED OK` before proceeding to requirements.
|
|
51
51
|
|
|
52
52
|
4. **After Step 7 (roadmap approved):** Do NOT display the done banner or suggest next steps. Generate AGENTS.md (Step 8) first.
|
|
53
53
|
|
|
@@ -141,13 +141,23 @@ Available actions: `adapt`, `animate`, `arrange`, `audit`, `bolder`, `clarify`,
|
|
|
141
141
|
|
|
142
142
|
## Parallel Execution
|
|
143
143
|
|
|
144
|
-
Cursor supports real parallel subagents. During `/new-project`
|
|
144
|
+
Cursor supports real parallel subagents. During `/new-project` Step 2 configuration, one of the questions asks about parallel execution:
|
|
145
145
|
|
|
146
|
-
"Do you want to enable parallel subagent execution?"
|
|
147
146
|
- **No** (recommended default) — Plans execute sequentially, one at a time. Safer, easier to follow.
|
|
148
147
|
- **Yes** — Each independent plan in a wave gets its own dedicated subagent with a fresh context budget. Faster, but uses more tokens.
|
|
149
148
|
|
|
150
|
-
Set `"parallelization": true|false` in `.planning/config.json` based on the user's choice.
|
|
149
|
+
Set `"parallelization": { "enabled": true|false }` in `.planning/config.json` based on the user's choice.
|
|
150
|
+
|
|
151
|
+
## Structured Questions
|
|
152
|
+
|
|
153
|
+
When workflows include `AskUserQuestion()` blocks, **Cursor has no native structured question tool**. Present each question as a numbered text list with descriptions and ask the user to reply with their choice number or label. Example:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
**Working Style**
|
|
157
|
+
How do you want to work?
|
|
158
|
+
1. YOLO (Recommended) — Auto-approve steps, just execute
|
|
159
|
+
2. Interactive — Confirm at each step
|
|
160
|
+
```
|
|
151
161
|
|
|
152
162
|
## Learning Mode
|
|
153
163
|
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "learnship",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Agentic engineering done right — 57 structured workflows, persistent memory across sessions, integrated learning partner, and impeccable UI design system.",
|
|
5
5
|
"author": "Favio Vazquez",
|
|
6
6
|
"homepage": "https://faviovazquez.github.io/learnship/",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// learnship-hook-version: 2.2.0
|
|
3
|
+
// Context Monitor — PostToolUse hook
|
|
4
|
+
// Reads context metrics from the statusline bridge file and injects
|
|
5
|
+
// warnings when context usage is high. Makes the AGENT aware of
|
|
6
|
+
// context limits (the statusline only shows the user).
|
|
7
|
+
//
|
|
8
|
+
// Thresholds:
|
|
9
|
+
// WARNING (remaining <= 35%): Agent should wrap up current task
|
|
10
|
+
// CRITICAL (remaining <= 25%): Agent should stop and save state
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const WARNING_THRESHOLD = 35;
|
|
17
|
+
const CRITICAL_THRESHOLD = 25;
|
|
18
|
+
const STALE_SECONDS = 60;
|
|
19
|
+
const DEBOUNCE_CALLS = 5;
|
|
20
|
+
|
|
21
|
+
let input = '';
|
|
22
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 10000);
|
|
23
|
+
process.stdin.setEncoding('utf8');
|
|
24
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
25
|
+
process.stdin.on('end', () => {
|
|
26
|
+
clearTimeout(stdinTimeout);
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(input);
|
|
29
|
+
const sessionId = data.session_id;
|
|
30
|
+
|
|
31
|
+
if (!sessionId) process.exit(0);
|
|
32
|
+
if (/[/\\]|\.\./.test(sessionId)) process.exit(0);
|
|
33
|
+
|
|
34
|
+
// Check if context warnings are disabled via config
|
|
35
|
+
const cwd = data.cwd || process.cwd();
|
|
36
|
+
const planningDir = path.join(cwd, '.planning');
|
|
37
|
+
if (fs.existsSync(planningDir)) {
|
|
38
|
+
try {
|
|
39
|
+
const configPath = path.join(planningDir, 'config.json');
|
|
40
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
41
|
+
if (config.hooks?.context_warnings === false) process.exit(0);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// Ignore config read errors
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tmpDir = os.tmpdir();
|
|
48
|
+
const metricsPath = path.join(tmpDir, `learnship-ctx-${sessionId}.json`);
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(metricsPath)) process.exit(0);
|
|
51
|
+
|
|
52
|
+
const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
|
|
53
|
+
const now = Math.floor(Date.now() / 1000);
|
|
54
|
+
|
|
55
|
+
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) process.exit(0);
|
|
56
|
+
|
|
57
|
+
const remaining = metrics.remaining_percentage;
|
|
58
|
+
const usedPct = metrics.used_pct;
|
|
59
|
+
|
|
60
|
+
if (remaining > WARNING_THRESHOLD) process.exit(0);
|
|
61
|
+
|
|
62
|
+
// Debounce
|
|
63
|
+
const warnPath = path.join(tmpDir, `learnship-ctx-${sessionId}-warned.json`);
|
|
64
|
+
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
|
65
|
+
let firstWarn = true;
|
|
66
|
+
|
|
67
|
+
if (fs.existsSync(warnPath)) {
|
|
68
|
+
try {
|
|
69
|
+
warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
|
|
70
|
+
firstWarn = false;
|
|
71
|
+
} catch (e) {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
|
|
75
|
+
|
|
76
|
+
const isCritical = remaining <= CRITICAL_THRESHOLD;
|
|
77
|
+
const currentLevel = isCritical ? 'critical' : 'warning';
|
|
78
|
+
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
|
79
|
+
|
|
80
|
+
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
|
81
|
+
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
warnData.callsSinceWarn = 0;
|
|
86
|
+
warnData.lastLevel = currentLevel;
|
|
87
|
+
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
88
|
+
|
|
89
|
+
const isProjectActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
|
|
90
|
+
|
|
91
|
+
let message;
|
|
92
|
+
if (isCritical) {
|
|
93
|
+
message = isProjectActive
|
|
94
|
+
? `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
95
|
+
'Context is nearly exhausted. Do NOT start new complex work. ' +
|
|
96
|
+
'Inform the user so they can run /pause-work at the next natural stopping point.'
|
|
97
|
+
: `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
98
|
+
'Context is nearly exhausted. Inform the user that context is low and ask how they want to proceed.';
|
|
99
|
+
} else {
|
|
100
|
+
message = isProjectActive
|
|
101
|
+
? `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
102
|
+
'Context is getting limited. Avoid starting new complex work. If not between ' +
|
|
103
|
+
'defined plan steps, inform the user so they can prepare to pause.'
|
|
104
|
+
: `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
105
|
+
'Be aware that context is getting limited. Avoid unnecessary exploration or starting new complex work.';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hookEventName = process.env.GEMINI_API_KEY ? 'AfterTool' : 'PostToolUse';
|
|
109
|
+
const output = {
|
|
110
|
+
hookSpecificOutput: {
|
|
111
|
+
hookEventName,
|
|
112
|
+
additionalContext: message
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
process.stdout.write(JSON.stringify(output));
|
|
117
|
+
} catch (e) {
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
});
|