@luanpdd/kit-mcp 1.18.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +648 -648
  3. package/kit/COMANDOS.md +138 -138
  4. package/kit/README.md +52 -52
  5. package/kit/agents/advisor-researcher.md +106 -106
  6. package/kit/agents/assumptions-analyzer.md +107 -107
  7. package/kit/agents/codebase-mapper.md +768 -768
  8. package/kit/agents/debugger.md +772 -772
  9. package/kit/agents/example-reviewer.md +21 -21
  10. package/kit/agents/executor.md +523 -523
  11. package/kit/agents/integration-checker.md +200 -200
  12. package/kit/agents/nyquist-auditor.md +178 -178
  13. package/kit/agents/phase-researcher.md +696 -696
  14. package/kit/agents/plan-checker.md +272 -272
  15. package/kit/agents/planner.md +891 -891
  16. package/kit/agents/project-researcher.md +652 -652
  17. package/kit/agents/research-synthesizer.md +245 -245
  18. package/kit/agents/roadmapper.md +677 -677
  19. package/kit/agents/ui-auditor.md +437 -437
  20. package/kit/agents/ui-checker.md +302 -302
  21. package/kit/agents/ui-researcher.md +355 -355
  22. package/kit/agents/user-profiler.md +175 -175
  23. package/kit/agents/verifier.md +728 -728
  24. package/kit/commands/adicionar-backlog.md +75 -75
  25. package/kit/commands/adicionar-fase.md +42 -42
  26. package/kit/commands/adicionar-tarefa.md +45 -45
  27. package/kit/commands/adicionar-testes.md +41 -41
  28. package/kit/commands/ajuda.md +21 -21
  29. package/kit/commands/atualizar.md +37 -37
  30. package/kit/commands/auditar-marco.md +179 -179
  31. package/kit/commands/auditar-uat.md +23 -23
  32. package/kit/commands/autonomo.md +40 -40
  33. package/kit/commands/branch-pr.md +24 -24
  34. package/kit/commands/burn-rate-status.md +338 -70
  35. package/kit/commands/concluir-marco.md +247 -247
  36. package/kit/commands/configuracoes.md +36 -36
  37. package/kit/commands/definir-perfil.md +10 -10
  38. package/kit/commands/depurar.md +190 -190
  39. package/kit/commands/discutir-fase.md +131 -131
  40. package/kit/commands/entrar-discord.md +17 -17
  41. package/kit/commands/estatisticas.md +18 -18
  42. package/kit/commands/example-greeting.md +33 -33
  43. package/kit/commands/executar-fase.md +58 -58
  44. package/kit/commands/expresso.md +56 -56
  45. package/kit/commands/fase-ui.md +34 -34
  46. package/kit/commands/fazer.md +57 -57
  47. package/kit/commands/fio.md +125 -125
  48. package/kit/commands/fluxos-trabalho.md +64 -64
  49. package/kit/commands/forense.md +176 -176
  50. package/kit/commands/gerenciador.md +38 -38
  51. package/kit/commands/inserir-fase.md +31 -31
  52. package/kit/commands/limpeza.md +17 -17
  53. package/kit/commands/listar-hipoteses-fase.md +45 -45
  54. package/kit/commands/listar-workspaces.md +18 -18
  55. package/kit/commands/mapear-codebase.md +70 -70
  56. package/kit/commands/nota.md +33 -33
  57. package/kit/commands/novo-marco.md +43 -43
  58. package/kit/commands/novo-projeto.md +41 -41
  59. package/kit/commands/novo-workspace.md +43 -43
  60. package/kit/commands/pausar-trabalho.md +37 -37
  61. package/kit/commands/perfil-usuario.md +45 -45
  62. package/kit/commands/pesquisar-fase.md +195 -195
  63. package/kit/commands/planejar-fase.md +67 -67
  64. package/kit/commands/planejar-lacunas.md +33 -33
  65. package/kit/commands/plantar-ideia.md +25 -25
  66. package/kit/commands/progresso.md +24 -24
  67. package/kit/commands/proximo.md +30 -30
  68. package/kit/commands/publicar.md +490 -490
  69. package/kit/commands/rapido.md +35 -35
  70. package/kit/commands/reaplicar-patches.md +124 -124
  71. package/kit/commands/relatorio-sessao.md +19 -19
  72. package/kit/commands/remover-fase.md +31 -31
  73. package/kit/commands/remover-workspace.md +26 -26
  74. package/kit/commands/resumo-marco.md +50 -50
  75. package/kit/commands/retomar-trabalho.md +40 -40
  76. package/kit/commands/revisar-backlog.md +60 -60
  77. package/kit/commands/revisar-ui.md +32 -32
  78. package/kit/commands/revisar.md +37 -37
  79. package/kit/commands/saude.md +21 -21
  80. package/kit/commands/setup-notion.md +93 -93
  81. package/kit/commands/sync-main.md +68 -68
  82. package/kit/commands/validar-fase.md +35 -35
  83. package/kit/commands/verificar-tarefas.md +44 -44
  84. package/kit/commands/verificar-trabalho.md +64 -64
  85. package/kit/file-manifest.json +3 -3
  86. package/kit/framework/bin/lib/commands.cjs +959 -959
  87. package/kit/framework/bin/lib/config.cjs +442 -442
  88. package/kit/framework/bin/lib/core.cjs +1230 -1230
  89. package/kit/framework/bin/lib/frontmatter.cjs +336 -336
  90. package/kit/framework/bin/lib/init.cjs +1442 -1442
  91. package/kit/framework/bin/lib/milestone.cjs +252 -252
  92. package/kit/framework/bin/lib/model-profiles.cjs +68 -68
  93. package/kit/framework/bin/lib/phase.cjs +888 -888
  94. package/kit/framework/bin/lib/profile-output.cjs +952 -952
  95. package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
  96. package/kit/framework/bin/lib/roadmap.cjs +329 -329
  97. package/kit/framework/bin/lib/security.cjs +382 -382
  98. package/kit/framework/bin/lib/state.cjs +1031 -1031
  99. package/kit/framework/bin/lib/template.cjs +222 -222
  100. package/kit/framework/bin/lib/uat.cjs +282 -282
  101. package/kit/framework/bin/lib/verify.cjs +888 -888
  102. package/kit/framework/bin/lib/workstream.cjs +491 -491
  103. package/kit/framework/bin/tools.cjs +918 -918
  104. package/kit/framework/commands/workstreams.md +63 -63
  105. package/kit/framework/references/checkpoints.md +778 -778
  106. package/kit/framework/references/continuation-format.md +249 -249
  107. package/kit/framework/references/decimal-phase-calculation.md +64 -64
  108. package/kit/framework/references/git-integration.md +295 -295
  109. package/kit/framework/references/git-planning-commit.md +38 -38
  110. package/kit/framework/references/model-profile-resolution.md +36 -36
  111. package/kit/framework/references/model-profiles.md +139 -139
  112. package/kit/framework/references/phase-argument-parsing.md +61 -61
  113. package/kit/framework/references/planning-config.md +202 -202
  114. package/kit/framework/references/questioning.md +162 -162
  115. package/kit/framework/references/tdd.md +263 -263
  116. package/kit/framework/references/ui-brand.md +160 -160
  117. package/kit/framework/references/user-profiling.md +657 -657
  118. package/kit/framework/references/verification-patterns.md +612 -612
  119. package/kit/framework/references/workstream-flag.md +58 -58
  120. package/kit/framework/templates/DEBUG.md +164 -164
  121. package/kit/framework/templates/UAT.md +265 -265
  122. package/kit/framework/templates/UI-SPEC.md +100 -100
  123. package/kit/framework/templates/VALIDATION.md +76 -76
  124. package/kit/framework/templates/claude-md.md +122 -122
  125. package/kit/framework/templates/codebase/architecture.md +185 -185
  126. package/kit/framework/templates/codebase/concerns.md +205 -205
  127. package/kit/framework/templates/codebase/conventions.md +204 -204
  128. package/kit/framework/templates/codebase/integrations.md +192 -192
  129. package/kit/framework/templates/codebase/stack.md +158 -158
  130. package/kit/framework/templates/codebase/structure.md +199 -199
  131. package/kit/framework/templates/codebase/testing.md +301 -301
  132. package/kit/framework/templates/config.json +44 -44
  133. package/kit/framework/templates/context.md +352 -352
  134. package/kit/framework/templates/continue-here.md +78 -78
  135. package/kit/framework/templates/copilot-instructions.md +7 -7
  136. package/kit/framework/templates/debug-subagent-prompt.md +91 -91
  137. package/kit/framework/templates/dev-preferences.md +20 -20
  138. package/kit/framework/templates/discovery.md +146 -146
  139. package/kit/framework/templates/discussion-log.md +63 -63
  140. package/kit/framework/templates/milestone-archive.md +123 -123
  141. package/kit/framework/templates/milestone.md +115 -115
  142. package/kit/framework/templates/phase-prompt.md +610 -610
  143. package/kit/framework/templates/planner-subagent-prompt.md +117 -117
  144. package/kit/framework/templates/project.md +186 -186
  145. package/kit/framework/templates/requirements.md +231 -231
  146. package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
  147. package/kit/framework/templates/research-project/FEATURES.md +147 -147
  148. package/kit/framework/templates/research-project/PITFALLS.md +200 -200
  149. package/kit/framework/templates/research-project/STACK.md +120 -120
  150. package/kit/framework/templates/research-project/SUMMARY.md +170 -170
  151. package/kit/framework/templates/research.md +419 -419
  152. package/kit/framework/templates/retrospective.md +54 -54
  153. package/kit/framework/templates/roadmap.md +202 -202
  154. package/kit/framework/templates/state.md +176 -176
  155. package/kit/framework/templates/summary-complex.md +59 -59
  156. package/kit/framework/templates/summary-minimal.md +41 -41
  157. package/kit/framework/templates/summary-standard.md +48 -48
  158. package/kit/framework/templates/summary.md +209 -209
  159. package/kit/framework/templates/user-profile.md +146 -146
  160. package/kit/framework/templates/user-setup.md +256 -256
  161. package/kit/framework/templates/verification-report.md +258 -258
  162. package/kit/framework/workflows/add-phase.md +112 -112
  163. package/kit/framework/workflows/add-tests.md +351 -351
  164. package/kit/framework/workflows/add-todo.md +158 -158
  165. package/kit/framework/workflows/audit-milestone.md +340 -340
  166. package/kit/framework/workflows/audit-uat.md +109 -109
  167. package/kit/framework/workflows/autonomous.md +891 -891
  168. package/kit/framework/workflows/check-todos.md +177 -177
  169. package/kit/framework/workflows/cleanup.md +152 -152
  170. package/kit/framework/workflows/complete-milestone.md +696 -696
  171. package/kit/framework/workflows/diagnose-issues.md +231 -231
  172. package/kit/framework/workflows/discovery-phase.md +289 -289
  173. package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
  174. package/kit/framework/workflows/discuss-phase.md +784 -784
  175. package/kit/framework/workflows/do.md +104 -104
  176. package/kit/framework/workflows/execute-phase.md +838 -838
  177. package/kit/framework/workflows/execute-plan.md +510 -510
  178. package/kit/framework/workflows/fast.md +102 -102
  179. package/kit/framework/workflows/forensics.md +265 -265
  180. package/kit/framework/workflows/health.md +181 -181
  181. package/kit/framework/workflows/help.md +619 -619
  182. package/kit/framework/workflows/insert-phase.md +130 -130
  183. package/kit/framework/workflows/list-phase-assumptions.md +178 -178
  184. package/kit/framework/workflows/list-workspaces.md +56 -56
  185. package/kit/framework/workflows/manager.md +362 -362
  186. package/kit/framework/workflows/map-codebase.md +377 -377
  187. package/kit/framework/workflows/milestone-summary.md +223 -223
  188. package/kit/framework/workflows/new-milestone.md +486 -486
  189. package/kit/framework/workflows/new-project.md +1159 -1159
  190. package/kit/framework/workflows/new-workspace.md +237 -237
  191. package/kit/framework/workflows/next.md +97 -97
  192. package/kit/framework/workflows/node-repair.md +92 -92
  193. package/kit/framework/workflows/note.md +156 -156
  194. package/kit/framework/workflows/pause-work.md +176 -176
  195. package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
  196. package/kit/framework/workflows/plan-phase.md +765 -765
  197. package/kit/framework/workflows/plant-seed.md +169 -169
  198. package/kit/framework/workflows/pr-branch.md +129 -129
  199. package/kit/framework/workflows/profile-user.md +450 -450
  200. package/kit/framework/workflows/progress.md +507 -507
  201. package/kit/framework/workflows/quick.md +757 -757
  202. package/kit/framework/workflows/remove-phase.md +155 -155
  203. package/kit/framework/workflows/remove-workspace.md +90 -90
  204. package/kit/framework/workflows/research-phase.md +82 -82
  205. package/kit/framework/workflows/resume-project.md +326 -326
  206. package/kit/framework/workflows/review.md +228 -228
  207. package/kit/framework/workflows/session-report.md +146 -146
  208. package/kit/framework/workflows/settings.md +283 -283
  209. package/kit/framework/workflows/ship.md +228 -228
  210. package/kit/framework/workflows/stats.md +60 -60
  211. package/kit/framework/workflows/transition.md +671 -671
  212. package/kit/framework/workflows/ui-phase.md +302 -302
  213. package/kit/framework/workflows/ui-review.md +165 -165
  214. package/kit/framework/workflows/update.md +323 -323
  215. package/kit/framework/workflows/validate-phase.md +174 -174
  216. package/kit/framework/workflows/verify-phase.md +252 -252
  217. package/kit/framework/workflows/verify-work.md +637 -637
  218. package/kit/hooks/check-update.js +118 -118
  219. package/kit/hooks/context-monitor.js +163 -163
  220. package/kit/hooks/prompt-guard.js +103 -103
  221. package/kit/hooks/statusline.js +125 -125
  222. package/kit/hooks/workflow-guard.js +101 -101
  223. package/kit/settings.json +45 -45
  224. package/kit/skills/example-skill/SKILL.md +42 -42
  225. package/package.json +63 -59
  226. package/src/core/kit.js +216 -216
  227. package/src/core/metrics.js +135 -10
  228. package/src/core/reflect.js +247 -247
  229. package/src/core/reverse-sync.js +372 -372
  230. package/src/core/sync.js +418 -418
  231. package/src/core/watch.js +121 -121
  232. package/src/mcp-server/index.js +34 -3
