popeye-cli 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.env.example +4 -1
  2. package/CONTRIBUTING.md +10 -0
  3. package/README.md +224 -17
  4. package/dist/adapters/claude.d.ts +3 -2
  5. package/dist/adapters/claude.d.ts.map +1 -1
  6. package/dist/adapters/claude.js +214 -0
  7. package/dist/adapters/claude.js.map +1 -1
  8. package/dist/adapters/gemini.d.ts +2 -2
  9. package/dist/adapters/gemini.d.ts.map +1 -1
  10. package/dist/adapters/grok.d.ts +2 -1
  11. package/dist/adapters/grok.d.ts.map +1 -1
  12. package/dist/adapters/grok.js.map +1 -1
  13. package/dist/adapters/index.d.ts +8 -0
  14. package/dist/adapters/index.d.ts.map +1 -0
  15. package/dist/adapters/index.js +12 -0
  16. package/dist/adapters/index.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +2 -2
  18. package/dist/adapters/openai.d.ts.map +1 -1
  19. package/dist/adapters/openai.js.map +1 -1
  20. package/dist/cli/commands/create.d.ts.map +1 -1
  21. package/dist/cli/commands/create.js +25 -5
  22. package/dist/cli/commands/create.js.map +1 -1
  23. package/dist/cli/index.d.ts +1 -0
  24. package/dist/cli/index.d.ts.map +1 -1
  25. package/dist/cli/index.js +5 -2
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/interactive.d.ts.map +1 -1
  28. package/dist/cli/interactive.js +354 -28
  29. package/dist/cli/interactive.js.map +1 -1
  30. package/dist/config/index.d.ts +2 -0
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/schema.d.ts +4 -0
  33. package/dist/config/schema.d.ts.map +1 -1
  34. package/dist/config/schema.js +2 -1
  35. package/dist/config/schema.js.map +1 -1
  36. package/dist/generators/all.d.ts +70 -0
  37. package/dist/generators/all.d.ts.map +1 -0
  38. package/dist/generators/all.js +826 -0
  39. package/dist/generators/all.js.map +1 -0
  40. package/dist/generators/fullstack.d.ts +9 -0
  41. package/dist/generators/fullstack.d.ts.map +1 -1
  42. package/dist/generators/fullstack.js.map +1 -1
  43. package/dist/generators/index.d.ts +3 -1
  44. package/dist/generators/index.d.ts.map +1 -1
  45. package/dist/generators/index.js +33 -0
  46. package/dist/generators/index.js.map +1 -1
  47. package/dist/generators/templates/index.d.ts +2 -0
  48. package/dist/generators/templates/index.d.ts.map +1 -1
  49. package/dist/generators/templates/index.js +2 -0
  50. package/dist/generators/templates/index.js.map +1 -1
  51. package/dist/generators/templates/website.d.ts +85 -0
  52. package/dist/generators/templates/website.d.ts.map +1 -0
  53. package/dist/generators/templates/website.js +877 -0
  54. package/dist/generators/templates/website.js.map +1 -0
  55. package/dist/generators/website.d.ts +56 -0
  56. package/dist/generators/website.d.ts.map +1 -0
  57. package/dist/generators/website.js +269 -0
  58. package/dist/generators/website.js.map +1 -0
  59. package/dist/types/consensus.d.ts +18 -23
  60. package/dist/types/consensus.d.ts.map +1 -1
  61. package/dist/types/consensus.js +8 -3
  62. package/dist/types/consensus.js.map +1 -1
  63. package/dist/types/index.d.ts +2 -2
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/types/index.js +2 -2
  66. package/dist/types/index.js.map +1 -1
  67. package/dist/types/project.d.ts +130 -17
  68. package/dist/types/project.d.ts.map +1 -1
  69. package/dist/types/project.js +55 -8
  70. package/dist/types/project.js.map +1 -1
  71. package/dist/types/workflow.d.ts +2 -0
  72. package/dist/types/workflow.d.ts.map +1 -1
  73. package/dist/types/workflow.js +2 -1
  74. package/dist/types/workflow.js.map +1 -1
  75. package/dist/upgrade/context.d.ts +37 -0
  76. package/dist/upgrade/context.d.ts.map +1 -0
  77. package/dist/upgrade/context.js +284 -0
  78. package/dist/upgrade/context.js.map +1 -0
  79. package/dist/upgrade/handlers.d.ts +103 -0
  80. package/dist/upgrade/handlers.d.ts.map +1 -0
  81. package/dist/upgrade/handlers.js +384 -0
  82. package/dist/upgrade/handlers.js.map +1 -0
  83. package/dist/upgrade/index.d.ts +26 -0
  84. package/dist/upgrade/index.d.ts.map +1 -0
  85. package/dist/upgrade/index.js +194 -0
  86. package/dist/upgrade/index.js.map +1 -0
  87. package/dist/upgrade/transitions.d.ts +34 -0
  88. package/dist/upgrade/transitions.d.ts.map +1 -0
  89. package/dist/upgrade/transitions.js +56 -0
  90. package/dist/upgrade/transitions.js.map +1 -0
  91. package/dist/workflow/consensus.d.ts +2 -1
  92. package/dist/workflow/consensus.d.ts.map +1 -1
  93. package/dist/workflow/consensus.js.map +1 -1
  94. package/dist/workflow/index.d.ts +6 -0
  95. package/dist/workflow/index.d.ts.map +1 -1
  96. package/dist/workflow/index.js +8 -0
  97. package/dist/workflow/index.js.map +1 -1
  98. package/dist/workflow/plan-mode.d.ts +3 -3
  99. package/dist/workflow/plan-mode.d.ts.map +1 -1
  100. package/dist/workflow/plan-mode.js +41 -5
  101. package/dist/workflow/plan-mode.js.map +1 -1
  102. package/dist/workflow/plan-parser.d.ts +97 -0
  103. package/dist/workflow/plan-parser.d.ts.map +1 -0
  104. package/dist/workflow/plan-parser.js +235 -0
  105. package/dist/workflow/plan-parser.js.map +1 -0
  106. package/dist/workflow/plan-storage.d.ts +40 -12
  107. package/dist/workflow/plan-storage.d.ts.map +1 -1
  108. package/dist/workflow/plan-storage.js +47 -20
  109. package/dist/workflow/plan-storage.js.map +1 -1
  110. package/dist/workflow/seo-tests.d.ts +43 -0
  111. package/dist/workflow/seo-tests.d.ts.map +1 -0
  112. package/dist/workflow/seo-tests.js +192 -0
  113. package/dist/workflow/seo-tests.js.map +1 -0
  114. package/dist/workflow/separation-guard.d.ts +35 -0
  115. package/dist/workflow/separation-guard.d.ts.map +1 -0
  116. package/dist/workflow/separation-guard.js +154 -0
  117. package/dist/workflow/separation-guard.js.map +1 -0
  118. package/dist/workflow/task-workflow.d.ts.map +1 -1
  119. package/dist/workflow/task-workflow.js +3 -2
  120. package/dist/workflow/task-workflow.js.map +1 -1
  121. package/dist/workflow/test-runner.d.ts.map +1 -1
  122. package/dist/workflow/test-runner.js +128 -0
  123. package/dist/workflow/test-runner.js.map +1 -1
  124. package/dist/workflow/workspace-manager.d.ts +31 -20
  125. package/dist/workflow/workspace-manager.d.ts.map +1 -1
  126. package/dist/workflow/workspace-manager.js +38 -9
  127. package/dist/workflow/workspace-manager.js.map +1 -1
  128. package/package.json +1 -1
  129. package/src/adapters/claude.ts +221 -4
  130. package/src/adapters/gemini.ts +2 -2
  131. package/src/adapters/grok.ts +2 -1
  132. package/src/adapters/index.ts +15 -0
  133. package/src/adapters/openai.ts +2 -2
  134. package/src/cli/commands/create.ts +25 -5
  135. package/src/cli/index.ts +5 -2
  136. package/src/cli/interactive.ts +400 -29
  137. package/src/config/schema.ts +2 -1
  138. package/src/generators/all.ts +897 -0
  139. package/src/generators/fullstack.ts +10 -0
  140. package/src/generators/index.ts +54 -0
  141. package/src/generators/templates/index.ts +2 -0
  142. package/src/generators/templates/website.ts +906 -0
  143. package/src/generators/website.ts +350 -0
  144. package/src/types/consensus.ts +20 -8
  145. package/src/types/index.ts +35 -0
  146. package/src/types/project.ts +157 -11
  147. package/src/types/workflow.ts +2 -1
  148. package/src/upgrade/context.ts +332 -0
  149. package/src/upgrade/handlers.ts +477 -0
  150. package/src/upgrade/index.ts +244 -0
  151. package/src/upgrade/transitions.ts +80 -0
  152. package/src/workflow/consensus.ts +3 -2
  153. package/src/workflow/index.ts +8 -0
  154. package/src/workflow/plan-mode.ts +44 -10
  155. package/src/workflow/plan-parser.ts +317 -0
  156. package/src/workflow/plan-storage.ts +69 -30
  157. package/src/workflow/seo-tests.ts +246 -0
  158. package/src/workflow/separation-guard.ts +200 -0
  159. package/src/workflow/task-workflow.ts +3 -2
  160. package/src/workflow/test-runner.ts +149 -0
  161. package/src/workflow/workspace-manager.ts +68 -31
  162. package/tests/cli/model-command.test.ts +93 -0
  163. package/tests/types/project.test.ts +90 -15
  164. package/tests/types/workflow-schema.test.ts +59 -0
  165. package/tests/upgrade/context.test.ts +211 -0
  166. package/tests/upgrade/transitions.test.ts +85 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Project type upgrade transitions
