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,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core — Shared utilities, constants, and internal helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { execFileSync } = require('child_process');
|
|
9
|
+
const {
|
|
10
|
+
PLANNING_DIR,
|
|
11
|
+
PHASES_DIR,
|
|
12
|
+
MILESTONES_DIR,
|
|
13
|
+
ROADMAP_FILE,
|
|
14
|
+
MAX_JSON_SIZE,
|
|
15
|
+
PHASE_NUM_RE,
|
|
16
|
+
PHASE_DIR_RE,
|
|
17
|
+
ARCHIVE_DIR_RE,
|
|
18
|
+
isPlanFile,
|
|
19
|
+
isSummaryFile,
|
|
20
|
+
isResearchFile,
|
|
21
|
+
isContextFile,
|
|
22
|
+
isVerificationFile,
|
|
23
|
+
getPlanId,
|
|
24
|
+
getSummaryId,
|
|
25
|
+
MILESTONE_VERSION_RE,
|
|
26
|
+
} = require('./constants.cjs');
|
|
27
|
+
|
|
28
|
+
// ─── Model Profile Table ─────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const MODEL_PROFILES = {
|
|
31
|
+
'pan-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
|
|
32
|
+
'pan-roadmapper': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
33
|
+
'pan-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
34
|
+
'pan-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
35
|
+
'pan-project-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
36
|
+
'pan-research-synthesizer': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
37
|
+
'pan-debugger': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
38
|
+
'pan-document_code': { quality: 'opus', balanced: 'haiku', budget: 'haiku' },
|
|
39
|
+
'pan-verifier': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
40
|
+
'pan-plan-checker': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
41
|
+
'pan-integration-checker': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
42
|
+
'pan-reviewer': { quality: 'opus', balanced: 'haiku', budget: 'haiku' },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ─── Output helpers ───────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Write result to stdout and exit. JSON by default, or raw string if --raw flag is set.
|
|
49
|
+
* Large JSON (>50KB) is written to a tmpfile with @file: prefix.
|
|
50
|
+
* @param {Object} result - The result object to serialize as JSON
|
|
51
|
+
* @param {boolean} [raw] - If true and rawValue is provided, output rawValue as plain string
|
|
52
|
+
* @param {string} [rawValue] - Plain string to output when raw mode is active
|
|
53
|
+
*/
|
|
54
|
+
function output(result, raw, rawValue) {
|
|
55
|
+
if (raw && rawValue !== undefined) {
|
|
56
|
+
process.stdout.write(String(rawValue));
|
|
57
|
+
} else {
|
|
58
|
+
const json = JSON.stringify(result, null, 2);
|
|
59
|
+
// Large payloads exceed Claude Code's Bash tool buffer (~50KB).
|
|
60
|
+
// Write to tmpfile and output the path prefixed with @file: so callers can detect it.
|
|
61
|
+
if (json.length > MAX_JSON_SIZE) {
|
|
62
|
+
const tmpPath = path.join(os.tmpdir(), `pan-${Date.now()}.json`);
|
|
63
|
+
try {
|
|
64
|
+
fs.writeFileSync(tmpPath, json, 'utf-8');
|
|
65
|
+
process.stdout.write('@file:' + tmpPath);
|
|
66
|
+
} catch {
|
|
67
|
+
// Tmpfile write failed (disk full, permissions) — truncate and write to stdout
|
|
68
|
+
const truncated = json.slice(0, MAX_JSON_SIZE);
|
|
69
|
+
process.stdout.write(truncated);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
process.stdout.write(json);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Write error message to stderr and exit with code 1.
|
|
80
|
+
* @param {string} message - Error message
|
|
81
|
+
*/
|
|
82
|
+
function error(message) {
|
|
83
|
+
process.stderr.write('Error: ' + message + '\n');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Write debug message to stderr when --verbose flag is active.
|
|
89
|
+
* @param {...any} args - Values to log (joined with space)
|
|
90
|
+
*/
|
|
91
|
+
function verbose(...args) {
|
|
92
|
+
if (process.env.PAN_VERBOSE === '1') {
|
|
93
|
+
process.stderr.write('[pan-tools] ' + args.join(' ') + '\n');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Path utilities ─────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/** Normalize a relative path to always use forward slashes (POSIX) for JSON output. */
|
|
100
|
+
function toPosix(p) {
|
|
101
|
+
return p.split(path.sep).join('/');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── File & Config utilities ──────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Read a file, returning null instead of throwing on failure.
|
|
108
|
+
* @param {string} filePath - Absolute path to the file
|
|
109
|
+
* @returns {string|null} File contents as UTF-8 string, or null if unreadable
|
|
110
|
+
*/
|
|
111
|
+
function safeReadFile(filePath) {
|
|
112
|
+
try {
|
|
113
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Load project config from .planning/config.json, merging with defaults.
|
|
121
|
+
* Handles nested config sections (planning.*, workflow.*, git.*) and flat keys.
|
|
122
|
+
* @param {string} cwd - Project root directory
|
|
123
|
+
* @returns {Object} Flattened config with keys: model_profile, commit_docs, search_gitignored,
|
|
124
|
+
* branching_strategy, phase_branch_template, milestone_branch_template, research,
|
|
125
|
+
* plan_checker, verifier, parallelization, brave_search
|
|
126
|
+
*/
|
|
127
|
+
function loadConfig(cwd) {
|
|
128
|
+
const configPath = path.join(cwd, PLANNING_DIR, 'config.json');
|
|
129
|
+
const defaults = {
|
|
130
|
+
model_profile: 'balanced',
|
|
131
|
+
commit_docs: true,
|
|
132
|
+
search_gitignored: false,
|
|
133
|
+
branching_strategy: 'none',
|
|
134
|
+
phase_branch_template: 'pan/phase-{phase}-{slug}',
|
|
135
|
+
milestone_branch_template: 'pan/{milestone}-{slug}',
|
|
136
|
+
research: true,
|
|
137
|
+
plan_checker: true,
|
|
138
|
+
verifier: true,
|
|
139
|
+
parallelization: true,
|
|
140
|
+
brave_search: false,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
145
|
+
const parsed = JSON.parse(raw);
|
|
146
|
+
|
|
147
|
+
// get() resolves a config key by checking flat keys first (parsed[key]),
|
|
148
|
+
// then falling back to nested section lookup (parsed[section][field]).
|
|
149
|
+
// This lets users write either { "commit_docs": true } or
|
|
150
|
+
// { "planning": { "commit_docs": true } } in config.json.
|
|
151
|
+
const get = (key, nested) => {
|
|
152
|
+
if (parsed[key] !== undefined) return parsed[key];
|
|
153
|
+
if (nested && parsed[nested.section] && parsed[nested.section][nested.field] !== undefined) {
|
|
154
|
+
return parsed[nested.section][nested.field];
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const parallelization = (() => {
|
|
160
|
+
const val = get('parallelization');
|
|
161
|
+
if (typeof val === 'boolean') return val;
|
|
162
|
+
if (typeof val === 'object' && val !== null && 'enabled' in val) return val.enabled;
|
|
163
|
+
return defaults.parallelization;
|
|
164
|
+
})();
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
model_profile: get('model_profile') ?? defaults.model_profile,
|
|
168
|
+
commit_docs: get('commit_docs', { section: 'planning', field: 'commit_docs' }) ?? defaults.commit_docs,
|
|
169
|
+
search_gitignored: get('search_gitignored', { section: 'planning', field: 'search_gitignored' }) ?? defaults.search_gitignored,
|
|
170
|
+
branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
|
|
171
|
+
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
|
|
172
|
+
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
|
|
173
|
+
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
|
|
174
|
+
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
|
175
|
+
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
|
176
|
+
parallelization,
|
|
177
|
+
brave_search: get('brave_search') ?? defaults.brave_search,
|
|
178
|
+
budget: parsed.budget || { default_points: 50, micro_threshold_tasks: 3, micro_threshold_files: 2 },
|
|
179
|
+
commit: parsed.commit || { safety_checks: true, conventional_types: true, sensitive_patterns: ['\\.env$', '\\.pem$', '\\.key$', 'credentials', 'secret', 'password', 'token'] },
|
|
180
|
+
execution: parsed.execution || { default_mode: 'wave_order', rollback_snapshots: true, error_pattern_learning: true },
|
|
181
|
+
focus: parsed.focus || { auto_commit: true },
|
|
182
|
+
};
|
|
183
|
+
} catch { // Config missing or malformed — use defaults
|
|
184
|
+
return {
|
|
185
|
+
...defaults,
|
|
186
|
+
budget: { default_points: 50, micro_threshold_tasks: 3, micro_threshold_files: 2 },
|
|
187
|
+
commit: { safety_checks: true, conventional_types: true, sensitive_patterns: ['\\.env$', '\\.pem$', '\\.key$', 'credentials', 'secret', 'password', 'token'] },
|
|
188
|
+
execution: { default_mode: 'wave_order', rollback_snapshots: true, error_pattern_learning: true },
|
|
189
|
+
focus: { auto_commit: true },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── Git utilities ────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if a path is gitignored using `git check-ignore`.
|
|
198
|
+
* @param {string} cwd - Project root directory
|
|
199
|
+
* @param {string} targetPath - Path to check (relative to cwd)
|
|
200
|
+
* @returns {boolean} True if gitignored
|
|
201
|
+
*/
|
|
202
|
+
function isGitIgnored(cwd, targetPath) {
|
|
203
|
+
try {
|
|
204
|
+
execFileSync('git', ['check-ignore', '-q', '--', targetPath], {
|
|
205
|
+
cwd,
|
|
206
|
+
stdio: 'pipe',
|
|
207
|
+
});
|
|
208
|
+
return true;
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if the given directory is inside a git repository.
|
|
216
|
+
* @param {string} cwd - Directory to check
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
function isGitRepo(cwd) {
|
|
220
|
+
try {
|
|
221
|
+
const stdout = execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
222
|
+
cwd,
|
|
223
|
+
stdio: 'pipe',
|
|
224
|
+
encoding: 'utf-8',
|
|
225
|
+
});
|
|
226
|
+
return stdout.trim() === 'true';
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Execute a git command safely with proper argument escaping.
|
|
234
|
+
* @param {string} cwd - Working directory for git
|
|
235
|
+
* @param {string[]} args - Git arguments (e.g., ['add', 'file.md'])
|
|
236
|
+
* @returns {{exitCode: number, stdout: string, stderr: string}}
|
|
237
|
+
*/
|
|
238
|
+
function execGit(cwd, args) {
|
|
239
|
+
try {
|
|
240
|
+
const stdout = execFileSync('git', args, {
|
|
241
|
+
cwd,
|
|
242
|
+
stdio: 'pipe',
|
|
243
|
+
encoding: 'utf-8',
|
|
244
|
+
});
|
|
245
|
+
return { exitCode: 0, stdout: stdout.trim(), stderr: '' };
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return {
|
|
248
|
+
exitCode: err.status ?? 1,
|
|
249
|
+
stdout: (err.stdout ?? '').toString().trim(),
|
|
250
|
+
stderr: (err.stderr ?? '').toString().trim(),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Phase utilities ──────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
function escapeRegex(value) {
|
|
258
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Normalize a phase identifier to zero-padded format.
|
|
263
|
+
* Examples: "1" → "01", "3A" → "03A", "12.1" → "12.1"
|
|
264
|
+
* @param {string|number} phase - Phase identifier
|
|
265
|
+
* @returns {string} Normalized phase string
|
|
266
|
+
*/
|
|
267
|
+
function normalizePhaseName(phase) {
|
|
268
|
+
const match = String(phase).match(PHASE_NUM_RE);
|
|
269
|
+
if (!match) return phase;
|
|
270
|
+
const padded = match[1].padStart(2, '0');
|
|
271
|
+
const letter = match[2] ? match[2].toUpperCase() : '';
|
|
272
|
+
const decimal = match[3] || '';
|
|
273
|
+
return padded + letter + decimal;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Compare two phase identifiers for sorting. Handles integer, letter-suffix,
|
|
278
|
+
* and multi-level decimal phases (e.g., 1 < 2 < 2A < 2A.1 < 3).
|
|
279
|
+
* @param {string} a - First phase identifier
|
|
280
|
+
* @param {string} b - Second phase identifier
|
|
281
|
+
* @returns {number} Negative if a < b, positive if a > b, 0 if equal
|
|
282
|
+
*/
|
|
283
|
+
function comparePhaseNum(a, b) {
|
|
284
|
+
// 3-level comparison for phase identifiers like "12A.1.2":
|
|
285
|
+
// 1. Integer prefix: compare the leading digits (e.g., 3 vs 12)
|
|
286
|
+
// 2. Letter suffix: no letter < A < B (e.g., 12 < 12A < 12B)
|
|
287
|
+
// 3. Decimal segments: segment-by-segment numeric comparison (e.g., 12A.1 < 12A.2)
|
|
288
|
+
const partsA = String(a).match(PHASE_NUM_RE);
|
|
289
|
+
const partsB = String(b).match(PHASE_NUM_RE);
|
|
290
|
+
if (!partsA || !partsB) return String(a).localeCompare(String(b));
|
|
291
|
+
const intDiff = parseInt(partsA[1], 10) - parseInt(partsB[1], 10);
|
|
292
|
+
if (intDiff !== 0) return intDiff;
|
|
293
|
+
// No letter sorts before letter: 12 < 12A < 12B
|
|
294
|
+
const la = (partsA[2] || '').toUpperCase();
|
|
295
|
+
const lb = (partsB[2] || '').toUpperCase();
|
|
296
|
+
if (la !== lb) {
|
|
297
|
+
if (!la) return -1;
|
|
298
|
+
if (!lb) return 1;
|
|
299
|
+
return la < lb ? -1 : 1;
|
|
300
|
+
}
|
|
301
|
+
// Segment-by-segment decimal comparison: 12A < 12A.1 < 12A.1.2 < 12A.2
|
|
302
|
+
const aDecParts = partsA[3] ? partsA[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
303
|
+
const bDecParts = partsB[3] ? partsB[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
304
|
+
const maxLen = Math.max(aDecParts.length, bDecParts.length);
|
|
305
|
+
if (aDecParts.length === 0 && bDecParts.length > 0) return -1;
|
|
306
|
+
if (bDecParts.length === 0 && aDecParts.length > 0) return 1;
|
|
307
|
+
for (let i = 0; i < maxLen; i++) {
|
|
308
|
+
const av = Number.isFinite(aDecParts[i]) ? aDecParts[i] : 0;
|
|
309
|
+
const bv = Number.isFinite(bDecParts[i]) ? bDecParts[i] : 0;
|
|
310
|
+
if (av !== bv) return av - bv;
|
|
311
|
+
}
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Search a directory of phase folders for one matching the normalized phase number.
|
|
316
|
+
// Lists all subdirectories, finds the first whose name starts with the normalized
|
|
317
|
+
// phase prefix, then inventories its plan/summary/research/context/verification files.
|
|
318
|
+
// completedPlanIds tracks which plans have matching summaries so we can derive
|
|
319
|
+
// incomplete_plans (plans without a corresponding summary).
|
|
320
|
+
function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
321
|
+
try {
|
|
322
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
323
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
324
|
+
const match = dirs.find(d => d.startsWith(normalized));
|
|
325
|
+
if (!match) return null;
|
|
326
|
+
|
|
327
|
+
const dirMatch = match.match(PHASE_DIR_RE);
|
|
328
|
+
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
329
|
+
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
330
|
+
const phaseDir = path.join(baseDir, match);
|
|
331
|
+
const phaseFiles = fs.readdirSync(phaseDir);
|
|
332
|
+
|
|
333
|
+
const plans = phaseFiles.filter(isPlanFile).sort();
|
|
334
|
+
const summaries = phaseFiles.filter(isSummaryFile).sort();
|
|
335
|
+
const hasResearch = phaseFiles.some(isResearchFile);
|
|
336
|
+
const hasContext = phaseFiles.some(isContextFile);
|
|
337
|
+
const hasVerification = phaseFiles.some(isVerificationFile);
|
|
338
|
+
|
|
339
|
+
const completedPlanIds = new Set(
|
|
340
|
+
summaries.map(s => getSummaryId(s))
|
|
341
|
+
);
|
|
342
|
+
const incompletePlans = plans.filter(p => {
|
|
343
|
+
return !completedPlanIds.has(getPlanId(p));
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
found: true,
|
|
348
|
+
directory: toPosix(path.join(relBase, match)),
|
|
349
|
+
phase_number: phaseNumber,
|
|
350
|
+
phase_name: phaseName,
|
|
351
|
+
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
352
|
+
plans,
|
|
353
|
+
summaries,
|
|
354
|
+
incomplete_plans: incompletePlans,
|
|
355
|
+
has_research: hasResearch,
|
|
356
|
+
has_context: hasContext,
|
|
357
|
+
has_verification: hasVerification,
|
|
358
|
+
};
|
|
359
|
+
} catch { // Phase directory unreadable
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Find a phase directory by number, searching current phases then archived milestones.
|
|
366
|
+
* @param {string} cwd - Project root directory
|
|
367
|
+
* @param {string} phase - Phase identifier (e.g., "1", "03", "2.1")
|
|
368
|
+
* @returns {Object|null} Phase info: { found, directory, phase_number, phase_name, phase_slug,
|
|
369
|
+
* plans, summaries, incomplete_plans, has_research, has_context, has_verification, archived? }
|
|
370
|
+
*/
|
|
371
|
+
function findPhaseInternal(cwd, phase) {
|
|
372
|
+
if (!phase) return null;
|
|
373
|
+
|
|
374
|
+
const phasesDir = path.join(cwd, PLANNING_DIR, PHASES_DIR);
|
|
375
|
+
const normalized = normalizePhaseName(phase);
|
|
376
|
+
|
|
377
|
+
// Two-phase search strategy:
|
|
378
|
+
// 1. Search the active phases directory (.planning/phases/) first.
|
|
379
|
+
// 2. If not found, search archived milestone directories (.planning/milestones/v*-phases/)
|
|
380
|
+
// in reverse order (newest archive first) so the most recent match wins.
|
|
381
|
+
const current = searchPhaseInDir(phasesDir, path.join(PLANNING_DIR, PHASES_DIR), normalized);
|
|
382
|
+
if (current) return current;
|
|
383
|
+
|
|
384
|
+
// Search archived milestone phases (newest first)
|
|
385
|
+
const milestonesDir = path.join(cwd, PLANNING_DIR, MILESTONES_DIR);
|
|
386
|
+
try {
|
|
387
|
+
const milestoneEntries = fs.readdirSync(milestonesDir, { withFileTypes: true });
|
|
388
|
+
const archiveDirs = milestoneEntries
|
|
389
|
+
.filter(e => e.isDirectory() && ARCHIVE_DIR_RE.test(e.name))
|
|
390
|
+
.map(e => e.name)
|
|
391
|
+
.sort()
|
|
392
|
+
.reverse();
|
|
393
|
+
|
|
394
|
+
for (const archiveName of archiveDirs) {
|
|
395
|
+
const vm = archiveName.match(/^(v[\d.]+)-phases$/);
|
|
396
|
+
if (!vm) continue;
|
|
397
|
+
const version = vm[1];
|
|
398
|
+
const archivePath = path.join(milestonesDir, archiveName);
|
|
399
|
+
const relBase = path.join(PLANNING_DIR, MILESTONES_DIR, archiveName);
|
|
400
|
+
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
401
|
+
if (result) {
|
|
402
|
+
result.archived = version;
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch (e) { verbose('findPhaseInArchives: milestones directory missing or unreadable:', e.message); }
|
|
407
|
+
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function getArchivedPhaseDirs(cwd) {
|
|
412
|
+
const milestonesDir = path.join(cwd, PLANNING_DIR, MILESTONES_DIR);
|
|
413
|
+
const results = [];
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const milestoneEntries = fs.readdirSync(milestonesDir, { withFileTypes: true });
|
|
417
|
+
// Find v*-phases directories, sort newest first
|
|
418
|
+
const phaseDirs = milestoneEntries
|
|
419
|
+
.filter(e => e.isDirectory() && ARCHIVE_DIR_RE.test(e.name))
|
|
420
|
+
.map(e => e.name)
|
|
421
|
+
.sort()
|
|
422
|
+
.reverse();
|
|
423
|
+
|
|
424
|
+
for (const archiveName of phaseDirs) {
|
|
425
|
+
const vm = archiveName.match(/^(v[\d.]+)-phases$/);
|
|
426
|
+
if (!vm) continue;
|
|
427
|
+
const version = vm[1];
|
|
428
|
+
const archivePath = path.join(milestonesDir, archiveName);
|
|
429
|
+
const entries = fs.readdirSync(archivePath, { withFileTypes: true });
|
|
430
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
431
|
+
|
|
432
|
+
for (const dir of dirs) {
|
|
433
|
+
results.push({
|
|
434
|
+
name: dir,
|
|
435
|
+
milestone: version,
|
|
436
|
+
basePath: path.join(PLANNING_DIR, MILESTONES_DIR, archiveName),
|
|
437
|
+
fullPath: path.join(archivePath, dir),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} catch (e) { verbose('getArchivedPhaseDirs: milestones directory missing or unreadable:', e.message); }
|
|
442
|
+
|
|
443
|
+
return results;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ─── Roadmap & model utilities ────────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Extract a phase section from roadmap.md by phase number.
|
|
450
|
+
* @param {string} cwd - Project root directory
|
|
451
|
+
* @param {string|number} phaseNum - Phase number to look up
|
|
452
|
+
* @returns {Object|null} { found, phase_number, phase_name, goal, section } or null
|
|
453
|
+
*/
|
|
454
|
+
function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
455
|
+
if (!phaseNum) return null;
|
|
456
|
+
const roadmapPath = path.join(cwd, PLANNING_DIR, ROADMAP_FILE);
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
460
|
+
const escapedPhase = escapeRegex(phaseNum.toString());
|
|
461
|
+
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
|
|
462
|
+
const headerMatch = content.match(phasePattern);
|
|
463
|
+
if (!headerMatch) return null;
|
|
464
|
+
|
|
465
|
+
const phaseName = headerMatch[1].trim();
|
|
466
|
+
const headerIndex = headerMatch.index;
|
|
467
|
+
const restOfContent = content.slice(headerIndex);
|
|
468
|
+
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
469
|
+
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
|
|
470
|
+
const section = content.slice(headerIndex, sectionEnd).trim();
|
|
471
|
+
|
|
472
|
+
const goalMatch = section.match(/\*\*Goal:\*\*\s*([^\n]+)/i);
|
|
473
|
+
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
found: true,
|
|
477
|
+
phase_number: phaseNum.toString(),
|
|
478
|
+
phase_name: phaseName,
|
|
479
|
+
goal,
|
|
480
|
+
section,
|
|
481
|
+
};
|
|
482
|
+
} catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Resolve the model for a given agent type based on profile and overrides.
|
|
489
|
+
* Returns "inherit" for opus-tier to let Claude Code use its configured opus version.
|
|
490
|
+
* @param {string} cwd - Project root directory
|
|
491
|
+
* @param {string} agentType - Agent name (e.g., "pan-planner", "pan-executor")
|
|
492
|
+
* @returns {string} Model identifier: "inherit" (opus), "sonnet", or "haiku"
|
|
493
|
+
*/
|
|
494
|
+
function resolveModelInternal(cwd, agentType) {
|
|
495
|
+
const config = loadConfig(cwd);
|
|
496
|
+
|
|
497
|
+
// Check per-agent override first
|
|
498
|
+
const override = config.model_overrides?.[agentType];
|
|
499
|
+
if (override) {
|
|
500
|
+
return override === 'opus' ? 'inherit' : override;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Fall back to profile lookup
|
|
504
|
+
const profile = config.model_profile || 'balanced';
|
|
505
|
+
const agentModels = MODEL_PROFILES[agentType];
|
|
506
|
+
if (!agentModels) return 'sonnet';
|
|
507
|
+
const resolved = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
|
508
|
+
return resolved === 'opus' ? 'inherit' : resolved;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ─── Misc utilities ───────────────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
function pathExistsInternal(cwd, targetPath) {
|
|
514
|
+
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
|
|
515
|
+
try {
|
|
516
|
+
fs.statSync(fullPath);
|
|
517
|
+
return true;
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Convert text to a URL-safe slug (lowercase, hyphens, no special chars).
|
|
525
|
+
* @param {string} text - Input text
|
|
526
|
+
* @returns {string|null} Slug string, or null if text is falsy
|
|
527
|
+
*/
|
|
528
|
+
function generateSlugInternal(text) {
|
|
529
|
+
if (!text) return null;
|
|
530
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Extract current milestone version and name from roadmap.md.
|
|
535
|
+
* @param {string} cwd - Project root directory
|
|
536
|
+
* @returns {{version: string, name: string}} Milestone info (defaults: v1.0, "milestone")
|
|
537
|
+
*/
|
|
538
|
+
function getMilestoneInfo(cwd) {
|
|
539
|
+
try {
|
|
540
|
+
const roadmap = fs.readFileSync(path.join(cwd, PLANNING_DIR, ROADMAP_FILE), 'utf-8');
|
|
541
|
+
const versionMatch = roadmap.match(MILESTONE_VERSION_RE);
|
|
542
|
+
const nameMatch = roadmap.match(/## .*v\d+\.\d+[:\s]+([^\n(]+)/);
|
|
543
|
+
return {
|
|
544
|
+
version: versionMatch ? versionMatch[0] : 'v1.0',
|
|
545
|
+
name: nameMatch ? nameMatch[1].trim() : 'milestone',
|
|
546
|
+
};
|
|
547
|
+
} catch {
|
|
548
|
+
return { version: 'v1.0', name: 'milestone' };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Scan pending todos directory and return matching items.
|
|
554
|
+
* @param {string} cwd - Project root
|
|
555
|
+
* @param {string|null} area - Optional area filter
|
|
556
|
+
* @returns {{ count: number, todos: Array<{file: string, created: string, title: string, area: string, path: string}> }}
|
|
557
|
+
*/
|
|
558
|
+
function scanPendingTodos(cwd, area) {
|
|
559
|
+
const pendingDir = path.join(cwd, PLANNING_DIR, 'todos', 'pending');
|
|
560
|
+
let count = 0;
|
|
561
|
+
const todos = [];
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
|
565
|
+
for (const file of files) {
|
|
566
|
+
try {
|
|
567
|
+
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
|
|
568
|
+
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
569
|
+
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
570
|
+
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
571
|
+
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
|
572
|
+
|
|
573
|
+
if (area && todoArea !== area) continue;
|
|
574
|
+
|
|
575
|
+
count++;
|
|
576
|
+
todos.push({
|
|
577
|
+
file,
|
|
578
|
+
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
579
|
+
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
580
|
+
area: todoArea,
|
|
581
|
+
path: path.join(PLANNING_DIR, 'todos', 'pending', file),
|
|
582
|
+
});
|
|
583
|
+
} catch { /* skip unreadable file */ }
|
|
584
|
+
}
|
|
585
|
+
} catch { /* pending dir does not exist */ }
|
|
586
|
+
|
|
587
|
+
return { count, todos };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Scan source files for TODO/FIXME/XXX/HACK comments.
|
|
592
|
+
* @param {string} cwd - Project root
|
|
593
|
+
* @returns {{ count: number, items: Array<{file: string, line: number, tag: string, text: string}> }}
|
|
594
|
+
*/
|
|
595
|
+
function scanSourceTodos(cwd) {
|
|
596
|
+
const items = [];
|
|
597
|
+
const libDir = path.join(cwd, 'pan-wizard-core', 'bin', 'lib');
|
|
598
|
+
const pattern = /\b(TODO|FIXME|XXX|HACK)\b[:\s]*(.*)/i;
|
|
599
|
+
|
|
600
|
+
let files;
|
|
601
|
+
try {
|
|
602
|
+
files = fs.readdirSync(libDir).filter(f => f.endsWith('.cjs'));
|
|
603
|
+
} catch { return { count: 0, items }; }
|
|
604
|
+
|
|
605
|
+
for (const file of files) {
|
|
606
|
+
try {
|
|
607
|
+
const content = fs.readFileSync(path.join(libDir, file), 'utf-8');
|
|
608
|
+
const lines = content.split('\n');
|
|
609
|
+
for (let i = 0; i < lines.length; i++) {
|
|
610
|
+
const match = lines[i].match(pattern);
|
|
611
|
+
if (match) {
|
|
612
|
+
items.push({
|
|
613
|
+
file: toPosix(path.join('pan-wizard-core', 'bin', 'lib', file)),
|
|
614
|
+
line: i + 1,
|
|
615
|
+
tag: match[1].toUpperCase(),
|
|
616
|
+
text: match[2].trim(),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
} catch { /* skip unreadable file */ }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return { count: items.length, items };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
module.exports = {
|
|
627
|
+
MODEL_PROFILES,
|
|
628
|
+
output,
|
|
629
|
+
error,
|
|
630
|
+
verbose,
|
|
631
|
+
safeReadFile,
|
|
632
|
+
loadConfig,
|
|
633
|
+
isGitIgnored,
|
|
634
|
+
isGitRepo,
|
|
635
|
+
execGit,
|
|
636
|
+
escapeRegex,
|
|
637
|
+
normalizePhaseName,
|
|
638
|
+
comparePhaseNum,
|
|
639
|
+
searchPhaseInDir,
|
|
640
|
+
findPhaseInternal,
|
|
641
|
+
getArchivedPhaseDirs,
|
|
642
|
+
getRoadmapPhaseInternal,
|
|
643
|
+
resolveModelInternal,
|
|
644
|
+
pathExistsInternal,
|
|
645
|
+
generateSlugInternal,
|
|
646
|
+
getMilestoneInfo,
|
|
647
|
+
toPosix,
|
|
648
|
+
scanPendingTodos,
|
|
649
|
+
scanSourceTodos,
|
|
650
|
+
};
|