popeye-cli 2.0.0 → 2.2.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 (161) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/CONTRIBUTING.md +23 -2
  3. package/README.md +47 -18
  4. package/dist/adapters/gemini.js +3 -3
  5. package/dist/adapters/openai.js +2 -2
  6. package/dist/adapters/openai.js.map +1 -1
  7. package/dist/auth/gemini.js +1 -1
  8. package/dist/cli/commands/create.d.ts.map +1 -1
  9. package/dist/cli/commands/create.js +11 -5
  10. package/dist/cli/commands/create.js.map +1 -1
  11. package/dist/cli/commands/resume.d.ts.map +1 -1
  12. package/dist/cli/commands/resume.js +9 -1
  13. package/dist/cli/commands/resume.js.map +1 -1
  14. package/dist/cli/interactive.d.ts.map +1 -1
  15. package/dist/cli/interactive.js +33 -4
  16. package/dist/cli/interactive.js.map +1 -1
  17. package/dist/config/defaults.d.ts.map +1 -1
  18. package/dist/config/defaults.js +7 -2
  19. package/dist/config/defaults.js.map +1 -1
  20. package/dist/config/index.d.ts +1 -7
  21. package/dist/config/index.d.ts.map +1 -1
  22. package/dist/config/popeye-md.d.ts +32 -0
  23. package/dist/config/popeye-md.d.ts.map +1 -0
  24. package/dist/config/popeye-md.js +111 -0
  25. package/dist/config/popeye-md.js.map +1 -0
  26. package/dist/config/schema.d.ts +3 -21
  27. package/dist/config/schema.d.ts.map +1 -1
  28. package/dist/config/schema.js +21 -8
  29. package/dist/config/schema.js.map +1 -1
  30. package/dist/generators/all.d.ts.map +1 -1
  31. package/dist/generators/all.js +23 -1
  32. package/dist/generators/all.js.map +1 -1
  33. package/dist/pipeline/artifact-manager.d.ts.map +1 -1
  34. package/dist/pipeline/artifact-manager.js +3 -0
  35. package/dist/pipeline/artifact-manager.js.map +1 -1
  36. package/dist/pipeline/bridges/review-bridge.d.ts +70 -0
  37. package/dist/pipeline/bridges/review-bridge.d.ts.map +1 -0
  38. package/dist/pipeline/bridges/review-bridge.js +266 -0
  39. package/dist/pipeline/bridges/review-bridge.js.map +1 -0
  40. package/dist/pipeline/consensus/consensus-runner.js +3 -3
  41. package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
  42. package/dist/pipeline/gate-engine.js +1 -1
  43. package/dist/pipeline/gate-engine.js.map +1 -1
  44. package/dist/pipeline/migration.d.ts.map +1 -1
  45. package/dist/pipeline/migration.js +3 -26
  46. package/dist/pipeline/migration.js.map +1 -1
  47. package/dist/pipeline/orchestrator.d.ts +2 -0
  48. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  49. package/dist/pipeline/orchestrator.js +10 -1
  50. package/dist/pipeline/orchestrator.js.map +1 -1
  51. package/dist/pipeline/phases/implementation.d.ts.map +1 -1
  52. package/dist/pipeline/phases/implementation.js +5 -2
  53. package/dist/pipeline/phases/implementation.js.map +1 -1
  54. package/dist/pipeline/phases/intake.d.ts +1 -0
  55. package/dist/pipeline/phases/intake.d.ts.map +1 -1
  56. package/dist/pipeline/phases/intake.js +56 -8
  57. package/dist/pipeline/phases/intake.js.map +1 -1
  58. package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
  59. package/dist/pipeline/phases/recovery-loop.js +2 -0
  60. package/dist/pipeline/phases/recovery-loop.js.map +1 -1
  61. package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
  62. package/dist/pipeline/phases/role-planning.js +2 -3
  63. package/dist/pipeline/phases/role-planning.js.map +1 -1
  64. package/dist/pipeline/skills/constitution-generator.d.ts +51 -0
  65. package/dist/pipeline/skills/constitution-generator.d.ts.map +1 -0
  66. package/dist/pipeline/skills/constitution-generator.js +210 -0
  67. package/dist/pipeline/skills/constitution-generator.js.map +1 -0
  68. package/dist/pipeline/skills/generator.d.ts +65 -0
  69. package/dist/pipeline/skills/generator.d.ts.map +1 -0
  70. package/dist/pipeline/skills/generator.js +221 -0
  71. package/dist/pipeline/skills/generator.js.map +1 -0
  72. package/dist/pipeline/skills/role-map.d.ts +38 -0
  73. package/dist/pipeline/skills/role-map.d.ts.map +1 -0
  74. package/dist/pipeline/skills/role-map.js +234 -0
  75. package/dist/pipeline/skills/role-map.js.map +1 -0
  76. package/dist/pipeline/skills/types.d.ts +47 -0
  77. package/dist/pipeline/skills/types.d.ts.map +1 -0
  78. package/dist/pipeline/skills/types.js +5 -0
  79. package/dist/pipeline/skills/types.js.map +1 -0
  80. package/dist/pipeline/type-defs/artifacts.d.ts +10 -0
  81. package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
  82. package/dist/pipeline/type-defs/artifacts.js +2 -0
  83. package/dist/pipeline/type-defs/artifacts.js.map +1 -1
  84. package/dist/pipeline/type-defs/audit.d.ts +6 -0
  85. package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
  86. package/dist/pipeline/type-defs/checks.d.ts +2 -0
  87. package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
  88. package/dist/pipeline/type-defs/packets.d.ts +30 -0
  89. package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
  90. package/dist/pipeline/type-defs/state.d.ts +11 -0
  91. package/dist/pipeline/type-defs/state.d.ts.map +1 -1
  92. package/dist/pipeline/type-defs/state.js +2 -0
  93. package/dist/pipeline/type-defs/state.js.map +1 -1
  94. package/dist/types/consensus.d.ts +5 -1
  95. package/dist/types/consensus.d.ts.map +1 -1
  96. package/dist/types/consensus.js +15 -4
  97. package/dist/types/consensus.js.map +1 -1
  98. package/dist/types/index.d.ts +1 -1
  99. package/dist/types/index.d.ts.map +1 -1
  100. package/dist/types/index.js +1 -1
  101. package/dist/types/index.js.map +1 -1
  102. package/dist/types/project.d.ts +1 -1
  103. package/dist/types/project.d.ts.map +1 -1
  104. package/dist/types/project.js +39 -10
  105. package/dist/types/project.js.map +1 -1
  106. package/dist/types/workflow.d.ts +1 -7
  107. package/dist/types/workflow.d.ts.map +1 -1
  108. package/dist/types/workflow.js +1 -1
  109. package/dist/types/workflow.js.map +1 -1
  110. package/dist/upgrade/handlers.js +5 -5
  111. package/dist/upgrade/handlers.js.map +1 -1
  112. package/dist/workflow/index.d.ts.map +1 -1
  113. package/dist/workflow/index.js +18 -14
  114. package/dist/workflow/index.js.map +1 -1
  115. package/dist/workflow/website-strategy.js +1 -1
  116. package/dist/workflow/website-strategy.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/adapters/gemini.ts +3 -3
  119. package/src/adapters/openai.ts +2 -2
  120. package/src/auth/gemini.ts +1 -1
  121. package/src/cli/commands/create.ts +12 -6
  122. package/src/cli/commands/resume.ts +9 -1
  123. package/src/cli/interactive.ts +36 -4
  124. package/src/config/defaults.ts +7 -2
  125. package/src/config/popeye-md.ts +139 -0
  126. package/src/config/schema.ts +21 -8
  127. package/src/generators/all.ts +23 -1
  128. package/src/pipeline/artifact-manager.ts +3 -0
  129. package/src/pipeline/bridges/review-bridge.ts +371 -0
  130. package/src/pipeline/consensus/consensus-runner.ts +3 -3
  131. package/src/pipeline/gate-engine.ts +1 -1
  132. package/src/pipeline/migration.ts +5 -30
  133. package/src/pipeline/orchestrator.ts +14 -0
  134. package/src/pipeline/phases/implementation.ts +6 -2
  135. package/src/pipeline/phases/intake.ts +73 -10
  136. package/src/pipeline/phases/recovery-loop.ts +2 -0
  137. package/src/pipeline/phases/role-planning.ts +2 -3
  138. package/src/pipeline/skills/constitution-generator.ts +236 -0
  139. package/src/pipeline/skills/generator.ts +287 -0
  140. package/src/pipeline/skills/role-map.ts +248 -0
  141. package/src/pipeline/skills/types.ts +53 -0
  142. package/src/pipeline/type-defs/artifacts.ts +2 -0
  143. package/src/pipeline/type-defs/state.ts +2 -0
  144. package/src/types/consensus.ts +16 -4
  145. package/src/types/index.ts +1 -0
  146. package/src/types/project.ts +39 -10
  147. package/src/types/workflow.ts +1 -1
  148. package/src/upgrade/handlers.ts +5 -5
  149. package/src/workflow/index.ts +18 -14
  150. package/src/workflow/website-strategy.ts +1 -1
  151. package/tests/cli/model-command.test.ts +19 -9
  152. package/tests/config/config.test.ts +3 -3
  153. package/tests/config/popeye-md.test.ts +168 -0
  154. package/tests/pipeline/bridges/review-bridge.test.ts +243 -0
  155. package/tests/pipeline/migration.test.ts +4 -3
  156. package/tests/pipeline/session-guidance.test.ts +205 -0
  157. package/tests/pipeline/skills/constitution-generator.test.ts +201 -0
  158. package/tests/pipeline/skills/generator.test.ts +213 -0
  159. package/tests/pipeline/skills/role-map.test.ts +198 -0
  160. package/tests/types/consensus.test.ts +1 -1
  161. package/tests/workflow/pipeline-bootstrap.test.ts +162 -0