3
+ * Defines valid upgrade paths between project types
4
+ */
5
+
6
+ import { languageToApps } from '../types/project.js';
7
+ import type { OutputLanguage, AppType } from '../types/project.js';
8
+
9
+ /**
10
+ * Details of a project type transition
11
+ */
12
+ export interface UpgradeTransition {
13
+ from: OutputLanguage;
14
+ to: OutputLanguage;
15
+ /** New apps that will be generated */
16
+ newApps: AppType[];
17
+ /** Whether existing code needs to be moved into apps/ directory */
18
+ requiresRestructure: boolean;
19
+ /** Description of what the upgrade does */
20
+ description: string;
21
+ }
22
+
23
+ /**
24
+ * Get valid upgrade targets for a given language
25
+ *
26
+ * @param from - Current project language
27
+ * @returns Array of valid target languages
28
+ */
29
+ export function getValidUpgradeTargets(from: OutputLanguage): OutputLanguage[] {
30
+ const targets: Record<OutputLanguage, OutputLanguage[]> = {
31
+ python: ['fullstack', 'all'],
32
+ typescript: ['fullstack', 'all'],
33
+ fullstack: ['all'],
34
+ website: ['all'],
35
+ all: [],
36
+ };
37
+ return targets[from];
38
+ }
39
+
40
+ /**
41
+ * Get detailed transition information for an upgrade
42
+ *
43
+ * @param from - Current project language
44
+ * @param to - Target project language
45
+ * @returns Transition details or null if invalid
46
+ */
47
+ export function getTransitionDetails(
48
+ from: OutputLanguage,
49
+ to: OutputLanguage,
50
+ ): UpgradeTransition | null {
51
+ const validTargets = getValidUpgradeTargets(from);
52
+ if (!validTargets.includes(to)) {
53
+ return null;
54
+ }
55
+
56
+ const currentApps = new Set(languageToApps(from));
57
+ const targetApps = languageToApps(to);
58
+ const newApps = targetApps.filter((app) => !currentApps.has(app));
59
+
60
+ // Single-app types need restructuring into apps/ monorepo layout
61
+ const singleAppTypes: OutputLanguage[] = ['python', 'typescript', 'website'];
62
+ const requiresRestructure = singleAppTypes.includes(from);
63
+
64
+ const descriptions: Record<string, string> = {
65
+ 'python->fullstack': 'Add frontend app, move backend to apps/backend/',
66
+ 'python->all': 'Add frontend + website, move backend to apps/backend/',
67
+ 'typescript->fullstack': 'Add backend app, move frontend to apps/frontend/',
68
+ 'typescript->all': 'Add backend + website, move frontend to apps/frontend/',
69
+ 'fullstack->all': 'Add website app to existing workspace',
70
+ 'website->all': 'Add frontend + backend, move website to apps/website/',
71
+ };
72
+
73
+ return {
74
+ from,
75
+ to,
76
+ newApps,
77
+ requiresRestructure,
78
+ description: descriptions[`${from}->${to}`] || `Upgrade from ${from} to ${to}`,
79
+ };
80
+ }
@@ -14,6 +14,7 @@ import type {
14
14
  AppConsensusScores,
15
15
  CorrectionRecord,
16
16
  } from '../types/consensus.js';
