pan-wizard 3.8.0 → 3.10.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 (49) hide show
  1. package/README.md +4 -1
  2. package/agents/pan-conductor.md +1 -2
  3. package/agents/pan-counterfactual.md +1 -2
  4. package/agents/pan-debugger.md +1 -2
  5. package/agents/pan-distiller.md +1 -2
  6. package/agents/pan-document_code.md +1 -0
  7. package/agents/pan-executor.md +1 -0
  8. package/agents/pan-experiment-runner.md +1 -2
  9. package/agents/pan-hardener.md +1 -2
  10. package/agents/pan-integration-checker.md +1 -2
  11. package/agents/pan-knowledge.md +1 -2
  12. package/agents/pan-meta-reviewer.md +1 -2
  13. package/agents/pan-optimizer.md +1 -0
  14. package/agents/pan-phase-researcher.md +1 -0
  15. package/agents/pan-plan-checker.md +1 -2
  16. package/agents/pan-planner.md +1 -0
  17. package/agents/pan-previewer.md +1 -2
  18. package/agents/pan-project-researcher.md +6 -0
  19. package/agents/pan-research-synthesizer.md +7 -0
  20. package/agents/pan-reviewer.md +2 -3
  21. package/agents/pan-roadmapper.md +1 -0
  22. package/agents/pan-verifier.md +1 -2
  23. package/bin/install-lib.cjs +661 -46
  24. package/bin/install.js +722 -116
  25. package/commands/pan/experiment.md +2 -0
  26. package/commands/pan/profile.md +2 -0
  27. package/hooks/dist/pan-cost-logger.js +22 -7
  28. package/package.json +5 -4
  29. package/pan-wizard-core/bin/lib/commands-learnings.cjs +544 -0
  30. package/pan-wizard-core/bin/lib/commands.cjs +12 -523
  31. package/pan-wizard-core/bin/lib/core.cjs +69 -0
  32. package/pan-wizard-core/bin/lib/cost.cjs +62 -8
  33. package/pan-wizard-core/bin/lib/git.cjs +6 -1
  34. package/pan-wizard-core/bin/lib/lock.cjs +108 -0
  35. package/pan-wizard-core/bin/lib/milestone.cjs +3 -2
  36. package/pan-wizard-core/bin/lib/phase-remove.cjs +392 -0
  37. package/pan-wizard-core/bin/lib/phase.cjs +4 -369
  38. package/pan-wizard-core/bin/lib/runner.cjs +5 -0
  39. package/pan-wizard-core/bin/lib/state.cjs +10 -1
  40. package/pan-wizard-core/bin/lib/verify-deploy.cjs +181 -0
  41. package/pan-wizard-core/bin/lib/verify-drift.cjs +255 -0
  42. package/pan-wizard-core/bin/lib/verify-preflight.cjs +261 -0
  43. package/pan-wizard-core/bin/lib/verify-retro.cjs +177 -0
  44. package/pan-wizard-core/bin/lib/verify.cjs +10 -797
  45. package/pan-wizard-core/bin/pan-tools.cjs +10 -0
  46. package/pan-wizard-core/workflows/plan-phase.md +11 -0
  47. package/scripts/build-plugin.js +105 -0
  48. package/scripts/install-git-hooks.js +64 -0
  49. package/scripts/release-check.js +13 -2
@@ -3,11 +3,12 @@
3
3
  */
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { safeReadFile, loadConfig, isGitIgnored, isGitRepo, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, detectProvider, resolveTierToModel, estimateCostMultiplier, MODEL_PROFILES, output, error, findPhaseInternal, scanPendingTodos, toPosix } = require('./core.cjs');
6
+ const { safeReadFile, loadConfig, isGitIgnored, isGitRepo, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, resolveEffortInternal, detectProvider, resolveTierToModel, estimateCostMultiplier, MODEL_PROFILES, output, error, findPhaseInternal, scanPendingTodos, toPosix } = require('./core.cjs');
7
7
  const { extractFrontmatter } = require('./frontmatter.cjs');
8
8
  const { PLANNING_DIR, PHASES_DIR, MILESTONES_DIR, QUICK_DIR, STATE_FILE, ROADMAP_FILE, PROJECT_FILE, PATTERNS_FILE, SESSION_HISTORY_FILE, LEARNINGS_FILE, CONTEXT_SUFFIX, UAT_SUFFIX, VERIFICATION_SUFFIX, isPlanFile, isSummaryFile, ARCHIVE_DIR_RE, PHASE_DIR_RE, CONTEXT_WINDOW, WARNING_THRESHOLD, CRITICAL_THRESHOLD, VALID_COMMIT_TYPES, DEFAULT_SENSITIVE_PATTERNS } = require('./constants.cjs');
