@ktpartners/dgs-platform 2.6.2

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 (256) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +851 -0
  3. package/agents/dgs-codebase-cross-analyzer.md +183 -0
  4. package/agents/dgs-codebase-mapper.md +782 -0
  5. package/agents/dgs-codebase-synthesizer.md +156 -0
  6. package/agents/dgs-debugger.md +1256 -0
  7. package/agents/dgs-executor.md +550 -0
  8. package/agents/dgs-integration-checker.md +481 -0
  9. package/agents/dgs-nyquist-auditor.md +178 -0
  10. package/agents/dgs-phase-researcher.md +563 -0
  11. package/agents/dgs-phase-verifier.md +450 -0
  12. package/agents/dgs-plan-checker.md +708 -0
  13. package/agents/dgs-planner.md +1324 -0
  14. package/agents/dgs-project-researcher.md +631 -0
  15. package/agents/dgs-research-synthesizer.md +249 -0
  16. package/agents/dgs-roadmapper.md +652 -0
  17. package/agents/dgs-verifier.md +607 -0
  18. package/bin/install.js +2073 -0
  19. package/commands/dgs/add-doc.md +45 -0
  20. package/commands/dgs/add-idea.md +38 -0
  21. package/commands/dgs/add-phase.md +43 -0
  22. package/commands/dgs/add-repo.md +54 -0
  23. package/commands/dgs/add-tests.md +41 -0
  24. package/commands/dgs/add-todo.md +47 -0
  25. package/commands/dgs/approve-spec.md +38 -0
  26. package/commands/dgs/audit-milestone.md +36 -0
  27. package/commands/dgs/audit-phase.md +37 -0
  28. package/commands/dgs/cancel-job.md +23 -0
  29. package/commands/dgs/capture-principle.md +143 -0
  30. package/commands/dgs/check-todos.md +45 -0
  31. package/commands/dgs/cleanup.md +18 -0
  32. package/commands/dgs/complete-milestone.md +136 -0
  33. package/commands/dgs/complete-project.md +70 -0
  34. package/commands/dgs/consolidate-ideas.md +50 -0
  35. package/commands/dgs/create-milestone-job.md +37 -0
  36. package/commands/dgs/debug.md +164 -0
  37. package/commands/dgs/develop-idea.md +53 -0
  38. package/commands/dgs/discuss-idea.md +41 -0
  39. package/commands/dgs/discuss-phase.md +83 -0
  40. package/commands/dgs/execute-phase.md +41 -0
  41. package/commands/dgs/fast.md +38 -0
  42. package/commands/dgs/find-related-ideas.md +43 -0
  43. package/commands/dgs/health.md +28 -0
  44. package/commands/dgs/help.md +22 -0
  45. package/commands/dgs/import-spec.md +36 -0
  46. package/commands/dgs/init-product.md +28 -0
  47. package/commands/dgs/insert-phase.md +32 -0
  48. package/commands/dgs/join-discord.md +18 -0
  49. package/commands/dgs/list-docs.md +40 -0
  50. package/commands/dgs/list-ideas.md +42 -0
  51. package/commands/dgs/list-jobs.md +22 -0
  52. package/commands/dgs/list-phase-assumptions.md +46 -0
  53. package/commands/dgs/list-projects.md +57 -0
  54. package/commands/dgs/list-specs.md +40 -0
  55. package/commands/dgs/map-codebase.md +92 -0
  56. package/commands/dgs/new-milestone.md +44 -0
  57. package/commands/dgs/new-project.md +42 -0
  58. package/commands/dgs/node-repair.md +26 -0
  59. package/commands/dgs/overlap-check.md +20 -0
  60. package/commands/dgs/pause-work.md +38 -0
  61. package/commands/dgs/plan-milestone-gaps.md +34 -0
  62. package/commands/dgs/plan-phase.md +44 -0
  63. package/commands/dgs/progress.md +24 -0
  64. package/commands/dgs/quick.md +41 -0
  65. package/commands/dgs/reactivate-project.md +70 -0
  66. package/commands/dgs/reapply-patches.md +110 -0
  67. package/commands/dgs/refine-spec.md +38 -0
  68. package/commands/dgs/reject-idea.md +43 -0
  69. package/commands/dgs/remove-doc.md +44 -0
  70. package/commands/dgs/remove-phase.md +31 -0
  71. package/commands/dgs/remove-repo.md +69 -0
  72. package/commands/dgs/research-idea.md +43 -0
  73. package/commands/dgs/research-phase.md +189 -0
  74. package/commands/dgs/restore-idea.md +45 -0
  75. package/commands/dgs/resume-work.md +40 -0
  76. package/commands/dgs/rollback-job.md +24 -0
  77. package/commands/dgs/run-job.md +35 -0
  78. package/commands/dgs/search.md +40 -0
  79. package/commands/dgs/set-profile.md +34 -0
  80. package/commands/dgs/settings.md +38 -0
  81. package/commands/dgs/switch-project.md +58 -0
  82. package/commands/dgs/undo-consolidation.md +42 -0
  83. package/commands/dgs/update-idea.md +44 -0
  84. package/commands/dgs/update.md +37 -0
  85. package/commands/dgs/validate-phase.md +35 -0
  86. package/commands/dgs/verify-work.md +39 -0
  87. package/commands/dgs/write-spec.md +49 -0
  88. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
  89. package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
  90. package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
  91. package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
  92. package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
  93. package/deliver-great-systems/bin/lib/commands.cjs +570 -0
  94. package/deliver-great-systems/bin/lib/config.cjs +417 -0
  95. package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
  96. package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
  97. package/deliver-great-systems/bin/lib/context.cjs +929 -0
  98. package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
  99. package/deliver-great-systems/bin/lib/core.cjs +744 -0
  100. package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
  101. package/deliver-great-systems/bin/lib/docs.cjs +919 -0
  102. package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
  103. package/deliver-great-systems/bin/lib/execution.cjs +705 -0
  104. package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
  105. package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
  106. package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
  107. package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
  108. package/deliver-great-systems/bin/lib/identity.cjs +125 -0
  109. package/deliver-great-systems/bin/lib/init.cjs +1114 -0
  110. package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
  111. package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
  112. package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
  113. package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
  114. package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
  115. package/deliver-great-systems/bin/lib/migration.cjs +352 -0
  116. package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
  117. package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
  118. package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
  119. package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
  120. package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
  121. package/deliver-great-systems/bin/lib/paths.cjs +144 -0
  122. package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
  123. package/deliver-great-systems/bin/lib/phase.cjs +910 -0
  124. package/deliver-great-systems/bin/lib/projects.cjs +691 -0
  125. package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
  126. package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
  127. package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
  128. package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
  129. package/deliver-great-systems/bin/lib/search.cjs +570 -0
  130. package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
  131. package/deliver-great-systems/bin/lib/state.cjs +893 -0
  132. package/deliver-great-systems/bin/lib/template.cjs +228 -0
  133. package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
  134. package/deliver-great-systems/bin/lib/verify.cjs +796 -0
  135. package/deliver-great-systems/references/checkpoints.md +776 -0
  136. package/deliver-great-systems/references/conflict-resolution.md +66 -0
  137. package/deliver-great-systems/references/context-tiers.md +166 -0
  138. package/deliver-great-systems/references/continuation-format.md +249 -0
  139. package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
  140. package/deliver-great-systems/references/git-integration.md +250 -0
  141. package/deliver-great-systems/references/git-planning-commit.md +40 -0
  142. package/deliver-great-systems/references/model-profile-resolution.md +36 -0
  143. package/deliver-great-systems/references/model-profiles.md +95 -0
  144. package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
  145. package/deliver-great-systems/references/planning-config.md +224 -0
  146. package/deliver-great-systems/references/questioning.md +162 -0
  147. package/deliver-great-systems/references/spec-review-loop.md +177 -0
  148. package/deliver-great-systems/references/tdd.md +265 -0
  149. package/deliver-great-systems/references/ui-brand.md +160 -0
  150. package/deliver-great-systems/references/verification-patterns.md +612 -0
  151. package/deliver-great-systems/templates/DEBUG.md +166 -0
  152. package/deliver-great-systems/templates/UAT.md +251 -0
  153. package/deliver-great-systems/templates/VALIDATION.md +95 -0
  154. package/deliver-great-systems/templates/claude-md.md +74 -0
  155. package/deliver-great-systems/templates/codebase/architecture.md +257 -0
  156. package/deliver-great-systems/templates/codebase/concerns.md +312 -0
  157. package/deliver-great-systems/templates/codebase/conventions.md +309 -0
  158. package/deliver-great-systems/templates/codebase/integrations.md +282 -0
  159. package/deliver-great-systems/templates/codebase/stack.md +188 -0
  160. package/deliver-great-systems/templates/codebase/structure.md +287 -0
  161. package/deliver-great-systems/templates/codebase/testing.md +482 -0
  162. package/deliver-great-systems/templates/config.json +38 -0
  163. package/deliver-great-systems/templates/context.md +354 -0
  164. package/deliver-great-systems/templates/continue-here.md +80 -0
  165. package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
  166. package/deliver-great-systems/templates/discovery.md +148 -0
  167. package/deliver-great-systems/templates/milestone-archive.md +125 -0
  168. package/deliver-great-systems/templates/milestone.md +117 -0
  169. package/deliver-great-systems/templates/phase-prompt.md +615 -0
  170. package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
  171. package/deliver-great-systems/templates/project.md +186 -0
  172. package/deliver-great-systems/templates/requirements.md +233 -0
  173. package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
  174. package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
  175. package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
  176. package/deliver-great-systems/templates/research-project/STACK.md +122 -0
  177. package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
  178. package/deliver-great-systems/templates/research.md +554 -0
  179. package/deliver-great-systems/templates/retrospective.md +54 -0
  180. package/deliver-great-systems/templates/roadmap.md +204 -0
  181. package/deliver-great-systems/templates/state.md +178 -0
  182. package/deliver-great-systems/templates/summary-complex.md +59 -0
  183. package/deliver-great-systems/templates/summary-minimal.md +41 -0
  184. package/deliver-great-systems/templates/summary-standard.md +48 -0
  185. package/deliver-great-systems/templates/summary.md +253 -0
  186. package/deliver-great-systems/templates/user-setup.md +313 -0
  187. package/deliver-great-systems/templates/verification-report.md +324 -0
  188. package/deliver-great-systems/workflows/add-doc.md +151 -0
  189. package/deliver-great-systems/workflows/add-idea.md +96 -0
  190. package/deliver-great-systems/workflows/add-phase.md +120 -0
  191. package/deliver-great-systems/workflows/add-tests.md +359 -0
  192. package/deliver-great-systems/workflows/add-todo.md +162 -0
  193. package/deliver-great-systems/workflows/approve-spec.md +194 -0
  194. package/deliver-great-systems/workflows/audit-milestone.md +364 -0
  195. package/deliver-great-systems/workflows/audit-phase.md +462 -0
  196. package/deliver-great-systems/workflows/cancel-job.md +108 -0
  197. package/deliver-great-systems/workflows/check-todos.md +181 -0
  198. package/deliver-great-systems/workflows/cleanup.md +247 -0
  199. package/deliver-great-systems/workflows/codereview.md +526 -0
  200. package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
  201. package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
  202. package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
  203. package/deliver-great-systems/workflows/develop-idea.md +544 -0
  204. package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
  205. package/deliver-great-systems/workflows/discovery-phase.md +301 -0
  206. package/deliver-great-systems/workflows/discuss-idea.md +263 -0
  207. package/deliver-great-systems/workflows/discuss-phase.md +733 -0
  208. package/deliver-great-systems/workflows/execute-phase.md +571 -0
  209. package/deliver-great-systems/workflows/execute-plan.md +592 -0
  210. package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
  211. package/deliver-great-systems/workflows/health.md +173 -0
  212. package/deliver-great-systems/workflows/help.md +997 -0
  213. package/deliver-great-systems/workflows/import-spec.md +381 -0
  214. package/deliver-great-systems/workflows/init-product.md +767 -0
  215. package/deliver-great-systems/workflows/insert-phase.md +138 -0
  216. package/deliver-great-systems/workflows/list-docs.md +119 -0
  217. package/deliver-great-systems/workflows/list-ideas.md +154 -0
  218. package/deliver-great-systems/workflows/list-jobs.md +89 -0
  219. package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
  220. package/deliver-great-systems/workflows/list-specs.md +101 -0
  221. package/deliver-great-systems/workflows/map-codebase.md +621 -0
  222. package/deliver-great-systems/workflows/new-milestone.md +591 -0
  223. package/deliver-great-systems/workflows/new-project.md +1113 -0
  224. package/deliver-great-systems/workflows/node-repair.md +94 -0
  225. package/deliver-great-systems/workflows/overlap-check.md +86 -0
  226. package/deliver-great-systems/workflows/pause-work.md +134 -0
  227. package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
  228. package/deliver-great-systems/workflows/plan-phase.md +698 -0
  229. package/deliver-great-systems/workflows/progress.md +386 -0
  230. package/deliver-great-systems/workflows/quick.md +845 -0
  231. package/deliver-great-systems/workflows/refine-spec.md +275 -0
  232. package/deliver-great-systems/workflows/reject-idea.md +109 -0
  233. package/deliver-great-systems/workflows/remove-doc.md +117 -0
  234. package/deliver-great-systems/workflows/remove-phase.md +163 -0
  235. package/deliver-great-systems/workflows/research-idea.md +325 -0
  236. package/deliver-great-systems/workflows/research-phase.md +81 -0
  237. package/deliver-great-systems/workflows/restore-idea.md +101 -0
  238. package/deliver-great-systems/workflows/resume-project.md +311 -0
  239. package/deliver-great-systems/workflows/rollback-job.md +130 -0
  240. package/deliver-great-systems/workflows/run-job.md +498 -0
  241. package/deliver-great-systems/workflows/search.md +130 -0
  242. package/deliver-great-systems/workflows/set-profile.md +83 -0
  243. package/deliver-great-systems/workflows/settings.md +470 -0
  244. package/deliver-great-systems/workflows/transition.md +563 -0
  245. package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
  246. package/deliver-great-systems/workflows/update-idea.md +157 -0
  247. package/deliver-great-systems/workflows/update.md +242 -0
  248. package/deliver-great-systems/workflows/validate-phase.md +177 -0
  249. package/deliver-great-systems/workflows/verify-phase.md +253 -0
  250. package/deliver-great-systems/workflows/verify-work.md +671 -0
  251. package/deliver-great-systems/workflows/write-spec.md +450 -0
  252. package/hooks/dist/dgs-check-update.js +62 -0
  253. package/hooks/dist/dgs-context-monitor.js +141 -0
  254. package/hooks/dist/dgs-statusline.js +115 -0
  255. package/package.json +60 -0
  256. package/scripts/build-hooks.js +43 -0
