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.
Files changed (164) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +772 -0
  3. package/agents/pan-debugger.md +1246 -0
  4. package/agents/pan-document_code.md +965 -0
  5. package/agents/pan-executor.md +469 -0
  6. package/agents/pan-integration-checker.md +443 -0
  7. package/agents/pan-phase-researcher.md +572 -0
  8. package/agents/pan-plan-checker.md +763 -0
  9. package/agents/pan-planner.md +1297 -0
  10. package/agents/pan-project-researcher.md +647 -0
  11. package/agents/pan-research-synthesizer.md +239 -0
  12. package/agents/pan-reviewer.md +112 -0
  13. package/agents/pan-roadmapper.md +642 -0
  14. package/agents/pan-verifier.md +672 -0
  15. package/assets/pan-logo-2000-transparent.svg +30 -0
  16. package/assets/pan-logo-2000.svg +43 -0
  17. package/assets/terminal.svg +119 -0
  18. package/bin/install-lib.cjs +616 -0
  19. package/bin/install.js +1936 -0
  20. package/commands/pan/add-phase.md +44 -0
  21. package/commands/pan/assumptions.md +47 -0
  22. package/commands/pan/audit-deployment.md +378 -0
  23. package/commands/pan/debug.md +168 -0
  24. package/commands/pan/discord.md +19 -0
  25. package/commands/pan/discuss-phase.md +84 -0
  26. package/commands/pan/exec-phase.md +45 -0
  27. package/commands/pan/focus-auto.md +323 -0
  28. package/commands/pan/focus-design.md +816 -0
  29. package/commands/pan/focus-exec.md +316 -0
  30. package/commands/pan/focus-plan.md +101 -0
  31. package/commands/pan/focus-scan.md +272 -0
  32. package/commands/pan/focus-sync.md +104 -0
  33. package/commands/pan/health.md +23 -0
  34. package/commands/pan/help.md +23 -0
  35. package/commands/pan/insert-phase.md +33 -0
  36. package/commands/pan/map-codebase.md +72 -0
  37. package/commands/pan/milestone-audit.md +37 -0
  38. package/commands/pan/milestone-cleanup.md +19 -0
  39. package/commands/pan/milestone-done.md +137 -0
  40. package/commands/pan/milestone-gaps.md +35 -0
  41. package/commands/pan/milestone-new.md +45 -0
  42. package/commands/pan/new-project.md +43 -0
  43. package/commands/pan/patches.md +110 -0
  44. package/commands/pan/pause.md +39 -0
  45. package/commands/pan/phase-budget.md +23 -0
  46. package/commands/pan/phase-tests.md +42 -0
  47. package/commands/pan/plan-phase.md +46 -0
  48. package/commands/pan/profile.md +36 -0
  49. package/commands/pan/progress.md +25 -0
  50. package/commands/pan/quick.md +42 -0
  51. package/commands/pan/remove-phase.md +32 -0
  52. package/commands/pan/research-phase.md +190 -0
  53. package/commands/pan/resume.md +41 -0
  54. package/commands/pan/retro.md +33 -0
  55. package/commands/pan/settings.md +37 -0
  56. package/commands/pan/todo-add.md +48 -0
  57. package/commands/pan/todo-check.md +46 -0
  58. package/commands/pan/update.md +38 -0
  59. package/commands/pan/verify-phase.md +39 -0
  60. package/hooks/dist/pan-check-update.js +62 -0
  61. package/hooks/dist/pan-context-monitor.js +122 -0
  62. package/hooks/dist/pan-statusline.js +108 -0
  63. package/package.json +66 -0
  64. package/pan-wizard-core/bin/lib/codebase.cjs +746 -0
  65. package/pan-wizard-core/bin/lib/commands.cjs +1435 -0
  66. package/pan-wizard-core/bin/lib/config.cjs +611 -0
  67. package/pan-wizard-core/bin/lib/constants.cjs +696 -0
  68. package/pan-wizard-core/bin/lib/context-budget.cjs +150 -0
  69. package/pan-wizard-core/bin/lib/core.cjs +650 -0
  70. package/pan-wizard-core/bin/lib/focus.cjs +900 -0
  71. package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -0
  72. package/pan-wizard-core/bin/lib/init.cjs +881 -0
  73. package/pan-wizard-core/bin/lib/milestone.cjs +276 -0
  74. package/pan-wizard-core/bin/lib/phase.cjs +1212 -0
  75. package/pan-wizard-core/bin/lib/roadmap.cjs +470 -0
  76. package/pan-wizard-core/bin/lib/state.cjs +1029 -0
  77. package/pan-wizard-core/bin/lib/template.cjs +314 -0
  78. package/pan-wizard-core/bin/lib/utils.cjs +171 -0
  79. package/pan-wizard-core/bin/lib/verify.cjs +1808 -0
  80. package/pan-wizard-core/bin/pan-tools.cjs +773 -0
  81. package/pan-wizard-core/references/checkpoints.md +776 -0
  82. package/pan-wizard-core/references/continuation-format.md +249 -0
  83. package/pan-wizard-core/references/decimal-phase-calculation.md +65 -0
  84. package/pan-wizard-core/references/git-integration.md +248 -0
  85. package/pan-wizard-core/references/git-planning-commit.md +38 -0
  86. package/pan-wizard-core/references/model-profile-resolution.md +34 -0
  87. package/pan-wizard-core/references/model-profiles.md +111 -0
  88. package/pan-wizard-core/references/phase-argument-parsing.md +61 -0
  89. package/pan-wizard-core/references/planning-config.md +196 -0
  90. package/pan-wizard-core/references/questioning.md +145 -0
  91. package/pan-wizard-core/references/tdd.md +263 -0
  92. package/pan-wizard-core/references/ui-brand.md +160 -0
  93. package/pan-wizard-core/references/verification-patterns.md +612 -0
  94. package/pan-wizard-core/templates/codebase/architecture.md +283 -0
  95. package/pan-wizard-core/templates/codebase/best-practices.md +133 -0
  96. package/pan-wizard-core/templates/codebase/concerns.md +325 -0
  97. package/pan-wizard-core/templates/codebase/conventions.md +307 -0
  98. package/pan-wizard-core/templates/codebase/integrations.md +305 -0
  99. package/pan-wizard-core/templates/codebase/relationships.md +124 -0
  100. package/pan-wizard-core/templates/codebase/stack.md +199 -0
  101. package/pan-wizard-core/templates/codebase/structure.md +298 -0
  102. package/pan-wizard-core/templates/codebase/testing.md +480 -0
  103. package/pan-wizard-core/templates/config.json +37 -0
  104. package/pan-wizard-core/templates/context.md +283 -0
  105. package/pan-wizard-core/templates/continue-here.md +78 -0
  106. package/pan-wizard-core/templates/debug-subagent-prompt.md +91 -0
  107. package/pan-wizard-core/templates/debug.md +164 -0
  108. package/pan-wizard-core/templates/discovery.md +146 -0
  109. package/pan-wizard-core/templates/milestone-archive.md +123 -0
  110. package/pan-wizard-core/templates/milestone.md +115 -0
  111. package/pan-wizard-core/templates/phase-prompt.md +593 -0
  112. package/pan-wizard-core/templates/planner-subagent-prompt.md +117 -0
  113. package/pan-wizard-core/templates/project.md +184 -0
  114. package/pan-wizard-core/templates/requirements.md +231 -0
  115. package/pan-wizard-core/templates/research-project/architecture.md +204 -0
  116. package/pan-wizard-core/templates/research-project/features.md +147 -0
  117. package/pan-wizard-core/templates/research-project/pitfalls.md +200 -0
  118. package/pan-wizard-core/templates/research-project/stack.md +120 -0
  119. package/pan-wizard-core/templates/research-project/summary.md +170 -0
  120. package/pan-wizard-core/templates/research.md +552 -0
  121. package/pan-wizard-core/templates/retrospective.md +54 -0
  122. package/pan-wizard-core/templates/roadmap.md +202 -0
  123. package/pan-wizard-core/templates/standards.md +24 -0
  124. package/pan-wizard-core/templates/state.md +176 -0
  125. package/pan-wizard-core/templates/summary-complex.md +59 -0
  126. package/pan-wizard-core/templates/summary-minimal.md +41 -0
  127. package/pan-wizard-core/templates/summary-standard.md +49 -0
  128. package/pan-wizard-core/templates/summary.md +249 -0
  129. package/pan-wizard-core/templates/uat.md +247 -0
  130. package/pan-wizard-core/templates/user-setup.md +311 -0
  131. package/pan-wizard-core/templates/validation.md +76 -0
  132. package/pan-wizard-core/templates/verification-report.md +322 -0
  133. package/pan-wizard-core/workflows/add-phase.md +111 -0
  134. package/pan-wizard-core/workflows/assumptions.md +178 -0
  135. package/pan-wizard-core/workflows/diagnose-issues.md +219 -0
  136. package/pan-wizard-core/workflows/discuss-phase.md +542 -0
  137. package/pan-wizard-core/workflows/exec-phase.md +572 -0
  138. package/pan-wizard-core/workflows/execute-plan.md +448 -0
  139. package/pan-wizard-core/workflows/health.md +156 -0
  140. package/pan-wizard-core/workflows/help.md +431 -0
  141. package/pan-wizard-core/workflows/insert-phase.md +129 -0
  142. package/pan-wizard-core/workflows/map-codebase.md +401 -0
  143. package/pan-wizard-core/workflows/milestone-audit.md +297 -0
  144. package/pan-wizard-core/workflows/milestone-cleanup.md +152 -0
  145. package/pan-wizard-core/workflows/milestone-gaps.md +274 -0
  146. package/pan-wizard-core/workflows/milestone-new.md +382 -0
  147. package/pan-wizard-core/workflows/new-project.md +1178 -0
  148. package/pan-wizard-core/workflows/pause.md +122 -0
  149. package/pan-wizard-core/workflows/phase-tests.md +388 -0
  150. package/pan-wizard-core/workflows/plan-phase.md +569 -0
  151. package/pan-wizard-core/workflows/profile.md +115 -0
  152. package/pan-wizard-core/workflows/progress.md +381 -0
  153. package/pan-wizard-core/workflows/quick.md +453 -0
  154. package/pan-wizard-core/workflows/remove-phase.md +154 -0
  155. package/pan-wizard-core/workflows/research-phase.md +73 -0
  156. package/pan-wizard-core/workflows/resume-project.md +306 -0
  157. package/pan-wizard-core/workflows/retro.md +121 -0
  158. package/pan-wizard-core/workflows/settings.md +213 -0
  159. package/pan-wizard-core/workflows/todo-add.md +157 -0
  160. package/pan-wizard-core/workflows/todo-check.md +176 -0
  161. package/pan-wizard-core/workflows/transition.md +544 -0
  162. package/pan-wizard-core/workflows/update.md +219 -0
  163. package/pan-wizard-core/workflows/verify-phase.md +301 -0
  164. package/scripts/build-hooks.js +43 -0
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Frontmatter -- YAML frontmatter parsing, serialization, and CRUD commands
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { safeReadFile, output, error } = require('./core.cjs');
8
+ const { FIELD_VALUE_RE, PRIORITY_LEVELS, EFFORT_SIZES } = require('./constants.cjs');
9
+
10
+ // --- Inline array rendering thresholds -------------------------------------------
11
+
12
+ /** Maximum number of items before switching from inline [a, b] to multi-line array */
13
+ const MAX_INLINE_ARRAY_ITEMS = 3;
14
+
15
+ /** Maximum character width of joined items before switching to multi-line array */
16
+ const MAX_INLINE_ARRAY_WIDTH = 60;
17
+
18
+ // --- Parsing engine --------------------------------------------------------------
19
+
20
+ /**
21
+ * Process a single YAML line: detect key-value pairs, inline arrays, nested objects, or list items.
22
+ * @param {string} line - The raw YAML line
23
+ * @param {number} indent - Indentation level of this line
24
+ * @param {Object} current - Current stack frame { obj, key, indent }
25
+ * @param {Object[]} stack - Full parser stack
26
+ */
27
+ function processYamlLine(line, indent, current, stack) {
28
+ const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
29
+ if (keyMatch) {
30
+ const key = keyMatch[2];
31
+ const value = keyMatch[3].trim();
32
+
33
+ if (value === '' || value === '[') {
34
+ current.obj[key] = value === '[' ? [] : {};
35
+ current.key = null;
36
+ stack.push({ obj: current.obj[key], key: null, indent });
37
+ } else if (value.startsWith('[') && value.endsWith(']')) {
38
+ current.obj[key] = value.slice(1, -1).split(',').map(item => item.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
39
+ current.key = null;
40
+ } else {
41
+ current.obj[key] = value.replace(/^["']|["']$/g, '');
42
+ current.key = null;
43
+ }
44
+ } else if (line.trim().startsWith('- ')) {
45
+ const itemValue = line.trim().slice(2).replace(/^["']|["']$/g, '');
46
+
47
+ if (typeof current.obj === 'object' && !Array.isArray(current.obj) && Object.keys(current.obj).length === 0) {
48
+ const parent = stack.length > 1 ? stack[stack.length - 2] : null;
49
+ if (parent) {
50
+ for (const parentKey of Object.keys(parent.obj)) {
51
+ if (parent.obj[parentKey] === current.obj) {
52
+ parent.obj[parentKey] = [itemValue];
53
+ current.obj = parent.obj[parentKey];
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ } else if (Array.isArray(current.obj)) {
59
+ current.obj.push(itemValue);
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Parse YAML frontmatter from markdown content into a plain object.
66
+ * @param {string} content - Markdown content with optional ---delimited frontmatter
67
+ * @returns {Object} Parsed frontmatter key-value pairs (empty object if none found)
68
+ */
69
+ function extractFrontmatter(content) {
70
+ /*
71
+ * Stack-based parser that tracks nesting depth via indentation.
72
+ * Each stack entry represents a nested object or array context.
73
+ *
74
+ * Algorithm overview:
75
+ * 1. Extract the raw YAML between the opening and closing --- delimiters.
76
+ * 2. Split into lines and iterate, tracking indentation level per line.
77
+ * 3. Maintain a stack where each frame holds:
78
+ * - obj: the current object or array being populated
79
+ * - key: (unused reservation for future array grouping)
80
+ * - indent: the indentation level at which this frame was pushed
81
+ * 4. On each line, pop stack frames whose indent is >= current indent,
82
+ * effectively "closing" nested contexts when de-indented.
83
+ * 5. Detect key-value pairs, inline arrays, nested objects, and list items
84
+ * and insert them into the current stack frame's object.
85
+ * 6. When a list item (- value) is found inside what was initialized as an
86
+ * empty object {}, convert that object to an array in the parent frame.
87
+ */
88
+
89
+ const frontmatter = {};
90
+
91
+ // --- Frontmatter delimiter detection ---
92
+ // Match the first YAML block bounded by opening "---\n" and closing "\n---"
93
+ const match = content.match(/^---\n([\s\S]+?)\n---/);
94
+ if (!match) return frontmatter;
95
+
96
+ const yaml = match[1];
97
+ const lines = yaml.split('\n');
98
+
99
+ // Stack to track nested objects: [{obj, key, indent}]
100
+ // obj = object to write to, key = current key collecting array items, indent = indentation level
101
+ let stack = [{ obj: frontmatter, key: null, indent: -1 }];
102
+
103
+ for (const line of lines) {
104
+ // Skip empty lines -- they carry no YAML data
105
+ if (line.trim() === '') continue;
106
+
107
+ // --- Indentation-based stack pop logic ---
108
+ // Calculate indentation as the count of leading whitespace characters.
109
+ // This determines which nesting level the current line belongs to.
110
+ const indentMatch = line.match(/^(\s*)/);
111
+ const indent = indentMatch ? indentMatch[1].length : 0;
112
+
113
+ // Pop stack frames until we find one whose indent is strictly less than
114
+ // the current line's indent. This "closes" any nested contexts that have
115
+ // ended due to de-indentation (e.g., returning from a nested object back
116
+ // to the parent level).
117
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
118
+ stack.pop();
119
+ }
120
+
121
+ const current = stack[stack.length - 1];
122
+ processYamlLine(line, indent, current, stack);
123
+ }
124
+
125
+ return frontmatter;
126
+ }
127
+
128
+ /**
129
+ * Serialize a plain object back into YAML frontmatter string (without --- delimiters).
130
+ *
131
+ * Serialization strategy:
132
+ * - Short string arrays (at most MAX_INLINE_ARRAY_ITEMS items whose joined width
133
+ * is under MAX_INLINE_ARRAY_WIDTH characters) render inline: key: [a, b, c]
134
+ * - Longer or non-string arrays render multi-line with "- item" syntax
135
+ * - Nested objects recurse up to 3 levels deep (top > sub > subsub)
136
+ * - Scalar values containing colons, hashes, or leading brackets are quoted
137
+ *
138
+ * @param {Object} obj - Key-value pairs to serialize as YAML
139
+ * @returns {string} YAML-formatted string
140
+ */
141
+ function reconstructFrontmatter(obj) {
142
+ const lines = [];
143
+ for (const [key, value] of Object.entries(obj)) {
144
+ renderYamlEntry(lines, key, value, 0);
145
+ }
146
+ return lines.join('\n');
147
+ }
148
+
149
+ /**
150
+ * Render a single key-value pair as YAML lines at the given indent depth.
151
+ * Handles scalars, arrays (inline or multi-line), and nested objects recursively.
152
+ */
153
+ function renderYamlEntry(lines, key, value, depth) {
154
+ if (value === null || value === undefined) return;
155
+ const indent = ' '.repeat(depth);
156
+
157
+ if (Array.isArray(value)) {
158
+ if (value.length === 0) {
159
+ lines.push(`${indent}${key}: []`);
160
+ } else if (value.every(item => typeof item === 'string') && value.length <= MAX_INLINE_ARRAY_ITEMS && value.join(', ').length < MAX_INLINE_ARRAY_WIDTH) {
161
+ lines.push(`${indent}${key}: [${value.join(', ')}]`);
162
+ } else {
163
+ lines.push(`${indent}${key}:`);
164
+ const itemIndent = ' '.repeat(depth + 1);
165
+ for (const item of value) {
166
+ lines.push(`${itemIndent}- ${quoteIfNeeded(item)}`);
167
+ }
168
+ }
169
+ } else if (typeof value === 'object') {
170
+ lines.push(`${indent}${key}:`);
171
+ for (const [childKey, childVal] of Object.entries(value)) {
172
+ renderYamlEntry(lines, childKey, childVal, depth + 1);
173
+ }
174
+ } else {
175
+ lines.push(`${indent}${key}: ${quoteIfNeeded(value)}`);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Quote a value if it contains YAML-special characters.
181
+ */
182
+ function quoteIfNeeded(value) {
183
+ const str = String(value);
184
+ if (str.includes(':') || str.includes('#') || str.startsWith('[') || str.startsWith('{')) {
185
+ return `"${str}"`;
186
+ }
187
+ return str;
188
+ }
189
+
190
+ /**
191
+ * Replace or insert frontmatter in markdown content with a new object.
192
+ * @param {string} content - Original markdown content
193
+ * @param {Object} newObj - New frontmatter key-value pairs
194
+ * @returns {string} Content with updated frontmatter block
195
+ */
196
+ function spliceFrontmatter(content, newObj) {
197
+ const yamlStr = reconstructFrontmatter(newObj);
198
+ const match = content.match(/^---\n[\s\S]+?\n---/);
199
+ if (match) {
200
+ return `---\n${yamlStr}\n---` + content.slice(match[0].length);
201
+ }
202
+ return `---\n${yamlStr}\n---\n\n` + content;
203
+ }
204
+
205
+ /**
206
+ * Parse a specific block (artifacts, key_links, truths) from must_haves in raw YAML frontmatter.
207
+ *
208
+ * Block structure being parsed:
209
+ * The must_haves section is a 3-level nested YAML structure:
210
+ * must_haves: (level 1 -- 2-space indent)
211
+ * artifacts: (level 2 -- 4-space indent, the blockName)
212
+ * - path: foo (level 3 -- 6-space indent, list items)
213
+ * provides: bar (level 3+ -- 8-space indent, continuation k-v pairs)
214
+ *
215
+ * This function finds the named block at the 4-space level, then parses
216
+ * its list items (at 6-space indent) and their nested key-value pairs
217
+ * (at 8+ space indent) into an array of objects or strings.
218
+ *
219
+ * @param {string} content - Full markdown content with frontmatter
220
+ * @param {string} blockName - Block name to extract (e.g., "artifacts", "key_links")
221
+ * @returns {Array} Parsed array of block items (objects or strings)
222
+ */
223
+ function parseMustHavesBlock(content, blockName) {
224
+ // Extract raw YAML between --- delimiters
225
+ const fmMatch = content.match(/^---\n([\s\S]+?)\n---/);
226
+ if (!fmMatch) return [];
227
+
228
+ const yaml = fmMatch[1];
229
+
230
+ // Locate the block header at exactly 4-space indent (must_haves child level)
231
+ const blockPattern = new RegExp(`^\\s{4}${blockName}:\\s*$`, 'm');
232
+ const blockStart = yaml.search(blockPattern);
233
+ if (blockStart === -1) return [];
234
+
235
+ const afterBlock = yaml.slice(blockStart);
236
+ // Skip the header line itself, then process remaining lines
237
+ const blockLines = afterBlock.split('\n').slice(1);
238
+
239
+ const items = [];
240
+ let currentItem = null;
241
+
242
+ for (const line of blockLines) {
243
+ // Skip blank lines within the block
244
+ if (line.trim() === '') continue;
245
+
246
+ // --- Indentation-based grouping ---
247
+ // Measure indent to detect when we've left the block (indent <= 4
248
+ // means we've returned to must_haves level or a sibling block)
249
+ const indent = line.match(/^(\s*)/)[1].length;
250
+ if (indent <= 4 && line.trim() !== '') break;
251
+
252
+ if (line.match(/^\s{6}-\s+/)) {
253
+ // New list item at 6-space indent (direct child of the block)
254
+ if (currentItem) items.push(currentItem);
255
+ currentItem = {};
256
+
257
+ // Check if it is a simple string item (no colon means not key-value)
258
+ const simpleMatch = line.match(/^\s{6}-\s+"?([^"]+)"?\s*$/);
259
+ if (simpleMatch && !line.includes(':')) {
260
+ currentItem = simpleMatch[1];
261
+ } else {
262
+ // Key-value on same line as dash: "- path: value"
263
+ const kvMatch = line.match(/^\s{6}-\s+(\w+):\s*"?([^"]*)"?\s*$/);
264
+ if (kvMatch) {
265
+ currentItem = {};
266
+ currentItem[kvMatch[1]] = kvMatch[2];
267
+ }
268
+ }
269
+ } else if (currentItem && typeof currentItem === 'object') {
270
+ // Continuation key-value at 8+ space indent (properties of current list item)
271
+ const kvMatch = line.match(/^\s{8,}(\w+):\s*"?([^"]*)"?\s*$/);
272
+ if (kvMatch) {
273
+ const val = kvMatch[2];
274
+ // Coerce pure-integer strings to numbers for convenience
275
+ currentItem[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
276
+ }
277
+ // Array items nested under a property at 10+ space indent
278
+ const arrMatch = line.match(/^\s{10,}-\s+"?([^"]+)"?\s*$/);
279
+ if (arrMatch) {
280
+ // Convert the most recently added key's scalar value into an array,
281
+ // then append this item to that array
282
+ const keys = Object.keys(currentItem);
283
+ const lastKey = keys[keys.length - 1];
284
+ if (lastKey && !Array.isArray(currentItem[lastKey])) {
285
+ currentItem[lastKey] = currentItem[lastKey] ? [currentItem[lastKey]] : [];
286
+ }
287
+ if (lastKey) currentItem[lastKey].push(arrMatch[1]);
288
+ }
289
+ }
290
+ }
291
+ // Push the final item if one was being accumulated
292
+ if (currentItem) items.push(currentItem);
293
+
294
+ return items;
295
+ }
296
+
297
+ // --- Focus field helpers ---------------------------------------------------------
298
+
299
+ /**
300
+ * Extract priority and effort from frontmatter with validation and defaults.
301
+ * @param {Object} fm - Parsed frontmatter object from extractFrontmatter()
302
+ * @returns {{ priority: string, effort: string, priorityValid: boolean, effortValid: boolean }}
303
+ */
304
+ function extractPriorityEffort(fm) {
305
+ const rawPriority = fm.priority ? String(fm.priority).toUpperCase() : null;
306
+ const rawEffort = fm.effort ? String(fm.effort).toUpperCase() : null;
307
+ return {
308
+ priority: PRIORITY_LEVELS.includes(rawPriority) ? rawPriority : 'P3',
309
+ effort: EFFORT_SIZES.includes(rawEffort) ? rawEffort : 'M',
310
+ priorityValid: rawPriority === null || PRIORITY_LEVELS.includes(rawPriority),
311
+ effortValid: rawEffort === null || EFFORT_SIZES.includes(rawEffort),
312
+ };
313
+ }
314
+
315
+ // --- Frontmatter CRUD commands ---------------------------------------------------
316
+
317
+ const FRONTMATTER_SCHEMAS = {
318
+ plan: { required: ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'] },
319
+ summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
320
+ verification: { required: ['phase', 'verified', 'status', 'score'] },
321
+ };
322
+
323
+ /**
324
+ * Get frontmatter from a file, optionally filtered to a single field.
325
+ * @param {string} cwd - Working directory path
326
+ * @param {string} filePath - Path to the markdown file
327
+ * @param {string} field - Optional specific field to extract
328
+ * @param {boolean} raw - If true, output raw value instead of JSON
329
+ * @returns {void}
330
+ */
331
+ function cmdFrontmatterGet(cwd, filePath, field, raw) {
332
+ if (!filePath) { error('file path required'); }
333
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
334
+ const content = safeReadFile(fullPath);
335
+ if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
336
+ const fm = extractFrontmatter(content);
337
+ if (field) {
338
+ const value = fm[field];
339
+ if (value === undefined) { output({ error: 'Field not found', field }, raw); return; }
340
+ output({ [field]: value }, raw, JSON.stringify(value));
341
+ } else {
342
+ output(fm, raw);
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Set a single frontmatter field value in a markdown file.
348
+ * @param {string} cwd - Working directory path
349
+ * @param {string} filePath - Path to the markdown file
350
+ * @param {string} field - Field name to set
351
+ * @param {string} value - Value to set (JSON-parsed if valid)
352
+ * @param {boolean} raw - If true, output raw value instead of JSON
353
+ * @returns {void}
354
+ */
355
+ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
356
+ if (!filePath || !field || value === undefined) { error('file, field, and value required'); }
357
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
358
+ let content;
359
+ try {
360
+ content = fs.readFileSync(fullPath, 'utf-8');
361
+ } catch {
362
+ output({ error: 'File not found', path: filePath }, raw); return;
363
+ }
364
+ const fm = extractFrontmatter(content);
365
+ let parsedValue;
366
+ // Attempt JSON parse so callers can pass structured values (arrays, objects);
367
+ // fall back to raw string if the value is not valid JSON
368
+ try { parsedValue = JSON.parse(value); } catch { parsedValue = value; }
369
+ fm[field] = parsedValue;
370
+ const newContent = spliceFrontmatter(content, fm);
371
+ try {
372
+ fs.writeFileSync(fullPath, newContent, 'utf-8');
373
+ } catch (err) {
374
+ output({ error: 'Failed to write file: ' + err.message, path: filePath }, raw); return;
375
+ }
376
+ output({ updated: true, field, value: parsedValue }, raw, 'true');
377
+ }
378
+
379
+ /**
380
+ * Merge multiple fields into existing frontmatter from a JSON data string.
381
+ * @param {string} cwd - Working directory path
382
+ * @param {string} filePath - Path to the markdown file
383
+ * @param {string} data - JSON string of key-value pairs to merge
384
+ * @param {boolean} raw - If true, output raw value instead of JSON
385
+ * @returns {void}
386
+ */
387
+ function cmdFrontmatterMerge(cwd, filePath, data, raw) {
388
+ if (!filePath || !data) { error('file and data required'); }
389
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
390
+ let content;
391
+ try {
392
+ content = fs.readFileSync(fullPath, 'utf-8');
393
+ } catch {
394
+ output({ error: 'File not found', path: filePath }, raw); return;
395
+ }
396
+ const fm = extractFrontmatter(content);
397
+ let mergeData;
398
+ // Parse the JSON data string; abort with error if the caller passed invalid JSON
399
+ try { mergeData = JSON.parse(data); } catch { error('Invalid JSON for --data'); return; }
400
+ Object.assign(fm, mergeData);
401
+ const newContent = spliceFrontmatter(content, fm);
402
+ try {
403
+ fs.writeFileSync(fullPath, newContent, 'utf-8');
404
+ } catch (err) {
405
+ output({ error: 'Failed to write file: ' + err.message, path: filePath }, raw); return;
406
+ }
407
+ output({ merged: true, fields: Object.keys(mergeData) }, raw, 'true');
408
+ }
409
+
410
+ /**
411
+ * Validate frontmatter against a named schema (plan, summary, or verification).
412
+ * @param {string} cwd - Working directory path
413
+ * @param {string} filePath - Path to the markdown file
414
+ * @param {string} schemaName - Schema name: "plan", "summary", or "verification"
415
+ * @param {boolean} raw - If true, output raw value instead of JSON
416
+ * @returns {void}
417
+ */
418
+ function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
419
+ if (!filePath || !schemaName) { error('file and schema required'); }
420
+ const schema = FRONTMATTER_SCHEMAS[schemaName];
421
+ if (!schema) { error(`Unknown schema: ${schemaName}. Available: ${Object.keys(FRONTMATTER_SCHEMAS).join(', ')}`); }
422
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
423
+ const content = safeReadFile(fullPath);
424
+ if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
425
+ const fm = extractFrontmatter(content);
426
+ const missing = schema.required.filter(field => fm[field] === undefined);
427
+ const present = schema.required.filter(field => fm[field] !== undefined);
428
+ output({ valid: missing.length === 0, missing, present, schema: schemaName }, raw, missing.length === 0 ? 'valid' : 'invalid');
429
+ }
430
+
431
+ module.exports = {
432
+ extractFrontmatter,
433
+ reconstructFrontmatter,
434
+ spliceFrontmatter,
435
+ parseMustHavesBlock,
436
+ extractPriorityEffort,
437
+ FRONTMATTER_SCHEMAS,
438
+ cmdFrontmatterGet,
439
+ cmdFrontmatterSet,
440
+ cmdFrontmatterMerge,
441
+ cmdFrontmatterValidate,
442
+ };