@@ -1,336 +1,336 @@
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, normalizeMd, output, error } = require('./core.cjs');
8
-
9
- // ─── Parsing engine ───────────────────────────────────────────────────────────
10
-
11
- function extractFrontmatter(content) {
12
- const frontmatter = {};
13
- // Find ALL frontmatter blocks at the start of the file.
14
- // If multiple blocks exist (corruption from CRLF mismatch), use the LAST one
15
- // since it represents the most recent state sync.
16
- const allBlocks = [...content.matchAll(/(?:^|\n)\s*---\r?\n([\s\S]+?)\r?\n---/g)];
17
- const match = allBlocks.length > 0 ? allBlocks[allBlocks.length - 1] : null;
18
- if (!match) return frontmatter;
19
-
20
- const yaml = match[1];
21
- const lines = yaml.split(/\r?\n/);
22
-
23
- // Stack to track nested objects: [{obj, key, indent}]
24
- // obj = object to write to, key = current key collecting array items, indent = indentation level
25
- let stack = [{ obj: frontmatter, key: null, indent: -1 }];
26
-
27
- for (const line of lines) {
28
- // Skip empty lines
29
- if (line.trim() === '') continue;
30
-
31
- // Calculate indentation (number of leading spaces)
32
- const indentMatch = line.match(/^(\s*)/);
33
- const indent = indentMatch ? indentMatch[1].length : 0;
34
-
35
- // Pop stack back to appropriate level
36
- while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
37
- stack.pop();
38
- }
39
-
40
- const current = stack[stack.length - 1];
41
-
42
- // Check for key: value pattern
43
- const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
44
- if (keyMatch) {
45
- const key = keyMatch[2];
46
- const value = keyMatch[3].trim();
47
-
48
- if (value === '' || value === '[') {
49
- // Key with no value or opening bracket — could be nested object or array
50
- // We'll determine based on next lines, for now create placeholder
51
- current.obj[key] = value === '[' ? [] : {};
52
- current.key = null;
53
- // Push new context for potential nested content
54
- stack.push({ obj: current.obj[key], key: null, indent });
55
- } else if (value.startsWith('[') && value.endsWith(']')) {
56
- // Inline array: key: [a, b, c]
57
- current.obj[key] = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
58
- current.key = null;
59
- } else {
60
- // Simple key: value
61
- current.obj[key] = value.replace(/^["']|["']$/g, '');
62
- current.key = null;
63
- }
64
- } else if (line.trim().startsWith('- ')) {
65
- // Array item
66
- const itemValue = line.trim().slice(2).replace(/^["']|["']$/g, '');
67
-
68
- // If current context is an empty object, convert to array
69
- if (typeof current.obj === 'object' && !Array.isArray(current.obj) && Object.keys(current.obj).length === 0) {
70
- // Find the key in parent that points to this object and convert it
71
- const parent = stack.length > 1 ? stack[stack.length - 2] : null;
72
- if (parent) {
73
- for (const k of Object.keys(parent.obj)) {
74
- if (parent.obj[k] === current.obj) {
75
- parent.obj[k] = [itemValue];
76
- current.obj = parent.obj[k];
77
- break;
78
- }
79
- }
80
- }
81
- } else if (Array.isArray(current.obj)) {
82
- current.obj.push(itemValue);
83
- }
84
- }
85
- }
86
-
87
- return frontmatter;
88
- }
89
-
90
- function reconstructFrontmatter(obj) {
91
- const lines = [];
92
- for (const [key, value] of Object.entries(obj)) {
93
- if (value === null || value === undefined) continue;
94
- if (Array.isArray(value)) {
95
- if (value.length === 0) {
96
- lines.push(`${key}: []`);
97
- } else if (value.every(v => typeof v === 'string') && value.length <= 3 && value.join(', ').length < 60) {
98
- lines.push(`${key}: [${value.join(', ')}]`);
99
- } else {
100
- lines.push(`${key}:`);
101
- for (const item of value) {
102
- lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
103
- }
104
- }
105
- } else if (typeof value === 'object') {
106
- lines.push(`${key}:`);
107
- for (const [subkey, subval] of Object.entries(value)) {
108
- if (subval === null || subval === undefined) continue;
109
- if (Array.isArray(subval)) {
110
- if (subval.length === 0) {
111
- lines.push(` ${subkey}: []`);
112
- } else if (subval.every(v => typeof v === 'string') && subval.length <= 3 && subval.join(', ').length < 60) {
113
- lines.push(` ${subkey}: [${subval.join(', ')}]`);
114
- } else {
115
- lines.push(` ${subkey}:`);
116
- for (const item of subval) {
117
- lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
118
- }
119
- }
120
- } else if (typeof subval === 'object') {
121
- lines.push(` ${subkey}:`);
122
- for (const [subsubkey, subsubval] of Object.entries(subval)) {
123
- if (subsubval === null || subsubval === undefined) continue;
124
- if (Array.isArray(subsubval)) {
125
- if (subsubval.length === 0) {
126
- lines.push(` ${subsubkey}: []`);
127
- } else {
128
- lines.push(` ${subsubkey}:`);
129
- for (const item of subsubval) {
130
- lines.push(` - ${item}`);
131
- }
132
- }
133
- } else {
134
- lines.push(` ${subsubkey}: ${subsubval}`);
135
- }
136
- }
137
- } else {
138
- const sv = String(subval);
139
- lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') ? `"${sv}"` : sv}`);
140
- }
141
- }
142
- } else {
143
- const sv = String(value);
144
- if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{')) {
145
- lines.push(`${key}: "${sv}"`);
146
- } else {
147
- lines.push(`${key}: ${sv}`);
148
- }
149
- }
150
- }
151
- return lines.join('\n');
152
- }
153
-
154
- function spliceFrontmatter(content, newObj) {
155
- const yamlStr = reconstructFrontmatter(newObj);
156
- const match = content.match(/^---\r?\n[\s\S]+?\r?\n---/);
157
- if (match) {
158
- return `---\n${yamlStr}\n---` + content.slice(match[0].length);
159
- }
160
- return `---\n${yamlStr}\n---\n\n` + content;
161
- }
162
-
163
- function parseMustHavesBlock(content, blockName) {
164
- // Extract a specific block from must_haves in raw frontmatter YAML
165
- // Handles 3-level nesting: must_haves > artifacts/key_links > [{path, provides, ...}]
166
- const fmMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
167
- if (!fmMatch) return [];
168
-
169
- const yaml = fmMatch[1];
170
-
171
- // Find must_haves: first to detect its indentation level
172
- const mustHavesMatch = yaml.match(/^(\s*)must_haves:\s*$/m);
173
- if (!mustHavesMatch) return [];
174
- const mustHavesIndent = mustHavesMatch[1].length;
175
-
176
- // Find the block (e.g., "truths:", "artifacts:", "key_links:") under must_haves
177
- // It must be indented more than must_haves but we detect the actual indent dynamically
178
- const blockPattern = new RegExp(`^(\\s+)${blockName}:\\s*$`, 'm');
179
- const blockMatch = yaml.match(blockPattern);
180
- if (!blockMatch) return [];
181
-
182
- const blockIndent = blockMatch[1].length;
183
- // The block must be nested under must_haves (more indented)
184
- if (blockIndent <= mustHavesIndent) return [];
185
-
186
- // Find where the block starts in the yaml string
187
- const blockStart = yaml.indexOf(blockMatch[0]);
188
- if (blockStart === -1) return [];
189
-
190
- const afterBlock = yaml.slice(blockStart);
191
- const blockLines = afterBlock.split(/\r?\n/).slice(1); // skip the header line
192
-
193
- // List items are indented one level deeper than blockIndent
194
- // Continuation KVs are indented one level deeper than list items
195
- const items = [];
196
- let current = null;
197
- let listItemIndent = -1; // detected from first "- " line
198
-
199
- for (const line of blockLines) {
200
- // Skip empty lines
201
- if (line.trim() === '') continue;
202
- const indent = line.match(/^(\s*)/)[1].length;
203
- // Stop at same or lower indent level than the block header
204
- if (indent <= blockIndent && line.trim() !== '') break;
205
-
206
- const trimmed = line.trim();
207
-
208
- if (trimmed.startsWith('- ')) {
209
- // Detect list item indent from the first occurrence
210
- if (listItemIndent === -1) listItemIndent = indent;
211
-
212
- // Only treat as a top-level list item if at the expected indent
213
- if (indent === listItemIndent) {
214
- if (current) items.push(current);
215
- current = {};
216
- const afterDash = trimmed.slice(2);
217
- // Check if it's a simple string item (no colon means not a key-value)
218
- if (!afterDash.includes(':')) {
219
- current = afterDash.replace(/^["']|["']$/g, '');
220
- } else {
221
- // Key-value on same line as dash: "- path: value"
222
- const kvMatch = afterDash.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
223
- if (kvMatch) {
224
- current = {};
225
- current[kvMatch[1]] = kvMatch[2];
226
- }
227
- }
228
- continue;
229
- }
230
- }
231
-
232
- if (current && typeof current === 'object' && indent > listItemIndent) {
233
- // Continuation key-value or nested array item
234
- if (trimmed.startsWith('- ')) {
235
- // Array item under a key
236
- const arrVal = trimmed.slice(2).replace(/^["']|["']$/g, '');
237
- const keys = Object.keys(current);
238
- const lastKey = keys[keys.length - 1];
239
- if (lastKey && !Array.isArray(current[lastKey])) {
240
- current[lastKey] = current[lastKey] ? [current[lastKey]] : [];
241
- }
242
- if (lastKey) current[lastKey].push(arrVal);
243
- } else {
244
- const kvMatch = trimmed.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
245
- if (kvMatch) {
246
- const val = kvMatch[2];
247
- // Try to parse as number
248
- current[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
249
- }
250
- }
251
- }
252
- }
253
- if (current) items.push(current);
254
-
255
- return items;
256
- }
257
-
258
- // ─── Frontmatter CRUD commands ────────────────────────────────────────────────
259
-
260
- const FRONTMATTER_SCHEMAS = {
261
- plan: { required: ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'] },
262
- summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
263
- verification: { required: ['phase', 'verified', 'status', 'score'] },
264
- };
265
-
266
- function cmdFrontmatterGet(cwd, filePath, field, raw) {
267
- if (!filePath) { error('file path required'); }
268
- // Path traversal guard: reject null bytes
269
- if (filePath.includes('\0')) { error('file path contains null bytes'); }
270
- const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
271
- const content = safeReadFile(fullPath);
272
- if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
273
- const fm = extractFrontmatter(content);
274
- if (field) {
275
- const value = fm[field];
276
- if (value === undefined) { output({ error: 'Field not found', field }, raw); return; }
277
- output({ [field]: value }, raw, JSON.stringify(value));
278
- } else {
279
- output(fm, raw);
280
- }
281
- }
282
-
283
- function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
284
- if (!filePath || !field || value === undefined) { error('file, field, and value required'); }
285
- // Path traversal guard: reject null bytes
286
- if (filePath.includes('\0')) { error('file path contains null bytes'); }
287
- const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
288
- if (!fs.existsSync(fullPath)) { output({ error: 'File not found', path: filePath }, raw); return; }
289
- const content = fs.readFileSync(fullPath, 'utf-8');
290
- const fm = extractFrontmatter(content);
291
- let parsedValue;
292
- try { parsedValue = JSON.parse(value); } catch { parsedValue = value; }
293
- fm[field] = parsedValue;
294
- const newContent = spliceFrontmatter(content, fm);
295
- fs.writeFileSync(fullPath, normalizeMd(newContent), 'utf-8');
296
- output({ updated: true, field, value: parsedValue }, raw, 'true');
297
- }
298
-
299
- function cmdFrontmatterMerge(cwd, filePath, data, raw) {
300
- if (!filePath || !data) { error('file and data required'); }
301
- const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
302
- if (!fs.existsSync(fullPath)) { output({ error: 'File not found', path: filePath }, raw); return; }
303
- const content = fs.readFileSync(fullPath, 'utf-8');
304
- const fm = extractFrontmatter(content);
305
- let mergeData;
306
- try { mergeData = JSON.parse(data); } catch { error('Invalid JSON for --data'); return; }
307
- Object.assign(fm, mergeData);
308
- const newContent = spliceFrontmatter(content, fm);
309
- fs.writeFileSync(fullPath, normalizeMd(newContent), 'utf-8');
310
- output({ merged: true, fields: Object.keys(mergeData) }, raw, 'true');
311
- }
312
-
313
- function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
314
- if (!filePath || !schemaName) { error('file and schema required'); }
315
- const schema = FRONTMATTER_SCHEMAS[schemaName];
316
- if (!schema) { error(`Unknown schema: ${schemaName}. Available: ${Object.keys(FRONTMATTER_SCHEMAS).join(', ')}`); }
317
- const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
318
- const content = safeReadFile(fullPath);
319
- if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
320
- const fm = extractFrontmatter(content);
321
- const missing = schema.required.filter(f => fm[f] === undefined);
322
- const present = schema.required.filter(f => fm[f] !== undefined);
323
- output({ valid: missing.length === 0, missing, present, schema: schemaName }, raw, missing.length === 0 ? 'valid' : 'invalid');
324
- }
325
-
326
- module.exports = {
327
- extractFrontmatter,
328
- reconstructFrontmatter,
329
- spliceFrontmatter,
330
- parseMustHavesBlock,
331
- FRONTMATTER_SCHEMAS,
332
- cmdFrontmatterGet,
333
- cmdFrontmatterSet,
334
- cmdFrontmatterMerge,
335
- cmdFrontmatterValidate,
336
- };
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, normalizeMd, output, error } = require('./core.cjs');
8
+
9
+ // ─── Parsing engine ───────────────────────────────────────────────────────────
10
+
11
+ function extractFrontmatter(content) {
12
+ const frontmatter = {};
13
+ // Find ALL frontmatter blocks at the start of the file.
14
+ // If multiple blocks exist (corruption from CRLF mismatch), use the LAST one
15
+ // since it represents the most recent state sync.
16
+ const allBlocks = [...content.matchAll(/(?:^|\n)\s*---\r?\n([\s\S]+?)\r?\n---/g)];
17
+ const match = allBlocks.length > 0 ? allBlocks[allBlocks.length - 1] : null;
18
+ if (!match) return frontmatter;
19
+
20
+ const yaml = match[1];
21
+ const lines = yaml.split(/\r?\n/);
22
+
23
+ // Stack to track nested objects: [{obj, key, indent}]
24
+ // obj = object to write to, key = current key collecting array items, indent = indentation level
25
+ let stack = [{ obj: frontmatter, key: null, indent: -1 }];
26
+
27
+ for (const line of lines) {
28
+ // Skip empty lines
29
+ if (line.trim() === '') continue;
30
+
31
+ // Calculate indentation (number of leading spaces)
32
+ const indentMatch = line.match(/^(\s*)/);
33
+ const indent = indentMatch ? indentMatch[1].length : 0;
34
+
35
+ // Pop stack back to appropriate level
36
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
37
+ stack.pop();
38
+ }
39
+
40
+ const current = stack[stack.length - 1];
41
+
42
+ // Check for key: value pattern
43
+ const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
44
+ if (keyMatch) {
45
+ const key = keyMatch[2];
46
+ const value = keyMatch[3].trim();
47
+
48
+ if (value === '' || value === '[') {
49
+ // Key with no value or opening bracket — could be nested object or array
50
+ // We'll determine based on next lines, for now create placeholder
51
+ current.obj[key] = value === '[' ? [] : {};
52
+ current.key = null;
53
+ // Push new context for potential nested content
54
+ stack.push({ obj: current.obj[key], key: null, indent });
55
+ } else if (value.startsWith('[') && value.endsWith(']')) {
56
+ // Inline array: key: [a, b, c]
57
+ current.obj[key] = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
58
+ current.key = null;
59
+ } else {
60
+ // Simple key: value
61
+ current.obj[key] = value.replace(/^["']|["']$/g, '');
62
+ current.key = null;
63
+ }
64
+ } else if (line.trim().startsWith('- ')) {
65
+ // Array item
66
+ const itemValue = line.trim().slice(2).replace(/^["']|["']$/g, '');
67
+
68
+ // If current context is an empty object, convert to array
69
+ if (typeof current.obj === 'object' && !Array.isArray(current.obj) && Object.keys(current.obj).length === 0) {
70
+ // Find the key in parent that points to this object and convert it
71
+ const parent = stack.length > 1 ? stack[stack.length - 2] : null;
72
+ if (parent) {
73
+ for (const k of Object.keys(parent.obj)) {
74
+ if (parent.obj[k] === current.obj) {
75
+ parent.obj[k] = [itemValue];
76
+ current.obj = parent.obj[k];
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ } else if (Array.isArray(current.obj)) {
82
+ current.obj.push(itemValue);
83
+ }
84
+ }
85
+ }
86
+
87
+ return frontmatter;
88
+ }
89
+
90
+ function reconstructFrontmatter(obj) {
91
+ const lines = [];
92
+ for (const [key, value] of Object.entries(obj)) {
93
+ if (value === null || value === undefined) continue;
94
+ if (Array.isArray(value)) {
95
+ if (value.length === 0) {
96
+ lines.push(`${key}: []`);
97
+ } else if (value.every(v => typeof v === 'string') && value.length <= 3 && value.join(', ').length < 60) {
98
+ lines.push(`${key}: [${value.join(', ')}]`);
99
+ } else {
100
+ lines.push(`${key}:`);
101
+ for (const item of value) {
102
+ lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
103
+ }
104
+ }
105
+ } else if (typeof value === 'object') {
106
+ lines.push(`${key}:`);
107
+ for (const [subkey, subval] of Object.entries(value)) {
108
+ if (subval === null || subval === undefined) continue;
109
+ if (Array.isArray(subval)) {
110
+ if (subval.length === 0) {
111
+ lines.push(` ${subkey}: []`);
112
+ } else if (subval.every(v => typeof v === 'string') && subval.length <= 3 && subval.join(', ').length < 60) {
113
+ lines.push(` ${subkey}: [${subval.join(', ')}]`);
114
+ } else {
115
+ lines.push(` ${subkey}:`);
116
+ for (const item of subval) {
117
+ lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
118
+ }
119
+ }
120
+ } else if (typeof subval === 'object') {
121
+ lines.push(` ${subkey}:`);
122
+ for (const [subsubkey, subsubval] of Object.entries(subval)) {
123
+ if (subsubval === null || subsubval === undefined) continue;
124
+ if (Array.isArray(subsubval)) {
125
+ if (subsubval.length === 0) {
126
+ lines.push(` ${subsubkey}: []`);
127
+ } else {
128
+ lines.push(` ${subsubkey}:`);
129
+ for (const item of subsubval) {
130
+ lines.push(` - ${item}`);
131
+ }
132
+ }
133
+ } else {
134
+ lines.push(` ${subsubkey}: ${subsubval}`);
135
+ }
136
+ }
137
+ } else {
138
+ const sv = String(subval);
139
+ lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') ? `"${sv}"` : sv}`);
140
+ }
141
+ }
142
+ } else {
143
+ const sv = String(value);
144
+ if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{')) {
145
+ lines.push(`${key}: "${sv}"`);
146
+ } else {
147
+ lines.push(`${key}: ${sv}`);
148
+ }
149
+ }
150
+ }
151
+ return lines.join('\n');
152
+ }
153
+
154
+ function spliceFrontmatter(content, newObj) {
155
+ const yamlStr = reconstructFrontmatter(newObj);
156
+ const match = content.match(/^---\r?\n[\s\S]+?\r?\n---/);
157
+ if (match) {
158
+ return `---\n${yamlStr}\n---` + content.slice(match[0].length);
159
+ }
160
+ return `---\n${yamlStr}\n---\n\n` + content;
161
+ }
162
+
163
+ function parseMustHavesBlock(content, blockName) {
164
+ // Extract a specific block from must_haves in raw frontmatter YAML
165
+ // Handles 3-level nesting: must_haves > artifacts/key_links > [{path, provides, ...}]
166
+ const fmMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
167
+ if (!fmMatch) return [];
168
+
169
+ const yaml = fmMatch[1];
170
+
171
+ // Find must_haves: first to detect its indentation level
172
+ const mustHavesMatch = yaml.match(/^(\s*)must_haves:\s*$/m);
173
+ if (!mustHavesMatch) return [];
174
+ const mustHavesIndent = mustHavesMatch[1].length;
175
+
176
+ // Find the block (e.g., "truths:", "artifacts:", "key_links:") under must_haves
177
+ // It must be indented more than must_haves but we detect the actual indent dynamically
178
+ const blockPattern = new RegExp(`^(\\s+)${blockName}:\\s*$`, 'm');
179
+ const blockMatch = yaml.match(blockPattern);
180
+ if (!blockMatch) return [];
181
+
182
+ const blockIndent = blockMatch[1].length;
183
+ // The block must be nested under must_haves (more indented)
184
+ if (blockIndent <= mustHavesIndent) return [];
185
+
186
+ // Find where the block starts in the yaml string
187
+ const blockStart = yaml.indexOf(blockMatch[0]);
188
+ if (blockStart === -1) return [];
189
+
190
+ const afterBlock = yaml.slice(blockStart);
191
+ const blockLines = afterBlock.split(/\r?\n/).slice(1); // skip the header line
192
+
193
+ // List items are indented one level deeper than blockIndent
194
+ // Continuation KVs are indented one level deeper than list items
195
+ const items = [];
196
+ let current = null;
197
+ let listItemIndent = -1; // detected from first "- " line
198
+
199
+ for (const line of blockLines) {
200
+ // Skip empty lines
201
+ if (line.trim() === '') continue;
202
+ const indent = line.match(/^(\s*)/)[1].length;
203
+ // Stop at same or lower indent level than the block header
204
+ if (indent <= blockIndent && line.trim() !== '') break;
205
+
206
+ const trimmed = line.trim();
207
+
208
+ if (trimmed.startsWith('- ')) {
209
+ // Detect list item indent from the first occurrence
210
+ if (listItemIndent === -1) listItemIndent = indent;
211
+
212
+ // Only treat as a top-level list item if at the expected indent
213
+ if (indent === listItemIndent) {
214
+ if (current) items.push(current);
215
+ current = {};
216
+ const afterDash = trimmed.slice(2);
217
+ // Check if it's a simple string item (no colon means not a key-value)
218
+ if (!afterDash.includes(':')) {
219
+ current = afterDash.replace(/^["']|["']$/g, '');
220
+ } else {
221
+ // Key-value on same line as dash: "- path: value"
222
+ const kvMatch = afterDash.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
223
+ if (kvMatch) {
224
+ current = {};
225
+ current[kvMatch[1]] = kvMatch[2];
226
+ }
227
+ }
228
+ continue;
229
+ }
230
+ }
231
+
232
+ if (current && typeof current === 'object' && indent > listItemIndent) {
233
+ // Continuation key-value or nested array item
234
+ if (trimmed.startsWith('- ')) {
235
+ // Array item under a key
236
+ const arrVal = trimmed.slice(2).replace(/^["']|["']$/g, '');
237
+ const keys = Object.keys(current);
238
+ const lastKey = keys[keys.length - 1];
239
+ if (lastKey && !Array.isArray(current[lastKey])) {
240
+ current[lastKey] = current[lastKey] ? [current[lastKey]] : [];
241
+ }
242
+ if (lastKey) current[lastKey].push(arrVal);
243
+ } else {
244
+ const kvMatch = trimmed.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
245
+ if (kvMatch) {
246
+ const val = kvMatch[2];
247
+ // Try to parse as number
248
+ current[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
249
+ }
250
+ }
251
+ }
252
+ }
253
+ if (current) items.push(current);
254
+
255
+ return items;
256
+ }
257
+
258
+ // ─── Frontmatter CRUD commands ────────────────────────────────────────────────
259
+
260
+ const FRONTMATTER_SCHEMAS = {
261
+ plan: { required: ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'] },
262
+ summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
263
+ verification: { required: ['phase', 'verified', 'status', 'score'] },
264
+ };
265
+
266
+ function cmdFrontmatterGet(cwd, filePath, field, raw) {
267
+ if (!filePath) { error('file path required'); }
268
+ // Path traversal guard: reject null bytes
269
+ if (filePath.includes('\0')) { error('file path contains null bytes'); }
270
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
271
+ const content = safeReadFile(fullPath);
272
+ if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
273
+ const fm = extractFrontmatter(content);
274
+ if (field) {
275
+ const value = fm[field];
276
+ if (value === undefined) { output({ error: 'Field not found', field }, raw); return; }
277
+ output({ [field]: value }, raw, JSON.stringify(value));
278
+ } else {
279
+ output(fm, raw);
280
+ }
281
+ }
282
+
283
+ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
284
+ if (!filePath || !field || value === undefined) { error('file, field, and value required'); }
285
+ // Path traversal guard: reject null bytes
286
+ if (filePath.includes('\0')) { error('file path contains null bytes'); }
287
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
288
+ if (!fs.existsSync(fullPath)) { output({ error: 'File not found', path: filePath }, raw); return; }
289
+ const content = fs.readFileSync(fullPath, 'utf-8');
290
+ const fm = extractFrontmatter(content);
291
+ let parsedValue;
292
+ try { parsedValue = JSON.parse(value); } catch { parsedValue = value; }
293
+ fm[field] = parsedValue;
294
+ const newContent = spliceFrontmatter(content, fm);
295
+ fs.writeFileSync(fullPath, normalizeMd(newContent), 'utf-8');
296
+ output({ updated: true, field, value: parsedValue }, raw, 'true');
297
+ }
298
+
299
+ function cmdFrontmatterMerge(cwd, filePath, data, raw) {
300
+ if (!filePath || !data) { error('file and data required'); }
301
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
302
+ if (!fs.existsSync(fullPath)) { output({ error: 'File not found', path: filePath }, raw); return; }
303
+ const content = fs.readFileSync(fullPath, 'utf-8');
304
+ const fm = extractFrontmatter(content);
305
+ let mergeData;
306
+ try { mergeData = JSON.parse(data); } catch { error('Invalid JSON for --data'); return; }
307
+ Object.assign(fm, mergeData);
308
+ const newContent = spliceFrontmatter(content, fm);
309
+ fs.writeFileSync(fullPath, normalizeMd(newContent), 'utf-8');
310
+ output({ merged: true, fields: Object.keys(mergeData) }, raw, 'true');
311
+ }
312
+
313
+ function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
314
+ if (!filePath || !schemaName) { error('file and schema required'); }
315
+ const schema = FRONTMATTER_SCHEMAS[schemaName];
316
+ if (!schema) { error(`Unknown schema: ${schemaName}. Available: ${Object.keys(FRONTMATTER_SCHEMAS).join(', ')}`); }
317
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
318
+ const content = safeReadFile(fullPath);
319
+ if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
320
+ const fm = extractFrontmatter(content);
321
+ const missing = schema.required.filter(f => fm[f] === undefined);
322
+ const present = schema.required.filter(f => fm[f] !== undefined);
323
+ output({ valid: missing.length === 0, missing, present, schema: schemaName }, raw, missing.length === 0 ? 'valid' : 'invalid');
324
+ }
325
+
326
+ module.exports = {
327
+ extractFrontmatter,
328
+ reconstructFrontmatter,
329
+ spliceFrontmatter,
330
+ parseMustHavesBlock,
331
+ FRONTMATTER_SCHEMAS,
332
+ cmdFrontmatterGet,
333
+ cmdFrontmatterSet,
334
+ cmdFrontmatterMerge,
335
+ cmdFrontmatterValidate,
336
+ };