@@ -3,9 +3,11 @@
3
3
  * Auto-triggered on load when pipelinePhase is missing from state.
4
4
  */
5
5
 
6
- import type { PipelinePhase, PipelineState, PipelineRole } from './types.js';
6
+ import type { PipelinePhase, PipelineState } from './types.js';
7
7
  import { createDefaultPipelineState } from './types.js';
8
8
  import type { ProjectState, WorkflowPhase } from '../types/workflow.js';
9
+ import { getActiveRoles } from './skills/role-map.js';
10
+ import type { OutputLanguage } from '../types/project.js';
9
11
 
10
12
  // ─── Phase Mapping ───────────────────────────────────────
11
13
 
@@ -52,8 +54,8 @@ export function migrateToPipelineState(state: ProjectState): PipelineState {
52
54
  // Map legacy phase
53
55
  pipeline.pipelinePhase = toPipelinePhase(state.phase);
54
56
 
55
- // Derive active roles from language
56
- pipeline.activeRoles = deriveActiveRoles(state.language);
57
+ // Derive active roles from language using shared role-map
58
+ pipeline.activeRoles = getActiveRoles(state.language as OutputLanguage);
57
59
 
58
60
  return pipeline;
59
61
  }
@@ -62,30 +64,3 @@ export function migrateToPipelineState(state: ProjectState): PipelineState {
62
64
  export function needsPipelineMigration(state: unknown): boolean {
63
65
  return !(state as Record<string, unknown>).pipeline;
64
66
  }
65
-
66
- // ─── Role Derivation ─────────────────────────────────────
67
-
68
- function deriveActiveRoles(language: string): PipelineRole[] {
69
- const baseRoles: PipelineRole[] = [
70
- 'DISPATCHER', 'ARCHITECT', 'REVIEWER', 'ARBITRATOR',
71
- 'DEBUGGER', 'AUDITOR', 'JOURNALIST', 'RELEASE_MANAGER',
72
- 'QA_TESTER',
73
- ];
74
-
75
- switch (language) {
76
- case 'fullstack':
77
- case 'all':
78
- return [
79
- ...baseRoles,
80
- 'DB_EXPERT', 'BACKEND_PROGRAMMER', 'FRONTEND_PROGRAMMER',
81
- 'WEBSITE_PROGRAMMER', 'UI_UX_SPECIALIST',
82
- ];
83
- case 'python':
84
- case 'typescript':
85
- return [...baseRoles, 'BACKEND_PROGRAMMER'];
86
- case 'website':
87
- return [...baseRoles, 'WEBSITE_PROGRAMMER', 'MARKETING_EXPERT', 'SOCIAL_EXPERT'];
88
- default:
89
- return [...baseRoles, 'BACKEND_PROGRAMMER'];
90
- }
91
- }
@@ -53,6 +53,8 @@ export interface PipelineOptions {
53
53
  projectDir: string;
54
54
  state: ProjectState;
55
55
  consensusConfig?: Partial<ConsensusConfig>;
56
+ /** User steering, upgrade context, or resume instructions */
57
+ additionalContext?: string;
56
58
  onPhaseStart?: (phase: PipelinePhase) => void;
57
59
  onPhaseComplete?: (phase: PipelinePhase, result: PhaseResult) => void;
58
60
  onProgress?: (message: string) => void;
@@ -88,6 +90,7 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
88
90
  projectDir,
89
91
  state,
90
92
  consensusConfig,
93
+ additionalContext,
91
94
  onPhaseStart,
92
95
  onPhaseComplete,
93
96
  onProgress,
@@ -97,6 +100,11 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
97
100
  const pipeline: PipelineState = (state as unknown as { pipeline?: PipelineState }).pipeline
98
101
  ?? createDefaultPipelineState();
99
102
 
103
+ // Persist user guidance in pipeline state so it survives resume
104
+ if (additionalContext && !pipeline.sessionGuidance) {
105
+ pipeline.sessionGuidance = additionalContext;
106
+ }
107
+
100
108
  // Create context dependencies
101
109
  const gateEngine = createGateEngine();
102
110
  const artifactManager = createArtifactManager(projectDir);
@@ -151,6 +159,11 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
151
159
 
152
160
  onPhaseComplete?.(phase, result);
153
161
 
162
+ // Log phase outcome — critical for diagnosing pipeline loops
163
+ if (!result.success) {
164
+ onProgress?.(`Phase ${phase} FAILED: ${result.message}${result.error ? ` — ${result.error}` : ''}`);
165
+ }
166
+
154
167
  // v1.1: Verify constitution integrity before evaluating gate
155
168
  const constitutionCheck = verifyConstitution(pipeline, projectDir);
156
169
 
@@ -193,6 +206,7 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
193
206
  }
194
207
  } else {
195
208
  // ─── FAIL ────────────────────────────────────────
209
+ onProgress?.(`Gate FAILED for ${phase}: ${gateResult.blockers.join('; ')}`);
196
210
  if (pipeline.recoveryCount >= pipeline.maxRecoveryIterations) {
197
211
  phase = 'STUCK';
198
212
  } else {
@@ -26,11 +26,15 @@ export async function runImplementation(context: PhaseContext): Promise<PhaseRes
26
26
  .join('\n\n');
27
27
  }
28
28
 
29
- // Run existing execution mode with optional role context
29
+ // Merge session guidance with role prompt so execution sees user intent
30
+ const guidance = pipeline.sessionGuidance;
31
+ const systemPrompt = [combinedRolePrompt, guidance].filter(Boolean).join('\n\n') || undefined;
32
+
33
+ // Run existing execution mode with optional role context + guidance
30
34
  const { runExecutionMode } = await import('../../workflow/execution-mode.js');
31
35
  await runExecutionMode({
32
36
  projectDir,
33
- ...(combinedRolePrompt ? { systemPrompt: combinedRolePrompt } : {}),
37
+ ...(systemPrompt ? { systemPrompt } : {}),
34
38
  });
35
39
 
36
40
  // Generate post-implementation repo snapshot
@@ -2,12 +2,19 @@
2
2
  * INTAKE phase — normalize user prompt into structured Master Plan v1.
3
3
  * Reuses expandIdea() and createPlan() from workflow.
4
4
  * v1.1: Creates constitution artifact and stores hash.
5
+ * v1.2: Generates project-specific skills and constitution.
5
6
  */
6
7
 
8
+ import { join } from 'node:path';
9
+
7
10
  import type { PhaseContext, PhaseResult } from './phase-context.js';
8
11
  import { successResult, failureResult } from './phase-context.js';
9
12
  import { generateRepoSnapshot, createSnapshotArtifact } from '../repo-snapshot.js';
10
13
  import { createConstitutionArtifact, computeConstitutionHash } from '../constitution.js';
14
+ import { getActiveRoles, inferTechStack } from '../skills/role-map.js';
15
+ import { generateProjectSkills } from '../skills/generator.js';
16
+ import { generateConstitution } from '../skills/constitution-generator.js';
17
+ import type { OutputLanguage } from '../../types/project.js';
11
18
 
12
19
  export async function runIntake(context: PhaseContext): Promise<PhaseResult> {
13
20
  const { projectDir, pipeline, artifactManager } = context;
@@ -20,31 +27,87 @@ export async function runIntake(context: PhaseContext): Promise<PhaseResult> {
20
27
  artifacts.push(snapshotEntry);
21
28
  pipeline.latestRepoSnapshot = artifactManager.toArtifactRef(snapshotEntry);
22
29
 
23
- // 2. Create constitution artifact and store hash
24
- const constitutionEntry = createConstitutionArtifact(projectDir, artifactManager);
25
- if (constitutionEntry) {
26
- artifacts.push(constitutionEntry);
30
+ // 2. Store additional_context artifact if session guidance provided
31
+ const guidance = pipeline.sessionGuidance ?? '';
32
+ if (guidance) {
33
+ const ctxEntry = artifactManager.createAndStoreText(
34
+ 'additional_context',
35
+ guidance,
36
+ 'INTAKE',
37
+ );
38
+ artifacts.push(ctxEntry);
27
39
  }
28
- pipeline.constitutionHash = computeConstitutionHash(projectDir);
29
40
 
30
- // 3. Expand idea using existing workflow
41
+ // 3. Push pre-AI artifacts to pipeline state now (survives if AI calls fail below)
42
+ pipeline.artifacts.push(...artifacts);
43
+
44
+ // 4. Expand idea using existing workflow
31
45
  const { expandIdea, createPlan } = await import('../../workflow/plan-mode.js');
32
46
  const expandedIdea = await expandIdea(
33
47
  context.state.specification ?? context.state.idea ?? '',
34
48
  context.state.language,
35
49
  );
36
50
 
37
- // 4. Create master plan using existing workflow
38
- const plan = await createPlan(expandedIdea, '', context.state.language);
51
+ // 5. Determine active roles
52
+ const language = context.state.language as OutputLanguage;
53
+ pipeline.activeRoles = getActiveRoles(language);
54
+
55
+ // 6-8. Generate project-specific skills and constitution (non-fatal)
56
+ const skillsDir = join(projectDir, 'skills');
57
+ try {
58
+ const projectName = context.state.name ?? 'Project';
59
+
60
+ await generateProjectSkills(
61
+ {
62
+ language,
63
+ expandedSpec: expandedIdea,
64
+ snapshot,
65
+ sessionGuidance: guidance || undefined,
66
+ activeRoles: pipeline.activeRoles,
67
+ skillsDir,
68
+ projectName,
69
+ },
70
+ artifactManager,
71
+ );
72
+
73
+ const techStack = inferTechStack(language, snapshot, expandedIdea);
74
+ generateConstitution({
75
+ language,
76
+ projectName,
77
+ techStack,
78
+ expandedSpec: expandedIdea,
79
+ sessionGuidance: guidance || undefined,
80
+ skillsDir,
81
+ });
39
82
 
40
- // 5. Store master plan as artifact
83
+ // Clear skill loader cache so it picks up new .md files
84
+ context.skillLoader.clearCache();
85
+ } catch {
86
+ // Skill/constitution generation is non-fatal — pipeline continues with defaults
87
+ }
88
+
89
+ // 9. Create constitution artifact and store hash (AFTER generation)
90
+ const constitutionEntry = createConstitutionArtifact(projectDir, artifactManager);
91
+ if (constitutionEntry) {
92
+ artifacts.push(constitutionEntry);
93
+ pipeline.artifacts.push(constitutionEntry);
94
+ }
95
+ pipeline.constitutionHash = computeConstitutionHash(projectDir);
96
+
97
+ // 10. Create master plan — prepend guidance so planner sees constraints first
98
+ const planInput = guidance
99
+ ? `${guidance}\n\n---\n\n${expandedIdea}`
100
+ : expandedIdea;
101
+ const plan = await createPlan(planInput, '', context.state.language);
102
+
103
+ // 11. Store master plan as artifact
41
104
  const planEntry = artifactManager.createAndStoreText(
42
105
  'master_plan',
43
106
  plan,
44
107
  'INTAKE',
45
108
  );
46
109
  artifacts.push(planEntry);
47
- pipeline.artifacts.push(...artifacts);
110
+ pipeline.artifacts.push(planEntry);
48
111
 
49
112
  return successResult('INTAKE', artifacts, 'Master Plan v1 created');
50
113
  } catch (err) {
@@ -33,9 +33,11 @@ export async function runRecoveryLoop(context: PhaseContext): Promise<PhaseResul
33
33
 
34
34
  // 3. Generate RCA via Claude with Debugger skill
35
35
  const { executePrompt } = await import('../../adapters/claude.js');
36
+ const guidance = pipeline.sessionGuidance;
36
37
  const rcaPrompt = [
37
38
  debuggerSkill.systemPrompt,
38
39
  '',
40
+ ...(guidance ? ['## User Guidance', guidance, ''] : []),
39
41
  '## Failure Evidence',
40
42
  failureEvidence,
41
43
  '',
@@ -46,10 +46,9 @@ export async function runRolePlanning(context: PhaseContext): Promise<PhaseResul
46
46
 
47
47
  const { executePrompt } = await import('../../adapters/claude.js');
48
48
 
49
- // Generate plan for each role
49
+ // Generate plan for each role (skip roles not in activeRoles)
50
50
  for (const role of PLANNING_ROLES) {
51
- // Skip website if not in active roles
52
- if (role === 'WEBSITE_PROGRAMMER' && !pipeline.activeRoles.includes('WEBSITE_PROGRAMMER')) {
51
+ if (!pipeline.activeRoles.includes(role)) {
53
52
  continue;
54
53
  }
55
54
 
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Deterministic constitution generation — no AI call required.
3
+ * Produces skills/POPEYE_CONSTITUTION.md from templates + inferred tech stack.
4
+ * Includes pipeline governance invariants that never change.
5
+ */
6
+
7
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ import type { OutputLanguage } from '../../types/project.js';
11
+ import type { ConstitutionContext, TechStack } from './types.js';
12
+
13
+ // ─── Constants ──────────────────────────────────────────
14
+
15
+ const CONSTITUTION_FILENAME = 'POPEYE_CONSTITUTION.md';
16
+ const PIPELINE_VERSION = '1.0';
17
+
18
+ // ─── Public API ─────────────────────────────────────────
19
+
20
+ /**
21
+ * Generate the project constitution file if it doesn't already exist.
22
+ * Entirely deterministic — built from templates and tech stack data.
23
+ *
24
+ * @param context - Constitution generation context
25
+ */
26
+ export function generateConstitution(context: ConstitutionContext): void {
27
+ const { skillsDir } = context;
28
+
29
+ if (shouldSkipConstitution(skillsDir)) {
30
+ return;
31
+ }
32
+
33
+ if (!existsSync(skillsDir)) {
34
+ mkdirSync(skillsDir, { recursive: true });
35
+ }
36
+
37
+ const content = buildConstitutionContent(context);
38
+ const constitutionPath = join(skillsDir, CONSTITUTION_FILENAME);
39
+ writeFileSync(constitutionPath, content, 'utf-8');
40
+ }
41
+
42
+ /**
43
+ * Check if constitution generation should be skipped.
44
+ * Returns true if the file already exists (hand-written or prior run).
45
+ *
46
+ * @param skillsDir - Path to the skills directory
47
+ * @returns true if generation should be skipped
48
+ */
49
+ export function shouldSkipConstitution(skillsDir: string): boolean {
50
+ const constitutionPath = join(skillsDir, CONSTITUTION_FILENAME);
51
+ return existsSync(constitutionPath);
52
+ }
53
+
54
+ // ─── Content Assembly ───────────────────────────────────
55
+
56
+ /**
57
+ * Build the full constitution markdown content.
58
+ *
59
+ * @param context - Constitution generation context
60
+ * @returns Complete markdown string
61
+ */
62
+ function buildConstitutionContent(context: ConstitutionContext): string {
63
+ const { projectName, language, techStack, sessionGuidance } = context;
64
+ const date = new Date().toISOString().split('T')[0];
65
+
66
+ const sections = [
67
+ `# Project Constitution: ${projectName}`,
68
+ '',
69
+ `Generated: ${date} | Language: ${language} | Pipeline: v${PIPELINE_VERSION}`,
70
+ '',
71
+ getTechStackSection(techStack),
72
+ getArchitectureRules(techStack),
73
+ getCodeQualityRules(),
74
+ getGovernanceRules(),
75
+ getConstraintsSection(language, sessionGuidance),
76
+ getImmutabilitySection(),
77
+ ];
78
+
79
+ return sections.join('\n');
80
+ }
81
+
82
+ // ─── Template Sections ──────────────────────────────────
83
+
84
+ /**
85
+ * Generate the tech stack section from inferred stack data.
86
+ *
87
+ * @param techStack - Inferred tech stack
88
+ * @returns Markdown section
89
+ */
90
+ export function getTechStackSection(techStack: TechStack): string {
91
+ const lines = ['## Tech Stack'];
92
+ if (techStack.language) lines.push(`- Language: ${techStack.language}`);
93
+ if (techStack.backend) lines.push(`- Framework: ${techStack.backend}`);
94
+ if (techStack.frontend) lines.push(`- Frontend: ${techStack.frontend}`);
95
+ if (techStack.database) lines.push(`- Database: ${techStack.database}`);
96
+ if (techStack.orm) lines.push(`- ORM: ${techStack.orm}`);
97
+ if (techStack.testing) lines.push(`- Testing: ${techStack.testing}`);
98
+ lines.push('');
99
+ return lines.join('\n');
100
+ }
101
+
102
+ /**
103
+ * Generate architecture rules based on the tech stack.
104
+ *
105
+ * @param techStack - Inferred tech stack
106
+ * @returns Markdown section
107
+ */
108
+ export function getArchitectureRules(techStack: TechStack): string {
109
+ const rules: string[] = [];
110
+ let ruleNum = 1;
111
+
112
+ if (techStack.backend?.includes('FastAPI')) {
113
+ rules.push(`${ruleNum++}. All API endpoints MUST use async/await`);
114
+ }
115
+ if (techStack.backend?.includes('Express')) {
116
+ rules.push(`${ruleNum++}. Use Express middleware pattern for cross-cutting concerns`);
117
+ }
118
+ if (techStack.backend?.includes('Django')) {
119
+ rules.push(`${ruleNum++}. Follow Django app structure conventions`);
120
+ }
121
+ if (techStack.orm?.includes('SQLAlchemy')) {
122
+ rules.push(`${ruleNum++}. Database access exclusively via SQLAlchemy ORM`);
123
+ }
124
+ if (techStack.orm?.includes('Prisma')) {
125
+ rules.push(`${ruleNum++}. Database access exclusively via Prisma client`);
126
+ }
127
+ if (techStack.language?.includes('Python')) {
128
+ rules.push(`${ruleNum++}. Environment variables via python-dotenv, never hardcoded`);
129
+ rules.push(`${ruleNum++}. PEP8 style with type hints on all functions`);
130
+ }
131
+ if (techStack.language?.includes('TypeScript')) {
132
+ rules.push(`${ruleNum++}. TypeScript strict mode, no implicit any`);
133
+ rules.push(`${ruleNum++}. Environment variables via dotenv, never hardcoded`);
134
+ }
135
+ if (techStack.frontend?.includes('React')) {
136
+ rules.push(`${ruleNum++}. React components use functional patterns with hooks`);
137
+ }
138
+ if (techStack.frontend?.includes('Next')) {
139
+ rules.push(`${ruleNum++}. Next.js App Router conventions for routing and layouts`);
140
+ }
141
+
142
+ // Always add a generic rule if nothing specific matched
143
+ if (rules.length === 0) {
144
+ rules.push('1. Environment variables never hardcoded in source code');
145
+ rules.push('2. Clear separation of concerns between modules');
146
+ }
147
+
148
+ return ['## Architecture Rules', ...rules, ''].join('\n');
149
+ }
150
+
151
+ /**
152
+ * Generate code quality rules (constant across all projects).
153
+ *
154
+ * @returns Markdown section
155
+ */
156
+ export function getCodeQualityRules(): string {
157
+ return [
158
+ '## Code Quality',
159
+ '1. Maximum 500 lines per source file',
160
+ '2. Unit tests for every module (happy path + edge case + failure)',
161
+ '3. Standard logging (no unstructured print statements)',
162
+ '4. Docstrings/JSDoc on public functions',
163
+ '',
164
+ ].join('\n');
165
+ }
166
+
167
+ /**
168
+ * Generate governance rules (pipeline invariants, constant).
169
+ *
170
+ * @returns Markdown section
171
+ */
172
+ function getGovernanceRules(): string {
173
+ return [
174
+ '## Governance Rules',
175
+ '1. Consensus threshold: 0.95 with minimum 2 reviewers',
176
+ '2. All artifacts are immutable once stored (new versions create new files)',
177
+ '3. No placeholder content in production code or generated output',
178
+ '4. Gate failures route to RECOVERY_LOOP before phase retry',
179
+ '5. Constitution modifications during pipeline execution are forbidden',
180
+ '6. Change Requests required for scope changes after INTAKE',
181
+ '',
182
+ ].join('\n');
183
+ }
184
+
185
+ /**
186
+ * Generate language-specific and session-specific constraints.
187
+ *
188
+ * @param language - Project language
189
+ * @param sessionGuidance - Optional session guidance text
190
+ * @returns Markdown section
191
+ */
192
+ export function getConstraintsSection(
193
+ language: OutputLanguage,
194
+ sessionGuidance?: string,
195
+ ): string {
196
+ const lines = ['## Project Constraints'];
197
+
198
+ const langConstraints: Record<string, string[]> = {
199
+ python: ['- Python 3.11+ required', '- Use virtual environment (venv) for all operations'],
200
+ typescript: ['- Node.js 18+ required', '- ESM modules (import/export, .js extensions)'],
201
+ fullstack: [
202
+ '- Python 3.11+ for backend, Node.js 18+ for frontend',
203
+ '- Monorepo structure with clear app boundaries',
204
+ ],
205
+ website: ['- Node.js 18+ required', '- SSG/SSR optimization for performance and SEO'],
206
+ all: [
207
+ '- Python 3.11+ for backend, Node.js 18+ for frontend and website',
208
+ '- Monorepo structure with clear app boundaries',
209
+ ],
210
+ };
211
+
212
+ const constraints = langConstraints[language] ?? langConstraints.python;
213
+ lines.push(...constraints);
214
+
215
+ if (sessionGuidance) {
216
+ lines.push('', '### Session-Specific Guidance');
217
+ lines.push(sessionGuidance.slice(0, 500));
218
+ }
219
+
220
+ lines.push('');
221
+ return lines.join('\n');
222
+ }
223
+
224
+ /**
225
+ * Generate the immutability notice (constant).
226
+ *
227
+ * @returns Markdown section
228
+ */
229
+ function getImmutabilitySection(): string {
230
+ return [
231
+ '## Immutability',
232
+ 'This document MUST NOT be modified during pipeline execution.',
233
+ 'Any modification triggers constitution verification failure at next gate.',
234
+ '',
235
+ ].join('\n');
236
+ }