@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.
- package/LICENSE +38 -0
- package/README.md +851 -0
- package/agents/dgs-codebase-cross-analyzer.md +183 -0
- package/agents/dgs-codebase-mapper.md +782 -0
- package/agents/dgs-codebase-synthesizer.md +156 -0
- package/agents/dgs-debugger.md +1256 -0
- package/agents/dgs-executor.md +550 -0
- package/agents/dgs-integration-checker.md +481 -0
- package/agents/dgs-nyquist-auditor.md +178 -0
- package/agents/dgs-phase-researcher.md +563 -0
- package/agents/dgs-phase-verifier.md +450 -0
- package/agents/dgs-plan-checker.md +708 -0
- package/agents/dgs-planner.md +1324 -0
- package/agents/dgs-project-researcher.md +631 -0
- package/agents/dgs-research-synthesizer.md +249 -0
- package/agents/dgs-roadmapper.md +652 -0
- package/agents/dgs-verifier.md +607 -0
- package/bin/install.js +2073 -0
- package/commands/dgs/add-doc.md +45 -0
- package/commands/dgs/add-idea.md +38 -0
- package/commands/dgs/add-phase.md +43 -0
- package/commands/dgs/add-repo.md +54 -0
- package/commands/dgs/add-tests.md +41 -0
- package/commands/dgs/add-todo.md +47 -0
- package/commands/dgs/approve-spec.md +38 -0
- package/commands/dgs/audit-milestone.md +36 -0
- package/commands/dgs/audit-phase.md +37 -0
- package/commands/dgs/cancel-job.md +23 -0
- package/commands/dgs/capture-principle.md +143 -0
- package/commands/dgs/check-todos.md +45 -0
- package/commands/dgs/cleanup.md +18 -0
- package/commands/dgs/complete-milestone.md +136 -0
- package/commands/dgs/complete-project.md +70 -0
- package/commands/dgs/consolidate-ideas.md +50 -0
- package/commands/dgs/create-milestone-job.md +37 -0
- package/commands/dgs/debug.md +164 -0
- package/commands/dgs/develop-idea.md +53 -0
- package/commands/dgs/discuss-idea.md +41 -0
- package/commands/dgs/discuss-phase.md +83 -0
- package/commands/dgs/execute-phase.md +41 -0
- package/commands/dgs/fast.md +38 -0
- package/commands/dgs/find-related-ideas.md +43 -0
- package/commands/dgs/health.md +28 -0
- package/commands/dgs/help.md +22 -0
- package/commands/dgs/import-spec.md +36 -0
- package/commands/dgs/init-product.md +28 -0
- package/commands/dgs/insert-phase.md +32 -0
- package/commands/dgs/join-discord.md +18 -0
- package/commands/dgs/list-docs.md +40 -0
- package/commands/dgs/list-ideas.md +42 -0
- package/commands/dgs/list-jobs.md +22 -0
- package/commands/dgs/list-phase-assumptions.md +46 -0
- package/commands/dgs/list-projects.md +57 -0
- package/commands/dgs/list-specs.md +40 -0
- package/commands/dgs/map-codebase.md +92 -0
- package/commands/dgs/new-milestone.md +44 -0
- package/commands/dgs/new-project.md +42 -0
- package/commands/dgs/node-repair.md +26 -0
- package/commands/dgs/overlap-check.md +20 -0
- package/commands/dgs/pause-work.md +38 -0
- package/commands/dgs/plan-milestone-gaps.md +34 -0
- package/commands/dgs/plan-phase.md +44 -0
- package/commands/dgs/progress.md +24 -0
- package/commands/dgs/quick.md +41 -0
- package/commands/dgs/reactivate-project.md +70 -0
- package/commands/dgs/reapply-patches.md +110 -0
- package/commands/dgs/refine-spec.md +38 -0
- package/commands/dgs/reject-idea.md +43 -0
- package/commands/dgs/remove-doc.md +44 -0
- package/commands/dgs/remove-phase.md +31 -0
- package/commands/dgs/remove-repo.md +69 -0
- package/commands/dgs/research-idea.md +43 -0
- package/commands/dgs/research-phase.md +189 -0
- package/commands/dgs/restore-idea.md +45 -0
- package/commands/dgs/resume-work.md +40 -0
- package/commands/dgs/rollback-job.md +24 -0
- package/commands/dgs/run-job.md +35 -0
- package/commands/dgs/search.md +40 -0
- package/commands/dgs/set-profile.md +34 -0
- package/commands/dgs/settings.md +38 -0
- package/commands/dgs/switch-project.md +58 -0
- package/commands/dgs/undo-consolidation.md +42 -0
- package/commands/dgs/update-idea.md +44 -0
- package/commands/dgs/update.md +37 -0
- package/commands/dgs/validate-phase.md +35 -0
- package/commands/dgs/verify-work.md +39 -0
- package/commands/dgs/write-spec.md +49 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
- package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
- package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
- package/deliver-great-systems/bin/lib/commands.cjs +570 -0
- package/deliver-great-systems/bin/lib/config.cjs +417 -0
- package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
- package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
- package/deliver-great-systems/bin/lib/context.cjs +929 -0
- package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
- package/deliver-great-systems/bin/lib/core.cjs +744 -0
- package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
- package/deliver-great-systems/bin/lib/docs.cjs +919 -0
- package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
- package/deliver-great-systems/bin/lib/execution.cjs +705 -0
- package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
- package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
- package/deliver-great-systems/bin/lib/identity.cjs +125 -0
- package/deliver-great-systems/bin/lib/init.cjs +1114 -0
- package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
- package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
- package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
- package/deliver-great-systems/bin/lib/migration.cjs +352 -0
- package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
- package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
- package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
- package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
- package/deliver-great-systems/bin/lib/paths.cjs +144 -0
- package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
- package/deliver-great-systems/bin/lib/phase.cjs +910 -0
- package/deliver-great-systems/bin/lib/projects.cjs +691 -0
- package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
- package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
- package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
- package/deliver-great-systems/bin/lib/search.cjs +570 -0
- package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
- package/deliver-great-systems/bin/lib/state.cjs +893 -0
- package/deliver-great-systems/bin/lib/template.cjs +228 -0
- package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
- package/deliver-great-systems/bin/lib/verify.cjs +796 -0
- package/deliver-great-systems/references/checkpoints.md +776 -0
- package/deliver-great-systems/references/conflict-resolution.md +66 -0
- package/deliver-great-systems/references/context-tiers.md +166 -0
- package/deliver-great-systems/references/continuation-format.md +249 -0
- package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
- package/deliver-great-systems/references/git-integration.md +250 -0
- package/deliver-great-systems/references/git-planning-commit.md +40 -0
- package/deliver-great-systems/references/model-profile-resolution.md +36 -0
- package/deliver-great-systems/references/model-profiles.md +95 -0
- package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
- package/deliver-great-systems/references/planning-config.md +224 -0
- package/deliver-great-systems/references/questioning.md +162 -0
- package/deliver-great-systems/references/spec-review-loop.md +177 -0
- package/deliver-great-systems/references/tdd.md +265 -0
- package/deliver-great-systems/references/ui-brand.md +160 -0
- package/deliver-great-systems/references/verification-patterns.md +612 -0
- package/deliver-great-systems/templates/DEBUG.md +166 -0
- package/deliver-great-systems/templates/UAT.md +251 -0
- package/deliver-great-systems/templates/VALIDATION.md +95 -0
- package/deliver-great-systems/templates/claude-md.md +74 -0
- package/deliver-great-systems/templates/codebase/architecture.md +257 -0
- package/deliver-great-systems/templates/codebase/concerns.md +312 -0
- package/deliver-great-systems/templates/codebase/conventions.md +309 -0
- package/deliver-great-systems/templates/codebase/integrations.md +282 -0
- package/deliver-great-systems/templates/codebase/stack.md +188 -0
- package/deliver-great-systems/templates/codebase/structure.md +287 -0
- package/deliver-great-systems/templates/codebase/testing.md +482 -0
- package/deliver-great-systems/templates/config.json +38 -0
- package/deliver-great-systems/templates/context.md +354 -0
- package/deliver-great-systems/templates/continue-here.md +80 -0
- package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
- package/deliver-great-systems/templates/discovery.md +148 -0
- package/deliver-great-systems/templates/milestone-archive.md +125 -0
- package/deliver-great-systems/templates/milestone.md +117 -0
- package/deliver-great-systems/templates/phase-prompt.md +615 -0
- package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
- package/deliver-great-systems/templates/project.md +186 -0
- package/deliver-great-systems/templates/requirements.md +233 -0
- package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
- package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
- package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
- package/deliver-great-systems/templates/research-project/STACK.md +122 -0
- package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
- package/deliver-great-systems/templates/research.md +554 -0
- package/deliver-great-systems/templates/retrospective.md +54 -0
- package/deliver-great-systems/templates/roadmap.md +204 -0
- package/deliver-great-systems/templates/state.md +178 -0
- package/deliver-great-systems/templates/summary-complex.md +59 -0
- package/deliver-great-systems/templates/summary-minimal.md +41 -0
- package/deliver-great-systems/templates/summary-standard.md +48 -0
- package/deliver-great-systems/templates/summary.md +253 -0
- package/deliver-great-systems/templates/user-setup.md +313 -0
- package/deliver-great-systems/templates/verification-report.md +324 -0
- package/deliver-great-systems/workflows/add-doc.md +151 -0
- package/deliver-great-systems/workflows/add-idea.md +96 -0
- package/deliver-great-systems/workflows/add-phase.md +120 -0
- package/deliver-great-systems/workflows/add-tests.md +359 -0
- package/deliver-great-systems/workflows/add-todo.md +162 -0
- package/deliver-great-systems/workflows/approve-spec.md +194 -0
- package/deliver-great-systems/workflows/audit-milestone.md +364 -0
- package/deliver-great-systems/workflows/audit-phase.md +462 -0
- package/deliver-great-systems/workflows/cancel-job.md +108 -0
- package/deliver-great-systems/workflows/check-todos.md +181 -0
- package/deliver-great-systems/workflows/cleanup.md +247 -0
- package/deliver-great-systems/workflows/codereview.md +526 -0
- package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
- package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
- package/deliver-great-systems/workflows/develop-idea.md +544 -0
- package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
- package/deliver-great-systems/workflows/discovery-phase.md +301 -0
- package/deliver-great-systems/workflows/discuss-idea.md +263 -0
- package/deliver-great-systems/workflows/discuss-phase.md +733 -0
- package/deliver-great-systems/workflows/execute-phase.md +571 -0
- package/deliver-great-systems/workflows/execute-plan.md +592 -0
- package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
- package/deliver-great-systems/workflows/health.md +173 -0
- package/deliver-great-systems/workflows/help.md +997 -0
- package/deliver-great-systems/workflows/import-spec.md +381 -0
- package/deliver-great-systems/workflows/init-product.md +767 -0
- package/deliver-great-systems/workflows/insert-phase.md +138 -0
- package/deliver-great-systems/workflows/list-docs.md +119 -0
- package/deliver-great-systems/workflows/list-ideas.md +154 -0
- package/deliver-great-systems/workflows/list-jobs.md +89 -0
- package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
- package/deliver-great-systems/workflows/list-specs.md +101 -0
- package/deliver-great-systems/workflows/map-codebase.md +621 -0
- package/deliver-great-systems/workflows/new-milestone.md +591 -0
- package/deliver-great-systems/workflows/new-project.md +1113 -0
- package/deliver-great-systems/workflows/node-repair.md +94 -0
- package/deliver-great-systems/workflows/overlap-check.md +86 -0
- package/deliver-great-systems/workflows/pause-work.md +134 -0
- package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
- package/deliver-great-systems/workflows/plan-phase.md +698 -0
- package/deliver-great-systems/workflows/progress.md +386 -0
- package/deliver-great-systems/workflows/quick.md +845 -0
- package/deliver-great-systems/workflows/refine-spec.md +275 -0
- package/deliver-great-systems/workflows/reject-idea.md +109 -0
- package/deliver-great-systems/workflows/remove-doc.md +117 -0
- package/deliver-great-systems/workflows/remove-phase.md +163 -0
- package/deliver-great-systems/workflows/research-idea.md +325 -0
- package/deliver-great-systems/workflows/research-phase.md +81 -0
- package/deliver-great-systems/workflows/restore-idea.md +101 -0
- package/deliver-great-systems/workflows/resume-project.md +311 -0
- package/deliver-great-systems/workflows/rollback-job.md +130 -0
- package/deliver-great-systems/workflows/run-job.md +498 -0
- package/deliver-great-systems/workflows/search.md +130 -0
- package/deliver-great-systems/workflows/set-profile.md +83 -0
- package/deliver-great-systems/workflows/settings.md +470 -0
- package/deliver-great-systems/workflows/transition.md +563 -0
- package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
- package/deliver-great-systems/workflows/update-idea.md +157 -0
- package/deliver-great-systems/workflows/update.md +242 -0
- package/deliver-great-systems/workflows/validate-phase.md +177 -0
- package/deliver-great-systems/workflows/verify-phase.md +253 -0
- package/deliver-great-systems/workflows/verify-work.md +671 -0
- package/deliver-great-systems/workflows/write-spec.md +450 -0
- package/hooks/dist/dgs-check-update.js +62 -0
- package/hooks/dist/dgs-context-monitor.js +141 -0
- package/hooks/dist/dgs-statusline.js +115 -0
- package/package.json +60 -0
- 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
|
+
};
|