@@ -0,0 +1,929 @@
1
+ /**
2
+ * Context -- Context tier engine for DGS workflow context loading
3
+ *
4
+ * Parses tier definitions from references/context-tiers.md (YAML code blocks)
5
+ * and resolves file lists against the current project's planning root.
6
+ * Handles scope flags (--phase, --idea, --spec) for context enrichment.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { safeReadFile, loadConfig, output, error, findPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter, toPosixPath } = require('./core.cjs');
12
+ const { getPlanningRoot } = require('./paths.cjs');
13
+
14
+ // ─── Tier Definition Cache ──────────────────────────────────────────────────
15
+
16
+ /** @type {Map<string, Object>|null} */
17
+ let _tierCache = null;
18
+
19
+ // ─── YAML Parsing ───────────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Lightweight YAML parser for our controlled schema.
23
+ * Handles key: value, arrays (- item), nested objects, and multi-line
24
+ * object items in arrays. Does NOT support full YAML spec -- only the
25
+ * subset used in context-tiers.md.
26
+ *
27
+ * Strategy: Build a flat list of {indent, type, key, value} tokens,
28
+ * then assemble into a tree structure.
29
+ *
30
+ * @param {string} yamlText - Raw YAML text from a code block
31
+ * @returns {Object} Parsed object
32
+ */
33
+ function parseSimpleYaml(yamlText) {
34
+ const lines = yamlText.split('\n');
35
+ return parseYamlBlock(lines, 0, lines.length, 0);
36
+ }
37
+
38
+ /**
39
+ * Parse a block of YAML lines at a given indent level.
40
+ *
41
+ * @param {string[]} lines - All lines
42
+ * @param {number} start - Start line index
43
+ * @param {number} end - End line index (exclusive)
44
+ * @param {number} baseIndent - Expected indent level for this block
45
+ * @returns {Object} Parsed object
46
+ */
47
+ function parseYamlBlock(lines, start, end, baseIndent) {
48
+ const result = {};
49
+ let i = start;
50
+
51
+ while (i < end) {
52
+ const line = lines[i];
53
+ const trimmed = line.trimEnd();
54
+ if (trimmed === '' || trimmed.trimStart().startsWith('#')) {
55
+ i++;
56
+ continue;
57
+ }
58
+
59
+ const indent = line.length - line.trimStart().length;
60
+ if (indent < baseIndent) break; // Dedent -- end of this block
61
+ if (indent > baseIndent) {
62
+ i++; // Skip unexpected extra-indented lines
63
+ continue;
64
+ }
65
+
66
+ const stripped = trimmed.trim();
67
+
68
+ // Array item at this indent level (shouldn't happen at top level in our schema)
69
+ if (stripped.startsWith('- ')) {
70
+ i++;
71
+ continue;
72
+ }
73
+
74
+ // Key: value pair
75
+ const kvMatch = stripped.match(/^([\w_]+):\s*(.*)/);
76
+ if (!kvMatch) {
77
+ i++;
78
+ continue;
79
+ }
80
+
81
+ const key = kvMatch[1];
82
+ const rawValue = kvMatch[2].trim();
83
+ i++;
84
+
85
+ // Check what follows at deeper indent
86
+ const childIndent = baseIndent + 2;
87
+ const childStart = i;
88
+
89
+ // Peek ahead to see if there are child lines
90
+ let hasChildren = false;
91
+ if (i < end) {
92
+ const nextLine = lines[i];
93
+ const nextTrimmed = nextLine.trimEnd();
94
+ if (nextTrimmed !== '' && !nextTrimmed.trimStart().startsWith('#')) {
95
+ const nextIndent = nextLine.length - nextLine.trimStart().length;
96
+ if (nextIndent >= childIndent) {
97
+ hasChildren = true;
98
+ }
99
+ }
100
+ }
101
+
102
+ if (!hasChildren) {
103
+ // Simple scalar, inline array, or empty
104
+ result[key] = parseYamlValue(rawValue);
105
+ continue;
106
+ }
107
+
108
+ // Has children -- determine if it's an array or object
109
+ const firstChildLine = lines[i].trimStart();
110
+ if (firstChildLine.startsWith('- ')) {
111
+ // Array
112
+ result[key] = parseYamlArray(lines, i, end, childIndent);
113
+ // Skip past all child lines
114
+ while (i < end) {
115
+ const ln = lines[i];
116
+ const tr = ln.trimEnd();
117
+ if (tr === '' || tr.trimStart().startsWith('#')) { i++; continue; }
118
+ const ind = ln.length - ln.trimStart().length;
119
+ if (ind < childIndent) break;
120
+ i++;
121
+ }
122
+ } else {
123
+ // Nested object
124
+ const childEnd = findBlockEnd(lines, i, end, childIndent);
125
+ result[key] = parseYamlBlock(lines, i, childEnd, childIndent);
126
+ i = childEnd;
127
+ }
128
+ }
129
+
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Parse an array starting at the given position.
135
+ *
136
+ * @param {string[]} lines - All lines
137
+ * @param {number} start - Start line index (first array item)
138
+ * @param {number} end - End line index (exclusive)
139
+ * @param {number} itemIndent - Indent level of array items (the "- " prefix)
140
+ * @returns {Array} Parsed array
141
+ */
142
+ function parseYamlArray(lines, start, end, itemIndent) {
143
+ const result = [];
144
+ let i = start;
145
+
146
+ while (i < end) {
147
+ const line = lines[i];
148
+ const trimmed = line.trimEnd();
149
+ if (trimmed === '' || trimmed.trimStart().startsWith('#')) {
150
+ i++;
151
+ continue;
152
+ }
153
+
154
+ const indent = line.length - line.trimStart().length;
155
+ if (indent < itemIndent) break; // Dedent
156
+
157
+ const stripped = trimmed.trim();
158
+
159
+ if (indent === itemIndent && stripped.startsWith('- ')) {
160
+ // New array item
161
+ const itemContent = stripped.slice(2).trim();
162
+ const itemKv = itemContent.match(/^([\w_]+):\s*(.*)/);
163
+
164
+ if (itemKv) {
165
+ // Object item (e.g., "- path: ...")
166
+ const obj = {};
167
+ obj[itemKv[1]] = parseYamlScalar(itemKv[2].trim());
168
+
169
+ // Look ahead for continuation lines (same object, deeper indent)
170
+ i++;
171
+ const contIndent = itemIndent + 2;
172
+ while (i < end) {
173
+ const contLine = lines[i];
174
+ const contTrimmed = contLine.trimEnd();
175
+ if (contTrimmed === '' || contTrimmed.trimStart().startsWith('#')) { i++; continue; }
176
+ const contInd = contLine.length - contLine.trimStart().length;
177
+ if (contInd < contIndent) break;
178
+ if (contInd === contIndent) {
179
+ const contKv = contTrimmed.trim().match(/^([\w_]+):\s*(.*)/);
180
+ if (contKv) {
181
+ obj[contKv[1]] = parseYamlScalar(contKv[2].trim());
182
+ i++;
183
+ continue;
184
+ }
185
+ }
186
+ break;
187
+ }
188
+
189
+ result.push(obj);
190
+ } else {
191
+ // Simple scalar item
192
+ result.push(parseYamlScalar(itemContent));
193
+ i++;
194
+ }
195
+ } else {
196
+ i++; // Skip unexpected lines
197
+ }
198
+ }
199
+
200
+ return result;
201
+ }
202
+
203
+ /**
204
+ * Find the end of a block at the given indent level.
205
+ *
206
+ * @param {string[]} lines - All lines
207
+ * @param {number} start - Start position
208
+ * @param {number} end - Maximum end
209
+ * @param {number} minIndent - Minimum indent that belongs to this block
210
+ * @returns {number} Line index where block ends
211
+ */
212
+ function findBlockEnd(lines, start, end, minIndent) {
213
+ let i = start;
214
+ while (i < end) {
215
+ const line = lines[i];
216
+ const trimmed = line.trimEnd();
217
+ if (trimmed === '' || trimmed.trimStart().startsWith('#')) {
218
+ i++;
219
+ continue;
220
+ }
221
+ const indent = line.length - line.trimStart().length;
222
+ if (indent < minIndent) break;
223
+ i++;
224
+ }
225
+ return i;
226
+ }
227
+
228
+ /**
229
+ * Parse a YAML value string (inline).
230
+ *
231
+ * @param {string} raw - Raw value text
232
+ * @returns {*} Parsed value (scalar, array, or empty object)
233
+ */
234
+ function parseYamlValue(raw) {
235
+ if (raw === '' || raw === undefined) return null;
236
+ if (raw === '{}') return {};
237
+ if (raw === '[]') return [];
238
+ // Inline array: [item1, item2]
239
+ if (raw.startsWith('[') && raw.endsWith(']')) {
240
+ const inner = raw.slice(1, -1).trim();
241
+ if (inner === '') return [];
242
+ return inner.split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
243
+ }
244
+ return parseYamlScalar(raw);
245
+ }
246
+
247
+ /**
248
+ * Parse a YAML scalar value.
249
+ *
250
+ * @param {string} raw - Raw scalar text
251
+ * @returns {string|number|boolean|null}
252
+ */
253
+ function parseYamlScalar(raw) {
254
+ if (raw === 'null' || raw === '~') return null;
255
+ if (raw === 'true') return true;
256
+ if (raw === 'false') return false;
257
+ // Strip quotes
258
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
259
+ return raw.slice(1, -1);
260
+ }
261
+ // Numbers
262
+ if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
263
+ return raw;
264
+ }
265
+
266
+ // ─── Tier Definition Parsing ────────────────────────────────────────────────
267
+
268
+ /**
269
+ * Parse tier definitions from references/context-tiers.md.
270
+ * Extracts YAML code blocks between ``` markers within each ## Tier section.
271
+ * Results are cached after first parse within the process.
272
+ *
273
+ * @returns {Map<string, Object>} Map of tier name to parsed tier definition
274
+ */
275
+ function parseTierDefinitions() {
276
+ if (_tierCache) return _tierCache;
277
+
278
+ const tiersPath = path.resolve(__dirname, '..', '..', 'references', 'context-tiers.md');
279
+ const content = safeReadFile(tiersPath);
280
+ if (!content) {
281
+ _tierCache = new Map();
282
+ return _tierCache;
283
+ }
284
+
285
+ const tiers = new Map();
286
+ // Find all yaml code blocks
287
+ const yamlBlockPattern = /```yaml\n([\s\S]*?)```/g;
288
+ let match;
289
+ while ((match = yamlBlockPattern.exec(content)) !== null) {
290
+ const parsed = parseSimpleYaml(match[1]);
291
+ if (parsed.tier) {
292
+ tiers.set(parsed.tier, parsed);
293
+ }
294
+ }
295
+
296
+ _tierCache = tiers;
297
+ return _tierCache;
298
+ }
299
+
300
+ /**
301
+ * Reset the tier definition cache. Used for testing.
302
+ */
303
+ function resetTierCache() {
304
+ _tierCache = null;
305
+ }
306
+
307
+ // ─── File Resolution ────────────────────────────────────────────────────────
308
+
309
+ /**
310
+ * Resolve the flat list of static files for a tier, including inherited files.
311
+ *
312
+ * @param {string} tierName - Tier name (none, lite, planning, execution, verification)
313
+ * @param {Map<string, Object>} tiers - Parsed tier definitions
314
+ * @returns {Array<{path: string, category: string}>} Static file entries
315
+ */
316
+ function resolveTierFiles(tierName, tiers) {
317
+ const tier = tiers.get(tierName);
318
+ if (!tier) return [];
319
+
320
+ let files = [];
321
+
322
+ // Recursively include inherited tier files
323
+ if (tier.includes_tier && tier.includes_tier !== 'null' && tiers.has(tier.includes_tier)) {
324
+ files = resolveTierFiles(tier.includes_tier, tiers);
325
+ }
326
+
327
+ // Add this tier's own static files
328
+ if (Array.isArray(tier.files)) {
329
+ for (const entry of tier.files) {
330
+ if (entry && typeof entry === 'object' && entry.path) {
331
+ files.push({ path: entry.path, category: entry.category || 'general' });
332
+ }
333
+ }
334
+ }
335
+
336
+ return files;
337
+ }
338
+
339
+ /**
340
+ * Resolve dynamic file rules (glob patterns) against the planning root.
341
+ * Uses recursive readdirSync for glob matching -- no external library.
342
+ *
343
+ * @param {Array<Object>} dynamicRules - Dynamic rules from tier definition
344
+ * @param {string} planningRoot - Absolute path to planning root
345
+ * @returns {Array<{path: string, category: string}>} Resolved file entries
346
+ */
347
+ function resolveDynamicFiles(dynamicRules, planningRoot) {
348
+ if (!Array.isArray(dynamicRules) || dynamicRules.length === 0) return [];
349
+
350
+ const results = [];
351
+ for (const rule of dynamicRules) {
352
+ if (!rule || rule.type !== 'glob' || !rule.pattern) continue;
353
+
354
+ const category = rule.category || 'dynamic';
355
+ const matched = globMatch(planningRoot, rule.pattern);
356
+ for (const filePath of matched) {
357
+ results.push({ path: filePath, category });
358
+ }
359
+ }
360
+ return results;
361
+ }
362
+
363
+ /**
364
+ * Simple glob matching using recursive readdirSync.
365
+ * Supports patterns like "codebase/ ** /*.md" and "UAT*.md".
366
+ *
367
+ * @param {string} baseDir - Base directory to search from
368
+ * @param {string} pattern - Glob pattern (relative to baseDir)
369
+ * @returns {string[]} Matched file paths relative to baseDir
370
+ */
371
+ function globMatch(baseDir, pattern) {
372
+ const results = [];
373
+
374
+ // Handle ** patterns (recursive)
375
+ if (pattern.includes('**')) {
376
+ const parts = pattern.split('**');
377
+ const prefix = parts[0].replace(/\/$/, '');
378
+ const suffix = (parts[1] || '').replace(/^\//, '');
379
+ const searchDir = prefix ? path.join(baseDir, prefix) : baseDir;
380
+
381
+ if (!fs.existsSync(searchDir)) return results;
382
+
383
+ const allFiles = readdirRecursive(searchDir);
384
+ const suffixPattern = globToRegex(suffix);
385
+
386
+ for (const file of allFiles) {
387
+ if (suffixPattern.test(file)) {
388
+ const relPath = prefix ? toPosixPath(path.join(prefix, file)) : toPosixPath(file);
389
+ results.push(relPath);
390
+ }
391
+ }
392
+ return results;
393
+ }
394
+
395
+ // Handle simple wildcard patterns (e.g., "UAT*.md")
396
+ if (pattern.includes('*')) {
397
+ const dir = path.dirname(pattern);
398
+ const filePattern = path.basename(pattern);
399
+ const searchDir = dir === '.' ? baseDir : path.join(baseDir, dir);
400
+
401
+ if (!fs.existsSync(searchDir)) return results;
402
+
403
+ try {
404
+ const entries = fs.readdirSync(searchDir);
405
+ const regex = globToRegex(filePattern);
406
+ for (const entry of entries) {
407
+ if (regex.test(entry)) {
408
+ const fullPath = path.join(searchDir, entry);
409
+ try {
410
+ if (fs.statSync(fullPath).isFile()) {
411
+ const relPath = dir === '.' ? entry : toPosixPath(path.join(dir, entry));
412
+ results.push(relPath);
413
+ }
414
+ } catch {}
415
+ }
416
+ }
417
+ } catch {}
418
+ return results;
419
+ }
420
+
421
+ // No wildcards -- just check if file exists
422
+ if (fs.existsSync(path.join(baseDir, pattern))) {
423
+ results.push(toPosixPath(pattern));
424
+ }
425
+ return results;
426
+ }
427
+
428
+ /**
429
+ * Convert a simple glob pattern to a RegExp.
430
+ * Supports * (any chars except /) and *.ext patterns.
431
+ *
432
+ * @param {string} pattern - Glob pattern
433
+ * @returns {RegExp}
434
+ */
435
+ function globToRegex(pattern) {
436
+ if (!pattern) return /^.*$/;
437
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
438
+ const regexStr = escaped.replace(/\*/g, '[^/]*');
439
+ return new RegExp('^' + regexStr + '$');
440
+ }
441
+
442
+ /**
443
+ * Recursively list all files in a directory.
444
+ *
445
+ * @param {string} dir - Directory to scan
446
+ * @param {string} [prefix=''] - Path prefix for relative results
447
+ * @returns {string[]} Relative file paths
448
+ */
449
+ function readdirRecursive(dir, prefix) {
450
+ const results = [];
451
+ prefix = prefix || '';
452
+ try {
453
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
454
+ for (const entry of entries) {
455
+ const relPath = prefix ? prefix + '/' + entry.name : entry.name;
456
+ if (entry.isDirectory()) {
457
+ results.push(...readdirRecursive(path.join(dir, entry.name), relPath));
458
+ } else if (entry.isFile()) {
459
+ results.push(relPath);
460
+ }
461
+ }
462
+ } catch {}
463
+ return results;
464
+ }
465
+
466
+ // ─── Scope Resolution ───────────────────────────────────────────────────────
467
+
468
+ /**
469
+ * Resolve phase-scoped files for the --phase flag.
470
+ * Includes CONTEXT.md, RESEARCH.md, all PLANs and SUMMARYs from the phase,
471
+ * plus all SUMMARYs from completed phases in the current milestone.
472
+ *
473
+ * @param {string|number} phaseNum - Phase number
474
+ * @param {string} planningRoot - Absolute path to planning root
475
+ * @param {string} cwd - Working directory
476
+ * @returns {Array<{path: string, category: string}>} Phase-scoped file entries
477
+ */
478
+ function resolvePhaseScope(phaseNum, planningRoot, cwd) {
479
+ const results = [];
480
+ const phaseInfo = findPhaseInternal(cwd, phaseNum);
481
+ if (!phaseInfo || !phaseInfo.found) return results;
482
+
483
+ const phaseDir = phaseInfo.directory;
484
+
485
+ // Add phase-specific files
486
+ const contextPath = path.join(phaseDir, phaseInfo.phase_number + '-CONTEXT.md');
487
+ results.push({ path: toPosixPath(contextPath), category: 'phase' });
488
+
489
+ const researchPath = path.join(phaseDir, phaseInfo.phase_number + '-RESEARCH.md');
490
+ results.push({ path: toPosixPath(researchPath), category: 'phase' });
491
+
492
+ // Add all PLANs
493
+ if (Array.isArray(phaseInfo.plans)) {
494
+ for (const plan of phaseInfo.plans) {
495
+ results.push({ path: toPosixPath(path.join(phaseDir, plan)), category: 'phase-plan' });
496
+ }
497
+ }
498
+
499
+ // Add all SUMMARYs from this phase
500
+ if (Array.isArray(phaseInfo.summaries)) {
501
+ for (const summary of phaseInfo.summaries) {
502
+ results.push({ path: toPosixPath(path.join(phaseDir, summary)), category: 'phase-summary' });
503
+ }
504
+ }
505
+
506
+ // Add all SUMMARYs from completed phases in the current milestone (not capped)
507
+ const milestoneSummaries = getMilestoneSummaries(cwd, planningRoot, phaseNum);
508
+ for (const entry of milestoneSummaries) {
509
+ results.push(entry);
510
+ }
511
+
512
+ return results;
513
+ }
514
+
515
+ /**
516
+ * Get all SUMMARY.md files from completed phases in the current milestone.
517
+ * Per user decision: ALL summaries included, not capped at 3.
518
+ *
519
+ * @param {string} cwd - Working directory
520
+ * @param {string} planningRoot - Absolute path to planning root
521
+ * @param {string|number} currentPhaseNum - Current phase number (to exclude)
522
+ * @returns {Array<{path: string, category: string}>}
523
+ */
524
+ function getMilestoneSummaries(cwd, planningRoot, currentPhaseNum) {
525
+ const results = [];
526
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
527
+
528
+ // Scan phases directory for milestone phases with summaries
529
+ let projectRoot;
530
+ try {
531
+ const { getProjectRoot } = require('./core.cjs');
532
+ projectRoot = getProjectRoot(cwd);
533
+ } catch {
534
+ projectRoot = path.relative(cwd, planningRoot) || '.planning';
535
+ }
536
+
537
+ const phasesDir = path.join(cwd, projectRoot, 'phases');
538
+ if (!fs.existsSync(phasesDir)) return results;
539
+
540
+ try {
541
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
542
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
543
+
544
+ const currentNum = String(currentPhaseNum).replace(/^0+/, '') || '0';
545
+
546
+ for (const dir of dirs) {
547
+ // Check if this phase is in the current milestone
548
+ if (!isDirInMilestone(dir)) continue;
549
+
550
+ // Extract phase number from directory name
551
+ const dirMatch = dir.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
552
+ if (!dirMatch) continue;
553
+ const dirPhaseNum = dirMatch[1];
554
+
555
+ // Skip the current phase
556
+ if (dirPhaseNum === currentNum) continue;
557
+
558
+ // Look for SUMMARY files in this phase
559
+ const phasePath = path.join(phasesDir, dir);
560
+ try {
561
+ const phaseFiles = fs.readdirSync(phasePath);
562
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
563
+ for (const summary of summaries) {
564
+ const relPath = toPosixPath(path.join(projectRoot, 'phases', dir, summary));
565
+ results.push({ path: relPath, category: 'milestone-summary' });
566
+ }
567
+ } catch {}
568
+ }
569
+ } catch {}
570
+
571
+ return results;
572
+ }
573
+
574
+ /**
575
+ * Resolve idea-scoped files for the --idea flag.
576
+ * Searches pending, done, rejected, consolidated directories.
577
+ *
578
+ * @param {string|number} ideaId - Idea ID or filename
579
+ * @param {string} planningRoot - Absolute path to planning root
580
+ * @param {string} [cwd] - Working directory for relative path resolution (defaults to process.cwd())
581
+ * @returns {Array<{path: string, category: string}>}
582
+ */
583
+ function resolveIdeaScope(ideaId, planningRoot, cwd) {
584
+ const results = [];
585
+ if (!ideaId) return results;
586
+
587
+ const idStr = String(ideaId).replace(/^0+/, '') || '0';
588
+ const paddedId = String(parseInt(idStr, 10)).padStart(3, '0');
589
+ const planRootRel = path.relative(cwd || process.cwd(), planningRoot) || '.';
590
+
591
+ const ideaStates = ['pending', 'done', 'rejected', 'consolidated'];
592
+
593
+ for (const state of ideaStates) {
594
+ const dir = path.join(planningRoot, 'ideas', state);
595
+ let files;
596
+ try {
597
+ files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
598
+ } catch {
599
+ continue;
600
+ }
601
+
602
+ for (const file of files) {
603
+ // Match by ID prefix (e.g., "005-some-idea.md")
604
+ if (file.startsWith(paddedId + '-') || file === ideaId || file === ideaId + '.md') {
605
+ const relPath = toPosixPath(path.join(planRootRel, 'ideas', state, file));
606
+ results.push({ path: relPath, category: 'idea' });
607
+
608
+ // Check for docs/ directory alongside the idea
609
+ // Idea docs/ is at ideas/<state>/<idea-slug>/docs/ where idea-slug matches the filename stem
610
+ const stem = file.replace(/\.md$/, '');
611
+ const docsDir = path.join(dir, stem, 'docs');
612
+ if (fs.existsSync(docsDir)) {
613
+ const docFiles = readdirRecursive(docsDir);
614
+ for (const docFile of docFiles) {
615
+ const docRelPath = toPosixPath(path.join(planRootRel, 'ideas', state, stem, 'docs', docFile));
616
+ results.push({ path: docRelPath, category: 'idea-docs' });
617
+ }
618
+ }
619
+
620
+ // Also check docs/ directly under the ideas state dir named by idea slug
621
+ const altDocsDir = path.join(planningRoot, 'ideas', state, 'docs');
622
+ // This is less common, skip unless the first pattern doesn't exist
623
+
624
+ return results; // Found the idea, stop searching
625
+ }
626
+ }
627
+ }
628
+
629
+ return results;
630
+ }
631
+
632
+ /**
633
+ * Resolve spec-scoped files for the --spec flag.
634
+ * For approved specs, truncates content to frontmatter + sections 1-3 only.
635
+ *
636
+ * @param {string|number} specId - Spec ID or filename
637
+ * @param {string} planningRoot - Absolute path to planning root
638
+ * @param {string} [cwd] - Working directory for relative path resolution (defaults to process.cwd())
639
+ * @returns {{files: Array<{path: string, category: string}>, approved_truncated: boolean}}
640
+ */
641
+ function resolveSpecScope(specId, planningRoot, cwd) {
642
+ const results = [];
643
+ let approvedTruncated = false;
644
+
645
+ if (!specId) return { files: results, approved_truncated: approvedTruncated };
646
+
647
+ const specsDir = path.join(planningRoot, 'specs');
648
+ const planRootRel = path.relative(cwd || process.cwd(), planningRoot) || '.';
649
+
650
+ let files;
651
+ try {
652
+ files = fs.readdirSync(specsDir).filter(f => f.endsWith('.md'));
653
+ } catch {
654
+ return { files: results, approved_truncated: approvedTruncated };
655
+ }
656
+
657
+ const searchLower = String(specId).toLowerCase();
658
+
659
+ for (const file of files) {
660
+ const fileLower = file.toLowerCase();
661
+ const matchById = fileLower.startsWith(searchLower + '-') ||
662
+ fileLower === searchLower + '.md' ||
663
+ fileLower === searchLower;
664
+
665
+ // Also check frontmatter id field
666
+ let matchByFrontmatterId = false;
667
+ let isApproved = false;
668
+ const fullPath = path.join(specsDir, file);
669
+ const content = safeReadFile(fullPath);
670
+
671
+ if (content) {
672
+ const fmMatch = content.match(/^---\n([\s\S]+?)\n---/);
673
+ if (fmMatch) {
674
+ const idMatch = fmMatch[1].match(/^id:\s*(.+)$/m);
675
+ if (idMatch && idMatch[1].trim().toLowerCase() === searchLower) {
676
+ matchByFrontmatterId = true;
677
+ }
678
+ const statusMatch = fmMatch[1].match(/^status:\s*(.+)$/m);
679
+ if (statusMatch) {
680
+ const status = statusMatch[1].trim().toLowerCase();
681
+ isApproved = status === 'approved' || status === 'final';
682
+ }
683
+ }
684
+ }
685
+
686
+ if (matchById || matchByFrontmatterId) {
687
+ const relPath = toPosixPath(path.join(planRootRel, 'specs', file));
688
+
689
+ if (isApproved) {
690
+ approvedTruncated = true;
691
+ // For approved specs: store metadata indicating truncation
692
+ results.push({ path: relPath, category: 'spec', truncated: true });
693
+ } else {
694
+ results.push({ path: relPath, category: 'spec' });
695
+ }
696
+
697
+ // Check for docs/ directory
698
+ const stem = file.replace(/\.md$/, '');
699
+ const docsDir = path.join(specsDir, stem, 'docs');
700
+ if (fs.existsSync(docsDir)) {
701
+ const docFiles = readdirRecursive(docsDir);
702
+ for (const docFile of docFiles) {
703
+ const docRelPath = toPosixPath(path.join(planRootRel, 'specs', stem, 'docs', docFile));
704
+ results.push({ path: docRelPath, category: 'spec-docs' });
705
+ }
706
+ }
707
+
708
+ return { files: results, approved_truncated: approvedTruncated };
709
+ }
710
+ }
711
+
712
+ return { files: results, approved_truncated: approvedTruncated };
713
+ }
714
+
715
+ /**
716
+ * Truncate an approved spec to frontmatter + sections 1-3 only.
717
+ * Sections are identified by ## headings in the body.
718
+ *
719
+ * @param {string} content - Full spec content
720
+ * @returns {string} Truncated content
721
+ */
722
+ function truncateApprovedSpec(content) {
723
+ if (!content) return '';
724
+
725
+ // Extract frontmatter
726
+ const fmMatch = content.match(/^(---\n[\s\S]+?\n---)\n?([\s\S]*)/);
727
+ if (!fmMatch) return content;
728
+
729
+ const frontmatter = fmMatch[1];
730
+ const body = fmMatch[2] || '';
731
+
732
+ // Find section headings (## level)
733
+ const sectionPattern = /^##\s+/gm;
734
+ let sectionCount = 0;
735
+ let lastIndex = 0;
736
+ let sectionMatch;
737
+ let cutoffIndex = body.length;
738
+
739
+ while ((sectionMatch = sectionPattern.exec(body)) !== null) {
740
+ sectionCount++;
741
+ if (sectionCount > 3) {
742
+ cutoffIndex = sectionMatch.index;
743
+ break;
744
+ }
745
+ lastIndex = sectionMatch.index;
746
+ }
747
+
748
+ const truncatedBody = body.slice(0, cutoffIndex).trimEnd();
749
+ return frontmatter + '\n' + truncatedBody + '\n\n*[Truncated: approved spec -- showing frontmatter + sections 1-3 only]*\n';
750
+ }
751
+
752
+ // ─── Main Engine ────────────────────────────────────────────────────────────
753
+
754
+ /**
755
+ * Load context files for a given tier with optional scope flags.
756
+ * This is the main engine function used by CLI and other modules.
757
+ *
758
+ * @param {string} tierName - Tier name (none, lite, planning, execution, verification)
759
+ * @param {string} planningRoot - Absolute path to planning root
760
+ * @param {Object} [options={}] - Scope options
761
+ * @param {string|number} [options.phase] - Phase number for --phase scope
762
+ * @param {string|number} [options.idea] - Idea ID for --idea scope
763
+ * @param {string|number} [options.spec] - Spec ID for --spec scope
764
+ * @param {string} [options.cwd] - Working directory (defaults to process.cwd())
765
+ * @returns {{files: Array<{path: string, exists: boolean, category: string}>, tier: string, scope: Object}}
766
+ */
767
+ function loadTierInternal(tierName, planningRoot, options) {
768
+ options = options || {};
769
+ const cwd = options.cwd || process.cwd();
770
+ const tiers = parseTierDefinitions();
771
+
772
+ if (!tiers.has(tierName)) {
773
+ return { files: [], tier: tierName, scope: {}, error: 'Unknown tier: ' + tierName };
774
+ }
775
+
776
+ // 1. Resolve static files for this tier (including inherited)
777
+ const staticFiles = resolveTierFiles(tierName, tiers);
778
+
779
+ // 2. Resolve dynamic files for this tier (including inherited)
780
+ const dynamicFiles = resolveDynamicFilesForTier(tierName, tiers, planningRoot);
781
+
782
+ // 3. Resolve scope flags
783
+ const scopeFiles = [];
784
+ const scope = {};
785
+
786
+ if (options.phase) {
787
+ scope.phase = options.phase;
788
+ const phaseFiles = resolvePhaseScope(options.phase, planningRoot, cwd);
789
+ scopeFiles.push(...phaseFiles);
790
+ }
791
+
792
+ if (options.idea) {
793
+ scope.idea = options.idea;
794
+ const ideaFiles = resolveIdeaScope(options.idea, planningRoot, cwd);
795
+ scopeFiles.push(...ideaFiles);
796
+ }
797
+
798
+ if (options.spec) {
799
+ scope.spec = options.spec;
800
+ const specResult = resolveSpecScope(options.spec, planningRoot, cwd);
801
+ scopeFiles.push(...specResult.files);
802
+ if (specResult.approved_truncated) {
803
+ scope.spec_truncated = true;
804
+ }
805
+ }
806
+
807
+ // 4. Combine all files
808
+ const allEntries = [...staticFiles, ...dynamicFiles, ...scopeFiles];
809
+
810
+ // 5. Resolve paths relative to cwd and filter to existing files
811
+ const planRootRel = path.relative(cwd, planningRoot) || '.';
812
+ const seen = new Set();
813
+ const files = [];
814
+
815
+ for (const entry of allEntries) {
816
+ // Static/dynamic files are relative to planning root
817
+ // Scope files are already relative to cwd
818
+ let relPath;
819
+ if (entry._fromScope) {
820
+ relPath = entry.path;
821
+ } else if (entry.path.startsWith(planRootRel) || entry.path.startsWith('.planning')) {
822
+ // Already relative to cwd
823
+ relPath = entry.path;
824
+ } else {
825
+ relPath = toPosixPath(path.join(planRootRel, entry.path));
826
+ }
827
+
828
+ // Deduplicate
829
+ if (seen.has(relPath)) continue;
830
+ seen.add(relPath);
831
+
832
+ // Check existence
833
+ const absPath = path.resolve(cwd, relPath);
834
+ if (fs.existsSync(absPath)) {
835
+ const fileEntry = { path: relPath, exists: true, category: entry.category };
836
+ if (entry.truncated) fileEntry.truncated = true;
837
+ files.push(fileEntry);
838
+ }
839
+ // Missing files are silently omitted (TOOL-09)
840
+ }
841
+
842
+ return { files, tier: tierName, scope };
843
+ }
844
+
845
+ /**
846
+ * Resolve dynamic files for a tier including inherited tiers.
847
+ *
848
+ * @param {string} tierName - Tier name
849
+ * @param {Map<string, Object>} tiers - Parsed tier definitions
850
+ * @param {string} planningRoot - Absolute path to planning root
851
+ * @returns {Array<{path: string, category: string}>}
852
+ */
853
+ function resolveDynamicFilesForTier(tierName, tiers, planningRoot) {
854
+ const tier = tiers.get(tierName);
855
+ if (!tier) return [];
856
+
857
+ let files = [];
858
+
859
+ // Recursively include inherited tier dynamic files
860
+ if (tier.includes_tier && tier.includes_tier !== 'null' && tiers.has(tier.includes_tier)) {
861
+ files = resolveDynamicFilesForTier(tier.includes_tier, tiers, planningRoot);
862
+ }
863
+
864
+ // Add this tier's own dynamic files
865
+ if (Array.isArray(tier.dynamic)) {
866
+ const resolved = resolveDynamicFiles(tier.dynamic, planningRoot);
867
+ files.push(...resolved);
868
+ }
869
+
870
+ return files;
871
+ }
872
+
873
+ // ─── CLI Command ────────────────────────────────────────────────────────────
874
+
875
+ /**
876
+ * CLI command handler for `context load-tier <tierName>`.
877
+ * Parses CLI args for --phase, --idea, --spec flags.
878
+ *
879
+ * @param {string} cwd - Working directory
880
+ * @param {string} tierName - Tier name
881
+ * @param {string[]} args - Remaining CLI arguments
882
+ * @param {boolean} raw - Raw output mode
883
+ */
884
+ function cmdContextLoadTier(cwd, tierName, args, raw) {
885
+ if (!tierName) {
886
+ error('tier name required. Valid tiers: none, lite, planning, execution, verification');
887
+ }
888
+
889
+ const validTiers = ['none', 'lite', 'planning', 'execution', 'verification'];
890
+ if (!validTiers.includes(tierName)) {
891
+ error('invalid tier: ' + tierName + '. Valid tiers: ' + validTiers.join(', '));
892
+ }
893
+
894
+ // Parse scope flags from args
895
+ const options = { cwd };
896
+ if (Array.isArray(args)) {
897
+ for (let i = 0; i < args.length; i++) {
898
+ if (args[i] === '--phase' && i + 1 < args.length) {
899
+ options.phase = args[++i];
900
+ } else if (args[i] === '--idea' && i + 1 < args.length) {
901
+ options.idea = args[++i];
902
+ } else if (args[i] === '--spec' && i + 1 < args.length) {
903
+ options.spec = args[++i];
904
+ }
905
+ }
906
+ }
907
+
908
+ const planningRoot = getPlanningRoot(cwd);
909
+ const result = loadTierInternal(tierName, planningRoot, options);
910
+
911
+ output(result, raw, result.files.map(f => f.path).join('\n'));
912
+ }
913
+
914
+ // ─── Exports ────────────────────────────────────────────────────────────────
915
+
916
+ module.exports = {
917
+ loadTierInternal,
918
+ cmdContextLoadTier,
919
+ truncateApprovedSpec,
920
+ resetTierCache,
921
+ // Internal exports for testing
922
+ parseTierDefinitions,
923
+ parseSimpleYaml,
924
+ resolvePhaseScope,
925
+ resolveIdeaScope,
926
+ resolveSpecScope,
927
+ resolveTierFiles,
928
+ resolveDynamicFiles,
929
+ };