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.
Files changed (45) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.cursor-plugin/plugin.json +1 -1
  3. package/README.md +172 -155
  4. package/SKILL.md +23 -2
  5. package/bin/install.js +305 -3
  6. package/commands/learnship/diagnose-issues.md +1 -0
  7. package/commands/learnship/discuss-phase.md +1 -0
  8. package/commands/learnship/ideate.md +1 -0
  9. package/commands/learnship/list-phase-assumptions.md +1 -0
  10. package/commands/learnship/quick.md +1 -0
  11. package/commands/learnship/research-phase.md +1 -0
  12. package/commands/learnship/secure-phase.md +1 -0
  13. package/commands/learnship/validate-phase.md +2 -0
  14. package/commands/learnship/verify-work.md +1 -0
  15. package/cursor-rules/learnship.mdc +14 -4
  16. package/gemini-extension.json +1 -1
  17. package/hooks/learnship-context-monitor.js +120 -0
  18. package/hooks/learnship-prompt-guard.js +75 -0
  19. package/hooks/learnship-session-state.js +136 -0
  20. package/hooks/learnship-statusline.js +179 -0
  21. package/learnship/contexts/dev.md +21 -0
  22. package/learnship/contexts/research.md +22 -0
  23. package/learnship/contexts/review.md +22 -0
  24. package/learnship/templates/research-project/ARCHITECTURE.md +140 -0
  25. package/learnship/templates/research-project/FEATURES.md +130 -0
  26. package/learnship/templates/research-project/PITFALLS.md +102 -0
  27. package/learnship/templates/research-project/STACK.md +105 -0
  28. package/learnship/templates/research-project/SUMMARY.md +111 -0
  29. package/learnship/workflows/challenge.md +16 -4
  30. package/learnship/workflows/debug.md +30 -6
  31. package/learnship/workflows/diagnose-issues.md +14 -1
  32. package/learnship/workflows/discuss-milestone.md +15 -1
  33. package/learnship/workflows/discuss-phase.md +83 -10
  34. package/learnship/workflows/ideate.md +25 -5
  35. package/learnship/workflows/list-phase-assumptions.md +12 -5
  36. package/learnship/workflows/new-milestone.md +12 -6
  37. package/learnship/workflows/new-project.md +229 -85
  38. package/learnship/workflows/quick.md +18 -4
  39. package/learnship/workflows/research-phase.md +43 -8
  40. package/learnship/workflows/secure-phase.md +57 -15
  41. package/learnship/workflows/settings.md +142 -142
  42. package/learnship/workflows/validate-phase.md +39 -12
  43. package/learnship/workflows/verify-work.md +27 -0
  44. package/package.json +1 -1
  45. 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. Post-install tips
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,
@@ -7,6 +7,7 @@ allowed-tools:
7
7
  - Bash
8
8
  - Write
9
9
  - Task
10
+ - AskUserQuestion
10
11
  ---
11
12
 
12
13
  <execution_context>
@@ -7,6 +7,7 @@ allowed-tools:
7
7
  - Bash
8
8
  - Write
9
9
  - Task
10
+ - AskUserQuestion
10
11
  ---
11
12
 
12
13
  <execution_context>
@@ -7,6 +7,7 @@ allowed-tools:
7
7
  - Bash
8
8
  - Write
9
9
  - Task
10
+ - AskUserQuestion
10
11
  ---
11
12
 
12
13
  <execution_context>
@@ -5,6 +5,7 @@ argument-hint: "[N]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Bash
8
+ - AskUserQuestion
8
9
  ---
9
10
 
10
11
  <execution_context>
@@ -7,6 +7,7 @@ allowed-tools:
7
7
  - Bash
8
8
  - Write
9
9
  - Task
10
+ - AskUserQuestion
10
11
  ---
11
12
 
12
13
  <execution_context>
@@ -7,6 +7,7 @@ allowed-tools:
7
7
  - Bash
8
8
  - Write
9
9
  - Task
10
+ - AskUserQuestion
10
11
  ---
11
12
 
12
13
  <execution_context>
@@ -7,6 +7,7 @@ allowed-tools:
7
7
  - Bash
8
8
  - Write
9
9
  - Task
10
+ - AskUserQuestion
10
11
  ---
11
12
 
12
13
  <execution_context>
@@ -6,6 +6,8 @@ allowed-tools:
6
6
  - Read
7
7
  - Bash
8
8
  - Write
9
+ - Task
10
+ - AskUserQuestion
9
11
  ---
10
12
 
11
13
  <execution_context>
@@ -7,6 +7,7 @@ allowed-tools:
7
7
  - Bash
8
8
  - Write
9
9
  - Task
10
+ - AskUserQuestion
10
11
  ---
11
12
 
12
13
  <execution_context>
@@ -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 separate files.** If the user chooses research, create exactly 5 files in `.planning/research/`: `STACK.md`, `FEATURES.md`, `ARCHITECTURE.md`, `PITFALLS.md`, `SUMMARY.md`. Do NOT write a single monolithic research file. Run the `node -e` verification command from the workflow — it must print `RESEARCH VERIFIED OK` before proceeding.
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` setup (Group D), ask:
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "learnship",
3
- "version": "2.1.1",
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
+ });