17
+ import type { OutputLanguage } from '../types/project.js';
17
18
  import { DEFAULT_CONSENSUS_CONFIG } from '../types/consensus.js';
18
19
  import { requestConsensus as requestOpenAIConsensus } from '../adapters/openai.js';
19
20
  import { requestConsensus as requestGeminiConsensus, requestArbitration as requestGeminiArbitration } from '../adapters/gemini.js';
@@ -36,7 +37,7 @@ export interface ConsensusOptions {
36
37
  /** Whether this is a fullstack project (enables per-app tracking) */
37
38
  isFullstack?: boolean;
38
39
  /** Project language for revision prompts */
39
- language?: 'python' | 'typescript' | 'fullstack';
40
+ language?: OutputLanguage;
40
41
  onIteration?: (iteration: number, result: ConsensusResult) => void;
41
42
  onRevision?: (iteration: number, revisedPlan: string) => void;
42
43
  onConcerns?: (concerns: string[], recommendations: string[]) => void;
@@ -947,7 +948,7 @@ export async function runOptimizedConsensusProcess(
947
948
  } = options;
948
949
 
949
950
  // Derive language from isFullstack for revision prompts
950
- const language: 'python' | 'typescript' | 'fullstack' = isFullstack ? 'fullstack' : 'python';
951
+ const language: OutputLanguage = isFullstack ? 'fullstack' : 'python';
951
952
 
952
953
  const {
953
954
  threshold = DEFAULT_CONSENSUS_CONFIG.threshold,
@@ -41,6 +41,14 @@ export * from './ui-designer.js';
41
41
  export * from './ui-verification.js';
42
42
  export * from './project-verification.js';
43
43
  export * from './auto-fix.js';
44
+ // Note: plan-parser.js exports are accessible but have naming conflicts with plan-mode.js
45
+ // Import directly from './plan-parser.js' if you need the extended TaskAppTag type (includes 'WEB')
46
+ export * from './separation-guard.js';
47
+ export * from './seo-tests.js';
48
+ export * from './task-workflow.js';
49
+ export * from './milestone-workflow.js';
50
+ export * from './plan-storage.js';
51
+ export * from './workspace-manager.js';
44
52
 
45
53
  /**
46
54
  * Workflow options
@@ -5,7 +5,8 @@
5
5
 
6
6
  import { promises as fs } from 'node:fs';
7
7
  import path from 'node:path';
8
- import type { ProjectSpec } from '../types/project.js';
8
+ import { isWorkspace } from '../types/project.js';
9
+ import type { ProjectSpec, OutputLanguage } from '../types/project.js';
9
10
  import type { ProjectState, Milestone, Task } from '../types/workflow.js';
10
11
  import type { ConsensusConfig } from '../types/consensus.js';
11
12
  import { expandIdea as openaiExpandIdea } from '../adapters/openai.js';
@@ -52,7 +53,7 @@ export interface PlanModeResult {
52
53
  */
53
54
  export async function expandIdea(
54
55
  idea: string,
55
- language: 'python' | 'typescript' | 'fullstack',
56
+ language: OutputLanguage,
56
57
  onProgress?: (message: string) => void
57
58
  ): Promise<string> {
58
59
  onProgress?.('Expanding idea into specification...');
@@ -75,7 +76,7 @@ export async function expandIdea(
75
76
  export async function createPlan(
76
77
  specification: string,
77
78
  context: string = '',
78
- language: 'python' | 'typescript' | 'fullstack' = 'python',
79
+ language: OutputLanguage = 'python',
79
80
  onProgress?: (message: string) => void
80
81
  ): Promise<string> {
81
82
  onProgress?.('Creating development plan...');
@@ -103,12 +104,45 @@ export async function getProjectContext(
103
104
  ): Promise<string> {
104
105
  onProgress?.('Analyzing existing codebase...');
105
106
 
106
- // Check if directory has any code
107
+ // Check if directory has any code - check root AND apps/ subdirectories
107
108
  try {
108
- const files = await fs.readdir(projectDir);
109
- const hasCode = files.some((f) =>
110
- ['.py', '.ts', '.js', '.tsx', '.jsx'].some((ext) => f.endsWith(ext))
111
- );
109
+ const codeExtensions = ['.py', '.ts', '.js', '.tsx', '.jsx'];
110
+ const hasCodeInDir = async (dir: string): Promise<boolean> => {
111
+ try {
112
+ const files = await fs.readdir(dir);
113
+ return files.some((f) => codeExtensions.some((ext) => f.endsWith(ext)));
114
+ } catch {
115
+ return false;
116
+ }
117
+ };
118
+
119
+ let hasCode = await hasCodeInDir(projectDir);
120
+
121
+ // Also check apps/ subdirectories for monorepo/workspace projects
122
+ if (!hasCode) {
123
+ const appsDir = path.join(projectDir, 'apps');
124
+ try {
125
+ const appEntries = await fs.readdir(appsDir, { withFileTypes: true });
126
+ for (const entry of appEntries) {
127
+ if (entry.isDirectory()) {
128
+ const appHasCode = await hasCodeInDir(path.join(appsDir, entry.name));
129
+ if (appHasCode) {
130
+ hasCode = true;
131
+ break;
132
+ }
133
+ // Check one level deeper (apps/frontend/src/)
134
+ const srcDir = path.join(appsDir, entry.name, 'src');
135
+ const srcHasCode = await hasCodeInDir(srcDir);
136
+ if (srcHasCode) {
137
+ hasCode = true;
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ } catch {
143
+ // No apps/ directory
144
+ }
145
+ }
112
146
 
113
147
  if (!hasCode) {
114
148
  onProgress?.('No existing code found');
@@ -927,7 +961,7 @@ export async function runPlanMode(
927
961
  });
928
962
 
929
963
  // Validate fullstack plan structure
930
- if (spec.language === 'fullstack') {
964
+ if (isWorkspace(spec.language)) {
931
965
  onProgress?.('create-plan', 'Validating fullstack plan structure...');
932
966
  const validation = validateFullstackPlan(plan);
933
967
 
@@ -958,7 +992,7 @@ export async function runPlanMode(
958
992
  {
959
993
  projectDir,
960
994
  config: consensusConfig,
961
- isFullstack: spec.language === 'fullstack',
995
+ isFullstack: isWorkspace(spec.language),
962
996
  language: spec.language,
963
997
  onIteration: (iteration, result) => {
964
998
  onProgress?.(
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Plan parsing utilities for task tagging and validation
3
+ * Parses [FE], [BE], [WEB], [INT] tags from plan content
4
+ */
5
+
6
+ import type { OutputLanguage } from '../types/project.js';
7
+
8
+ /**
9
+ * Task app tags for workspace projects
10
+ */
11
+ export type TaskAppTag = 'FE' | 'BE' | 'WEB' | 'INT';
12
+
13
+ /**
14
+ * App target derived from task tag
15
+ */
16
+ export type AppTarget = 'frontend' | 'backend' | 'website' | 'unified';
17
+
18
+ /**
19
+ * Parsed task with app context
20
+ */
21
+ export interface ParsedTask {
22
+ name: string;
23
+ description?: string;
24
+ appTag?: TaskAppTag;
25
+ appTarget?: AppTarget;
26
+ files?: string[];
27
+ dependencies?: string[];
28
+ acceptanceCriteria?: string[];
29
+ }
30
+
31
+ /**
32
+ * Parse task tag from task name
33
+ *
34
+ * @param taskName - Task name potentially containing [FE], [BE], [WEB], [INT]
35
+ * @returns The parsed tag or undefined
36
+ */
37
+ export function parseTaskTag(taskName: string): TaskAppTag | undefined {
38
+ const tagMatch = taskName.match(/\[(FE|BE|WEB|INT)\]/i);
39
+ if (tagMatch) {
40
+ return tagMatch[1].toUpperCase() as TaskAppTag;
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ /**
46
+ * Convert task tag to app target
47
+ *
48
+ * @param tag - The task tag
49
+ * @returns The app target
50
+ */
51
+ export function tagToAppTarget(tag: TaskAppTag): AppTarget {
52
+ const mapping: Record<TaskAppTag, AppTarget> = {
53
+ FE: 'frontend',
54
+ BE: 'backend',
55
+ WEB: 'website',
56
+ INT: 'unified',
57
+ };
58
+ return mapping[tag];
59
+ }
60
+
61
+ /**
62
+ * Validation issues for a task
63
+ */
64
+ export interface TaskValidationResult {
65
+ valid: boolean;
66
+ issues: string[];
67
+ }
68
+
69
+ /**
70
+ * Validate a task has proper app targeting for workspace projects
71
+ *
72
+ * @param task - The parsed task
73
+ * @param hasWebsite - Whether the project includes a website app
74
+ * @returns Validation result with issues
75
+ */
76
+ export function validateWorkspaceTask(
77
+ task: ParsedTask,
78
+ hasWebsite: boolean = false
79
+ ): TaskValidationResult {
80
+ const issues: string[] = [];
81
+
82
+ const validTags = hasWebsite
83
+ ? '[FE], [BE], [WEB], or [INT]'
84
+ : '[FE], [BE], or [INT]';
85
+
86
+ if (!task.appTag) {
87
+ issues.push(`Task "${task.name}" missing ${validTags} tag`);
88
+ }
89
+
90
+ // Validate consistency between tag and appTarget
91
+ if (task.appTag && task.appTarget) {
92
+ const expectedTarget = tagToAppTarget(task.appTag);
93
+ if (task.appTarget !== expectedTarget) {
94
+ issues.push(
95
+ `Task "${task.name}" has [${task.appTag}] tag but App: is "${task.appTarget}" (expected "${expectedTarget}")`
96
+ );
97
+ }
98
+ }
99
+
100
+ // Validate file paths match app
101
+ if (task.files && task.appTag) {
102
+ const pathValidation: Record<TaskAppTag, { pattern: RegExp; expected: string }> = {
103
+ FE: { pattern: /\/frontend\//, expected: 'apps/frontend/' },
104
+ BE: { pattern: /\/backend\//, expected: 'apps/backend/' },
105
+ WEB: { pattern: /\/website\//, expected: 'apps/website/' },
106
+ INT: { pattern: /.*/, expected: 'any (unified)' },
107
+ };
108
+
109
+ const { pattern, expected } = pathValidation[task.appTag];
110
+
111
+ // Only validate FE/BE/WEB, not INT
112
+ if (task.appTag !== 'INT') {
113
+ const invalidFiles = task.files.filter((f) => !pattern.test(f));
114
+ if (invalidFiles.length > 0) {
115
+ issues.push(
116
+ `[${task.appTag}] task has files outside ${expected}: ${invalidFiles.join(', ')}`
117
+ );
118
+ }
119
+ }
120
+ }
121
+
122
+ return {
123
+ valid: issues.length === 0,
124
+ issues,
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Parse a task block from plan content
130
+ *
131
+ * @param taskBlock - The raw task block text
132
+ * @returns Parsed task
133
+ */
134
+ export function parseTaskBlock(taskBlock: string): ParsedTask {
135
+ const lines = taskBlock.split('\n');
136
+
137
+ // Extract task name from first line (### Task X.X [TAG]: Name)
138
+ const titleMatch = lines[0].match(/#{1,4}\s*(?:Task\s+\d+(?:\.\d+)?)?[:\s]*(.+)/i);
139
+ const name = titleMatch ? titleMatch[1].trim() : lines[0].trim();
140
+
141
+ const task: ParsedTask = {
142
+ name,
143
+ appTag: parseTaskTag(name),
144
+ };
145
+
146
+ // Parse App field
147
+ const appMatch = taskBlock.match(/\*\*App\*\*:\s*(\w+)/i);
148
+ if (appMatch) {
149
+ task.appTarget = appMatch[1].toLowerCase() as AppTarget;
150
+ } else if (task.appTag) {
151
+ // Derive from tag if App field not found
152
+ task.appTarget = tagToAppTarget(task.appTag);
153
+ }
154
+
155
+ // Parse Files field
156
+ const filesMatch = taskBlock.match(/\*\*Files\*\*:([\s\S]*?)(?=\*\*|$)/i);
157
+ if (filesMatch) {
158
+ const filesContent = filesMatch[1];
159
+ const fileMatches = filesContent.match(/`([^`]+)`/g);
160
+ if (fileMatches) {
161
+ task.files = fileMatches.map((f) => f.replace(/`/g, ''));
162
+ }
163
+ }
164
+
165
+ // Parse Dependencies
166
+ const depsMatch = taskBlock.match(/\*\*Dependencies\*\*:\s*(.+)/i);
167
+ if (depsMatch && depsMatch[1].trim().toLowerCase() !== 'none') {
168
+ task.dependencies = depsMatch[1]
169
+ .split(',')
170
+ .map((d) => d.trim())
171
+ .filter((d) => d);
172
+ }
173
+
174
+ // Parse Acceptance Criteria
175
+ const criteriaMatch = taskBlock.match(/\*\*Acceptance Criteria\*\*:([\s\S]*?)(?=###|##|$)/i);
176
+ if (criteriaMatch) {
177
+ const criteriaContent = criteriaMatch[1];
178
+ const criteriaItems = criteriaContent.match(/[-*[\]]\s*(.+)/g);
179
+ if (criteriaItems) {
180
+ task.acceptanceCriteria = criteriaItems.map((c) =>
181
+ c.replace(/^[-*[\]x\s]+/i, '').trim()
182
+ );
183
+ }
184
+ }
185
+
186
+ return task;
187
+ }
188
+
189
+ /**
190
+ * Extract all tasks from a plan
191
+ *
192
+ * @param planContent - The full plan markdown content
193
+ * @returns Array of parsed tasks
194
+ */
195
+ export function extractTasksFromPlan(planContent: string): ParsedTask[] {
196
+ const tasks: ParsedTask[] = [];
197
+
198
+ // Match task blocks (### Task X.X or #### Task X.X)
199
+ const taskBlockRegex = /#{3,4}\s*(?:Task\s+)?[\d.]+[^#]*?(?=#{2,4}|$)/gi;
200
+ const matches = planContent.matchAll(taskBlockRegex);
201
+
202
+ for (const match of matches) {
203
+ if (match[0].trim()) {
204
+ tasks.push(parseTaskBlock(match[0]));
205
+ }
206
+ }
207
+
208
+ return tasks;
209
+ }
210
+
211
+ /**
212
+ * Validate all tasks in a plan for workspace projects
213
+ *
214
+ * @param planContent - The full plan markdown content
215
+ * @param language - The project language
216
+ * @returns Validation results
217
+ */
218
+ export function validatePlanTasks(
219
+ planContent: string,
220
+ language: OutputLanguage
221
+ ): {
222
+ valid: boolean;
223
+ tasks: ParsedTask[];
224
+ issues: string[];
225
+ warnings: string[];
226
+ } {
227
+ const isWorkspaceProject = ['fullstack', 'website', 'all'].includes(language);
228
+ const hasWebsite = ['website', 'all'].includes(language);
229
+
230
+ const tasks = extractTasksFromPlan(planContent);
231
+ const issues: string[] = [];
232
+ const warnings: string[] = [];
233
+
234
+ if (!isWorkspaceProject) {
235
+ // Non-workspace projects don't require tagging
236
+ return {
237
+ valid: true,
238
+ tasks,
239
+ issues: [],
240
+ warnings: [],
241
+ };
242
+ }
243
+
244
+ // Validate each task
245
+ for (const task of tasks) {
246
+ const validation = validateWorkspaceTask(task, hasWebsite);
247
+ if (!validation.valid) {
248
+ // For now, treat as warnings rather than hard errors
249
+ warnings.push(...validation.issues);
250
+ }
251
+ }
252
+
253
+ // Check for minimum coverage
254
+ if (language === 'fullstack' || language === 'all') {
255
+ const feTasks = tasks.filter((t) => t.appTag === 'FE');
256
+ const beTasks = tasks.filter((t) => t.appTag === 'BE');
257
+ const intTasks = tasks.filter((t) => t.appTag === 'INT');
258
+
259
+ if (feTasks.length === 0) {
260
+ warnings.push('No [FE] frontend tasks found in plan');
261
+ }
262
+ if (beTasks.length === 0) {
263
+ warnings.push('No [BE] backend tasks found in plan');
264
+ }
265
+ if (intTasks.length === 0) {
266
+ warnings.push('No [INT] integration tasks found in plan');
267
+ }
268
+ }
269
+
270
+ if (language === 'all') {
271
+ const webTasks = tasks.filter((t) => t.appTag === 'WEB');
272
+ if (webTasks.length === 0) {
273
+ warnings.push('No [WEB] website tasks found in plan');
274
+ }
275
+ }
276
+
277
+ return {
278
+ valid: issues.length === 0,
279
+ tasks,
280
+ issues,
281
+ warnings,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Get app-specific tasks from a parsed plan
287
+ *
288
+ * @param tasks - Array of parsed tasks
289
+ * @param appTarget - The app target to filter by
290
+ * @returns Filtered tasks
291
+ */
292
+ export function getTasksByApp(tasks: ParsedTask[], appTarget: AppTarget): ParsedTask[] {
293
+ return tasks.filter((t) => t.appTarget === appTarget);
294
+ }
295
+
296
+ /**
297
+ * Get task counts by app from a parsed plan
298
+ *
299
+ * @param tasks - Array of parsed tasks
300
+ * @returns Count of tasks per app
301
+ */
302
+ export function getTaskCountsByApp(tasks: ParsedTask[]): Record<AppTarget, number> {
303
+ const counts: Record<AppTarget, number> = {
304
+ frontend: 0,
305
+ backend: 0,
306
+ website: 0,
307
+ unified: 0,
308
+ };
309
+
310
+ for (const task of tasks) {
311
+ if (task.appTarget) {
312
+ counts[task.appTarget]++;
313
+ }
314
+ }
315
+
316
+ return counts;
317
+ }