popeye-cli 1.10.0 → 2.0.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 (253) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CONTRIBUTING.md +15 -1
  3. package/README.md +57 -0
  4. package/dist/pipeline/artifact-manager.d.ts +47 -0
  5. package/dist/pipeline/artifact-manager.d.ts.map +1 -0
  6. package/dist/pipeline/artifact-manager.js +251 -0
  7. package/dist/pipeline/artifact-manager.js.map +1 -0
  8. package/dist/pipeline/artifact-validators.d.ts +29 -0
  9. package/dist/pipeline/artifact-validators.d.ts.map +1 -0
  10. package/dist/pipeline/artifact-validators.js +173 -0
  11. package/dist/pipeline/artifact-validators.js.map +1 -0
  12. package/dist/pipeline/change-request.d.ts +47 -0
  13. package/dist/pipeline/change-request.d.ts.map +1 -0
  14. package/dist/pipeline/change-request.js +91 -0
  15. package/dist/pipeline/change-request.js.map +1 -0
  16. package/dist/pipeline/check-runner.d.ts +47 -0
  17. package/dist/pipeline/check-runner.d.ts.map +1 -0
  18. package/dist/pipeline/check-runner.js +417 -0
  19. package/dist/pipeline/check-runner.js.map +1 -0
  20. package/dist/pipeline/command-resolver.d.ts +9 -0
  21. package/dist/pipeline/command-resolver.d.ts.map +1 -0
  22. package/dist/pipeline/command-resolver.js +140 -0
  23. package/dist/pipeline/command-resolver.js.map +1 -0
  24. package/dist/pipeline/consensus/consensus-runner.d.ts +44 -0
  25. package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -0
  26. package/dist/pipeline/consensus/consensus-runner.js +212 -0
  27. package/dist/pipeline/consensus/consensus-runner.js.map +1 -0
  28. package/dist/pipeline/constitution.d.ts +45 -0
  29. package/dist/pipeline/constitution.d.ts.map +1 -0
  30. package/dist/pipeline/constitution.js +82 -0
  31. package/dist/pipeline/constitution.js.map +1 -0
  32. package/dist/pipeline/gate-engine.d.ts +55 -0
  33. package/dist/pipeline/gate-engine.d.ts.map +1 -0
  34. package/dist/pipeline/gate-engine.js +270 -0
  35. package/dist/pipeline/gate-engine.js.map +1 -0
  36. package/dist/pipeline/index.d.ts +26 -0
  37. package/dist/pipeline/index.d.ts.map +1 -0
  38. package/dist/pipeline/index.js +35 -0
  39. package/dist/pipeline/index.js.map +1 -0
  40. package/dist/pipeline/migration.d.ts +15 -0
  41. package/dist/pipeline/migration.d.ts.map +1 -0
  42. package/dist/pipeline/migration.js +76 -0
  43. package/dist/pipeline/migration.js.map +1 -0
  44. package/dist/pipeline/orchestrator.d.ts +28 -0
  45. package/dist/pipeline/orchestrator.d.ts.map +1 -0
  46. package/dist/pipeline/orchestrator.js +238 -0
  47. package/dist/pipeline/orchestrator.js.map +1 -0
  48. package/dist/pipeline/packets/audit-report-builder.d.ts +11 -0
  49. package/dist/pipeline/packets/audit-report-builder.d.ts.map +1 -0
  50. package/dist/pipeline/packets/audit-report-builder.js +32 -0
  51. package/dist/pipeline/packets/audit-report-builder.js.map +1 -0
  52. package/dist/pipeline/packets/consensus-packet-builder.d.ts +35 -0
  53. package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -0
  54. package/dist/pipeline/packets/consensus-packet-builder.js +80 -0
  55. package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -0
  56. package/dist/pipeline/packets/index.d.ts +12 -0
  57. package/dist/pipeline/packets/index.d.ts.map +1 -0
  58. package/dist/pipeline/packets/index.js +8 -0
  59. package/dist/pipeline/packets/index.js.map +1 -0
  60. package/dist/pipeline/packets/plan-packet-builder.d.ts +21 -0
  61. package/dist/pipeline/packets/plan-packet-builder.d.ts.map +1 -0
  62. package/dist/pipeline/packets/plan-packet-builder.js +27 -0
  63. package/dist/pipeline/packets/plan-packet-builder.js.map +1 -0
  64. package/dist/pipeline/packets/rca-packet-builder.d.ts +19 -0
  65. package/dist/pipeline/packets/rca-packet-builder.d.ts.map +1 -0
  66. package/dist/pipeline/packets/rca-packet-builder.js +22 -0
  67. package/dist/pipeline/packets/rca-packet-builder.js.map +1 -0
  68. package/dist/pipeline/phases/architecture.d.ts +7 -0
  69. package/dist/pipeline/phases/architecture.d.ts.map +1 -0
  70. package/dist/pipeline/phases/architecture.js +60 -0
  71. package/dist/pipeline/phases/architecture.js.map +1 -0
  72. package/dist/pipeline/phases/audit.d.ts +8 -0
  73. package/dist/pipeline/phases/audit.d.ts.map +1 -0
  74. package/dist/pipeline/phases/audit.js +144 -0
  75. package/dist/pipeline/phases/audit.js.map +1 -0
  76. package/dist/pipeline/phases/consensus-architecture.d.ts +7 -0
  77. package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -0
  78. package/dist/pipeline/phases/consensus-architecture.js +84 -0
  79. package/dist/pipeline/phases/consensus-architecture.js.map +1 -0
  80. package/dist/pipeline/phases/consensus-master-plan.d.ts +7 -0
  81. package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -0
  82. package/dist/pipeline/phases/consensus-master-plan.js +81 -0
  83. package/dist/pipeline/phases/consensus-master-plan.js.map +1 -0
  84. package/dist/pipeline/phases/consensus-role-plans.d.ts +7 -0
  85. package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -0
  86. package/dist/pipeline/phases/consensus-role-plans.js +85 -0
  87. package/dist/pipeline/phases/consensus-role-plans.js.map +1 -0
  88. package/dist/pipeline/phases/done.d.ts +7 -0
  89. package/dist/pipeline/phases/done.d.ts.map +1 -0
  90. package/dist/pipeline/phases/done.js +45 -0
  91. package/dist/pipeline/phases/done.js.map +1 -0
  92. package/dist/pipeline/phases/implementation.d.ts +8 -0
  93. package/dist/pipeline/phases/implementation.d.ts.map +1 -0
  94. package/dist/pipeline/phases/implementation.js +42 -0
  95. package/dist/pipeline/phases/implementation.js.map +1 -0
  96. package/dist/pipeline/phases/index.d.ts +20 -0
  97. package/dist/pipeline/phases/index.d.ts.map +1 -0
  98. package/dist/pipeline/phases/index.js +19 -0
  99. package/dist/pipeline/phases/index.js.map +1 -0
  100. package/dist/pipeline/phases/intake.d.ts +8 -0
  101. package/dist/pipeline/phases/intake.d.ts.map +1 -0
  102. package/dist/pipeline/phases/intake.js +40 -0
  103. package/dist/pipeline/phases/intake.js.map +1 -0
  104. package/dist/pipeline/phases/phase-context.d.ts +30 -0
  105. package/dist/pipeline/phases/phase-context.d.ts.map +1 -0
  106. package/dist/pipeline/phases/phase-context.js +33 -0
  107. package/dist/pipeline/phases/phase-context.js.map +1 -0
  108. package/dist/pipeline/phases/production-gate.d.ts +8 -0
  109. package/dist/pipeline/phases/production-gate.d.ts.map +1 -0
  110. package/dist/pipeline/phases/production-gate.js +84 -0
  111. package/dist/pipeline/phases/production-gate.js.map +1 -0
  112. package/dist/pipeline/phases/qa-validation.d.ts +7 -0
  113. package/dist/pipeline/phases/qa-validation.d.ts.map +1 -0
  114. package/dist/pipeline/phases/qa-validation.js +50 -0
  115. package/dist/pipeline/phases/qa-validation.js.map +1 -0
  116. package/dist/pipeline/phases/recovery-loop.d.ts +7 -0
  117. package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -0
  118. package/dist/pipeline/phases/recovery-loop.js +91 -0
  119. package/dist/pipeline/phases/recovery-loop.js.map +1 -0
  120. package/dist/pipeline/phases/review.d.ts +8 -0
  121. package/dist/pipeline/phases/review.d.ts.map +1 -0
  122. package/dist/pipeline/phases/review.js +127 -0
  123. package/dist/pipeline/phases/review.js.map +1 -0
  124. package/dist/pipeline/phases/role-planning.d.ts +7 -0
  125. package/dist/pipeline/phases/role-planning.d.ts.map +1 -0
  126. package/dist/pipeline/phases/role-planning.js +75 -0
  127. package/dist/pipeline/phases/role-planning.js.map +1 -0
  128. package/dist/pipeline/phases/stuck.d.ts +7 -0
  129. package/dist/pipeline/phases/stuck.d.ts.map +1 -0
  130. package/dist/pipeline/phases/stuck.js +51 -0
  131. package/dist/pipeline/phases/stuck.js.map +1 -0
  132. package/dist/pipeline/repo-snapshot.d.ts +24 -0
  133. package/dist/pipeline/repo-snapshot.d.ts.map +1 -0
  134. package/dist/pipeline/repo-snapshot.js +343 -0
  135. package/dist/pipeline/repo-snapshot.js.map +1 -0
  136. package/dist/pipeline/role-execution-adapter.d.ts +59 -0
  137. package/dist/pipeline/role-execution-adapter.d.ts.map +1 -0
  138. package/dist/pipeline/role-execution-adapter.js +159 -0
  139. package/dist/pipeline/role-execution-adapter.js.map +1 -0
  140. package/dist/pipeline/skill-loader.d.ts +34 -0
  141. package/dist/pipeline/skill-loader.d.ts.map +1 -0
  142. package/dist/pipeline/skill-loader.js +156 -0
  143. package/dist/pipeline/skill-loader.js.map +1 -0
  144. package/dist/pipeline/skills/defaults.d.ts +16 -0
  145. package/dist/pipeline/skills/defaults.d.ts.map +1 -0
  146. package/dist/pipeline/skills/defaults.js +189 -0
  147. package/dist/pipeline/skills/defaults.js.map +1 -0
  148. package/dist/pipeline/type-defs/artifacts.d.ts +202 -0
  149. package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -0
  150. package/dist/pipeline/type-defs/artifacts.js +66 -0
  151. package/dist/pipeline/type-defs/artifacts.js.map +1 -0
  152. package/dist/pipeline/type-defs/audit.d.ts +256 -0
  153. package/dist/pipeline/type-defs/audit.d.ts.map +1 -0
  154. package/dist/pipeline/type-defs/audit.js +54 -0
  155. package/dist/pipeline/type-defs/audit.js.map +1 -0
  156. package/dist/pipeline/type-defs/checks.d.ts +81 -0
  157. package/dist/pipeline/type-defs/checks.d.ts.map +1 -0
  158. package/dist/pipeline/type-defs/checks.js +38 -0
  159. package/dist/pipeline/type-defs/checks.js.map +1 -0
  160. package/dist/pipeline/type-defs/enums.d.ts +43 -0
  161. package/dist/pipeline/type-defs/enums.d.ts.map +1 -0
  162. package/dist/pipeline/type-defs/enums.js +55 -0
  163. package/dist/pipeline/type-defs/enums.js.map +1 -0
  164. package/dist/pipeline/type-defs/index.d.ts +12 -0
  165. package/dist/pipeline/type-defs/index.d.ts.map +1 -0
  166. package/dist/pipeline/type-defs/index.js +12 -0
  167. package/dist/pipeline/type-defs/index.js.map +1 -0
  168. package/dist/pipeline/type-defs/packets.d.ts +806 -0
  169. package/dist/pipeline/type-defs/packets.d.ts.map +1 -0
  170. package/dist/pipeline/type-defs/packets.js +109 -0
  171. package/dist/pipeline/type-defs/packets.js.map +1 -0
  172. package/dist/pipeline/type-defs/snapshot.d.ts +52 -0
  173. package/dist/pipeline/type-defs/snapshot.d.ts.map +1 -0
  174. package/dist/pipeline/type-defs/snapshot.js +35 -0
  175. package/dist/pipeline/type-defs/snapshot.js.map +1 -0
  176. package/dist/pipeline/type-defs/state.d.ts +449 -0
  177. package/dist/pipeline/type-defs/state.d.ts.map +1 -0
  178. package/dist/pipeline/type-defs/state.js +88 -0
  179. package/dist/pipeline/type-defs/state.js.map +1 -0
  180. package/dist/pipeline/types.d.ts +16 -0
  181. package/dist/pipeline/types.d.ts.map +1 -0
  182. package/dist/pipeline/types.js +16 -0
  183. package/dist/pipeline/types.js.map +1 -0
  184. package/dist/types/audit.d.ts +6 -6
  185. package/dist/workflow/index.d.ts.map +1 -1
  186. package/dist/workflow/index.js +48 -0
  187. package/dist/workflow/index.js.map +1 -1
  188. package/package.json +1 -1
  189. package/skills/PHASE_GATE_ENGINE_SPEC.md +113 -20
  190. package/skills/POPEYE_FULL_AUTONOMY_PIPELINE.md +66 -13
  191. package/src/pipeline/artifact-manager.ts +339 -0
  192. package/src/pipeline/artifact-validators.ts +224 -0
  193. package/src/pipeline/change-request.ts +119 -0
  194. package/src/pipeline/check-runner.ts +504 -0
  195. package/src/pipeline/command-resolver.ts +168 -0
  196. package/src/pipeline/consensus/consensus-runner.ts +317 -0
  197. package/src/pipeline/constitution.ts +109 -0
  198. package/src/pipeline/gate-engine.ts +347 -0
  199. package/src/pipeline/index.ts +82 -0
  200. package/src/pipeline/migration.ts +91 -0
  201. package/src/pipeline/orchestrator.ts +314 -0
  202. package/src/pipeline/packets/audit-report-builder.ts +47 -0
  203. package/src/pipeline/packets/consensus-packet-builder.ts +112 -0
  204. package/src/pipeline/packets/index.ts +15 -0
  205. package/src/pipeline/packets/plan-packet-builder.ts +52 -0
  206. package/src/pipeline/packets/rca-packet-builder.ts +38 -0
  207. package/src/pipeline/phases/architecture.ts +73 -0
  208. package/src/pipeline/phases/audit.ts +193 -0
  209. package/src/pipeline/phases/consensus-architecture.ts +104 -0
  210. package/src/pipeline/phases/consensus-master-plan.ts +100 -0
  211. package/src/pipeline/phases/consensus-role-plans.ts +105 -0
  212. package/src/pipeline/phases/done.ts +68 -0
  213. package/src/pipeline/phases/implementation.ts +48 -0
  214. package/src/pipeline/phases/index.ts +21 -0
  215. package/src/pipeline/phases/intake.ts +54 -0
  216. package/src/pipeline/phases/phase-context.ts +86 -0
  217. package/src/pipeline/phases/production-gate.ts +113 -0
  218. package/src/pipeline/phases/qa-validation.ts +63 -0
  219. package/src/pipeline/phases/recovery-loop.ts +118 -0
  220. package/src/pipeline/phases/review.ts +149 -0
  221. package/src/pipeline/phases/role-planning.ts +92 -0
  222. package/src/pipeline/phases/stuck.ts +62 -0
  223. package/src/pipeline/repo-snapshot.ts +395 -0
  224. package/src/pipeline/role-execution-adapter.ts +238 -0
  225. package/src/pipeline/skill-loader.ts +192 -0
  226. package/src/pipeline/skills/defaults.ts +215 -0
  227. package/src/pipeline/type-defs/artifacts.ts +81 -0
  228. package/src/pipeline/type-defs/audit.ts +67 -0
  229. package/src/pipeline/type-defs/checks.ts +47 -0
  230. package/src/pipeline/type-defs/enums.ts +62 -0
  231. package/src/pipeline/type-defs/index.ts +12 -0
  232. package/src/pipeline/type-defs/packets.ts +131 -0
  233. package/src/pipeline/type-defs/snapshot.ts +55 -0
  234. package/src/pipeline/type-defs/state.ts +165 -0
  235. package/src/pipeline/types.ts +16 -0
  236. package/src/workflow/index.ts +48 -0
  237. package/tests/pipeline/artifact-manager.test.ts +183 -0
  238. package/tests/pipeline/artifact-validators.test.ts +207 -0
  239. package/tests/pipeline/change-request.test.ts +180 -0
  240. package/tests/pipeline/check-runner.test.ts +157 -0
  241. package/tests/pipeline/command-resolver.test.ts +159 -0
  242. package/tests/pipeline/consensus-runner.test.ts +206 -0
  243. package/tests/pipeline/consensus-scoring.test.ts +163 -0
  244. package/tests/pipeline/constitution.test.ts +122 -0
  245. package/tests/pipeline/gate-engine.test.ts +195 -0
  246. package/tests/pipeline/migration.test.ts +133 -0
  247. package/tests/pipeline/orchestrator.test.ts +614 -0
  248. package/tests/pipeline/packets/builders.test.ts +347 -0
  249. package/tests/pipeline/repo-snapshot.test.ts +189 -0
  250. package/tests/pipeline/role-execution-adapter.test.ts +299 -0
  251. package/tests/pipeline/skill-loader.test.ts +186 -0
  252. package/tests/pipeline/start-env-checks.test.ts +123 -0
  253. package/tests/pipeline/types.test.ts +156 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Role Execution Adapter tests — context building, prompt injection, role detection.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import {
9
+ buildRoleExecutionContext,
10
+ executeWithRoleContext,
11
+ buildAllRoleContexts,
12
+ } from '../../src/pipeline/role-execution-adapter.js';
13
+ import type { RoleExecutionContext, ClaudeExecuteOptions } from '../../src/pipeline/role-execution-adapter.js';
14
+ import type { ArtifactEntry, PipelineRole } from '../../src/pipeline/types.js';
15
+ import { createDefaultPipelineState } from '../../src/pipeline/types.js';
16
+
17
+ const TEST_DIR = join(process.cwd(), 'tmp-role-adapter-test');
18
+
19
+ function makeMockSkill(role: string) {
20
+ return {
21
+ role,
22
+ systemPrompt: `You are ${role}. Follow best practices.`,
23
+ constraints: ['Stay in scope', 'Do not modify tests'],
24
+ tools: [],
25
+ };
26
+ }
27
+
28
+ function makeRolePlanArtifact(role: string): ArtifactEntry {
29
+ return {
30
+ id: `plan-${role}`,
31
+ type: 'role_plan',
32
+ phase: 'ROLE_PLANNING',
33
+ version: 1,
34
+ path: `docs/role-plans/${role}.md`,
35
+ sha256: 'abc123',
36
+ timestamp: new Date().toISOString(),
37
+ immutable: true,
38
+ content_type: 'markdown',
39
+ group_id: `group-${role}`,
40
+ };
41
+ }
42
+
43
+ beforeEach(() => {
44
+ mkdirSync(join(TEST_DIR, 'docs', 'role-plans'), { recursive: true });
45
+ });
46
+
47
+ afterEach(() => {
48
+ if (existsSync(TEST_DIR)) {
49
+ rmSync(TEST_DIR, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ describe('buildRoleExecutionContext', () => {
54
+ it('should build context with role, system prompt, and task scope', () => {
55
+ const planContent = [
56
+ '# FRONTEND_PROGRAMMER Role Plan',
57
+ '## Tasks',
58
+ '- Build login page in src/app/login/',
59
+ '- Implement dashboard components',
60
+ '## Dependencies',
61
+ 'Requires API contracts from BACKEND_PROGRAMMER.',
62
+ ].join('\n');
63
+ const artifact = makeRolePlanArtifact('FRONTEND_PROGRAMMER');
64
+ writeFileSync(join(TEST_DIR, artifact.path), planContent);
65
+
66
+ const ctx = buildRoleExecutionContext(
67
+ 'FRONTEND_PROGRAMMER',
68
+ makeMockSkill('FRONTEND_PROGRAMMER'),
69
+ artifact,
70
+ TEST_DIR,
71
+ );
72
+
73
+ expect(ctx.role).toBe('FRONTEND_PROGRAMMER');
74
+ expect(ctx.systemPrompt).toContain('FRONTEND_PROGRAMMER');
75
+ expect(ctx.systemPrompt).toContain('You are FRONTEND_PROGRAMMER');
76
+ expect(ctx.taskScope).toContain('Build login page');
77
+ expect(ctx.allowedPaths.length).toBeGreaterThan(0);
78
+ });
79
+
80
+ it('should include forbidden patterns for frontend role', () => {
81
+ const artifact = makeRolePlanArtifact('FRONTEND_PROGRAMMER');
82
+ writeFileSync(join(TEST_DIR, artifact.path), '# FRONTEND_PROGRAMMER Plan\n## Tasks\n- UI work');
83
+
84
+ const ctx = buildRoleExecutionContext(
85
+ 'FRONTEND_PROGRAMMER',
86
+ makeMockSkill('FRONTEND_PROGRAMMER'),
87
+ artifact,
88
+ TEST_DIR,
89
+ );
90
+
91
+ expect(ctx.forbiddenPatterns).toContain('server/');
92
+ expect(ctx.forbiddenPatterns).toContain('prisma/');
93
+ });
94
+
95
+ it('should include forbidden patterns in system prompt', () => {
96
+ const artifact = makeRolePlanArtifact('FRONTEND_PROGRAMMER');
97
+ writeFileSync(join(TEST_DIR, artifact.path), '# FRONTEND_PROGRAMMER Plan\n## Tasks\n- UI');
98
+
99
+ const ctx = buildRoleExecutionContext(
100
+ 'FRONTEND_PROGRAMMER',
101
+ makeMockSkill('FRONTEND_PROGRAMMER'),
102
+ artifact,
103
+ TEST_DIR,
104
+ );
105
+
106
+ expect(ctx.systemPrompt).toContain('Forbidden Paths');
107
+ expect(ctx.systemPrompt).toContain('server/');
108
+ });
109
+
110
+ it('should have no forbidden patterns for QA_TESTER', () => {
111
+ const artifact = makeRolePlanArtifact('QA_TESTER');
112
+ writeFileSync(join(TEST_DIR, artifact.path), '# QA_TESTER Plan\n## Tasks\n- Test all');
113
+
114
+ const ctx = buildRoleExecutionContext(
115
+ 'QA_TESTER',
116
+ makeMockSkill('QA_TESTER'),
117
+ artifact,
118
+ TEST_DIR,
119
+ );
120
+
121
+ expect(ctx.forbiddenPatterns).toEqual([]);
122
+ expect(ctx.systemPrompt).not.toContain('Forbidden Paths');
123
+ });
124
+
125
+ it('should include skill constraints in system prompt', () => {
126
+ const artifact = makeRolePlanArtifact('BACKEND_PROGRAMMER');
127
+ writeFileSync(join(TEST_DIR, artifact.path), '# BACKEND_PROGRAMMER Plan\n## Tasks\n- API');
128
+
129
+ const ctx = buildRoleExecutionContext(
130
+ 'BACKEND_PROGRAMMER',
131
+ makeMockSkill('BACKEND_PROGRAMMER'),
132
+ artifact,
133
+ TEST_DIR,
134
+ );
135
+
136
+ expect(ctx.systemPrompt).toContain('Stay in scope');
137
+ expect(ctx.systemPrompt).toContain('Do not modify tests');
138
+ });
139
+
140
+ it('should extract file paths from plan content as allowed paths', () => {
141
+ const planContent = [
142
+ '# BACKEND_PROGRAMMER Plan',
143
+ '## Tasks',
144
+ '- Implement API routes in src/api/routes.ts',
145
+ '- Update server/middleware.ts',
146
+ ].join('\n');
147
+ const artifact = makeRolePlanArtifact('BACKEND_PROGRAMMER');
148
+ writeFileSync(join(TEST_DIR, artifact.path), planContent);
149
+
150
+ const ctx = buildRoleExecutionContext(
151
+ 'BACKEND_PROGRAMMER',
152
+ makeMockSkill('BACKEND_PROGRAMMER'),
153
+ artifact,
154
+ TEST_DIR,
155
+ );
156
+
157
+ expect(ctx.allowedPaths).toContain('src/api/routes.ts');
158
+ expect(ctx.allowedPaths).toContain('server/middleware.ts');
159
+ });
160
+
161
+ it('should handle missing plan file gracefully', () => {
162
+ const artifact = makeRolePlanArtifact('BACKEND_PROGRAMMER');
163
+ // Don't write the file
164
+
165
+ const ctx = buildRoleExecutionContext(
166
+ 'BACKEND_PROGRAMMER',
167
+ makeMockSkill('BACKEND_PROGRAMMER'),
168
+ artifact,
169
+ TEST_DIR,
170
+ );
171
+
172
+ expect(ctx.role).toBe('BACKEND_PROGRAMMER');
173
+ expect(ctx.taskScope).toBe('');
174
+ expect(ctx.systemPrompt).toContain('BACKEND_PROGRAMMER');
175
+ });
176
+ });
177
+
178
+ describe('executeWithRoleContext', () => {
179
+ it('should inject systemPrompt into options', () => {
180
+ const ctx: RoleExecutionContext = {
181
+ role: 'FRONTEND_PROGRAMMER',
182
+ systemPrompt: 'You are FRONTEND_PROGRAMMER. Build the UI.',
183
+ allowedPaths: ['src/'],
184
+ forbiddenPatterns: ['server/'],
185
+ taskScope: 'Build login page',
186
+ };
187
+
188
+ const options: ClaudeExecuteOptions = {
189
+ projectDir: '/test/project',
190
+ };
191
+
192
+ const result = executeWithRoleContext(ctx, options);
193
+ expect(result.systemPrompt).toBe('You are FRONTEND_PROGRAMMER. Build the UI.');
194
+ expect(result.projectDir).toBe('/test/project');
195
+ });
196
+
197
+ it('should override existing systemPrompt', () => {
198
+ const ctx: RoleExecutionContext = {
199
+ role: 'BACKEND_PROGRAMMER',
200
+ systemPrompt: 'Role-specific prompt',
201
+ allowedPaths: [],
202
+ forbiddenPatterns: [],
203
+ taskScope: '',
204
+ };
205
+
206
+ const options: ClaudeExecuteOptions = {
207
+ projectDir: '/test',
208
+ systemPrompt: 'Old prompt',
209
+ };
210
+
211
+ const result = executeWithRoleContext(ctx, options);
212
+ expect(result.systemPrompt).toBe('Role-specific prompt');
213
+ });
214
+
215
+ it('should preserve other options', () => {
216
+ const ctx: RoleExecutionContext = {
217
+ role: 'QA_TESTER',
218
+ systemPrompt: 'Test prompt',
219
+ allowedPaths: [],
220
+ forbiddenPatterns: [],
221
+ taskScope: '',
222
+ };
223
+
224
+ const options: ClaudeExecuteOptions = {
225
+ projectDir: '/test',
226
+ customField: 'preserved',
227
+ };
228
+
229
+ const result = executeWithRoleContext(ctx, options);
230
+ expect(result.customField).toBe('preserved');
231
+ });
232
+ });
233
+
234
+ describe('buildAllRoleContexts', () => {
235
+ it('should build contexts for all detected roles', () => {
236
+ const pipeline = createDefaultPipelineState();
237
+ pipeline.activeRoles = ['FRONTEND_PROGRAMMER', 'BACKEND_PROGRAMMER'];
238
+
239
+ // Create plan files
240
+ const fePlan = makeRolePlanArtifact('FRONTEND_PROGRAMMER');
241
+ const bePlan = makeRolePlanArtifact('BACKEND_PROGRAMMER');
242
+ pipeline.artifacts.push(fePlan, bePlan);
243
+
244
+ writeFileSync(
245
+ join(TEST_DIR, fePlan.path),
246
+ '# FRONTEND_PROGRAMMER Role Plan\n## Tasks\n- Build UI',
247
+ );
248
+ writeFileSync(
249
+ join(TEST_DIR, bePlan.path),
250
+ '# BACKEND_PROGRAMMER Role Plan\n## Tasks\n- Build API',
251
+ );
252
+
253
+ const mockSkillLoader = {
254
+ loadSkill: (role: string) => makeMockSkill(role),
255
+ listSkills: () => [],
256
+ };
257
+
258
+ const contexts = buildAllRoleContexts(pipeline, mockSkillLoader as any, TEST_DIR);
259
+ expect(contexts.size).toBe(2);
260
+ expect(contexts.has('FRONTEND_PROGRAMMER')).toBe(true);
261
+ expect(contexts.has('BACKEND_PROGRAMMER')).toBe(true);
262
+ });
263
+
264
+ it('should skip roles with missing plan files', () => {
265
+ const pipeline = createDefaultPipelineState();
266
+ pipeline.activeRoles = ['FRONTEND_PROGRAMMER', 'BACKEND_PROGRAMMER'];
267
+
268
+ const fePlan = makeRolePlanArtifact('FRONTEND_PROGRAMMER');
269
+ pipeline.artifacts.push(fePlan);
270
+
271
+ // Only write FE plan, not BE
272
+ writeFileSync(
273
+ join(TEST_DIR, fePlan.path),
274
+ '# FRONTEND_PROGRAMMER Role Plan\n## Tasks\n- Build UI',
275
+ );
276
+
277
+ const mockSkillLoader = {
278
+ loadSkill: (role: string) => makeMockSkill(role),
279
+ listSkills: () => [],
280
+ };
281
+
282
+ const contexts = buildAllRoleContexts(pipeline, mockSkillLoader as any, TEST_DIR);
283
+ expect(contexts.size).toBe(1);
284
+ expect(contexts.has('FRONTEND_PROGRAMMER')).toBe(true);
285
+ });
286
+
287
+ it('should return empty map when no role plans exist', () => {
288
+ const pipeline = createDefaultPipelineState();
289
+ pipeline.activeRoles = ['FRONTEND_PROGRAMMER'];
290
+
291
+ const mockSkillLoader = {
292
+ loadSkill: (role: string) => makeMockSkill(role),
293
+ listSkills: () => [],
294
+ };
295
+
296
+ const contexts = buildAllRoleContexts(pipeline, mockSkillLoader as any, TEST_DIR);
297
+ expect(contexts.size).toBe(0);
298
+ });
299
+ });
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Skill Loader tests — frontmatter parsing, raw fallback, merge, caching.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import {
9
+ SkillLoader,
10
+ parseSkillMarkdown,
11
+ getDefaultSkill,
12
+ createSkillLoader,
13
+ } from '../../src/pipeline/skill-loader.js';
14
+
15
+ const TEST_DIR = join(process.cwd(), '.test-skill-loader');
16
+ const SKILLS_DIR = join(TEST_DIR, 'skills');
17
+
18
+ describe('SkillLoader', () => {
19
+ beforeEach(() => {
20
+ if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
21
+ mkdirSync(SKILLS_DIR, { recursive: true });
22
+ });
23
+
24
+ afterEach(() => {
25
+ if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
26
+ });
27
+
28
+ describe('parseSkillMarkdown', () => {
29
+ it('should parse frontmatter with body', () => {
30
+ const md = `---
31
+ role: ARCHITECT
32
+ version: 2.0
33
+ required_outputs:
34
+ - architecture_doc
35
+ - api_contracts
36
+ constraints:
37
+ - no implementation details
38
+ - all contracts explicit
39
+ ---
40
+ # System Prompt
41
+ You are the Architect responsible for system design.`;
42
+
43
+ const result = parseSkillMarkdown(md);
44
+ expect(result.role).toBe('ARCHITECT');
45
+ expect(result.version).toBe('2.0');
46
+ expect(result.required_outputs).toEqual(['architecture_doc', 'api_contracts']);
47
+ expect(result.constraints).toEqual(['no implementation details', 'all contracts explicit']);
48
+ expect(result.systemPrompt).toContain('You are the Architect');
49
+ });
50
+
51
+ it('should treat entire content as systemPrompt when no frontmatter', () => {
52
+ const md = 'You are a specialized reviewer.\nReview all code carefully.';
53
+ const result = parseSkillMarkdown(md);
54
+
55
+ expect(result.systemPrompt).toBe(md);
56
+ expect(result.role).toBeUndefined();
57
+ expect(result.version).toBeUndefined();
58
+ });
59
+
60
+ it('should handle frontmatter with scalar values', () => {
61
+ const md = `---
62
+ role: DEBUGGER
63
+ version: 1.5
64
+ ---
65
+ Debug the issue.`;
66
+
67
+ const result = parseSkillMarkdown(md);
68
+ expect(result.role).toBe('DEBUGGER');
69
+ expect(result.version).toBe('1.5');
70
+ expect(result.systemPrompt).toBe('Debug the issue.');
71
+ });
72
+
73
+ it('should handle empty body after frontmatter', () => {
74
+ const md = `---
75
+ role: QA_TESTER
76
+ version: 1.0
77
+ ---
78
+ `;
79
+
80
+ const result = parseSkillMarkdown(md);
81
+ expect(result.role).toBe('QA_TESTER');
82
+ expect(result.systemPrompt).toBeUndefined();
83
+ });
84
+ });
85
+
86
+ describe('getDefaultSkill', () => {
87
+ it('should return a valid skill for known roles', () => {
88
+ const skill = getDefaultSkill('ARCHITECT');
89
+ expect(skill.role).toBe('ARCHITECT');
90
+ expect(skill.systemPrompt).toBeDefined();
91
+ expect(skill.systemPrompt.length).toBeGreaterThan(0);
92
+ expect(Array.isArray(skill.required_outputs)).toBe(true);
93
+ expect(Array.isArray(skill.constraints)).toBe(true);
94
+ });
95
+
96
+ it('should return fallback for unknown roles', () => {
97
+ const skill = getDefaultSkill('NONEXISTENT' as any);
98
+ expect(skill.role).toBe('NONEXISTENT');
99
+ expect(skill.systemPrompt).toContain('NONEXISTENT');
100
+ });
101
+ });
102
+
103
+ describe('SkillLoader class', () => {
104
+ it('should load default skills when no override exists', () => {
105
+ const loader = new SkillLoader(SKILLS_DIR);
106
+ const skill = loader.loadSkill('ARCHITECT');
107
+
108
+ expect(skill.role).toBe('ARCHITECT');
109
+ expect(skill.systemPrompt).toBeDefined();
110
+ });
111
+
112
+ it('should merge .md override with default', () => {
113
+ writeFileSync(join(SKILLS_DIR, 'ARCHITECT.md'), `---
114
+ version: 3.0
115
+ constraints:
116
+ - custom constraint
117
+ ---
118
+ Custom architect prompt.`);
119
+
120
+ const loader = new SkillLoader(SKILLS_DIR);
121
+ const skill = loader.loadSkill('ARCHITECT');
122
+
123
+ expect(skill.role).toBe('ARCHITECT');
124
+ expect(skill.version).toBe('3.0');
125
+ expect(skill.systemPrompt).toBe('Custom architect prompt.');
126
+ expect(skill.constraints).toEqual(['custom constraint']);
127
+ });
128
+
129
+ it('should cache loaded skills', () => {
130
+ const loader = new SkillLoader(SKILLS_DIR);
131
+ const s1 = loader.loadSkill('DEBUGGER');
132
+ const s2 = loader.loadSkill('DEBUGGER');
133
+ expect(s1).toBe(s2); // Same reference = cached
134
+ });
135
+
136
+ it('should clear cache', () => {
137
+ const loader = new SkillLoader(SKILLS_DIR);
138
+ const s1 = loader.loadSkill('DEBUGGER');
139
+ loader.clearCache();
140
+ const s2 = loader.loadSkill('DEBUGGER');
141
+ expect(s1).not.toBe(s2); // Different reference after cache clear
142
+ });
143
+
144
+ it('should load all skills for given roles', () => {
145
+ const loader = new SkillLoader(SKILLS_DIR);
146
+ const skills = loader.loadAllSkills(['ARCHITECT', 'DEBUGGER', 'QA_TESTER']);
147
+
148
+ expect(skills.size).toBe(3);
149
+ expect(skills.has('ARCHITECT')).toBe(true);
150
+ expect(skills.has('DEBUGGER')).toBe(true);
151
+ expect(skills.has('QA_TESTER')).toBe(true);
152
+ });
153
+
154
+ it('should list available overrides', () => {
155
+ writeFileSync(join(SKILLS_DIR, 'ARCHITECT.md'), 'prompt');
156
+ writeFileSync(join(SKILLS_DIR, 'REVIEWER.md'), 'prompt');
157
+
158
+ const loader = new SkillLoader(SKILLS_DIR);
159
+ const overrides = loader.listAvailableOverrides();
160
+
161
+ expect(overrides).toContain('ARCHITECT');
162
+ expect(overrides).toContain('REVIEWER');
163
+ });
164
+
165
+ it('should handle no skills directory gracefully', () => {
166
+ const loader = new SkillLoader('/nonexistent/path');
167
+ const skill = loader.loadSkill('ARCHITECT');
168
+
169
+ // Falls back to default
170
+ expect(skill.role).toBe('ARCHITECT');
171
+ expect(skill.systemPrompt).toBeDefined();
172
+ });
173
+ });
174
+
175
+ describe('createSkillLoader', () => {
176
+ it('should create loader with skills dir', () => {
177
+ const loader = createSkillLoader(TEST_DIR);
178
+ expect(loader).toBeInstanceOf(SkillLoader);
179
+ });
180
+
181
+ it('should create loader without project dir', () => {
182
+ const loader = createSkillLoader();
183
+ expect(loader).toBeInstanceOf(SkillLoader);
184
+ });
185
+ });
186
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Start & Env Checks tests — application start check,
3
+ * environment variable validation from .env.example.
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { runStartCheck, runEnvCheck } from '../../src/pipeline/check-runner.js';
10
+
11
+ const TEST_DIR = join(process.cwd(), 'tmp-start-env-test');
12
+
13
+ beforeEach(() => {
14
+ mkdirSync(TEST_DIR, { recursive: true });
15
+ });
16
+
17
+ afterEach(() => {
18
+ if (existsSync(TEST_DIR)) {
19
+ rmSync(TEST_DIR, { recursive: true, force: true });
20
+ }
21
+ });
22
+
23
+ describe('runStartCheck', () => {
24
+ it('should pass when process stays alive past timeout', async () => {
25
+ // 'sleep 10' will stay alive for 10s, well past our 1s timeout
26
+ const result = await runStartCheck('sleep 10', TEST_DIR, { timeoutMs: 1000 });
27
+ expect(result.check_type).toBe('start');
28
+ expect(result.status).toBe('pass');
29
+ expect(result.exit_code).toBe(0);
30
+ }, 10000);
31
+
32
+ it('should fail when process crashes immediately', async () => {
33
+ const result = await runStartCheck('exit 1', TEST_DIR, { timeoutMs: 3000 });
34
+ expect(result.check_type).toBe('start');
35
+ expect(result.status).toBe('fail');
36
+ }, 10000);
37
+
38
+ it('should fail for dangerous commands', async () => {
39
+ const result = await runStartCheck('sudo rm -rf /', TEST_DIR);
40
+ expect(result.status).toBe('fail');
41
+ expect(result.stderr_summary).toContain('rejected');
42
+ });
43
+
44
+ it('should return start check type', async () => {
45
+ const result = await runStartCheck('echo hello', TEST_DIR, { timeoutMs: 1000 });
46
+ expect(result.check_type).toBe('start');
47
+ }, 5000);
48
+ });
49
+
50
+ describe('runEnvCheck', () => {
51
+ it('should pass when no .env.example exists', () => {
52
+ const result = runEnvCheck(TEST_DIR);
53
+ expect(result.check_type).toBe('env_check');
54
+ expect(result.status).toBe('pass');
55
+ expect(result.stderr_summary).toContain('No .env.example');
56
+ });
57
+
58
+ it('should fail when .env.example exists but .env is missing', () => {
59
+ writeFileSync(join(TEST_DIR, '.env.example'), 'API_KEY=\nDB_URL=');
60
+
61
+ const result = runEnvCheck(TEST_DIR);
62
+ expect(result.status).toBe('fail');
63
+ expect(result.stderr_summary).toContain('.env file not found');
64
+ expect(result.stderr_summary).toContain('API_KEY');
65
+ expect(result.stderr_summary).toContain('DB_URL');
66
+ });
67
+
68
+ it('should pass when all required vars are present', () => {
69
+ writeFileSync(join(TEST_DIR, '.env.example'), 'API_KEY=your-key\nDB_URL=postgres://');
70
+ writeFileSync(join(TEST_DIR, '.env'), 'API_KEY=real-key\nDB_URL=postgres://localhost/db');
71
+
72
+ const result = runEnvCheck(TEST_DIR);
73
+ expect(result.status).toBe('pass');
74
+ });
75
+
76
+ it('should fail when required vars are missing from .env', () => {
77
+ writeFileSync(join(TEST_DIR, '.env.example'), 'API_KEY=\nDB_URL=\nSECRET=');
78
+ writeFileSync(join(TEST_DIR, '.env'), 'API_KEY=real-key');
79
+
80
+ const result = runEnvCheck(TEST_DIR);
81
+ expect(result.status).toBe('fail');
82
+ expect(result.stderr_summary).toContain('Missing vars');
83
+ expect(result.stderr_summary).toContain('DB_URL');
84
+ expect(result.stderr_summary).toContain('SECRET');
85
+ });
86
+
87
+ it('should warn about empty vars but still pass', () => {
88
+ writeFileSync(join(TEST_DIR, '.env.example'), 'API_KEY=\nOPTIONAL_VAR=');
89
+ writeFileSync(join(TEST_DIR, '.env'), 'API_KEY=real-key\nOPTIONAL_VAR=');
90
+
91
+ const result = runEnvCheck(TEST_DIR);
92
+ expect(result.status).toBe('pass');
93
+ expect(result.stderr_summary).toContain('Empty vars');
94
+ expect(result.stderr_summary).toContain('OPTIONAL_VAR');
95
+ });
96
+
97
+ it('should skip comments in .env.example', () => {
98
+ writeFileSync(
99
+ join(TEST_DIR, '.env.example'),
100
+ '# Database config\nDB_URL=postgres://\n# API keys\nAPI_KEY=',
101
+ );
102
+ writeFileSync(join(TEST_DIR, '.env'), 'DB_URL=postgres://localhost\nAPI_KEY=key123');
103
+
104
+ const result = runEnvCheck(TEST_DIR);
105
+ expect(result.status).toBe('pass');
106
+ });
107
+
108
+ it('should handle quoted values in .env', () => {
109
+ writeFileSync(join(TEST_DIR, '.env.example'), 'SECRET=');
110
+ writeFileSync(join(TEST_DIR, '.env'), 'SECRET="my-secret-value"');
111
+
112
+ const result = runEnvCheck(TEST_DIR);
113
+ expect(result.status).toBe('pass');
114
+ });
115
+
116
+ it('should skip empty lines in .env.example', () => {
117
+ writeFileSync(join(TEST_DIR, '.env.example'), '\nAPI_KEY=\n\nDB_URL=\n\n');
118
+ writeFileSync(join(TEST_DIR, '.env'), 'API_KEY=key\nDB_URL=url');
119
+
120
+ const result = runEnvCheck(TEST_DIR);
121
+ expect(result.status).toBe('pass');
122
+ });
123
+ });