pan-wizard 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +772 -0
- package/agents/pan-debugger.md +1246 -0
- package/agents/pan-document_code.md +965 -0
- package/agents/pan-executor.md +469 -0
- package/agents/pan-integration-checker.md +443 -0
- package/agents/pan-phase-researcher.md +572 -0
- package/agents/pan-plan-checker.md +763 -0
- package/agents/pan-planner.md +1297 -0
- package/agents/pan-project-researcher.md +647 -0
- package/agents/pan-research-synthesizer.md +239 -0
- package/agents/pan-reviewer.md +112 -0
- package/agents/pan-roadmapper.md +642 -0
- package/agents/pan-verifier.md +672 -0
- package/assets/pan-logo-2000-transparent.svg +30 -0
- package/assets/pan-logo-2000.svg +43 -0
- package/assets/terminal.svg +119 -0
- package/bin/install-lib.cjs +616 -0
- package/bin/install.js +1936 -0
- package/commands/pan/add-phase.md +44 -0
- package/commands/pan/assumptions.md +47 -0
- package/commands/pan/audit-deployment.md +378 -0
- package/commands/pan/debug.md +168 -0
- package/commands/pan/discord.md +19 -0
- package/commands/pan/discuss-phase.md +84 -0
- package/commands/pan/exec-phase.md +45 -0
- package/commands/pan/focus-auto.md +323 -0
- package/commands/pan/focus-design.md +816 -0
- package/commands/pan/focus-exec.md +316 -0
- package/commands/pan/focus-plan.md +101 -0
- package/commands/pan/focus-scan.md +272 -0
- package/commands/pan/focus-sync.md +104 -0
- package/commands/pan/health.md +23 -0
- package/commands/pan/help.md +23 -0
- package/commands/pan/insert-phase.md +33 -0
- package/commands/pan/map-codebase.md +72 -0
- package/commands/pan/milestone-audit.md +37 -0
- package/commands/pan/milestone-cleanup.md +19 -0
- package/commands/pan/milestone-done.md +137 -0
- package/commands/pan/milestone-gaps.md +35 -0
- package/commands/pan/milestone-new.md +45 -0
- package/commands/pan/new-project.md +43 -0
- package/commands/pan/patches.md +110 -0
- package/commands/pan/pause.md +39 -0
- package/commands/pan/phase-budget.md +23 -0
- package/commands/pan/phase-tests.md +42 -0
- package/commands/pan/plan-phase.md +46 -0
- package/commands/pan/profile.md +36 -0
- package/commands/pan/progress.md +25 -0
- package/commands/pan/quick.md +42 -0
- package/commands/pan/remove-phase.md +32 -0
- package/commands/pan/research-phase.md +190 -0
- package/commands/pan/resume.md +41 -0
- package/commands/pan/retro.md +33 -0
- package/commands/pan/settings.md +37 -0
- package/commands/pan/todo-add.md +48 -0
- package/commands/pan/todo-check.md +46 -0
- package/commands/pan/update.md +38 -0
- package/commands/pan/verify-phase.md +39 -0
- package/hooks/dist/pan-check-update.js +62 -0
- package/hooks/dist/pan-context-monitor.js +122 -0
- package/hooks/dist/pan-statusline.js +108 -0
- package/package.json +66 -0
- package/pan-wizard-core/bin/lib/codebase.cjs +746 -0
- package/pan-wizard-core/bin/lib/commands.cjs +1435 -0
- package/pan-wizard-core/bin/lib/config.cjs +611 -0
- package/pan-wizard-core/bin/lib/constants.cjs +696 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +150 -0
- package/pan-wizard-core/bin/lib/core.cjs +650 -0
- package/pan-wizard-core/bin/lib/focus.cjs +900 -0
- package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -0
- package/pan-wizard-core/bin/lib/init.cjs +881 -0
- package/pan-wizard-core/bin/lib/milestone.cjs +276 -0
- package/pan-wizard-core/bin/lib/phase.cjs +1212 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +470 -0
- package/pan-wizard-core/bin/lib/state.cjs +1029 -0
- package/pan-wizard-core/bin/lib/template.cjs +314 -0
- package/pan-wizard-core/bin/lib/utils.cjs +171 -0
- package/pan-wizard-core/bin/lib/verify.cjs +1808 -0
- package/pan-wizard-core/bin/pan-tools.cjs +773 -0
- package/pan-wizard-core/references/checkpoints.md +776 -0
- package/pan-wizard-core/references/continuation-format.md +249 -0
- package/pan-wizard-core/references/decimal-phase-calculation.md +65 -0
- package/pan-wizard-core/references/git-integration.md +248 -0
- package/pan-wizard-core/references/git-planning-commit.md +38 -0
- package/pan-wizard-core/references/model-profile-resolution.md +34 -0
- package/pan-wizard-core/references/model-profiles.md +111 -0
- package/pan-wizard-core/references/phase-argument-parsing.md +61 -0
- package/pan-wizard-core/references/planning-config.md +196 -0
- package/pan-wizard-core/references/questioning.md +145 -0
- package/pan-wizard-core/references/tdd.md +263 -0
- package/pan-wizard-core/references/ui-brand.md +160 -0
- package/pan-wizard-core/references/verification-patterns.md +612 -0
- package/pan-wizard-core/templates/codebase/architecture.md +283 -0
- package/pan-wizard-core/templates/codebase/best-practices.md +133 -0
- package/pan-wizard-core/templates/codebase/concerns.md +325 -0
- package/pan-wizard-core/templates/codebase/conventions.md +307 -0
- package/pan-wizard-core/templates/codebase/integrations.md +305 -0
- package/pan-wizard-core/templates/codebase/relationships.md +124 -0
- package/pan-wizard-core/templates/codebase/stack.md +199 -0
- package/pan-wizard-core/templates/codebase/structure.md +298 -0
- package/pan-wizard-core/templates/codebase/testing.md +480 -0
- package/pan-wizard-core/templates/config.json +37 -0
- package/pan-wizard-core/templates/context.md +283 -0
- package/pan-wizard-core/templates/continue-here.md +78 -0
- package/pan-wizard-core/templates/debug-subagent-prompt.md +91 -0
- package/pan-wizard-core/templates/debug.md +164 -0
- package/pan-wizard-core/templates/discovery.md +146 -0
- package/pan-wizard-core/templates/milestone-archive.md +123 -0
- package/pan-wizard-core/templates/milestone.md +115 -0
- package/pan-wizard-core/templates/phase-prompt.md +593 -0
- package/pan-wizard-core/templates/planner-subagent-prompt.md +117 -0
- package/pan-wizard-core/templates/project.md +184 -0
- package/pan-wizard-core/templates/requirements.md +231 -0
- package/pan-wizard-core/templates/research-project/architecture.md +204 -0
- package/pan-wizard-core/templates/research-project/features.md +147 -0
- package/pan-wizard-core/templates/research-project/pitfalls.md +200 -0
- package/pan-wizard-core/templates/research-project/stack.md +120 -0
- package/pan-wizard-core/templates/research-project/summary.md +170 -0
- package/pan-wizard-core/templates/research.md +552 -0
- package/pan-wizard-core/templates/retrospective.md +54 -0
- package/pan-wizard-core/templates/roadmap.md +202 -0
- package/pan-wizard-core/templates/standards.md +24 -0
- package/pan-wizard-core/templates/state.md +176 -0
- package/pan-wizard-core/templates/summary-complex.md +59 -0
- package/pan-wizard-core/templates/summary-minimal.md +41 -0
- package/pan-wizard-core/templates/summary-standard.md +49 -0
- package/pan-wizard-core/templates/summary.md +249 -0
- package/pan-wizard-core/templates/uat.md +247 -0
- package/pan-wizard-core/templates/user-setup.md +311 -0
- package/pan-wizard-core/templates/validation.md +76 -0
- package/pan-wizard-core/templates/verification-report.md +322 -0
- package/pan-wizard-core/workflows/add-phase.md +111 -0
- package/pan-wizard-core/workflows/assumptions.md +178 -0
- package/pan-wizard-core/workflows/diagnose-issues.md +219 -0
- package/pan-wizard-core/workflows/discuss-phase.md +542 -0
- package/pan-wizard-core/workflows/exec-phase.md +572 -0
- package/pan-wizard-core/workflows/execute-plan.md +448 -0
- package/pan-wizard-core/workflows/health.md +156 -0
- package/pan-wizard-core/workflows/help.md +431 -0
- package/pan-wizard-core/workflows/insert-phase.md +129 -0
- package/pan-wizard-core/workflows/map-codebase.md +401 -0
- package/pan-wizard-core/workflows/milestone-audit.md +297 -0
- package/pan-wizard-core/workflows/milestone-cleanup.md +152 -0
- package/pan-wizard-core/workflows/milestone-gaps.md +274 -0
- package/pan-wizard-core/workflows/milestone-new.md +382 -0
- package/pan-wizard-core/workflows/new-project.md +1178 -0
- package/pan-wizard-core/workflows/pause.md +122 -0
- package/pan-wizard-core/workflows/phase-tests.md +388 -0
- package/pan-wizard-core/workflows/plan-phase.md +569 -0
- package/pan-wizard-core/workflows/profile.md +115 -0
- package/pan-wizard-core/workflows/progress.md +381 -0
- package/pan-wizard-core/workflows/quick.md +453 -0
- package/pan-wizard-core/workflows/remove-phase.md +154 -0
- package/pan-wizard-core/workflows/research-phase.md +73 -0
- package/pan-wizard-core/workflows/resume-project.md +306 -0
- package/pan-wizard-core/workflows/retro.md +121 -0
- package/pan-wizard-core/workflows/settings.md +213 -0
- package/pan-wizard-core/workflows/todo-add.md +157 -0
- package/pan-wizard-core/workflows/todo-check.md +176 -0
- package/pan-wizard-core/workflows/transition.md +544 -0
- package/pan-wizard-core/workflows/update.md +219 -0
- package/pan-wizard-core/workflows/verify-phase.md +301 -0
- package/scripts/build-hooks.js +43 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus — Strategic project management: scan, plan, sync, exec
|
|
3
|
+
*
|
|
4
|
+
* Provides focus-scan (work item collection + priority classification),
|
|
5
|
+
* focus-plan (capacity-budgeted batch creation), focus-sync (doc staleness check),
|
|
6
|
+
* and focus-exec (batch execution pipeline).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { output, error, safeReadFile, loadConfig, scanPendingTodos, scanSourceTodos, toPosix, isGitRepo, execGit } = require('./core.cjs');
|
|
12
|
+
const {
|
|
13
|
+
PLANNING_DIR, PHASES_DIR, ROADMAP_FILE, PATTERNS_FILE, EFFORT_POINTS, PRIORITY_LEVELS, EFFORT_SIZES,
|
|
14
|
+
FOCUS_MODES, FOCUS_TIERS, FOCUS_DIR,
|
|
15
|
+
BUDGET_LIMIT_BUGFIX, BUDGET_LIMIT_FULL, STABILITY_RATIO, FEATURE_RATIO,
|
|
16
|
+
DIMINISHING_RETURNS_THRESHOLD,
|
|
17
|
+
AUTO_RUN_FILE, FOCUS_CATEGORIES, CATEGORY_PRIORITY_RANGE, CATEGORY_DEFAULTS,
|
|
18
|
+
DEFAULT_MAX_CYCLES, DEFAULT_TOTAL_BUDGET,
|
|
19
|
+
BUDGET_MIN, BUDGET_MAX, MAX_CYCLES_MIN, MAX_CYCLES_MAX, TOTAL_BUDGET_MIN, TOTAL_BUDGET_MAX,
|
|
20
|
+
AUTORUN_STATUSES, DOC_SYNC_FILES, COMMAND_RENAME_MAP,
|
|
21
|
+
} = require('./constants.cjs');
|
|
22
|
+
const { extractFrontmatter, extractPriorityEffort } = require('./frontmatter.cjs');
|
|
23
|
+
const { enumerateRoadmapPhases } = require('./roadmap.cjs');
|
|
24
|
+
const { readErrorPatterns } = require('./commands.cjs');
|
|
25
|
+
const { planningPath, listPhaseDirs, classifyPhaseStatus, filterPlanFiles, filterSummaryFiles } = require('./utils.cjs');
|
|
26
|
+
|
|
27
|
+
// ─── Scan helpers ───────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Collect work items from all project sources.
|
|
31
|
+
* Sources: phase plans (priority/effort from frontmatter), pending todos, error patterns.
|
|
32
|
+
* @param {string} cwd - Project root
|
|
33
|
+
* @returns {{ items: Array, sources: Object }}
|
|
34
|
+
*/
|
|
35
|
+
function collectWorkItems(cwd) {
|
|
36
|
+
const items = [];
|
|
37
|
+
const sources = { phases: 0, todos: 0, patterns: 0 };
|
|
38
|
+
|
|
39
|
+
// 1. Phase-based items from ROADMAP + plan.md frontmatter
|
|
40
|
+
const roadmapPath = path.join(cwd, PLANNING_DIR, ROADMAP_FILE);
|
|
41
|
+
const roadmapContent = safeReadFile(roadmapPath);
|
|
42
|
+
if (roadmapContent) {
|
|
43
|
+
const phases = enumerateRoadmapPhases(roadmapContent);
|
|
44
|
+
const phasesDir = path.join(cwd, PLANNING_DIR, PHASES_DIR);
|
|
45
|
+
let dirs;
|
|
46
|
+
try { dirs = fs.readdirSync(phasesDir); } catch { dirs = []; }
|
|
47
|
+
|
|
48
|
+
for (const phase of phases) {
|
|
49
|
+
const dirName = dirs.find(d => d.startsWith(phase.number + '-') || d === phase.number);
|
|
50
|
+
if (!dirName) continue;
|
|
51
|
+
|
|
52
|
+
const phaseDir = path.join(phasesDir, dirName);
|
|
53
|
+
let files;
|
|
54
|
+
try { files = fs.readdirSync(phaseDir); } catch { continue; }
|
|
55
|
+
|
|
56
|
+
const planFiles = filterPlanFiles(files);
|
|
57
|
+
const summaryFiles = filterSummaryFiles(files);
|
|
58
|
+
const status = classifyPhaseStatus(planFiles.length, summaryFiles.length, {});
|
|
59
|
+
|
|
60
|
+
if (status === 'complete') continue;
|
|
61
|
+
|
|
62
|
+
// Read first plan file for frontmatter
|
|
63
|
+
let priority = 'P3';
|
|
64
|
+
let effort = 'M';
|
|
65
|
+
if (planFiles.length > 0) {
|
|
66
|
+
const planContent = safeReadFile(path.join(phaseDir, planFiles[0]));
|
|
67
|
+
if (planContent) {
|
|
68
|
+
const pe = extractPriorityEffort(extractFrontmatter(planContent));
|
|
69
|
+
priority = pe.priority;
|
|
70
|
+
effort = pe.effort;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
items.push({
|
|
75
|
+
id: `phase-${phase.number}`,
|
|
76
|
+
title: `Phase ${phase.number}: ${phase.name}`,
|
|
77
|
+
source: 'phase',
|
|
78
|
+
priority,
|
|
79
|
+
effort,
|
|
80
|
+
points: EFFORT_POINTS[effort] || 4,
|
|
81
|
+
status,
|
|
82
|
+
file: toPosix(path.join(PLANNING_DIR, PHASES_DIR, dirName)),
|
|
83
|
+
});
|
|
84
|
+
sources.phases++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Pending todos
|
|
89
|
+
const todoResult = scanPendingTodos(cwd);
|
|
90
|
+
if (todoResult && todoResult.todos) {
|
|
91
|
+
for (const todo of todoResult.todos) {
|
|
92
|
+
items.push({
|
|
93
|
+
id: `todo-${todo.file}`,
|
|
94
|
+
title: todo.title || todo.file,
|
|
95
|
+
source: 'todo',
|
|
96
|
+
priority: 'P5',
|
|
97
|
+
effort: 'S',
|
|
98
|
+
points: EFFORT_POINTS.S,
|
|
99
|
+
status: 'pending',
|
|
100
|
+
file: toPosix(path.join(PLANNING_DIR, 'todos', 'pending', todo.file)),
|
|
101
|
+
});
|
|
102
|
+
sources.todos++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Error patterns
|
|
107
|
+
const patterns = readErrorPatterns(cwd);
|
|
108
|
+
for (const pattern of patterns) {
|
|
109
|
+
items.push({
|
|
110
|
+
id: `pattern-${pattern.id || 'unknown'}`,
|
|
111
|
+
title: pattern.title || `Error pattern: ${pattern.id}`,
|
|
112
|
+
source: 'pattern',
|
|
113
|
+
priority: 'P1',
|
|
114
|
+
effort: 'S',
|
|
115
|
+
points: EFFORT_POINTS.S,
|
|
116
|
+
status: 'active',
|
|
117
|
+
file: toPosix(path.join(PLANNING_DIR, PATTERNS_FILE)),
|
|
118
|
+
});
|
|
119
|
+
sources.patterns++;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { items, sources };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Assign priority P0-P6 based on source type and content.
|
|
127
|
+
* Phase items use frontmatter priority; todos default to P5; patterns default to P1.
|
|
128
|
+
* Exported for test coverage — no internal callers.
|
|
129
|
+
* @param {Object} item - Work item
|
|
130
|
+
* @returns {string} Priority level
|
|
131
|
+
*/
|
|
132
|
+
function classifyItemPriority(item) {
|
|
133
|
+
return item.priority || 'P3';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sort items by priority (P0 first), then by effort (smallest first within tier).
|
|
138
|
+
* @param {Array} items - Work items
|
|
139
|
+
* @returns {Array} Sorted items
|
|
140
|
+
*/
|
|
141
|
+
function sortByPriority(items) {
|
|
142
|
+
return [...items].sort((a, b) => {
|
|
143
|
+
const pa = PRIORITY_LEVELS.indexOf(a.priority);
|
|
144
|
+
const pb = PRIORITY_LEVELS.indexOf(b.priority);
|
|
145
|
+
if (pa !== pb) return pa - pb;
|
|
146
|
+
const ea = EFFORT_SIZES.indexOf(a.effort);
|
|
147
|
+
const eb = EFFORT_SIZES.indexOf(b.effort);
|
|
148
|
+
return ea - eb;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Compute Reality Score for a work item.
|
|
154
|
+
* RS = (UV + TC + RR) / JS where JS = effort points.
|
|
155
|
+
* @param {Object} item - Work item with uv, tc, rr fields (or defaults)
|
|
156
|
+
* @returns {number} Reality Score
|
|
157
|
+
*/
|
|
158
|
+
function computeRealityScore(item) {
|
|
159
|
+
const uv = item.uv || 3;
|
|
160
|
+
const tc = item.tc || 2;
|
|
161
|
+
const rr = item.rr || 2;
|
|
162
|
+
const js = EFFORT_POINTS[item.effort] || 4;
|
|
163
|
+
return Number(((uv + tc + rr) / js).toFixed(1));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* focus scan — Collect, classify, sort, and output all work items.
|
|
168
|
+
* @param {string} cwd - Project root
|
|
169
|
+
* @param {boolean} raw - Raw output mode
|
|
170
|
+
* @param {...string} args - Flags: --lean (filter by RS >= 1.5)
|
|
171
|
+
*/
|
|
172
|
+
function cmdFocusScan(cwd, raw, ...args) {
|
|
173
|
+
const lean = args.includes('--lean');
|
|
174
|
+
|
|
175
|
+
const { items, sources } = collectWorkItems(cwd);
|
|
176
|
+
|
|
177
|
+
if (items.length === 0) {
|
|
178
|
+
output({ items: [], sources, total: 0, message: 'No work items found' }, raw);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Compute RS for P3-P6 items
|
|
183
|
+
for (const item of items) {
|
|
184
|
+
const pi = PRIORITY_LEVELS.indexOf(item.priority);
|
|
185
|
+
if (pi >= 3) {
|
|
186
|
+
item.realityScore = computeRealityScore(item);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let sorted = sortByPriority(items);
|
|
191
|
+
|
|
192
|
+
// --lean: filter items with RS < 1.5
|
|
193
|
+
if (lean) {
|
|
194
|
+
sorted = sorted.filter(item => {
|
|
195
|
+
if (item.realityScore === undefined) return true;
|
|
196
|
+
return item.realityScore >= 1.5;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const sourceTodos = scanSourceTodos(cwd);
|
|
201
|
+
|
|
202
|
+
output({
|
|
203
|
+
items: sorted,
|
|
204
|
+
sources,
|
|
205
|
+
total: sorted.length,
|
|
206
|
+
priorities: summarizePriorities(sorted),
|
|
207
|
+
source_todos: sourceTodos,
|
|
208
|
+
}, raw);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Summarize items by priority level.
|
|
213
|
+
* @param {Array} items - Sorted work items
|
|
214
|
+
* @returns {Object} Count per priority level
|
|
215
|
+
*/
|
|
216
|
+
function summarizePriorities(items) {
|
|
217
|
+
const counts = {};
|
|
218
|
+
for (const level of PRIORITY_LEVELS) {
|
|
219
|
+
const matching = items.filter(i => i.priority === level);
|
|
220
|
+
if (matching.length > 0) {
|
|
221
|
+
counts[level] = matching.length;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return counts;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Plan helpers ───────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Classify execution tier from effort size.
|
|
231
|
+
* @param {string} effort - XS, S, M, L, XL
|
|
232
|
+
* @returns {string} MICRO, STANDARD, or FULL
|
|
233
|
+
*/
|
|
234
|
+
function classifyTier(effort) {
|
|
235
|
+
if (effort === 'XS' || effort === 'S') return FOCUS_TIERS.MICRO;
|
|
236
|
+
if (effort === 'M') return FOCUS_TIERS.STANDARD;
|
|
237
|
+
return FOCUS_TIERS.FULL;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Allocate items into a budget using mode-specific algorithm.
|
|
242
|
+
* @param {Array} items - Sorted work items
|
|
243
|
+
* @param {number} budget - Total capacity points
|
|
244
|
+
* @param {string} mode - bugfix, balanced, features, full
|
|
245
|
+
* @returns {{ batch: Array, allocated: number, remaining: Array }}
|
|
246
|
+
*/
|
|
247
|
+
/**
|
|
248
|
+
* Allocate items in a single pass up to a budget with optional priority filter.
|
|
249
|
+
* @param {Array} items - Work items
|
|
250
|
+
* @param {number} budget - Points budget
|
|
251
|
+
* @param {Object} [opts] - Options
|
|
252
|
+
* @param {number} [opts.maxPriority] - Max priority index to include
|
|
253
|
+
* @param {number} [opts.minPriority] - Min priority index to include
|
|
254
|
+
* @param {string} [opts.track] - Track label to apply
|
|
255
|
+
* @param {Set} [opts.exclude] - Item IDs to skip
|
|
256
|
+
* @returns {{ picked: Array, used: number }}
|
|
257
|
+
*/
|
|
258
|
+
function allocatePass(items, budget, opts = {}) {
|
|
259
|
+
const picked = [];
|
|
260
|
+
let used = 0;
|
|
261
|
+
const exclude = opts.exclude || new Set();
|
|
262
|
+
for (const item of items) {
|
|
263
|
+
if (exclude.has(item.id)) continue;
|
|
264
|
+
const pi = PRIORITY_LEVELS.indexOf(item.priority);
|
|
265
|
+
if (opts.maxPriority !== undefined && pi > opts.maxPriority) continue;
|
|
266
|
+
if (opts.minPriority !== undefined && pi < opts.minPriority) continue;
|
|
267
|
+
if (used + item.points <= budget) {
|
|
268
|
+
picked.push({ ...item, tier: classifyTier(item.effort), ...(opts.track ? { track: opts.track } : {}) });
|
|
269
|
+
used += item.points;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { picked, used };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function allocateBudget(items, budget, mode) {
|
|
276
|
+
const batch = [];
|
|
277
|
+
let allocated = 0;
|
|
278
|
+
const batchIds = new Set();
|
|
279
|
+
const addToBatch = (picked) => { for (const p of picked) { batch.push(p); batchIds.add(p.id); } };
|
|
280
|
+
|
|
281
|
+
if (mode === 'bugfix') {
|
|
282
|
+
const { picked, used } = allocatePass(items, budget, { maxPriority: 4 });
|
|
283
|
+
addToBatch(picked);
|
|
284
|
+
allocated = used;
|
|
285
|
+
} else if (mode === 'balanced') {
|
|
286
|
+
const stabilityBudget = Math.floor(budget * STABILITY_RATIO);
|
|
287
|
+
const { picked: sPicked, used: sUsed } = allocatePass(items, stabilityBudget, { maxPriority: 2, track: 'stability' });
|
|
288
|
+
addToBatch(sPicked);
|
|
289
|
+
const { picked: fPicked, used: fUsed } = allocatePass(items, budget - stabilityBudget, { minPriority: 3, track: 'feature', exclude: batchIds });
|
|
290
|
+
addToBatch(fPicked);
|
|
291
|
+
allocated = sUsed + fUsed;
|
|
292
|
+
} else if (mode === 'features') {
|
|
293
|
+
const { picked: mPicked, used: mUsed } = allocatePass(items, budget, { maxPriority: 0, minPriority: 0, track: 'mandatory' });
|
|
294
|
+
addToBatch(mPicked);
|
|
295
|
+
const featureBudget = Math.floor(budget * FEATURE_RATIO);
|
|
296
|
+
const { picked: fPicked, used: fUsed } = allocatePass(items, featureBudget, { minPriority: 3, maxPriority: 5, track: 'feature', exclude: batchIds });
|
|
297
|
+
addToBatch(fPicked);
|
|
298
|
+
const { picked: sPicked, used: sUsed } = allocatePass(items, budget - featureBudget, { minPriority: 1, maxPriority: 2, track: 'stability', exclude: batchIds });
|
|
299
|
+
addToBatch(sPicked);
|
|
300
|
+
allocated = mUsed + fUsed + sUsed;
|
|
301
|
+
} else {
|
|
302
|
+
const { picked, used } = allocatePass(items, budget);
|
|
303
|
+
addToBatch(picked);
|
|
304
|
+
allocated = used;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const remaining = items.filter(item => !batchIds.has(item.id));
|
|
308
|
+
return { batch, allocated, remaining };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* focus plan — Create a capacity-budgeted execution batch.
|
|
313
|
+
* @param {string} cwd - Project root
|
|
314
|
+
* @param {boolean} raw - Raw output mode
|
|
315
|
+
* @param {...string} args - Flags: --budget N, --mode MODE, --priority P0-P6, --lean
|
|
316
|
+
*/
|
|
317
|
+
function cmdFocusPlan(cwd, raw, ...args) {
|
|
318
|
+
// Parse flags
|
|
319
|
+
let budget = 50;
|
|
320
|
+
let mode = 'balanced';
|
|
321
|
+
let priorityFilter = null;
|
|
322
|
+
const lean = args.includes('--lean');
|
|
323
|
+
|
|
324
|
+
for (let i = 0; i < args.length; i++) {
|
|
325
|
+
if (args[i] === '--budget' && args[i + 1]) {
|
|
326
|
+
const n = parseInt(args[i + 1], 10);
|
|
327
|
+
if (!isNaN(n) && n >= BUDGET_MIN && n <= BUDGET_MAX) budget = n;
|
|
328
|
+
i++;
|
|
329
|
+
} else if (args[i] === '--mode' && args[i + 1]) {
|
|
330
|
+
if (FOCUS_MODES.includes(args[i + 1])) mode = args[i + 1];
|
|
331
|
+
i++;
|
|
332
|
+
} else if (args[i] === '--priority' && args[i + 1]) {
|
|
333
|
+
if (PRIORITY_LEVELS.includes(args[i + 1].toUpperCase())) {
|
|
334
|
+
priorityFilter = args[i + 1].toUpperCase();
|
|
335
|
+
}
|
|
336
|
+
i++;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Mode-specific budget overrides
|
|
341
|
+
if (mode === 'bugfix') budget = Math.min(budget, BUDGET_LIMIT_BUGFIX);
|
|
342
|
+
if (mode === 'full') budget = Math.max(budget, BUDGET_LIMIT_FULL);
|
|
343
|
+
|
|
344
|
+
// Collect items
|
|
345
|
+
const { items, sources } = collectWorkItems(cwd);
|
|
346
|
+
if (items.length === 0) {
|
|
347
|
+
output({ error: 'No work items found. Run focus scan first or add phases/todos.' }, raw);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Apply priority filter
|
|
352
|
+
let filtered = items;
|
|
353
|
+
if (priorityFilter) {
|
|
354
|
+
const maxPriority = PRIORITY_LEVELS.indexOf(priorityFilter);
|
|
355
|
+
filtered = items.filter(i => PRIORITY_LEVELS.indexOf(i.priority) <= maxPriority);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Compute RS and filter if lean
|
|
359
|
+
for (const item of filtered) {
|
|
360
|
+
const pi = PRIORITY_LEVELS.indexOf(item.priority);
|
|
361
|
+
if (pi >= 3) item.realityScore = computeRealityScore(item);
|
|
362
|
+
}
|
|
363
|
+
if (lean) {
|
|
364
|
+
filtered = filtered.filter(i => i.realityScore === undefined || i.realityScore >= 1.5);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const sorted = sortByPriority(filtered);
|
|
368
|
+
const { batch, allocated, remaining } = allocateBudget(sorted, budget, mode);
|
|
369
|
+
|
|
370
|
+
// Write batch file
|
|
371
|
+
const focusDir = path.join(cwd, PLANNING_DIR, FOCUS_DIR);
|
|
372
|
+
try { fs.mkdirSync(focusDir, { recursive: true }); } catch { /* exists */ }
|
|
373
|
+
|
|
374
|
+
const date = new Date().toISOString().split('T')[0];
|
|
375
|
+
const batchPath = path.join(focusDir, `batch-${date}.json`);
|
|
376
|
+
const batchData = { date, mode, budget, allocated, batch, remaining: remaining.length };
|
|
377
|
+
try {
|
|
378
|
+
fs.writeFileSync(batchPath, JSON.stringify(batchData, null, 2), 'utf-8');
|
|
379
|
+
} catch (err) {
|
|
380
|
+
output({ error: `Failed to write batch file: ${err.message}` }, raw);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
output({
|
|
385
|
+
mode,
|
|
386
|
+
budget,
|
|
387
|
+
allocated,
|
|
388
|
+
items_selected: batch.length,
|
|
389
|
+
items_remaining: remaining.length,
|
|
390
|
+
batch,
|
|
391
|
+
batch_file: toPosix(path.relative(cwd, batchPath)),
|
|
392
|
+
}, raw);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ─── Sync helpers ───────────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Check documentation staleness by comparing actual counts against documented counts.
|
|
399
|
+
* @param {string} cwd - Project root
|
|
400
|
+
* @returns {{ stale: Array, current: Array }}
|
|
401
|
+
*/
|
|
402
|
+
function checkDocStaleness(cwd, opts) {
|
|
403
|
+
const stale = [];
|
|
404
|
+
const current = [];
|
|
405
|
+
const options = opts || {};
|
|
406
|
+
|
|
407
|
+
// Count actuals
|
|
408
|
+
let commandCount = 0;
|
|
409
|
+
try {
|
|
410
|
+
const cmdDir = path.join(cwd, 'commands', 'pan');
|
|
411
|
+
commandCount = fs.readdirSync(cmdDir).filter(f => f.endsWith('.md')).length;
|
|
412
|
+
} catch { /* no commands dir */ }
|
|
413
|
+
|
|
414
|
+
let agentCount = 0;
|
|
415
|
+
try {
|
|
416
|
+
const agentDir = path.join(cwd, 'agents');
|
|
417
|
+
agentCount = fs.readdirSync(agentDir).filter(f => f.endsWith('.md')).length;
|
|
418
|
+
} catch { /* no agents dir */ }
|
|
419
|
+
|
|
420
|
+
let moduleCount = 0;
|
|
421
|
+
try {
|
|
422
|
+
const libDir = path.join(cwd, 'pan-wizard-core', 'bin', 'lib');
|
|
423
|
+
moduleCount = fs.readdirSync(libDir).filter(f => f.endsWith('.cjs')).length;
|
|
424
|
+
} catch { /* no lib dir */ }
|
|
425
|
+
|
|
426
|
+
const actuals = { commands: commandCount, agents: agentCount, modules: moduleCount };
|
|
427
|
+
|
|
428
|
+
// Check all doc files for count staleness
|
|
429
|
+
for (const relFile of DOC_SYNC_FILES) {
|
|
430
|
+
const content = safeReadFile(path.join(cwd, relFile));
|
|
431
|
+
if (!content) continue;
|
|
432
|
+
checkCount(content, relFile, 'commands', commandCount, stale, current);
|
|
433
|
+
checkCount(content, relFile, 'agents', agentCount, stale, current);
|
|
434
|
+
checkCount(content, relFile, 'modules', moduleCount, stale, current);
|
|
435
|
+
|
|
436
|
+
// Check test/suite counts if provided
|
|
437
|
+
if (options.tests != null) {
|
|
438
|
+
checkCount(content, relFile, 'tests', options.tests, stale, current);
|
|
439
|
+
}
|
|
440
|
+
if (options.suites != null) {
|
|
441
|
+
checkCount(content, relFile, 'suites', options.suites, stale, current);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Check for old command names
|
|
445
|
+
checkOldCommandNames(content, relFile, stale);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Version cross-reference: package.json vs CHANGELOG.md
|
|
449
|
+
checkVersionCrossRef(cwd, stale, current);
|
|
450
|
+
|
|
451
|
+
return { stale, current, actuals };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Check if a document references old (renamed) command names.
|
|
456
|
+
*/
|
|
457
|
+
function checkOldCommandNames(content, file, stale) {
|
|
458
|
+
for (const [oldName, newName] of Object.entries(COMMAND_RENAME_MAP)) {
|
|
459
|
+
// Match /pan:old-name or pan:old-name (command references)
|
|
460
|
+
const pattern = new RegExp(`pan:${oldName.replace(/-/g, '\\-')}\\b`);
|
|
461
|
+
if (pattern.test(content)) {
|
|
462
|
+
stale.push({ file, entity: 'renamed_command', old: oldName, new: newName });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Check package.json version matches latest CHANGELOG entry.
|
|
469
|
+
*/
|
|
470
|
+
function checkVersionCrossRef(cwd, stale, current) {
|
|
471
|
+
let pkgVersion;
|
|
472
|
+
try {
|
|
473
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
474
|
+
pkgVersion = pkg.version;
|
|
475
|
+
} catch { return; }
|
|
476
|
+
|
|
477
|
+
const changelog = safeReadFile(path.join(cwd, 'CHANGELOG.md'));
|
|
478
|
+
if (!changelog || !pkgVersion) return;
|
|
479
|
+
|
|
480
|
+
const versionMatch = changelog.match(/^##\s+\[?v?([\d.]+)\]?/m);
|
|
481
|
+
if (versionMatch) {
|
|
482
|
+
const changelogVersion = versionMatch[1];
|
|
483
|
+
if (changelogVersion === pkgVersion) {
|
|
484
|
+
current.push({ file: 'CHANGELOG.md', entity: 'version', count: pkgVersion });
|
|
485
|
+
} else {
|
|
486
|
+
stale.push({ file: 'CHANGELOG.md', entity: 'version', documented: changelogVersion, actual: pkgVersion });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Check if a document contains a stale count for a given entity type.
|
|
493
|
+
*/
|
|
494
|
+
function checkCount(content, file, entity, actual, stale, current) {
|
|
495
|
+
// Match patterns like "32 commands", "11 agents", "14 modules"
|
|
496
|
+
const pattern = new RegExp(`(\\d+)\\s+${entity}`, 'i');
|
|
497
|
+
const match = content.match(pattern);
|
|
498
|
+
if (match) {
|
|
499
|
+
const documented = parseInt(match[1], 10);
|
|
500
|
+
if (documented !== actual) {
|
|
501
|
+
stale.push({ file, entity, documented, actual });
|
|
502
|
+
} else {
|
|
503
|
+
current.push({ file, entity, count: actual });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* focus sync — Check doc staleness and optionally report.
|
|
510
|
+
* @param {string} cwd - Project root
|
|
511
|
+
* @param {boolean} raw - Raw output mode
|
|
512
|
+
* @param {...string} args - Flags: --check-only, --readme, --commands, --agents, --all
|
|
513
|
+
*/
|
|
514
|
+
function cmdFocusSync(cwd, raw, ...args) {
|
|
515
|
+
const checkOnly = args.includes('--check-only') || args.length === 0;
|
|
516
|
+
|
|
517
|
+
const opts = {};
|
|
518
|
+
const testsIdx = args.indexOf('--tests');
|
|
519
|
+
if (testsIdx !== -1 && args[testsIdx + 1]) {
|
|
520
|
+
opts.tests = parseInt(args[testsIdx + 1], 10);
|
|
521
|
+
}
|
|
522
|
+
const suitesIdx = args.indexOf('--suites');
|
|
523
|
+
if (suitesIdx !== -1 && args[suitesIdx + 1]) {
|
|
524
|
+
opts.suites = parseInt(args[suitesIdx + 1], 10);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const { stale, current, actuals } = checkDocStaleness(cwd, opts);
|
|
528
|
+
|
|
529
|
+
output({
|
|
530
|
+
actuals,
|
|
531
|
+
stale,
|
|
532
|
+
current,
|
|
533
|
+
stale_count: stale.length,
|
|
534
|
+
needs_sync: stale.length > 0,
|
|
535
|
+
check_only: checkOnly,
|
|
536
|
+
}, raw);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ─── Exec helpers ───────────────────────────────────────────────────────────
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Read the most recent batch file from .planning/focus/.
|
|
543
|
+
* @param {string} cwd - Project root
|
|
544
|
+
* @returns {Object|null} Parsed batch data or null
|
|
545
|
+
*/
|
|
546
|
+
function readLatestBatch(cwd) {
|
|
547
|
+
const focusDir = path.join(cwd, PLANNING_DIR, FOCUS_DIR);
|
|
548
|
+
let files;
|
|
549
|
+
try {
|
|
550
|
+
files = fs.readdirSync(focusDir).filter(f => f.startsWith('batch-') && f.endsWith('.json'));
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
if (files.length === 0) return null;
|
|
555
|
+
files.sort().reverse();
|
|
556
|
+
const content = safeReadFile(path.join(focusDir, files[0]));
|
|
557
|
+
if (!content) return null;
|
|
558
|
+
try {
|
|
559
|
+
return JSON.parse(content);
|
|
560
|
+
} catch {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* focus exec — Execute items from the latest batch.
|
|
567
|
+
* This is the core module function that provides the data layer.
|
|
568
|
+
* The full 6-stage pipeline is orchestrated by the command .md workflow.
|
|
569
|
+
* @param {string} cwd - Project root
|
|
570
|
+
* @param {boolean} raw - Raw output mode
|
|
571
|
+
* @param {...string} args - Flags: --dry-run, --budget N, --mode MODE, --continue
|
|
572
|
+
*/
|
|
573
|
+
function cmdFocusExec(cwd, raw, ...args) {
|
|
574
|
+
const dryRun = args.includes('--dry-run');
|
|
575
|
+
const force = args.includes('--force');
|
|
576
|
+
|
|
577
|
+
// Git cleanliness gate (skip for dry-run or --force)
|
|
578
|
+
if (!dryRun && !force && isGitRepo(cwd)) {
|
|
579
|
+
const status = execGit(cwd, ['status', '--porcelain']);
|
|
580
|
+
if (status.exitCode === 0 && status.stdout) {
|
|
581
|
+
const uncommitted = status.stdout.split('\n').filter(Boolean).length;
|
|
582
|
+
output({ error: 'dirty_working_tree', uncommitted_count: uncommitted, hint: 'Commit or stash changes before running focus-exec, or use --force to override' }, raw);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const batch = readLatestBatch(cwd);
|
|
588
|
+
if (!batch) {
|
|
589
|
+
output({ error: 'No batch file found. Run focus plan first.' }, raw);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (batch.batch.length === 0) {
|
|
594
|
+
output({ error: 'Batch is empty. Run focus plan with items.' }, raw);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Classify items by tier
|
|
599
|
+
const micro = batch.batch.filter(i => i.tier === FOCUS_TIERS.MICRO);
|
|
600
|
+
const standard = batch.batch.filter(i => i.tier === FOCUS_TIERS.STANDARD);
|
|
601
|
+
const full = batch.batch.filter(i => i.tier === FOCUS_TIERS.FULL);
|
|
602
|
+
|
|
603
|
+
const result = {
|
|
604
|
+
dry_run: dryRun,
|
|
605
|
+
mode: batch.mode,
|
|
606
|
+
budget: batch.budget,
|
|
607
|
+
allocated: batch.allocated,
|
|
608
|
+
total_items: batch.batch.length,
|
|
609
|
+
tiers: {
|
|
610
|
+
micro: micro.length,
|
|
611
|
+
standard: standard.length,
|
|
612
|
+
full: full.length,
|
|
613
|
+
},
|
|
614
|
+
items: batch.batch,
|
|
615
|
+
batch_file: toPosix(path.join(PLANNING_DIR, FOCUS_DIR, `batch-${batch.date}.json`)),
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
output(result, raw);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ─── Focus Auto-Runner ──────────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Filter work items by category's priority range.
|
|
625
|
+
* @param {Array} items - Work items with .priority field
|
|
626
|
+
* @param {string|null} category - One of FOCUS_CATEGORIES, or null for all
|
|
627
|
+
* @returns {Array} Filtered items
|
|
628
|
+
*/
|
|
629
|
+
function categoryFilter(items, category) {
|
|
630
|
+
if (!category) return items;
|
|
631
|
+
const range = CATEGORY_PRIORITY_RANGE[category];
|
|
632
|
+
if (!range) return items;
|
|
633
|
+
return items.filter(item => {
|
|
634
|
+
const idx = PRIORITY_LEVELS.indexOf(item.priority);
|
|
635
|
+
return idx >= range.min && idx <= range.max;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Read auto-run.json state file.
|
|
641
|
+
* @param {string} cwd - Project root
|
|
642
|
+
* @returns {object|null} Parsed auto-run state or null
|
|
643
|
+
*/
|
|
644
|
+
function readAutoRun(cwd) {
|
|
645
|
+
const filePath = path.join(cwd, PLANNING_DIR, FOCUS_DIR, AUTO_RUN_FILE);
|
|
646
|
+
const content = safeReadFile(filePath);
|
|
647
|
+
if (!content) return null;
|
|
648
|
+
try {
|
|
649
|
+
return JSON.parse(content);
|
|
650
|
+
} catch {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Write auto-run.json state file.
|
|
657
|
+
* @param {string} cwd - Project root
|
|
658
|
+
* @param {object} data - State object to persist
|
|
659
|
+
* @returns {boolean} true on success
|
|
660
|
+
*/
|
|
661
|
+
function writeAutoRun(cwd, data) {
|
|
662
|
+
const dirPath = path.join(cwd, PLANNING_DIR, FOCUS_DIR);
|
|
663
|
+
try {
|
|
664
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
665
|
+
fs.writeFileSync(path.join(dirPath, AUTO_RUN_FILE), JSON.stringify(data, null, 2));
|
|
666
|
+
return true;
|
|
667
|
+
} catch {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Generate a unique run ID for the day.
|
|
674
|
+
* @param {string} cwd - Project root
|
|
675
|
+
* @returns {string} Run ID like "auto-2026-03-03-1"
|
|
676
|
+
*/
|
|
677
|
+
function generateRunId(cwd) {
|
|
678
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
679
|
+
const existing = readAutoRun(cwd);
|
|
680
|
+
if (existing && existing.run_id && existing.run_id.startsWith(`auto-${date}`)) {
|
|
681
|
+
const match = existing.run_id.match(/-(\d+)$/);
|
|
682
|
+
const num = match ? Number(match[1]) + 1 : 1;
|
|
683
|
+
return `auto-${date}-${num}`;
|
|
684
|
+
}
|
|
685
|
+
return `auto-${date}-1`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Focus auto-runner: init, status, update, stop.
|
|
690
|
+
* @param {string} cwd - Project root
|
|
691
|
+
* @param {boolean} raw - Raw output mode
|
|
692
|
+
* @param {...string} args - CLI arguments
|
|
693
|
+
*/
|
|
694
|
+
function focusAutoStatus(cwd, raw) {
|
|
695
|
+
const run = readAutoRun(cwd);
|
|
696
|
+
if (!run) return error('No auto-run found. Start with: focus auto --category <name>');
|
|
697
|
+
const budgetRemaining = run.total_budget - (run.totals ? run.totals.points_used : 0);
|
|
698
|
+
const cyclesRemaining = run.max_cycles - (run.totals ? run.totals.cycles_completed : 0);
|
|
699
|
+
return output({ ...run, budget_remaining: budgetRemaining, cycles_remaining: cyclesRemaining }, raw);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function focusAutoStop(cwd, raw) {
|
|
703
|
+
const run = readAutoRun(cwd);
|
|
704
|
+
if (!run) return error('No auto-run in progress. Nothing to stop.');
|
|
705
|
+
if (run.status !== AUTORUN_STATUSES.IN_PROGRESS && run.status !== AUTORUN_STATUSES.INITIALIZED) {
|
|
706
|
+
return error(`Auto-run is already ${run.status}. Nothing to stop.`);
|
|
707
|
+
}
|
|
708
|
+
run.status = AUTORUN_STATUSES.STOPPED;
|
|
709
|
+
run.stop_reason = 'user_stop';
|
|
710
|
+
writeAutoRun(cwd, run);
|
|
711
|
+
return output({
|
|
712
|
+
run_id: run.run_id, status: AUTORUN_STATUSES.STOPPED, stop_reason: 'user_stop',
|
|
713
|
+
cycles_completed: run.totals ? run.totals.cycles_completed : 0,
|
|
714
|
+
total_items_completed: run.totals ? run.totals.items_completed : 0,
|
|
715
|
+
}, raw);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function focusAutoUpdate(cwd, raw, getVal) {
|
|
719
|
+
const run = readAutoRun(cwd);
|
|
720
|
+
if (!run) return error('No auto-run in progress. Cannot update.');
|
|
721
|
+
if (run.status !== AUTORUN_STATUSES.IN_PROGRESS && run.status !== AUTORUN_STATUSES.INITIALIZED) {
|
|
722
|
+
return error(`Auto-run is ${run.status}. Cannot update.`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const cycle = {
|
|
726
|
+
cycle: (run.totals ? run.totals.cycles_completed : 0) + 1,
|
|
727
|
+
items_completed: Number(getVal('--items-completed', '0')),
|
|
728
|
+
items_failed: Number(getVal('--items-failed', '0')),
|
|
729
|
+
points_used: Number(getVal('--points-used', '0')),
|
|
730
|
+
tests_before: Number(getVal('--tests-before', '0')),
|
|
731
|
+
tests_after: Number(getVal('--tests-after', '0')),
|
|
732
|
+
batch_file: getVal('--batch-file', ''),
|
|
733
|
+
timestamp: new Date().toISOString(),
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
if (!run.cycles) run.cycles = [];
|
|
737
|
+
run.cycles.push(cycle);
|
|
738
|
+
|
|
739
|
+
if (!run.totals) {
|
|
740
|
+
run.totals = { cycles_completed: 0, items_completed: 0, items_failed: 0, points_used: 0, tests_current: 0 };
|
|
741
|
+
}
|
|
742
|
+
run.totals.cycles_completed += 1;
|
|
743
|
+
run.totals.items_completed += cycle.items_completed;
|
|
744
|
+
run.totals.items_failed += cycle.items_failed;
|
|
745
|
+
run.totals.points_used += cycle.points_used;
|
|
746
|
+
run.totals.tests_current = cycle.tests_after;
|
|
747
|
+
run.status = AUTORUN_STATUSES.IN_PROGRESS;
|
|
748
|
+
|
|
749
|
+
const stopReason = determineStopReason(cycle, run);
|
|
750
|
+
if (stopReason) {
|
|
751
|
+
run.status = stopReason === 'regression' ? AUTORUN_STATUSES.STOPPED : AUTORUN_STATUSES.COMPLETED;
|
|
752
|
+
run.stop_reason = stopReason;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
writeAutoRun(cwd, run);
|
|
756
|
+
const commitHash = focusAutoCheckpointCommit(cwd, cycle, run);
|
|
757
|
+
return output({
|
|
758
|
+
run_id: run.run_id, status: run.status, cycle_recorded: cycle.cycle,
|
|
759
|
+
total_items_completed: run.totals.items_completed,
|
|
760
|
+
total_points_used: run.totals.points_used, stop_reason: stopReason,
|
|
761
|
+
commit_hash: commitHash,
|
|
762
|
+
}, raw);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function focusAutoCheckpointCommit(cwd, cycle, run) {
|
|
766
|
+
if (!isGitRepo(cwd)) return null;
|
|
767
|
+
const config = loadConfig(cwd);
|
|
768
|
+
const autoCommit = config.focus && config.focus.auto_commit !== undefined ? config.focus.auto_commit : true;
|
|
769
|
+
if (!autoCommit) return null;
|
|
770
|
+
const status = execGit(cwd, ['status', '--porcelain', PLANNING_DIR + '/']);
|
|
771
|
+
if (status.exitCode !== 0 || !status.stdout) return null;
|
|
772
|
+
execGit(cwd, ['add', PLANNING_DIR + '/']);
|
|
773
|
+
const msg = `docs: focus-auto cycle ${cycle.cycle} — ${cycle.items_completed} items completed`;
|
|
774
|
+
const commitResult = execGit(cwd, ['commit', '-m', msg]);
|
|
775
|
+
if (commitResult.exitCode !== 0) return null;
|
|
776
|
+
const hashResult = execGit(cwd, ['rev-parse', '--short', 'HEAD']);
|
|
777
|
+
return hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function determineStopReason(cycle, run) {
|
|
781
|
+
if (cycle.tests_after < cycle.tests_before) return 'regression';
|
|
782
|
+
if (run.totals.points_used >= run.total_budget) return 'budget_cap';
|
|
783
|
+
if (run.totals.cycles_completed >= run.max_cycles) return 'max_cycles';
|
|
784
|
+
if (cycle.items_completed === 0) return 'zero_completed';
|
|
785
|
+
|
|
786
|
+
// Optimize category: stop when efficiency drops below threshold of previous cycle
|
|
787
|
+
if (run.category === 'optimize' && run.cycles.length >= 2) {
|
|
788
|
+
const prev = run.cycles[run.cycles.length - 2];
|
|
789
|
+
const prevEff = prev.items_completed / (prev.points_used || 1);
|
|
790
|
+
const currEff = cycle.items_completed / (cycle.points_used || 1);
|
|
791
|
+
if (currEff > 0 && currEff < prevEff * DIMINISHING_RETURNS_THRESHOLD) {
|
|
792
|
+
return 'diminishing_returns';
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function focusAutoContinue(cwd, raw) {
|
|
800
|
+
const run = readAutoRun(cwd);
|
|
801
|
+
if (!run) return error('No auto-run in progress. Start with: focus auto --category <name>');
|
|
802
|
+
if (run.status !== AUTORUN_STATUSES.STOPPED && run.status !== AUTORUN_STATUSES.INITIALIZED) {
|
|
803
|
+
return error(`Cannot continue: auto-run is ${run.status}.`);
|
|
804
|
+
}
|
|
805
|
+
run.status = run.totals && run.totals.cycles_completed > 0 ? AUTORUN_STATUSES.IN_PROGRESS : AUTORUN_STATUSES.INITIALIZED;
|
|
806
|
+
run.stop_reason = null;
|
|
807
|
+
writeAutoRun(cwd, run);
|
|
808
|
+
const budgetRemaining = run.total_budget - (run.totals ? run.totals.points_used : 0);
|
|
809
|
+
const cyclesRemaining = run.max_cycles - (run.totals ? run.totals.cycles_completed : 0);
|
|
810
|
+
return output({ ...run, budget_remaining: budgetRemaining, cycles_remaining: cyclesRemaining }, raw);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function focusAutoInit(cwd, raw, getVal, hasFlag) {
|
|
814
|
+
const category = getVal('--category', null);
|
|
815
|
+
if (category && !FOCUS_CATEGORIES.includes(category)) {
|
|
816
|
+
return error(`Category must be one of: ${FOCUS_CATEGORIES.join(', ')}`);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const existing = readAutoRun(cwd);
|
|
820
|
+
if (existing && (existing.status === AUTORUN_STATUSES.IN_PROGRESS || existing.status === AUTORUN_STATUSES.INITIALIZED)) {
|
|
821
|
+
return error('Auto-run already in progress. Use --stop to end it, or --continue to resume.');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const defaults = category ? CATEGORY_DEFAULTS[category] : { mode: 'balanced', budget: 50 };
|
|
825
|
+
const mode = getVal('--mode', defaults.mode);
|
|
826
|
+
const budget = Number(getVal('--budget', String(defaults.budget)));
|
|
827
|
+
const maxCycles = Number(getVal('--max-cycles', String(DEFAULT_MAX_CYCLES)));
|
|
828
|
+
const totalBudget = Number(getVal('--total-budget', String(DEFAULT_TOTAL_BUDGET)));
|
|
829
|
+
|
|
830
|
+
if (!FOCUS_MODES.includes(mode)) return error(`Mode must be one of: ${FOCUS_MODES.join(', ')}`);
|
|
831
|
+
if (budget < BUDGET_MIN || budget > BUDGET_MAX) return error(`Budget must be between ${BUDGET_MIN} and ${BUDGET_MAX}`);
|
|
832
|
+
if (maxCycles < MAX_CYCLES_MIN || maxCycles > MAX_CYCLES_MAX) return error(`Max cycles must be between ${MAX_CYCLES_MIN} and ${MAX_CYCLES_MAX}`);
|
|
833
|
+
if (totalBudget < TOTAL_BUDGET_MIN || totalBudget > TOTAL_BUDGET_MAX) return error(`Total budget must be between ${TOTAL_BUDGET_MIN} and ${TOTAL_BUDGET_MAX}`);
|
|
834
|
+
|
|
835
|
+
const runData = {
|
|
836
|
+
run_id: generateRunId(cwd),
|
|
837
|
+
status: AUTORUN_STATUSES.INITIALIZED,
|
|
838
|
+
category: category,
|
|
839
|
+
mode: mode,
|
|
840
|
+
budget_per_cycle: budget,
|
|
841
|
+
max_cycles: maxCycles,
|
|
842
|
+
total_budget: totalBudget,
|
|
843
|
+
priority_range: category ? CATEGORY_PRIORITY_RANGE[category] : { min: 0, max: 6 },
|
|
844
|
+
tests_baseline: null,
|
|
845
|
+
cycles: [],
|
|
846
|
+
totals: { cycles_completed: 0, items_completed: 0, items_failed: 0, points_used: 0, tests_current: 0 },
|
|
847
|
+
stop_reason: null,
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
if (hasFlag('--dry-run')) {
|
|
851
|
+
return output({ dry_run: true, ...runData, run_file: toPosix(path.join(PLANNING_DIR, FOCUS_DIR, AUTO_RUN_FILE)) }, raw);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
writeAutoRun(cwd, runData);
|
|
855
|
+
output({ ...runData, run_file: toPosix(path.join(PLANNING_DIR, FOCUS_DIR, AUTO_RUN_FILE)) }, raw);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function cmdFocusAuto(cwd, raw, ...args) {
|
|
859
|
+
const hasFlag = (flag) => args.includes(flag);
|
|
860
|
+
const getVal = (flag, def) => {
|
|
861
|
+
const idx = args.indexOf(flag);
|
|
862
|
+
return (idx !== -1 && idx + 1 < args.length) ? args[idx + 1] : def;
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
if (hasFlag('--status')) return focusAutoStatus(cwd, raw);
|
|
866
|
+
if (hasFlag('--stop')) return focusAutoStop(cwd, raw);
|
|
867
|
+
if (hasFlag('--update')) return focusAutoUpdate(cwd, raw, getVal);
|
|
868
|
+
if (hasFlag('--continue')) return focusAutoContinue(cwd, raw);
|
|
869
|
+
return focusAutoInit(cwd, raw, getVal, hasFlag);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ─── Module exports ─────────────────────────────────────────────────────────
|
|
873
|
+
|
|
874
|
+
module.exports = {
|
|
875
|
+
// Scan
|
|
876
|
+
cmdFocusScan,
|
|
877
|
+
collectWorkItems,
|
|
878
|
+
classifyItemPriority,
|
|
879
|
+
sortByPriority,
|
|
880
|
+
computeRealityScore,
|
|
881
|
+
summarizePriorities,
|
|
882
|
+
// Plan
|
|
883
|
+
cmdFocusPlan,
|
|
884
|
+
classifyTier,
|
|
885
|
+
allocateBudget,
|
|
886
|
+
// Sync
|
|
887
|
+
cmdFocusSync,
|
|
888
|
+
checkDocStaleness,
|
|
889
|
+
checkOldCommandNames,
|
|
890
|
+
checkVersionCrossRef,
|
|
891
|
+
// Exec
|
|
892
|
+
cmdFocusExec,
|
|
893
|
+
readLatestBatch,
|
|
894
|
+
// Auto
|
|
895
|
+
categoryFilter,
|
|
896
|
+
readAutoRun,
|
|
897
|
+
writeAutoRun,
|
|
898
|
+
cmdFocusAuto,
|
|
899
|
+
determineStopReason,
|
|
900
|
+
};
|