9
9
  const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles } = require('./utils.cjs');
10
10
  const { estimateTokens } = require('./context-budget.cjs');
11
+ const { collectPhaseSummaries, readErrorPatterns, appendErrorPattern, appendSessionSummary, parseLearnings, formatLearningEntry, cmdLearningsExtract, cmdLearningsList, cmdLearningsPrune } = require('./commands-learnings.cjs');
11
12
 
12
13
  /**
13
14
  * Generate a URL-safe slug from text by lowercasing and replacing non-alphanumeric chars.
@@ -93,64 +94,7 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
93
94
 
94
95
  // ---- History digest helper functions ----------------------------------------
95
96
 
96
- /**
97
- * Scan all phase directories (archived + current) and read summary frontmatter.
98
- * Returns an array of { phaseNum, dirName, frontmatter } objects for each summary found.
99
- *
100
- * Algorithm overview:
101
- * 1. Collect archived phase dirs from milestone archives (oldest milestones first)
102
- * 2. Collect current phase dirs from .planning/phases/
103
- * 3. For each directory, read all *-summary.md files and extract frontmatter
104
- *
105
- * @param {string} cwd - Working directory path
106
- * @returns {{ allPhaseDirs: Array, summaries: Array<{phaseNum: string, dirName: string, frontmatter: Object}> }}
107
- */
108
- function collectPhaseSummaries(cwd) {
109
- const phasesDir = phasesPath(cwd);
110
-
111
- // Collect all phase directories: archived + current
112
- const allPhaseDirs = [];
113
-
114
- // Add archived phases first (oldest milestones first)
115
- const archived = getArchivedPhaseDirs(cwd);
116
- for (const archiveEntry of archived) {
117
- allPhaseDirs.push({ name: archiveEntry.name, fullPath: archiveEntry.fullPath, milestone: archiveEntry.milestone });
118
- }
119
-
120
- // Add current phases
121
- try {
122
- const currentDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
123
- .filter(entry => entry.isDirectory())
124
- .map(entry => entry.name)
125
- .sort();
126
- for (const dirName of currentDirs) {
127
- allPhaseDirs.push({ name: dirName, fullPath: path.join(phasesDir, dirName), milestone: null });
128
- }
129
- } catch { /* phases dir missing or unreadable */ }
130
-
131
- const summaries = [];
132
-
133
- for (const { name: dirName, fullPath: dirPath } of allPhaseDirs) {
134
- let summaryFiles;
135
- try {
136
- summaryFiles = fs.readdirSync(dirPath).filter(filename => isSummaryFile(filename));
137
- } catch { continue; }
138
-
139
- for (const summaryFile of summaryFiles) {
140
- try {
141
- const content = fs.readFileSync(path.join(dirPath, summaryFile), 'utf-8');
142
- const frontmatter = extractFrontmatter(content);
143
- const phaseNum = frontmatter.phase || dirName.split('-')[0];
144
-
145
- summaries.push({ phaseNum, dirName, frontmatter });
146
- } catch {
147
- // Skip malformed summary files (broken YAML, unreadable)
148
- }
149
- }
150
- }
151
-
152
- return { allPhaseDirs, summaries };
153
- }
97
+ // collectPhaseSummaries — extracted to commands-learnings.cjs (imported above)
154
98
 
155
99
  /**
156
100
  * Aggregate tech stack entries from all summary frontmatters into a unified Set.
@@ -293,13 +237,14 @@ function cmdResolveModel(cwd, agentType, raw, metadataJson) {
293
237
  const agentModels = MODEL_PROFILES[agentType];
294
238
  if (!agentModels) {
295
239
  const model = resolveTierToModel('mid', detectProvider(cwd, config));
296
- const result = { model, profile, strategy, unknown_agent: true };
240
+ const result = { model, profile, strategy, effort: resolveEffortInternal(cwd, agentType), unknown_agent: true };
297
241
  output(result, raw, model);
298
242
  return;
299
243
  }
300
244
 
301
245
  const model = resolveModelInternal(cwd, agentType, taskMetadata);
302
- const result = { model, profile, strategy };
246
+ const effort = resolveEffortInternal(cwd, agentType);
247
+ const result = { model, profile, strategy, effort };
303
248
  output(result, raw, model);
304
249
  }
305
250
 
@@ -902,12 +847,14 @@ function cmdRollbackSnapshot(cwd, phase, raw) {
902
847
  }
903
848
  const hash = headResult.stdout;
904
849
 
905
- // Create tag
906
- let tagResult = execGit(cwd, ['tag', tagName]);
850
+ // Create tag. tag.gpgsign=true in user config turns plain `git tag` into a
851
+ // sign-or-fail operation ("fatal: no tag message?") in non-interactive runs —
852
+ // PAN snapshot tags are automation markers, so signing is explicitly disabled.
853
+ let tagResult = execGit(cwd, ['-c', 'tag.gpgsign=false', 'tag', tagName]);
907
854
  if (tagResult.exitCode !== 0) {
908
855
  // Tag might already exist — try with suffix
909
856
  tagName = tagName + '-1';
910
- tagResult = execGit(cwd, ['tag', tagName]);
857
+ tagResult = execGit(cwd, ['-c', 'tag.gpgsign=false', 'tag', tagName]);
911
858
  if (tagResult.exitCode !== 0) {
912
859
  const result = { tag: null, hash, phase, warning: 'Failed to create tag: ' + tagResult.stderr };
913
860
  output(result, raw, '');
@@ -978,465 +925,7 @@ function shouldSkipTests(files) {
978
925
  return files.every(f => /\.md$/i.test(f));
979
926
  }
980
927
 
981
- /**
982
- * Read error patterns from .planning/patterns.md.
983
- * Parses PAT-NNN entries into structured objects.
984
- * @param {string} cwd - Working directory path
985
- * @returns {Array<{id: string, title: string, wrong: string, right: string, context: string|null, date: string|null}>}
986
- */
987
- function readErrorPatterns(cwd) {
988
- const filePath = path.join(cwd, PLANNING_DIR, PATTERNS_FILE);
989
- let content;
990
- try {
991
- content = fs.readFileSync(filePath, 'utf-8');
992
- } catch {
993
- return [];
994
- }
995
-
996
- if (!content || !content.trim()) {
997
- return [];
998
- }
999
-
1000
- const patterns = [];
1001
- // Split on PAT-NNN headers
1002
- const sections = content.split(/^### (PAT-\d+):\s*/m);
1003
- // sections[0] = preamble, then alternating [id, body, id, body, ...]
1004
- for (let i = 1; i < sections.length; i += 2) {
1005
- const id = sections[i];
1006
- const body = sections[i + 1] || '';
1007
-
1008
- // Title is the first line of the body
1009
- const lines = body.split('\n');
1010
- const title = lines[0] ? lines[0].trim() : '';
1011
- const rest = lines.slice(1).join('\n');
1012
-
1013
- const wrongMatch = rest.match(/\*\*Wrong:\*\*\s*(.+)/);
1014
- const rightMatch = rest.match(/\*\*Right:\*\*\s*(.+)/);
1015
- const contextMatch = rest.match(/\*\*Context:\*\*\s*(.+)/);
1016
- const dateMatch = rest.match(/\*\*Date:\*\*\s*(.+)/);
1017
-
1018
- // Skip entries missing required fields
1019
- if (!wrongMatch || !rightMatch) continue;
1020
-
1021
- patterns.push({
1022
- id,
1023
- title,
1024
- wrong: wrongMatch ? wrongMatch[1].trim() : null,
1025
- right: rightMatch ? rightMatch[1].trim() : null,
1026
- context: contextMatch ? contextMatch[1].trim() : null,
1027
- date: dateMatch ? dateMatch[1].trim() : null,
1028
- });
1029
- }
1030
-
1031
- return patterns;
1032
- }
1033
-
1034
- /**
1035
- * Append a new error pattern entry to .planning/patterns.md.
1036
- * Auto-increments the PAT-NNN ID. Creates file if missing.
1037
- * @param {string} cwd - Working directory path
1038
- * @param {Object} pattern - Pattern to append
1039
- * @param {string} pattern.wrong - What went wrong
1040
- * @param {string} pattern.right - What is correct
1041
- * @param {string} [pattern.title] - Short title
1042
- * @param {string} [pattern.context] - Additional context
1043
- * @param {string} [pattern.date] - Date string (defaults to today)
1044
- * @returns {{ id: string } | { error: string }}
1045
- */
1046
- function appendErrorPattern(cwd, pattern) {
1047
- if (!pattern || !pattern.wrong || !pattern.right) {
1048
- return { error: "Pattern requires 'wrong' and 'right' fields" };
1049
- }
1050
-
1051
- const filePath = path.join(cwd, PLANNING_DIR, PATTERNS_FILE);
1052
- const existing = readErrorPatterns(cwd);
1053
-
1054
- // Determine next ID
1055
- let maxNum = 0;
1056
- for (const p of existing) {
1057
- const m = p.id.match(/PAT-(\d+)/);
1058
- if (m) maxNum = Math.max(maxNum, parseInt(m[1], 10));
1059
- }
1060
- const nextId = `PAT-${String(maxNum + 1).padStart(3, '0')}`;
1061
-
1062
- const date = pattern.date || new Date().toISOString().split('T')[0];
1063
- const title = pattern.title || 'Untitled';
1064
-
1065
- const entry = [
1066
- '',
1067
- `### ${nextId}: ${title}`,
1068
- `**Wrong:** ${pattern.wrong}`,
1069
- `**Right:** ${pattern.right}`,
1070
- pattern.context ? `**Context:** ${pattern.context}` : null,
1071
- `**Date:** ${date}`,
1072
- '',
1073
- ].filter(line => line !== null).join('\n');
1074
-
1075
- try {
1076
- let existingContent = '';
1077
- try {
1078
- existingContent = fs.readFileSync(filePath, 'utf-8');
1079
- } catch {
1080
- // File doesn't exist — create with header
1081
- existingContent = '# Error Patterns\n';
1082
- }
1083
- fs.writeFileSync(filePath, existingContent.trimEnd() + '\n' + entry, 'utf-8');
1084
- return { id: nextId };
1085
- } catch (e) {
1086
- return { error: `Failed to write pattern: ${e.message}` };
1087
- }
1088
- }
1089
-
1090
- /**
1091
- * Append a session summary to .planning/session-history.md.
1092
- * Creates file with header if missing. Keeps last 20 entries.
1093
- * @param {string} cwd - Working directory path
1094
- * @param {Object} summary - Session summary
1095
- * @param {string} summary.phase - Phase identifier
1096
- * @param {number} [summary.plans_executed] - Plans executed
1097
- * @param {number} [summary.tests_before] - Test count before
1098
- * @param {number} [summary.tests_after] - Test count after
1099
- * @param {string} [summary.key_decisions] - Key decisions made
1100
- * @param {string} [summary.date] - Date string (defaults to today)
1101
- * @returns {{ appended: boolean } | { error: string }}
1102
- */
1103
- function appendSessionSummary(cwd, summary) {
1104
- if (!summary || !summary.phase) {
1105
- return { error: "Summary requires 'phase' field" };
1106
- }
1107
-
1108
- const filePath = path.join(cwd, PLANNING_DIR, SESSION_HISTORY_FILE);
1109
- const date = summary.date || new Date().toISOString().split('T')[0];
1110
-
1111
- const entry = [
1112
- `### Session — ${date}`,
1113
- `- **Phase:** ${summary.phase}`,
1114
- summary.plans_executed != null ? `- **Plans Executed:** ${summary.plans_executed}` : null,
1115
- summary.tests_before != null ? `- **Tests Before:** ${summary.tests_before}` : null,
1116
- summary.tests_after != null ? `- **Tests After:** ${summary.tests_after}` : null,
1117
- summary.key_decisions ? `- **Key Decisions:** ${summary.key_decisions}` : null,
1118
- '',
1119
- ].filter(line => line !== null).join('\n');
1120
-
1121
- try {
1122
- let content = '';
1123
- try {
1124
- content = fs.readFileSync(filePath, 'utf-8');
1125
- } catch {
1126
- content = '# Session History\n\n';
1127
- }
1128
-
1129
- content = content.trimEnd() + '\n\n' + entry;
1130
-
1131
- // Keep last 20 entries — split on session headers, trim oldest
1132
- const SESSION_HEADER_RE = /^### Session — /m;
1133
- const parts = content.split(SESSION_HEADER_RE);
1134
- // parts[0] = header, parts[1..N] = session entries
1135
- if (parts.length > 21) { // header + 20 entries
1136
- const header = parts[0];
1137
- const kept = parts.slice(parts.length - 20);
1138
- content = header.trimEnd() + '\n\n' + kept.map(p => '### Session — ' + p).join('');
1139
- }
1140
-
1141
- fs.writeFileSync(filePath, content, 'utf-8');
1142
- return { appended: true };
1143
- } catch (e) {
1144
- return { error: `Failed to write session summary: ${e.message}` };
1145
- }
1146
- }
1147
-
1148
- // ---- Session Learnings ---------------------------------------------------------
1149
-
1150
- /**
1151
- * Parse learnings.md into structured entries.
1152
- * Each learning has: id, type, title, detail, files (optional), date.
1153
- * @param {string} content - Raw content of learnings.md
1154
- * @returns {Array<{id: string, type: string, title: string, detail: string, files: string[], date: string|null}>}
1155
- */
1156
- function parseLearnings(content) {
1157
- if (!content || !content.trim()) return [];
1158
-
1159
- const learnings = [];
1160
- const sections = content.split(/^### (LEARN-\d+):\s*/m);
1161
- // sections[0] = preamble, then alternating [id, body, ...]
1162
- for (let i = 1; i < sections.length; i += 2) {
1163
- const id = sections[i];
1164
- const body = sections[i + 1] || '';
1165
-
1166
- const lines = body.split('\n');
1167
- const title = lines[0] ? lines[0].trim() : '';
1168
- const rest = lines.slice(1).join('\n');
1169
-
1170
- const typeMatch = rest.match(/\*\*Type:\*\*\s*(.+)/);
1171
- const detailMatch = rest.match(/\*\*Detail:\*\*\s*(.+)/);
1172
- const filesMatch = rest.match(/\*\*Files:\*\*\s*(.+)/);
1173
- const dateMatch = rest.match(/\*\*Date:\*\*\s*(.+)/);
1174
-
1175
- learnings.push({
1176
- id,
1177
- type: typeMatch ? typeMatch[1].trim() : 'unknown',
1178
- title,
1179
- detail: detailMatch ? detailMatch[1].trim() : '',
1180
- files: filesMatch ? filesMatch[1].trim().split(/,\s*/) : [],
1181
- date: dateMatch ? dateMatch[1].trim() : null,
1182
- });
1183
- }
1184
-
1185
- return learnings;
1186
- }
1187
-
1188
- /**
1189
- * Format a learning entry as markdown text.
1190
- * @param {Object} learning - Learning entry
1191
- * @returns {string}
1192
- */
1193
- function formatLearningEntry(learning) {
1194
- const lines = [
1195
- `### ${learning.id}: ${learning.title}`,
1196
- `**Type:** ${learning.type}`,
1197
- `**Detail:** ${learning.detail}`,
1198
- ];
1199
- if (learning.files && learning.files.length > 0) {
1200
- lines.push(`**Files:** ${learning.files.join(', ')}`);
1201
- }
1202
- if (learning.date) {
1203
- lines.push(`**Date:** ${learning.date}`);
1204
- }
1205
- lines.push('');
1206
- return lines.join('\n');
1207
- }
1208
-
1209
- /**
1210
- * Extract learnings from session summaries and error patterns.
1211
- * Reads session history + error patterns, extracts file co-change patterns
1212
- * and error resolutions, writes to .planning/learnings.md.
1213
- * @param {string} cwd - Working directory path
1214
- * @param {boolean} raw - If true, output raw count instead of JSON
1215
- * @returns {void}
1216
- */
1217
- function cmdLearningsExtract(cwd, raw) {
1218
- const learningsPath = path.join(cwd, PLANNING_DIR, LEARNINGS_FILE);
1219
- const newLearnings = [];
1220
- const today = new Date().toISOString().split('T')[0];
1221
-
1222
- // Read existing learnings to get next ID and avoid duplicates
1223
- let existingContent = '';
1224
- try { existingContent = fs.readFileSync(learningsPath, 'utf-8'); } catch { /* new file */ }
1225
- const existing = parseLearnings(existingContent);
1226
- let maxNum = 0;
1227
- for (const l of existing) {
1228
- const m = l.id.match(/LEARN-(\d+)/);
1229
- if (m) maxNum = Math.max(maxNum, parseInt(m[1], 10));
1230
- }
1231
-
1232
- // Existing detail strings for dedup
1233
- const existingDetails = new Set(existing.map(l => l.detail));
1234
-
1235
- // 1. Extract error resolutions from patterns.md
1236
- const patterns = readErrorPatterns(cwd);
1237
- for (const pat of patterns) {
1238
- const detail = `${pat.wrong} -> ${pat.right}`;
1239
- if (existingDetails.has(detail)) continue;
1240
- existingDetails.add(detail);
1241
- maxNum++;
1242
- newLearnings.push({
1243
- id: `LEARN-${String(maxNum).padStart(3, '0')}`,
1244
- type: 'error-resolution',
1245
- title: pat.title || 'Error pattern',
1246
- detail,
1247
- files: [],
1248
- date: pat.date || today,
1249
- });
1250
- }
1251
-
1252
- // 2. Extract file co-change patterns from summary frontmatters
1253
- const { summaries } = collectPhaseSummaries(cwd);
1254
- const fileCoChanges = new Map(); // file -> Set of co-changed files
1255
-
1256
- for (const { frontmatter } of summaries) {
1257
- const keyFiles = Array.isArray(frontmatter['key-files']) ? frontmatter['key-files'] : [];
1258
- if (keyFiles.length < 2) continue;
1259
- for (const file of keyFiles) {
1260
- if (!fileCoChanges.has(file)) fileCoChanges.set(file, new Set());
1261
- for (const other of keyFiles) {
1262
- if (other !== file) fileCoChanges.get(file).add(other);
1263
- }
1264
- }
1265
- }
1266
-
1267
- // Emit co-change learnings for files that appear together 2+ times
1268
- const emittedPairs = new Set();
1269
- for (const [file, coFiles] of fileCoChanges) {
1270
- for (const coFile of coFiles) {
1271
- const pair = [file, coFile].sort().join(' + ');
1272
- if (emittedPairs.has(pair)) continue;
1273
- emittedPairs.add(pair);
1274
-
1275
- // Count co-occurrences
1276
- let count = 0;
1277
- for (const { frontmatter } of summaries) {
1278
- const kf = frontmatter['key-files'] || [];
1279
- if (kf.includes(file) && kf.includes(coFile)) count++;
1280
- }
1281
- if (count < 2) continue;
1282
-
1283
- const detail = `${file} and ${coFile} changed together ${count} times`;
1284
- if (existingDetails.has(detail)) continue;
1285
- existingDetails.add(detail);
1286
- maxNum++;
1287
- newLearnings.push({
1288
- id: `LEARN-${String(maxNum).padStart(3, '0')}`,
1289
- type: 'co-change',
1290
- title: `Co-change: ${path.basename(file)} + ${path.basename(coFile)}`,
1291
- detail,
1292
- files: [file, coFile],
1293
- date: today,
1294
- });
1295
- }
1296
- }
1297
-
1298
- // 3. Extract successful patterns from summaries
1299
- for (const { frontmatter } of summaries) {
1300
- const patterns_established = frontmatter['patterns-established'] || [];
1301
- for (const pattern of patterns_established) {
1302
- const detail = String(pattern);
1303
- if (existingDetails.has(detail)) continue;
1304
- existingDetails.add(detail);
1305
- maxNum++;
1306
- newLearnings.push({
1307
- id: `LEARN-${String(maxNum).padStart(3, '0')}`,
1308
- type: 'pattern',
1309
- title: detail.length > 60 ? detail.substring(0, 57) + '...' : detail,
1310
- detail,
1311
- files: [],
1312
- date: today,
1313
- });
1314
- }
1315
- }
1316
-
1317
- // Write new learnings to file
1318
- if (newLearnings.length > 0) {
1319
- let content = existingContent;
1320
- if (!content || !content.trim()) {
1321
- content = '# Session Learnings\n\n';
1322
- }
1323
-
1324
- for (const learning of newLearnings) {
1325
- content = content.trimEnd() + '\n\n' + formatLearningEntry(learning);
1326
- }
1327
-
1328
- try {
1329
- fs.mkdirSync(path.dirname(learningsPath), { recursive: true });
1330
- fs.writeFileSync(learningsPath, content, 'utf-8');
1331
- } catch (e) {
1332
- error('Failed to write learnings: ' + e.message);
1333
- }
1334
- }
1335
-
1336
- const result = {
1337
- extracted: newLearnings.length,
1338
- total: existing.length + newLearnings.length,
1339
- by_type: {
1340
- 'error-resolution': newLearnings.filter(l => l.type === 'error-resolution').length,
1341
- 'co-change': newLearnings.filter(l => l.type === 'co-change').length,
1342
- 'pattern': newLearnings.filter(l => l.type === 'pattern').length,
1343
- },
1344
- };
1345
- output(result, raw, `Extracted ${newLearnings.length} new learnings (${result.total} total)`);
1346
- }
1347
-
1348
- /**
1349
- * List all learnings from .planning/learnings.md.
1350
- * @param {string} cwd - Working directory path
1351
- * @param {boolean} raw - If true, output raw formatted list instead of JSON
1352
- * @returns {void}
1353
- */
1354
- function cmdLearningsList(cwd, raw) {
1355
- const learningsPath = path.join(cwd, PLANNING_DIR, LEARNINGS_FILE);
1356
-
1357
- let content;
1358
- try {
1359
- content = fs.readFileSync(learningsPath, 'utf-8');
1360
- } catch {
1361
- output({ learnings: [], count: 0 }, raw, 'No learnings found');
1362
- return;
1363
- }
1364
-
1365
- const learnings = parseLearnings(content);
1366
- const result = {
1367
- learnings,
1368
- count: learnings.length,
1369
- by_type: {},
1370
- };
1371
-
1372
- for (const l of learnings) {
1373
- result.by_type[l.type] = (result.by_type[l.type] || 0) + 1;
1374
- }
1375
-
1376
- if (raw) {
1377
- const lines = learnings.map(l => `${l.id} [${l.type}] ${l.title}`);
1378
- output(result, true, lines.join('\n') || 'No learnings found');
1379
- } else {
1380
- output(result, false);
1381
- }
1382
- }
1383
-
1384
- /**
1385
- * Prune learnings by age (--days) or by ID (--id).
1386
- * @param {string} cwd - Working directory path
1387
- * @param {Object} opts - Prune options
1388
- * @param {number|null} opts.days - Remove entries older than N days
1389
- * @param {string|null} opts.id - Remove specific entry by ID
1390
- * @param {boolean} raw - If true, output raw count instead of JSON
1391
- * @returns {void}
1392
- */
1393
- function cmdLearningsPrune(cwd, opts, raw) {
1394
- const learningsPath = path.join(cwd, PLANNING_DIR, LEARNINGS_FILE);
1395
-
1396
- if (!opts || (opts.days == null && opts.id == null)) {
1397
- error('Prune requires --days N or --id LEARN-NNN');
1398
- }
1399
-
1400
- let content;
1401
- try {
1402
- content = fs.readFileSync(learningsPath, 'utf-8');
1403
- } catch {
1404
- output({ pruned: 0, remaining: 0 }, raw, 'No learnings file found');
1405
- return;
1406
- }
1407
-
1408
- const learnings = parseLearnings(content);
1409
- const before = learnings.length;
1410
- let kept;
1411
-
1412
- if (opts.id) {
1413
- kept = learnings.filter(l => l.id !== opts.id);
1414
- } else if (opts.days != null) {
1415
- const cutoff = new Date();
1416
- cutoff.setDate(cutoff.getDate() - opts.days);
1417
- const cutoffStr = cutoff.toISOString().split('T')[0];
1418
- kept = learnings.filter(l => !l.date || l.date >= cutoffStr);
1419
- } else {
1420
- kept = learnings;
1421
- }
1422
-
1423
- const pruned = before - kept.length;
1424
-
1425
- // Rewrite file
1426
- let newContent = '# Session Learnings\n';
1427
- for (const learning of kept) {
1428
- newContent += '\n' + formatLearningEntry(learning);
1429
- }
1430
-
1431
- try {
1432
- fs.writeFileSync(learningsPath, newContent, 'utf-8');
1433
- } catch (e) {
1434
- error('Failed to write learnings: ' + e.message);
1435
- }
1436
-
1437
- const result = { pruned, remaining: kept.length };
1438
- output(result, raw, `Pruned ${pruned} learnings (${kept.length} remaining)`);
1439
- }
928
+ // ---- Error patterns, session history, learnings — extracted to commands-learnings.cjs (re-exported below)
1440
929
 
1441
930
  module.exports = {
1442
931
  cmdGenerateSlug,
@@ -75,6 +75,70 @@ const MODEL_PROFILES = {
75
75
  'pan-experiment-runner': { quality: 'reasoning', balanced: 'fast', budget: 'fast' },
76
76
  };
77
77
 
78
+ // ─── Effort Profiles (2026-06, adaptive-thinking era) ───────────────────────
79
+ //
80
+ // Per-agent base reasoning effort (low|medium|high|xhigh). `effort` is the
81
+ // primary within-model cost/intelligence dial on current models — it replaced
82
+ // fixed thinking budgets. The base values here mirror the `effort:`
83
+ // frontmatter shipped in agents/*.md (a drift test keeps them in sync).
84
+ //
85
+ // Profile modulation: `budget` steps effort down one level (floor: low) as
86
+ // its cost lever; `quality` and `balanced` keep the base. Per-agent override
87
+ // via config.json → effort_overrides.
88
+
89
+ const EFFORT_ORDER = ['low', 'medium', 'high', 'xhigh'];
90
+
91
+ const AGENT_BASE_EFFORT = {
92
+ // Heavy planning/orchestration/debugging — deepest reasoning
93
+ 'pan-planner': 'xhigh',
94
+ 'pan-conductor': 'xhigh',
95
+ 'pan-debugger': 'xhigh',
96
+ 'pan-plan-checker': 'xhigh',
97
+ // Execution and verification — thorough but bounded
98
+ 'pan-executor': 'high',
99
+ 'pan-roadmapper': 'high',
100
+ 'pan-verifier': 'high',
101
+ 'pan-integration-checker': 'high',
102
+ 'pan-hardener': 'high',
103
+ 'pan-counterfactual': 'high',
104
+ 'pan-previewer': 'high',
105
+ 'pan-experiment-runner': 'high',
106
+ 'pan-optimizer': 'high',
107
+ // Research/synthesis/review — moderate depth
108
+ 'pan-phase-researcher': 'medium',
109
+ 'pan-project-researcher': 'medium',
110
+ 'pan-research-synthesizer': 'medium',
111
+ 'pan-knowledge': 'medium',
112
+ 'pan-distiller': 'medium',
113
+ 'pan-meta-reviewer': 'medium',
114
+ 'pan-reviewer': 'medium',
115
+ // Mechanical documentation pass — fast and scoped
116
+ 'pan-document_code': 'low',
117
+ };
118
+
119
+ /**
120
+ * Resolve the reasoning effort level for an agent under the active profile.
121
+ * Priority: config.effort_overrides[agent] → base effort modulated by
122
+ * model_profile (budget steps down one level) → 'medium' for unknown agents.
123
+ *
124
+ * @param {string} cwd - Project root directory
125
+ * @param {string} agentType - e.g. "pan-planner"
126
+ * @returns {string} One of 'low' | 'medium' | 'high' | 'xhigh'
127
+ */
128
+ function resolveEffortInternal(cwd, agentType) {
129
+ const config = loadConfig(cwd);
130
+ const override = config.effort_overrides?.[agentType];
131
+ if (typeof override === 'string' && EFFORT_ORDER.includes(override.toLowerCase().trim())) {
132
+ return override.toLowerCase().trim();
133
+ }
134
+ const base = AGENT_BASE_EFFORT[agentType] || 'medium';
135
+ const profile = config.model_profile || 'balanced';
136
+ if (profile === 'budget') {
137
+ return EFFORT_ORDER[Math.max(0, EFFORT_ORDER.indexOf(base) - 1)];
138
+ }
139
+ return base;
140
+ }
141
+
78
142
  // ─── Output helpers ───────────────────────────────────────────────────────────
79
143
 
80
144
  /**
@@ -213,6 +277,7 @@ function loadConfig(cwd) {
213
277
  execution: parsed.execution || { default_mode: 'wave_order', rollback_snapshots: true, error_pattern_learning: true },
214
278
  focus: parsed.focus || { auto_commit: true },
215
279
  model_overrides: parsed.model_overrides || {},
280
+ effort_overrides: parsed.effort_overrides || {},
216
281
  routing: parsed.routing || { strategy: 'static', provider: 'auto' },
217
282
  };
218
283
  } catch { // Config missing or malformed — use defaults
@@ -223,6 +288,7 @@ function loadConfig(cwd) {
223
288
  execution: { default_mode: 'wave_order', rollback_snapshots: true, error_pattern_learning: true },
224
289
  focus: { auto_commit: true },
225
290
  model_overrides: {},
291
+ effort_overrides: {},
226
292
  routing: { strategy: 'static', provider: 'auto' },
227
293
  };
228
294
  }
@@ -860,6 +926,9 @@ function scanSourceTodos(cwd) {
860
926
 
861
927
  module.exports = {
862
928
  MODEL_PROFILES,
929
+ AGENT_BASE_EFFORT,
930
+ EFFORT_ORDER,
931
+ resolveEffortInternal,
863
932
  PROVIDER_MODELS,
864
933
  LEGACY_ALIASES,
865
934
  COST_MULTIPLIERS,